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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=2.0, user-scalable=0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
\ No newline at end of file
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "app",
};
</script>
\ No newline at end of file
import axios from "utils/request";
/**
* 获取博客列表
* @param data
* @returns {AxiosPromise}
*/
export function apiGetBlogList(params) {
return axios.get("/blog/list", params);
}
/**
* 获取博客详情
* @param data
* @returns {AxiosPromise}
*/
export function apiGetBlogDetail(params) {
return axios.get("/blog/info", params);
}
/**
* 新增博客
* @param data
* @returns {AxiosPromise}
*/
export function apiAddBlog(params) {
return axios.postFile("/blog/add", params);
}
/**
* 修改博客
* @param data
* @returns {AxiosPromise}
*/
export function apiUpdateBlog(params) {
return axios.postFile("/blog/update", params);
}
/**
* 删除博客
* @param data
* @returns {AxiosPromise}
*/
export function apiDelBlog(params) {
return axios.get("/blog/del", params);
}
import axios from "utils/request";
/**
* 获取标签列表
* @param data
* @returns {AxiosPromise}
*/
export function apiGetLabelList(params) {
return axios.get("/label/list", params);
}
/**
* 新增标签
* @param data
* @returns {AxiosPromise}
*/
export function apiAddLabel(params) {
return axios.postFile("/label/add", params);
}
/**
* 修改标签
* @param data
* @returns {AxiosPromise}
*/
export function apiUpdateLabel(params) {
return axios.post("/label/update", params);
}
/**
* 删除标签
* @param data
* @returns {AxiosPromise}
*/
export function apiDelLabel(params) {
return axios.get("/label/del", params);
}
import axios from "utils/request";
/**
* 获取留言列表
* @param data
* @returns {AxiosPromise}
*/
export function apiGetMessageList(params) {
return axios.get("/message/list", params);
}
/**
* 删除留言
* @param data
* @returns {AxiosPromise}
*/
export function apiDelMessage(params) {
return axios.get("/message/del", params);
}
/**
* 删除回复
* @param data
* @returns {AxiosPromise}
*/
export function apiDelReply(params) {
return axios.postFile("/message/delReply", params);
}
import axios from "utils/request";
/**
* 上传图片
* @param data
* @returns {AxiosPromise}
*/
export function apiUploadImg(params) {
return axios.postFile("/uploadImage", params);
}
/**
* 删除图片
* @param data
* @returns {AxiosPromise}
*/
export function apiDelUploadImg(params) {
return axios.post("/delUploadImage", params);
}
<template>
<div id="markdowm">
<div class="md-title">
<ul class="cf">
<li>
<span>图片</span>
<input type="file" class="uploadFile" @change="insertImg" />
</li>
<li @click="insertCode">
<span>代码块</span>
</li>
<li @click="setCursorPosition($refs.text, '***')">
<span>分割线</span>
</li>
<li @click="setCursorPosition($refs.text, '****', 2)">
<span>粗体</span>
</li>
<li @click="setCursorPosition($refs.text, '**', 1)">
<span>斜体</span>
</li>
<li @click="setCursorPosition($refs.text, '> ', 2)">
<span>引用</span>
</li>
</ul>
</div>
<textarea v-model="val" ref="text" @keydown.tab="tabMarkdown"></textarea>
<div class="render fmt" v-html="renderHtml"></div>
</div>
</template>
<script>
import marked from "marked";
import highlightJs from "highlight.js";
import { apiUploadImg } from "src/api/upload";
export default {
props: ["value"],
computed: {
renderHtml() {
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true, //允许 Git Hub标准的markdown.
tables: true, //允许支持表格语法。该选项要求 gfm 为true。
breaks: true, //允许回车换行。该选项要求 gfm 为true。
pedantic: false, //尽可能地兼容 markdown.pl的晦涩部分。不纠正原始模型任何的不良行为和错误。
sanitize: true, //对输出进行过滤(清理),将忽略任何已经输入的html代码(标签)
smartLists: true, //使用比原生markdown更时髦的列表。 旧的列表将可能被作为pedantic的处理内容过滤掉.
smartypants: false, //使用更为时髦的标点,比如在引用语法中加入破折号。
highlight: function (code) {
return highlightJs.highlightAuto(code).value;
},
});
return marked(this.val);
},
},
watch: {
val(newVal) {
this.handleModelInput(newVal);
},
},
data() {
return {
val: this.value,
link: "",
textarea: null,
};
},
mounted() {
this.textarea = this.$refs.text;
},
methods: {
handleModelInput(newVal) {
this.$emit("input", newVal);
},
tabMarkdown(e) {
// tab键
e.preventDefault();
let indent = " ";
let start = this.textarea.selectionStart;
let end = this.textarea.selectionEnd;
let selected = window.getSelection().toString();
selected = indent + selected.replace(/\n/g, "\n" + indent);
this.textarea.value =
this.textarea.value.substring(0, start) +
selected +
this.textarea.value.substring(end);
this.textarea.setSelectionRange(
start + indent.length,
start + selected.length
);
},
insertImg(e) {
// 插入图片
let formData = new FormData(),
img = "";
formData.append("markdown_img", e.target.files[0]);
return apiUploadImg(formData)
.then((res) => {
img = res.data.markdown_img;
let val = `![图片描述](${img})`;
this.setCursorPosition(this.$refs.text, val, 6);
})
.catch((err) => {
console.log(err);
})
.finally(() => {});
},
insertCode() {
let val = `
\`\`\`
\`\`\``;
this.setCursorPosition(this.$refs.text, val, val.length - 8);
},
setCursorPosition(dom, val, posLen) {
// 设置光标位置
var cursorPosition = 0;
if (dom.selectionStart) {
cursorPosition = dom.selectionStart;
}
this.insertAtCursor(dom, val);
dom.focus();
dom.setSelectionRange(
dom.value.length,
cursorPosition + (posLen || val.length)
);
this.val = dom.value;
},
insertAtCursor(dom, val) {
// 光标所在位置插入字符
if (document.selection) {
dom.focus();
sel = document.selection.createRange();
sel.text = val;
sel.select();
} else if (dom.selectionStart || dom.selectionStart == "0") {
let startPos = dom.selectionStart;
let endPos = dom.selectionEnd;
let restoreTop = dom.scrollTop;
dom.value =
dom.value.substring(0, startPos) +
val +
dom.value.substring(endPos, dom.value.length);
if (restoreTop > 0) {
dom.scrollTop = restoreTop;
}
dom.focus();
dom.selectionStart = startPos + val.length;
dom.selectionEnd = startPos + val.length;
} else {
dom.value += val;
dom.focus();
}
},
},
};
</script>
<style lang="less" scoped>
@import "./markdown.less";
@import "../../../../../node_modules/highlight.js/styles/tomorrow-night-eighties.css";
@md-bd-color: #dcdfe6;
@md-title-color: rgb(233, 234, 237);
@md-bg-color: #fff;
@btn-hover: #3b7cff;
#markdowm {
width: 100%;
height: 500px;
text-align: left;
overflow: hidden;
border: 1px solid @md-bd-color;
position: relative;
.md-title {
width: 100%;
height: 40px;
border-bottom: 1px solid #dcdfe6;
background: @md-title-color;
position: absolute;
left: 0;
top: 0;
z-index: 99;
li {
width: 100px;
height: 100%;
text-align: center;
position: relative;
float: left;
cursor: pointer;
color: #606266;
&:hover {
color: @btn-hover;
}
&:after {
content: "";
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 20px;
background: @borderBoldColor;
}
&:last-child::after {
content: none;
}
.uploadFile {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
}
}
textarea,
.render {
float: left;
width: 50%;
height: 100%;
vertical-align: top;
box-sizing: border-box;
line-height: 22px;
padding: 0 20px;
}
textarea {
border: none;
border-right: 1px solid @md-bd-color;
resize: none;
outline: none;
background-color: @md-bg-color;
color: @mainColor;
font-size: 14px;
line-height: 22px;
padding: 20px;
padding-top: 50px;
}
.render {
background-color: @md-title-color;
overflow-y: scroll;
padding-top: 50px;
}
.mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 10;
}
.link-text {
width: 500px;
text-align: center;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
.link-input {
width: 400px;
}
}
}
</style>
\ No newline at end of file
.fmt {
line-height: 1.6;
word-wrap: break-word;
color: @mainColor;
}
.fmt a {
color: #009a61;
text-decoration: none;
}
.fmt h1 {
font-size: 2.25em;
}
.fmt h2 {
font-size: 1.75em;
}
.fmt h3 {
font-size: 1.5em;
}
.fmt h4 {
font-size: 1.25em;
}
.fmt h5 {
font-size: 1em;
}
.fmt h6 {
font-size: 0.86em;
}
.fmt p {
margin-top: 0.86em;
line-height: 1.8em;
}
.fmt h1,
.fmt h2,
.fmt h3,
.fmt h4,
.fmt h5,
.fmt h6 {
margin-top: 1.2em;
}
.fmt h1 + .widget-codetool + pre,
.fmt h2 + .widget-codetool + pre,
.fmt h3 + .widget-codetool + pre {
margin-top: 1.2em !important;
}
.fmt h1,
.fmt h2 {
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.fmt > h1:first-child,
.fmt h2:first-child,
.fmt h3:first-child,
.fmt h4:first-child,
.fmt p:first-child,
.fmt ul:first-child,
.fmt ol:first-child,
.fmt blockquote:first-child {
margin-top: 0;
}
.fmt ul,
.fmt ol {
margin-left: 2em;
margin-top: 0.86em;
padding-left: 0;
}
.fmt ul li,
.fmt ol li {
margin: 0.3em 0;
list-style: unset;
}
.fmt ul ul,
.fmt ul ol,
.fmt ol ul,
.fmt ol ol {
margin-top: 0;
margin-bottom: 0;
}
.fmt ul p,
.fmt ol p {
margin: 0;
}
.fmt p:last-child {
margin-bottom: 0;
}
.fmt p > p:empty,
.fmt div > p:empty,
.fmt p > div:empty,
.fmt div > div:empty,
.fmt div > br:only-child,
.fmt p + br,
.fmt img + br {
display: none;
}
.fmt img,
.fmt video,
.fmt audio {
position: static !important;
max-width: 100%;
}
.fmt img {
padding: 3px;
border: 1px solid #ddd;
}
.fmt img.emoji {
padding: 0;
border: none;
}
.fmt blockquote {
border-left: 2px solid #009a61;
background: @thinBgColor;
color: @thinColor;
font-size: 1em;
}
.fmt pre,
.fmt code {
font-size: 0.93em;
margin-top: 0.86em;
}
.fmt pre {
font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New",
monospace;
padding: 1em;
margin-top: 0.86em;
border: none;
overflow: auto;
line-height: 1.45;
max-height: 35em;
position: relative;
background: url("./blueprint.png") @thinBgColor;
background-size: 30px, 30px;
font-size: 12px;
-webkit-overflow-scrolling: touch;
border-radius: 5px;
}
.fmt pre code {
background: none;
font-size: 1em;
overflow-wrap: normal;
white-space: inherit;
}
.fmt hr {
margin: 1.5em auto;
border-top: 2px dotted #eee;
}
.fmt kbd {
margin: 0 4px;
padding: 3px 4px;
background: #eee;
color: @thinColor;
}
.fmt .x-scroll {
overflow-x: auto;
}
.fmt table {
width: 100%;
}
.fmt table th,
.fmt table td {
border: 1px solid #e6e6e6;
padding: 5px 8px;
word-break: normal;
}
.fmt table th {
background: #f3f3f3;
}
.fmt a:not(.btn) {
border-bottom: 1px solid rgba(0, 154, 97, 0.25);
padding-bottom: 1px;
}
.fmt a:not(.btn):hover {
border-bottom: 1px solid #009a61;
text-decoration: none;
}
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: @mainColor;
background: #f8f8f8;
}
.hljs-comment,
.hljs-quote {
color: #998;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: @mainColor;
font-weight: bold;
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
color: #008080;
}
.hljs-string,
.hljs-doctag {
color: #d14;
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
color: #900;
font-weight: bold;
}
.hljs-subst {
font-weight: normal;
}
.hljs-type,
.hljs-class .hljs-title {
color: #458;
font-weight: bold;
}
.hljs-tag,
.hljs-name,
.hljs-attribute {
color: #000080;
font-weight: normal;
}
.hljs-regexp,
.hljs-link {
color: #009926;
}
.hljs-symbol,
.hljs-bullet {
color: #990073;
}
.hljs-built_in,
.hljs-builtin-name {
color: #0086b3;
}
.hljs-meta {
color: @assistColor;
font-weight: bold;
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
/**
* 注入公共组件
*/
import Vue from "vue";
// 检索当前目录的vue文件,便检索子文件夹
const componentsContext = require.context("./", true, /.vue$/);
componentsContext.keys().forEach((component) => {
// 获取文件中的 default 模块
const componentConfig = componentsContext(component).default;
componentConfig.name && Vue.component(componentConfig.name, componentConfig);
});
<template>
<zp-page>
<!-- 页面标题 -->
<div slot="header">
<zp-page-header :back="back" @back="$emit('back')">
<slot name="header">{{ header }}</slot>
</zp-page-header>
</div>
<!-- 主体body -->
<div class="zp-page-edit">
<slot></slot>
<!-- 保存等操作按钮 -->
<div class="zp-page-edit-button" v-if="this.$slots.button">
<slot name="button"></slot>
</div>
</div>
</zp-page>
</template>
<script>
import zpPage from "./zp-page";
import zpPageHeader from "./zp-page-header";
export default {
name: "zpPageEdit",
components: {
zpPage,
zpPageHeader,
},
props: {
header: {
type: String,
default: "",
},
back: {
type: Boolean,
default: true,
},
},
created() {},
methods: {},
};
</script>
<style lang="less" scope>
.zp-page-edit {
.el-form {
margin-top: 20px;
}
.el-input {
width: 220px;
}
.el-textarea {
width: 440px;
.el-textarea__inner {
min-height: 100px !important;
}
}
.zp-notice-subtitle {
width: 540px;
box-sizing: border-box;
}
}
.zp-page-edit-button {
padding: 20px 20px 20px 120px;
}
</style>
<template>
<div class="zp-page-filter">
<el-form ref="pageFilter" :inline="true" :label-width="labelWidth + 'px'">
<el-form-item
v-for="(searchItem, searchIndex) in searchForm"
:key="searchIndex"
:label="searchItem.label"
>
<!-- 输入框 -->
<el-input
v-if="searchItem.type == 'text'"
v-model="searchItem.value"
:placeholder="searchItem.placeholder || '请输入' + searchItem.label"
/>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: "zpPageFilter",
components: {},
props: {
labelWidth: {
type: Number,
default: 100,
},
searchForm: {
type: Array,
default: () => [],
},
},
data() {
return {};
},
watch: {},
methods: {},
};
</script>
<style lang="less" scope></style>
<!--
* @Descripttion:
* @Author: givon.chen
* @Date: 2020-06-02 14:48:49
* @LastEditTime: 2020-06-30 15:41:59
-->
<template>
<div class="zp-page-header">
<!-- 带返回 -->
<el-page-header
:class="{ hideBack: !back }"
@back="$emit('back')"
:title="title"
>
<template v-slot:content>
<slot>
{{ header }}
</slot>
</template>
</el-page-header>
</div>
</template>
<script>
export default {
name: "zpPageHeader",
props: {
header: {
type: String,
default: "",
},
back: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "返回",
},
},
};
</script>
<style lang="less" scope>
.zp-page-header {
display: flex;
align-items: center;
height: 54px;
font-weight: bold;
.el-page-header {
flex-grow: 1;
padding-left: 0;
box-shadow: none;
line-height: 28px !important;
.el-page-header__content {
// header 里 tabs的情形
.el-tabs {
.el-tabs__header {
margin: 0;
.el-tabs__nav-wrap {
padding-left: 0;
}
}
}
}
}
.hideBack {
.el-page-header__left {
display: none;
}
}
}
</style>
<!--
* @Descripttion:
* @Author: givon.chen
* @Date: 2020-05-07 21:05:06
* @LastEditTime: 2020-06-29 12:09:31
-->
<template>
<zp-page>
<!-- 页面标题 -->
<template v-slot:header v-if="!hideHeader">
<zp-page-header :back="back" @back="back && $emit('back')">
<slot name="header">{{ header }}</slot>
</zp-page-header>
</template>
<!-- 搜索框 -->
<div class="zp-page-filter" v-if="this.$slots['filter']">
<slot name="filter"></slot>
</div>
<!-- 搜索框按钮组 -->
<div class="zp-search-button" v-if="this.$slots['button']">
<slot name="button"></slot>
</div>
<!-- 列表、分页 -->
<div class="zp-page-list">
<slot name="list"></slot>
</div>
</zp-page>
</template>
<script>
import zpPage from "./zp-page";
import zpPageHeader from "./zp-page-header";
export default {
name: "zpPageList",
components: {
zpPage,
zpPageHeader,
},
props: {
back: {
type: Boolean,
default: false,
},
header: {
type: String,
default: "",
},
hideHeader: {
type: Boolean,
default: false,
},
},
created() {},
methods: {},
};
</script>
<style lang="less" scope>
.zp-search-button {
padding-bottom: 20px;
padding-top: 0;
.el-button {
border-radius: 1px;
width: 100px !important;
}
}
.el-table {
th {
font-size: 14px;
}
td {
font-size: 13px;
}
.el-button--text {
padding-top: 0;
padding-bottom: 0;
height: 18px;
line-height: 18px;
font-size: 13px;
border: 0 none;
span {
vertical-align: top;
display: inline-block;
line-height: 18px;
}
}
}
.el-pagination {
padding-bottom: 0 !important;
margin-top: 24px;
}
.el-pagination__sizes {
.el-select {
.el-input {
.el-input__inner {
padding-left: 0 !important;
}
}
}
}
</style>
<!--
* @Descripttion:
* @Author: givon.chen
* @Date: 2020-05-15 12:48:29
* @LastEditTime: 2020-06-30 15:43:16
-->
<template>
<div class="zp-page-container">
<el-card class="zp-page-el-card" shadow="never">
<template v-slot:header class="clearfix" v-if="this.$slots.header">
<slot name="header"></slot>
</template>
<div class="zp-page-body">
<slot></slot>
</div>
</el-card>
</div>
</template>
<script>
export default {
name: "zpPage",
created() {},
methods: {},
};
</script>
<style lang="less" scope>
.zp-page-container {
background-color: #fff;
.zp-page-el-card {
border-radius: 0;
border: 0 none;
.el-card__header {
font-size: 16px;
font-weight: bold;
color: $strongFontColor;
background: #ffffff;
padding: 0px 20px !important;
line-height: 28px;
}
}
.el-icon-edit {
position: relative;
width: 16px;
height: 16px;
display: inline-block !important;
vertical-align: middle !important;
padding: 10px;
cursor: pointer;
&:before {
position: absolute;
left: 10px;
top: 10px;
content: "" !important;
width: 16px;
height: 16px;
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=")
no-repeat;
background-size: 16px;
}
}
}
</style>
<!--
table 配置项:
1. loading
2. source: 数据list
3. count: 数据总数
4. columns: 列配置
同el-table-column 属性
补充:
1. zpMark: 特殊几个字段固定宽度,具体查看 @zpAdmin/utils/config/styleConfig
2. slot: 自定义列名
5. leftFix: 左侧固定列栏数
6. rightFix: 右侧固定列栏数
7. pageable: 是否显示分页
8. pageSize: 默认每页条数
9. layout: 分页配置
事件:
1. sizeChange: 分页切换 | 参数: { pageNo, pageSize }
2. selectChange: 多选切换 | 参数: 同element table
-->
<template>
<div class="zp-table-list">
<el-table
ref="table"
v-loading="loading"
:stripe="stripe"
:data="source"
:row-key="rowKey"
:show-header="showHeader"
:empty-text="loading ? ' ' : '暂无数据'"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
>
<template v-for="(column, columnKey) in columns">
<template v-if="column.type">
<template v-if="column.type === 'selection'">
<!-- 选择栏固定宽度 -->
<el-table-column
type="selection"
width="55"
:show-overflow-tooltip="column.showTooltip"
:align="column.align"
:key="columnKey"
:reserve-selection="column.reserveSelection"
:selectable="
column.selectable
? column.selectable
: () => {
return true;
}
"
></el-table-column>
</template>
</template>
<template v-else>
<el-table-column
:type="column.type"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:width="getWidth(column)"
:fixed="getFixed(columnKey)"
:class-name="column.className"
:min-width="column.minWidth"
:render-header="column.renderHeader"
:show-overflow-tooltip="column.showTooltip"
:align="column.align"
:header-align="column.headerAlign"
:label-class-name="column.labelClassName"
:formatter="column.formatter"
:sortable="column.sortable || false"
>
<template v-if="column.slotHeader" slot="header">
<slot :name="column.slotHeader"></slot>
</template>
<template slot-scope="scope">
<!-- 自定义列: 比如操作列 start -->
<slot
v-if="column.slot"
:name="column.slot"
:$index="scope.$index"
:column="scope.column"
:row="scope.row"
></slot>
<!-- 自定义列: 比如操作列 end -->
<template v-else>
<template v-if="column.formatter">
{{ column.formatter(scope.row, column) }}
</template>
<template v-else>
{{ scope.row[column.prop] }}
</template>
</template>
</template>
</el-table-column>
</template>
</template>
</el-table>
<el-pagination
v-if="pageable && count > 0"
@size-change="(value) => handleSizeChange(value)"
@current-change="(value) => handleCurrentChange(value)"
:current-page="page"
:page-sizes="pageSizes"
:page-size="currentPageSize"
:layout="pageLayout"
:total="count"
></el-pagination>
</div>
</template>
<script>
import { tableFixWidths } from "src/utils/config/styleConfig";
export default {
name: "zpTableList",
data() {
return {
page: 1,
currentPageSize: 20,
pageSizes: [10, 20, 30, 50],
pageLayout: "",
};
},
props: {
// 是否显示表头
showHeader: {
type: Boolean,
default: true,
},
// 是否斑马纹
stripe: {
type: Boolean,
default: true,
},
// 加载状态
loading: {
type: Boolean,
default: false,
},
// 数据资源
source: {
type: Array,
default: () => [],
},
// 列配置
columns: {
type: Array,
default: () => [],
},
// 列表总数量
count: {
type: Number,
default: 0,
},
// 左侧固定列栏数
leftFix: {
type: Number,
default: 1,
},
// 右侧固定列栏数
rightFix: {
type: Number,
default: 1,
},
// 是否分页
pageable: {
type: Boolean,
default: true,
},
// 每页条数
pageSize: {
type: Number,
default: 10,
},
// 分页组件布局
layout: {
type: String,
default: "total, sizes, prev, pager, next, jumper",
},
rowKey: [String, Function],
},
watch: {
count: {
handler(value) {
// 解决查询列表,分页器页码没同步更新问题
if (value === 0) {
this.page = 1;
}
},
},
pageSize: {
handler(value) {
value && (this.currentPageSize = value);
},
immediate: true,
},
layout: {
handler(value) {
value && (this.pageLayout = value);
},
immediate: true,
},
},
created() {},
methods: {
doLayout() {
this.$refs.table.doLayout();
},
handleSortChange(value) {
console.log(value);
this.$emit("sort-change", value);
},
/**
* 选择操作
* @param value
*/
handleSelectionChange(value) {
this.$emit("selection-change", value);
},
/**
* 当前页码变化
* @param val
*/
handleCurrentChange(val) {
this.page = val;
this.$emit("current-change", val);
},
/**
* 每页数量变化
* @param val
*/
handleSizeChange(val) {
this.currentPageSize = val;
this.$emit("size-change", val);
},
// 是否固定
getFixed(columnKey) {
if (columnKey < this.leftFix) {
return "left";
} else if (columnKey > this.columns.length - this.rightFix - 1) {
return "right";
}
return false;
},
// 宽度
getWidth(column) {
// 1. 优先 zpMark 固定宽度项
// 2. width属性
// 3. 不设宽度
if (column.zpMark) {
if (Object.keys(tableFixWidths).includes(column.zpMark)) {
return tableFixWidths[column.zpMark];
}
}
if (column.width) {
return column.width;
}
return "";
},
},
};
</script>
<style lang="less" scope>
.zp-table-list {
.el-table {
.el-button--text {
padding: 0 2px;
}
.el-tag {
height: 24px;
line-height: 22px;
border-radius: 12px;
}
}
.el-table__fixed,
.el-table__fixed-right {
&:before {
background-color: transparent;
}
}
}
</style>
<style lang="less" scoped>
.zp-table-list {
height: 100%;
display: flex;
flex-direction: column;
.el-pagination {
text-align: right;
.el-pagination__total {
float: left;
}
}
}
</style>
<template>
<svg class="icon" aria-hidden="true">
<use :xlink:href="iconName"></use>
</svg>
</template>
<script>
export default {
name: "Icon",
props: {
name: {
type: String,
required: true,
},
},
computed: {
iconName() {
return `#icon-${this.name}`;
},
},
};
</script>
<template>
<el-dialog
:title="title"
class="zp-dialog__wrapper"
v-bind="$attrs"
v-on="$listeners"
>
<template v-slot:title v-if="this.$slots.title">
<slot name="title"></slot>
</template>
<template v-slot:footer>
<slot name="footer"></slot>
</template>
<slot></slot>
</el-dialog>
</template>
<script>
export default {
name: "ZpDialog",
components: {},
props: {
title: {
type: String,
default: "",
},
},
computed: {},
data() {
return {};
},
watch: {},
created() {},
mounted() {},
beforeDestroy() {},
methods: {},
};
</script>
<style lang="less" scoped>
.zp-dialog__wrapper {
/deep/ .el-dialog {
.el-dialog__body {
box-sizing: border-box;
max-height: calc(~"85vh - 112px");
overflow-y: auto;
}
}
}
</style>
<template>
<div class="zp-single-img-upload__container">
<el-upload
ref="upload"
:action="action"
:accept="accept"
:file-list="fileList"
:headers="headers"
:data="data"
list-type="picture-card"
:auto-upload="true"
:on-change="onChange"
:on-success="onSuccess"
:before-upload="beforeUpload"
>
<div ref="triggerWrapper" class="zp-single-img-upload__trigger-wrapper">
<slot v-if="this.$slots['trigger']" name="trigger"></slot>
<i v-else slot="default" class="el-icon-plus"></i>
</div>
<div
class="zp-single-img-upload__img-wrapper"
slot="file"
slot-scope="{ file }"
>
<el-image
ref="zpSingleImg"
class="zp-single-img-upload__img"
v-if="imgUrl"
:src="imgUrl"
:preview-src-list="[imgUrl]"
fit="contain"
></el-image>
<span class="el-upload-list__item-actions" v-if="preview">
<span
class="el-upload-list__item-preview"
@click="handlePictureCardPreview(file)"
>
<i class="el-icon-zoom-in"></i>
</span>
<span class="el-upload-list__item-delete" @click="handleDelete()">
<i class="el-icon-delete"></i>
</span>
</span>
<div class="zp-single-img-upload__reload" @click="handleReload" v-else>
更换图片
</div>
</div>
<div class="el-upload__tip" slot="tip">
<slot v-if="this.$slots['tip']" name="tip"></slot>
<div v-else>支持图片格式:jpg、png</div>
</div>
</el-upload>
</div>
</template>
<script>
import { getToken } from "utils/auth";
export default {
name: "zpSingleImgUpload",
props: {
//图片url
imgUrl: {
type: String,
default: "",
},
//图片上传接口地址
action: {
type: String,
default: "/admin_api/uploadImage",
},
//支持的图片格式
accept: {
type: String,
default: "image/*",
},
//文件最大大小(M)
maxSize: {
type: Number,
default: 6,
},
//文件存储是否公有(公有存储读取的时候不需要签名,敏感文件私有,否则公有)
public: {
type: Boolean,
default: false,
},
//是否可以预览图片
preview: {
type: Boolean,
default: false,
},
},
model: {
prop: "imgUrl",
event: "input",
},
data() {
return {
headers: {}, //请求头
data: {}, //请求额外参数
fileList: [], //文件列表
limitCount: 1, //最多支持个数
};
},
watch: {
imgUrl: {
immediate: true,
handler(val) {
console.log("imgUrl:", val);
if (val) {
this.fileList = [{ url: val }];
} else {
this.fileList = [];
}
},
},
},
computed: {},
created() {
this.headers["Token-Auth"] = getToken();
if (this.public) {
this.data["acl"] = "public-read";
}
},
methods: {
/**
* 上传图片校验
*/
beforeUpload(file) {
let isIMG;
const regList = this.accept.split(",");
if (this.accept === "image/*") {
isIMG = /image/i.test(file.type);
} else {
isIMG = regList.includes(file.type); // 是否符合类型
}
const isLt6M = file.size / 1024 / 1024 < this.maxSize;
if (!isIMG) {
this.fileList = [];
this.$message.error("图片格式仅支持png、jpg,请更换图片!");
}
if (!isLt6M) {
this.$message.error(`上传图片大小不能超过${this.maxSize}MB!`);
}
console.log("beforeUpload", file, isIMG && isLt6M);
return isIMG && isLt6M;
},
/**
* 文件修改
*/
onChange(file, fileList) {
console.log("onChange file:", file);
console.log("onChange fileList:", fileList);
const overMaxSize = file.size / 1024 / 1024 < this.maxSize;
if (overMaxSize && fileList.length > 0) {
this.fileList = [fileList[fileList.length - 1]];
} else {
this.fileList = [];
}
},
/**
* 上传成功
*/
onSuccess(res, file, fileList) {
console.log("onSuccess res:", res);
console.log("onSuccess file:", file);
console.log("onSuccess fileList:", fileList);
const { data } = res;
this.$emit("input", data.file);
},
/**
* 删除
*/
handleDelete() {
console.log("handleDelete");
this.$emit("input", "");
},
/**
* 预览
* @param file
*/
handlePictureCardPreview(file) {
this.$refs.zpSingleImg.clickHandler();
},
handleReload() {
this.$refs.triggerWrapper.click();
},
},
};
</script>
<style lang="less" scoped>
.zp-single-img-upload__container {
display: inline-block;
width: 148px;
margin-right: 16px;
/deep/ .el-upload {
border: 1px dashed #d8dce5;
}
/deep/ .el-upload-list__item {
border: 1px solid #d8dce5;
}
.zp-single-img-upload__img-wrapper {
height: 100%;
.zp-single-img-upload__img {
height: 100%;
width: 100%;
}
}
.zp-single-img-upload__reload {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
text-align: center;
background: rgba(51, 51, 51, 0.8);
color: #fff;
font-size: 12px;
cursor: pointer;
}
/deep/ .el-upload-list {
position: absolute;
}
}
</style>
<template>
<a
:href="githubLink"
target="_blank"
class="github-corner"
aria-label="View source on Github"
>
<svg
width="80"
height="80"
viewBox="0 0 250 250"
style=""
aria-hidden="true"
>
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path
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"
fill="currentColor"
style="transform-origin: 130px 106px"
class="octo-arm"
></path>
<path
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"
fill="currentColor"
class="octo-body"
></path>
</svg>
</a>
</template>
<script>
export default {
name: "Github",
props: {
githubLink: {
type: String,
default: null,
},
},
};
</script>
<style lang="less" scoped>
.github-corner {
fill: #4ab7bd;
color: #fff;
position: absolute;
top: 50px;
border: 0;
right: 0;
&:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0);
}
20%,
60% {
transform: rotate(-25deg);
}
40%,
80% {
transform: rotate(10deg);
}
}
</style>
<template>
<div>
<svg
t="1492500959545"
@click="toggleClick"
class="wscn-icon hamburger"
:class="{ 'is-active': isActive }"
style=""
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1691"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
>
<path
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"
p-id="1692"
></path>
<path
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"
p-id="1693"
></path>
<path
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"
p-id="1694"
></path>
</svg>
</div>
</template>
<script>
export default {
name: "hamburger",
props: {
isActive: {
type: Boolean,
default: false,
},
toggleClick: {
type: Function,
default: null,
},
},
};
</script>
<style scoped>
.hamburger {
display: inline-block;
cursor: pointer;
width: 20px;
height: 20px;
transform: rotate(0deg);
transition: 0.38s;
transform-origin: 50% 50%;
}
.hamburger.is-active {
transform: rotate(90deg);
}
</style>
// 来源
export const sources = [
{ name: "原创", id: 1 },
{ name: "转载", id: 2 },
{ name: "翻译", id: 3 },
];
/**
* 时间日期格式化
* 用法 formatTime(new Date(), 'yyyy-MM-dd hh:mm:ss')
* @param time
* @param fmt
*/
export function formatTime(time, fmt) {
time = parseInt(time);
if (!time) {
return "";
}
const date = new Date(time);
let o = {
"M+": date.getMonth() + 1, // 月份
"d+": date.getDate(), // 日
"h+": date.getHours(), // 小时
"m+": date.getMinutes(), // 分
"s+": date.getSeconds(), // 秒
"q+": Math.floor((date.getMonth() + 3) / 3), // 季度
S: date.getMilliseconds(), // 毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
(date.getFullYear() + "").substr(4 - RegExp.$1.length)
);
}
for (let k in o) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length)
);
}
}
return fmt;
}
import Vue from "vue";
import App from "./App.vue";
import router from "./router/permission";
import store from "./store";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
import "./styles/index.less";
import pageInfoMixin from "src/mixins/pageInfo";
import "./utils/config/iconfont";
import "src/components/common/index.js";
import * as filters from "./filters";
Object.keys(filters).forEach((key) => {
Vue.filter(key, filters[key]);
});
Vue.mixin(pageInfoMixin);
Vue.use(ElementUI);
new Vue({
el: "#app",
router,
store,
template: "<App/>",
components: { App },
});
/**
* 1、先引入 pageInfo 的mixin文件,注入mixin
* 2、给 requestPageData 方法赋值页面翻页查询方法
*/
export default {
data() {
return {
requestPageData: null, // 每个table组建的数据源获取方法
pageInfo: {
total: 0,
pageNum: 1,
pageSize: 10,
},
};
},
watch: {
"pageInfo.pageNum"() {
this.requestPageData && this.requestPageData();
},
"pageInfo.pageSize"() {
this.requestPageData && this.requestPageData();
},
},
methods: {
handleCurrentChange(page) {
this.pageInfo.pageNum = page;
},
handleSizeChange(val) {
this.pageInfo.pageSize = val;
},
},
};
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
// 解决同一页面,参数不同的路由报错
const VueRouterPush = Router.prototype.push;
Router.prototype.push = function push(to) {
return VueRouterPush.call(this, to).catch((err) => err);
};
/**
* 加载模块
* @param {string | Component} component 路径或组件
* @param {boolean} lazy 是否懒加载
* @returns {Function | object} 懒加载方法或组件对象
*/
const loadComponent = (component, lazy = true) =>
lazy ? () => import(`views/${component}.vue`) : component;
export const constantRouterMap = [
{
path: "/login",
name: "登录",
component: loadComponent("Login/index"),
hidden: true,
},
{
path: "/",
name: "首页",
component: loadComponent("Layout/index"),
redirect: "/home",
icon: "home",
children: [
{ path: "home", component: loadComponent("Home/index"), name: "首页" },
],
},
];
export const asyncRouterMap = [
{
path: "/permission",
name: "权限管理",
meta: { role: ["admin"] },
component: loadComponent("Layout/index"),
redirect: "/permission/list",
requireAuth: true, // 是否需要登录
dropdown: true,
icon: "authority",
children: [
{
path: "list",
component: loadComponent("Permission/list"),
name: "管理员列表",
},
{
path: "add",
component: loadComponent("Permission/add"),
name: "添加管理员",
},
],
},
{
path: "/article",
name: "文章管理",
component: loadComponent("Layout/index"),
redirect: "/article/list",
dropdown: true,
icon: "article",
children: [
{
path: "list",
component: loadComponent("Article/list"),
name: "文章列表",
},
{
path: "edit",
component: loadComponent("Article/edit"),
name: "文章编辑",
hidden: true,
},
{
path: "add",
component: loadComponent("Article/add"),
name: "添加文章",
},
],
},
{
path: "/label",
name: "标签管理",
component: loadComponent("Layout/index"),
redirect: "/label/list",
dropdown: true,
icon: "label",
children: [
{
path: "list",
component: loadComponent("Label/list"),
name: "标签列表",
},
{
path: "add",
component: loadComponent("Label/add"),
name: "添加标签",
},
],
},
{
path: "/message",
name: "留言管理",
component: loadComponent("Layout/index"),
redirect: "/message/list",
dropdown: true,
icon: "message",
children: [
{
path: "list",
component: loadComponent("Message/list"),
name: "留言列表",
},
{
path: "reply",
component: loadComponent("Message/replyList"),
name: "回复列表",
},
],
},
];
export const router = new Router({
// mode: 'history',
routes: constantRouterMap,
});
import store from "../store";
import { getToken } from "src/utils/auth";
import { router } from "./index";
import NProgress from "nprogress"; // Progress 进度条
import "nprogress/nprogress.css"; // Progress 进度条样式
router.beforeEach((to, from, next) => {
NProgress.start();
if (getToken()) {
if (!store.state.user.roles) {
// 重新拉取用户信息
store.dispatch("getUserInfo").then((res) => {
// 如果token过期,则需重新登录
if (res.code === 401) {
next("/login");
} else {
let roles = res.data.roles;
store.dispatch("setRoutes", { roles }).then(() => {
// 根据权限动态添加路由
router.addRoutes(store.state.permission.addRouters);
next({ ...to }); // hash模式 确保路由加载完成
});
}
});
} else {
next();
}
} else {
to.path === "/login" ? next() : next("/login");
}
});
router.afterEach((to, from) => {
document.title = to.name;
NProgress.done();
});
export default router;
const getters = {
userName: (state) => state.user.username,
userList: (state) => state.user.list,
userTotal: (state) => state.user.total,
};
export default getters;
import Vue from "vue";
import Vuex from "vuex";
import getters from "./getters";
import app from "./modules/app";
import user from "./modules/user";
import permission from "./modules/permission";
Vue.use(Vuex);
const store = new Vuex.Store({
modules: {
app,
user,
permission,
},
getters,
});
export default store;
import { Local } from "src/utils/storage";
const app = {
state: {
slideBar: {
opened: Local.get("slideBarStatus"),
},
tagViews: JSON.parse(Local.get("tagViews")) || [],
is_add_router: false,
},
mutations: {
TOGGLE_SIDEBAR(state) {
if (state.slideBar.opened) {
Local.set("slideBarStatus", false);
} else {
Local.set("slideBarStatus", true);
}
state.slideBar.opened = !state.slideBar.opened;
},
ADD_TAGVIEW(state, tag) {
if (state.tagViews.some((v) => v.name === tag.name)) return;
state.tagViews.push({ name: tag.name, path: tag.path });
Local.set("tagViews", JSON.stringify(state.tagViews));
},
DEL_TAGVIEW(state, tag) {
let index;
for (let [i, v] of state.tagViews.entries()) {
if (v.name === tag.name) index = i;
}
state.tagViews.splice(index, 1);
Local.set("tagViews", JSON.stringify(state.tagViews));
},
},
actions: {
toggleSideBar({ commit }) {
commit("TOGGLE_SIDEBAR");
},
addTagView({ commit }, tag) {
commit("ADD_TAGVIEW", tag);
},
delTagView({ commit }, tag) {
commit("DEL_TAGVIEW", tag);
},
},
};
export default app;
import { constantRouterMap, asyncRouterMap } from "src/router";
/**
* 通过meta.role判断是否与当前用户权限匹配
* @param role
* @param route
*/
const hasPermission = (roles, route) => {
if (route.meta && route.meta.role) {
return roles.some((role) => route.meta.role.indexOf(role) >= 0);
} else {
return true;
}
};
/**
* 递归过滤异步路由表,返回符合用户角色权限的路由表
* @param asyncRouterMap
* @param role
*/
const filterAsyncRouter = (asyncRouterMap, roles) => {
const accessedRouters = asyncRouterMap.filter((route) => {
if (hasPermission(roles, route)) {
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, roles);
}
return true;
}
return false;
});
return accessedRouters;
};
const permission = {
state: {
routes: constantRouterMap.concat(asyncRouterMap),
addRouters: [],
},
mutations: {
SETROUTES(state, routers) {
state.addRouters = routers;
state.routes = constantRouterMap.concat(routers);
},
},
actions: {
setRoutes({ commit }, info) {
return new Promise((resolve, reject) => {
let { roles } = info;
let accessedRouters = [];
if (roles.indexOf("admin") >= 0) {
accessedRouters = asyncRouterMap;
} else {
accessedRouters = filterAsyncRouter(asyncRouterMap, roles);
}
commit("SETROUTES", accessedRouters);
resolve();
});
},
},
};
export default permission;
import axios from "src/utils/request";
import { getToken } from "src/utils/auth";
import md5 from "js-md5";
const user = {
state: {
list: [],
total: 0,
username: "",
roles: null,
token: getToken(),
otherList: [],
},
mutations: {
SET_TOKEN(state, token) {
state.token = token;
},
SET_USERINFO(state, info) {
state.username = info.username;
state.roles = info.roles;
},
USERLIST(state, data) {
state.list = data.list;
state.total = data.list.length || 0;
},
GET_INFOLIST(state, data) {
state.otherList = data;
},
CLEARINFO(state) {
state.username = "";
state.roles = null;
},
},
actions: {
clearInfo({ commit }) {
commit("CLEARINFO");
},
userLogin({ state, commit }, info) {
let { username, pwd } = info;
return new Promise((resolve, reject) => {
axios
.post("/user/login", {
username: username,
pwd: md5(pwd),
})
.then((res) => {
state.token = getToken();
resolve(res);
})
.catch((err) => {
reject(err);
});
});
},
getUserInfo({ state, commit }) {
return new Promise((resolve, reject) => {
axios
.get("/user/info", {
token: state.token,
})
.then((res) => {
commit("SET_USERINFO", res.data);
resolve(res);
})
.catch((err) => {
// console.log(err)
reject(err);
});
});
},
getUserList({ commit }, params) {
return new Promise((resolve, reject) => {
axios
.get("/user/list", params)
.then((res) => {
commit("USERLIST", res.data);
resolve(res);
})
.catch((err) => {
// console.log(err)
reject(err);
});
});
},
addUser({ commit }, info) {
info.pwd = md5(info.pwd);
return new Promise((resolve, reject) => {
axios
.post("/user/add", info)
.then((res) => {
resolve(res);
})
.catch((err) => {
reject(err);
});
});
},
delUser({ commit }, id) {
return new Promise((resolve, reject) => {
axios
.get("/user/del", { id: id })
.then((res) => {
resolve(res);
})
.catch((err) => {
reject(err);
});
});
},
updateUser({ commit }, info) {
info.pwd = md5(info.pwd);
info.old_pwd = md5(info.old_pwd);
return new Promise((resolve, reject) => {
axios
.post("/user/update", info)
.then((res) => {
resolve(res);
})
.catch((err) => {
reject(err);
});
});
},
},
};
export default user;
@import "./less/reset.less";
@import "./less/init.less";
@import "./less/element-ui.less";
@bg-color: #324157;
@font-color: #bfcbd9;
@font-hover-color: #48576a;
@active-font-color: #409EFF;
@submenu-color: #1f2d3d;
.el-menu-vertical {
background-color: @bg-color;
.el-submenu__title {
color: @font-color;
background-color: @bg-color;
}
.el-submenu .el-menu {
background-color: @submenu-color;
.el-menu-item {
background: transparent;
}
}
.el-menu-item {
color: @font-color;
background: @bg-color;
}
.el-menu-item:hover,
.el-submenu__title:hover {
background-color: @font-hover-color;
}
.el-menu-item.is-active {
color: @active-font-color;
background-color: transparent;
}
}
.music-cover-uploader {
.el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
}
.el-breadcrumb__inner, .el-breadcrumb__inner a {
color: #48576a;
font-weight: 400;
}
.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 {
color: #97a8be;
cursor: text;
font-weight: 400;
}
\ No newline at end of file
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-thumb {
opacity: 0.8;
background: #ddd;
border-radius: 4px;
transition: all 0.5s;
}
/* normalize.css */
html {
line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
body {
margin: 0;
}
article,
aside,
footer,
header,
nav,
section {
display: block;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
figcaption,
figure,
main {
/* 1 */
display: block;
}
figure {
margin: 1em 40px;
}
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
b,
strong {
font-weight: inherit;
}
b,
strong {
font-weight: bolder;
}
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
dfn {
font-style: italic;
}
mark {
background-color: #ff0;
color: @mainColor;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
audio,
video {
display: inline-block;
}
audio:not([controls]) {
display: none;
height: 0;
}
img {
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
button,
input {
/* 1 */
overflow: visible;
}
button,
select {
/* 1 */
text-transform: none;
}
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
fieldset {
padding: 0.35em 0.75em 0.625em;
}
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
textarea {
overflow: auto;
}
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
details, /* 1 */
menu {
display: block;
}
summary {
display: list-item;
}
canvas {
display: inline-block;
}
template {
display: none;
}
[hidden] {
display: none;
}
/* reset */
* {
box-sizing: border-box;
}
html,
body {
font: Oswald, "Open Sans", Helvetica, Arial, sans-serif;
}
/* 禁止长按链接与图片弹出菜单 */
a,
img {
-webkit-touch-callout: none;
}
/*ios android去除自带阴影的样式*/
a,
input {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
input[type="text"] {
-webkit-appearance: none;
}
html,
body,
h1,
h2,
h3,
h4,
h5,
h6,
div,
dl,
dt,
dd,
ul,
ol,
li,
p,
blockquote,
pre,
hr,
figure,
table,
caption,
th,
td,
form,
fieldset,
legend,
input,
button,
textarea,
menu {
margin: 0;
padding: 0;
}
header,
footer,
section,
article,
aside,
nav,
hgroup,
address,
figure,
figcaption,
menu,
details {
display: block;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
caption,
th {
text-align: left;
font-weight: normal;
}
html,
body,
fieldset,
img,
iframe,
abbr {
border: 0;
}
i,
cite,
em,
var,
address,
dfn {
font-style: normal;
}
[hidefocus],
summary {
outline: 0;
}
li {
list-style: none;
}
h1,
h2,
h3,
h4,
h5,
h6,
small {
font-size: 100%;
}
sup,
sub {
font-size: 83%;
}
pre,
code,
kbd,
samp {
font-family: inherit;
}
q:before,
q:after {
content: none;
}
textarea {
overflow: auto;
resize: none;
}
label,
summary {
cursor: default;
}
a,
button {
cursor: pointer;
}
h1,
h2,
h3,
h4,
h5,
h6,
em,
strong,
b {
font-weight: bold;
}
del,
ins,
u,
s,
a,
a:hover {
text-decoration: none;
}
import { Cookie } from "./storage";
const TokenKey = "Token-Auth";
export function getToken() {
return Cookie.get(TokenKey);
}
export function setToken(token) {
return Cookie.set(TokenKey, token);
}
export function removeToken() {
return Cookie.remove(TokenKey);
}
!(function(c) {
var l,
t,
a,
e,
o,
i =
'<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>',
n = (n = document.getElementsByTagName("script"))[
n.length - 1
].getAttribute("data-injectcss"),
h = function(c, l) {
l.parentNode.insertBefore(c, l);
};
if (n && !c.__iconfont__svg__cssinject__) {
c.__iconfont__svg__cssinject__ = !0;
try {
document.write(
"<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>"
);
} catch (c) {
console && console.log(c);
}
}
function s() {
o || ((o = !0), a());
}
function d() {
try {
e.documentElement.doScroll("left");
} catch (c) {
return void setTimeout(d, 50);
}
s();
}
(l = function() {
var c, l;
((l = document.createElement("div")).innerHTML = i),
(i = null),
(c = l.getElementsByTagName("svg")[0]) &&
(c.setAttribute("aria-hidden", "true"),
(c.style.position = "absolute"),
(c.style.width = 0),
(c.style.height = 0),
(c.style.overflow = "hidden"),
(l = c),
(c = document.body).firstChild ? h(l, c.firstChild) : c.appendChild(l));
}),
document.addEventListener
? ~["complete", "loaded", "interactive"].indexOf(document.readyState)
? setTimeout(l, 0)
: ((t = function() {
document.removeEventListener("DOMContentLoaded", t, !1), l();
}),
document.addEventListener("DOMContentLoaded", t, !1))
: document.attachEvent &&
((a = l),
(e = c.document),
(o = !1),
d(),
(e.onreadystatechange = function() {
"complete" == e.readyState && ((e.onreadystatechange = null), s());
}));
})(window);
/*
列表表格里 固定宽度 栏
*/
const TABLE_FIX_WIDTH_PHONE = 'phone'; // 手机号
const TABLE_FIX_WIDTH_USERNAME = 'username'; // 名字
const TABLE_FIX_WIDTH_ID_CARD = 'idCard'; // 身份证
const TABLE_FIX_WIDTH_BANK_CARD = 'bankCard'; // 银行卡号码
const TABLE_FIX_WIDTH_LABEL = 'label'; // 标签
const TABLE_FIX_WIDTH_TIME = 'datetime'; // 时间
const TABLE_FIX_WIDTH_DATE = 'date'; // 日期
const tableFixWidths = {
[TABLE_FIX_WIDTH_PHONE]: 120,
[TABLE_FIX_WIDTH_USERNAME]: 88,
[TABLE_FIX_WIDTH_LABEL]: 88,
[TABLE_FIX_WIDTH_TIME]: 168,
[TABLE_FIX_WIDTH_DATE]: 120,
[TABLE_FIX_WIDTH_ID_CARD]: 180,
[TABLE_FIX_WIDTH_BANK_CARD]: 190,
};
export {
TABLE_FIX_WIDTH_PHONE,
TABLE_FIX_WIDTH_USERNAME,
TABLE_FIX_WIDTH_LABEL,
TABLE_FIX_WIDTH_TIME,
TABLE_FIX_WIDTH_DATE,
TABLE_FIX_WIDTH_ID_CARD,
TABLE_FIX_WIDTH_BANK_CARD,
tableFixWidths,
};
import axios from 'axios';
import qs from 'qs';
import { Message } from 'element-ui';
import { removeToken } from './auth';
axios.defaults.withCredentials = true;
// 发送时
axios.interceptors.request.use(
(config) => config,
(err) => Promise.reject(err)
);
// 响应时
axios.interceptors.response.use(
(response) => response,
(err) => Promise.resolve(err.response)
);
// 检查状态码
function checkStatus(res) {
if (res.status === 200 || res.status === 304) {
return res.data;
}
// token过期清掉
if (res.status === 401) {
removeToken();
Message({
message: res.data,
type: 'error',
duration: 2 * 1000,
});
return {
code: 401,
msg: res.data || res.statusText,
data: res.data,
};
}
return {
code: 0,
msg: res.data.msg || res.statusText,
data: res.statusText,
};
}
// 检查CODE值
function checkCode(res) {
if (res.code === 0) {
Message({
message: res.msg,
type: 'error',
duration: 2 * 1000,
});
throw new Error(res.msg);
}
return res;
}
const prefix = '/admin_api';
export default {
get(url, params) {
if (!url) return;
return axios({
method: 'get',
url: prefix + url,
params,
timeout: 30000,
})
.then(checkStatus)
.then(checkCode);
},
post(url, data) {
if (!url) return;
return axios({
method: 'post',
url: prefix + url,
data: qs.stringify(data),
timeout: 30000,
})
.then(checkStatus)
.then(checkCode);
},
postFile(url, data) {
if (!url) return;
return axios({
method: 'post',
url: prefix + url,
data,
})
.then(checkStatus)
.then(checkCode);
},
};
const ls = window.localStorage;
const ss = window.sessionStorage;
export const Cookie = {
get(key) {
let arr = document.cookie.split('; ');
for (let i = 0; i < arr.length; i++) {
let arr2 = arr[i].trim().split('=');
if (arr2[0] == key) {
return arr2[1];
}
}
return '';
},
set(key, value, day) {
let setting = arguments[0];
if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object') {
for (let i in setting) {
let oDate = new Date();
oDate.setDate(oDate.getDate() + day);
document.cookie = i + '=' + setting[i] + ';expires=' + oDate;
}
} else {
let oDate = new Date();
oDate.setDate(oDate.getDate() + day);
document.cookie = key + '=' + value + ';expires=' + oDate;
}
},
remove(key) {
this.set(key, 1, -1);
},
};
export const Local = {
get(key) {
if (key) return JSON.parse(ls.getItem(key));
return null;
},
set(key, val) {
const setting = arguments[0];
if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object') {
for (const i in setting) {
ls.setItem(i, JSON.stringify(setting[i]));
}
} else {
ls.setItem(key, JSON.stringify(val));
}
},
remove(key) {
ls.removeItem(key);
},
clear() {
ls.clear();
},
};
export const Session = {
get(key) {
if (key) return JSON.parse(ss.getItem(key));
return null;
},
set(key, val) {
const setting = arguments[0];
if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object') {
for (const i in setting) {
ss.setItem(i, JSON.stringify(setting[i]));
}
} else {
ss.setItem(key, JSON.stringify(val));
}
},
remove(key) {
ss.removeItem(key);
},
clear() {
ss.clear();
},
};
<template>
<div class="article-add">
<zp-page-edit :back="true" @back="$router.back()">
<div slot="header">
{{ header }}
</div>
<el-form
:model="info"
:rules="rules"
ref="form"
label-width="100px"
class="form"
>
<el-form-item label="博客类型:" prop="type">
<el-select
v-model="info.type"
multiple
clearable
placeholder="请选择博客类型"
class="block"
>
<el-option
v-for="item in labelList"
:key="item.label"
:label="item.label"
:value="item.label"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="文章标题:" prop="title">
<el-input type="text" v-model="info.title"></el-input>
</el-form-item>
<el-form-item label="文章描述:" prop="desc">
<el-input type="textarea" v-model="info.desc"></el-input>
</el-form-item>
<el-form-item label="文章封面:" prop="fileCoverImgUrl">
<zp-single-img-upload v-model="info.fileCoverImgUrl" :public="true">
</zp-single-img-upload>
</el-form-item>
<el-form-item label="文章内容:" prop="markdown" class="markdown">
<Markdown v-model="info.markdown"></Markdown>
</el-form-item>
<el-form-item label="级别:" prop="album">
<el-select
v-model="info.level"
placeholder="请选择级别"
class="block"
>
<el-option
v-for="item in [1, 2, 3, 4, 5, 6]"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="来源:" prop="source">
<el-select
v-model="info.source"
placeholder="请选择来源"
class="block"
>
<el-option
v-for="item in sources"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="Github:" prop="github">
<el-input type="text" v-model="info.github"></el-input>
</el-form-item>
<el-form-item label="Auth:" prop="auth">
<el-input type="text" v-model="info.auth"></el-input>
</el-form-item>
<el-form-item label="是否可见:" prop="isVisible" class="left-item">
<el-switch v-model="info.isVisible"></el-switch>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleSubmit('form')"
:loading="loading"
>立即创建</el-button
>
</el-form-item>
</el-form>
</zp-page-edit>
</div>
</template>
<script>
import { sources } from "src/constant/classify";
import { apiGetLabelList } from "src/api/label";
import { apiAddBlog } from "src/api/blog";
import Markdown from "components/markdown";
export default {
name: "articleAdd",
components: { Markdown },
computed: {},
data() {
return {
sources,
header: "添加文章",
labelList: [],
info: {
type: ["JavaScript"],
title: "",
desc: "",
fileCoverImgUrl: "",
html: "",
markdown: "",
level: 1,
source: 1,
github: "",
auth: "",
isVisible: true,
releaseTime: new Date().getTime() + "",
},
loading: false,
rules: {
type: [
{
required: true,
message: "请选择至少选择一个文章类型",
trigger: "change",
type: "array",
},
],
title: [{ required: true, message: "请填写文章标题", trigger: "blur" }],
desc: [{ required: true, message: "请填写文章描述", trigger: "blur" }],
isVisible: [
{
required: true,
message: "请选择",
trigger: "change",
type: "boolean",
},
],
fileCoverImgUrl: [
{ required: true, message: "请上传文章封面", trigger: "change" },
],
},
};
},
created() {
this.getLabelList();
},
methods: {
handleSubmit(formName) {
this.loading = true;
if (!this.info.markdown) {
this.$message.warning("请填写文章内容");
this.loading = false;
return;
}
this.$refs[formName].validate((valid) => {
if (valid) {
this.info.html = this.info.markdown;
this.handleAddBlog();
} else {
this.loading = false;
return false;
}
});
},
handleAddBlog() {
return apiAddBlog(this.info)
.then((res) => {
this.$message.success("新增文章成功");
this.$router.push("/article/list");
})
.catch((err) => {
console.log(err);
this.$message.info("新增文章失败");
})
.finally(() => {
this.loading = false;
});
},
getLabelList() {
let params = {
pageindex: 1,
pagesize: 50,
};
return apiGetLabelList(params)
.then((res) => {
let { list } = res.data;
this.labelList = list;
})
.catch((err) => {
console.log(err);
})
.finally(() => {
});
},
},
};
</script>
<style lang="less" scoped>
/deep/ .markdown {
.el-form-item__content {
width: 1400px;
}
}
</style>
\ No newline at end of file
<template>
<div class="article-edit" v-show="hasLoad">
<zp-page-edit :back="true" @back="$router.back()">
<div slot="header">
{{ header }}
</div>
<el-form
:model="info"
:rules="rules"
ref="form"
label-width="100px"
class="form"
>
<el-form-item label="文章类型:" prop="type">
<el-select
v-model="info.type"
multiple
clearable
placeholder="请选择文章类型"
class="block"
>
<el-option
v-for="item in labelList"
:key="item.label"
:label="item.label"
:value="item.label"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="文章标题:" prop="title">
<el-input type="text" v-model="info.title"></el-input>
</el-form-item>
<el-form-item label="文章描述:" prop="desc">
<el-input type="textarea" v-model="info.desc"></el-input>
</el-form-item>
<el-form-item label="文章封面:" prop="fileCoverImgUrl">
<zp-single-img-upload v-model="info.fileCoverImgUrl" :public="true">
</zp-single-img-upload>
</el-form-item>
<el-form-item
label="文章内容:"
prop="markdown"
v-if="info.markdown"
class="markdown"
>
<Markdown v-model="info.markdown"></Markdown>
</el-form-item>
<el-form-item label="级别:" prop="album">
<el-select
v-model="info.level"
placeholder="请选择级别"
class="block"
>
<el-option
v-for="item in [1, 2, 3, 4, 5, 6]"
:key="item"
:label="item"
:value="item"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="来源:" prop="source">
<el-select
v-model="info.source"
placeholder="请选择来源"
class="block"
>
<el-option
v-for="item in sources"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="Github:" prop="github">
<el-input type="text" v-model="info.github"></el-input>
</el-form-item>
<el-form-item label="Auth:" prop="auth">
<el-input type="text" v-model="info.auth"></el-input>
</el-form-item>
<el-form-item label="是否可见:" prop="isVisible" class="left-item">
<el-switch v-model="info.isVisible"></el-switch>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleSubmit('form')"
:loading="loading"
>更新</el-button
>
</el-form-item>
</el-form>
</zp-page-edit>
</div>
</template>
<script>
import { apiUpdateBlog, apiGetBlogDetail } from "src/api/blog";
import { apiGetLabelList } from "src/api/label";
import { sources } from "src/constant/classify";
import Markdown from "components/markdown";
export default {
name: "articleEdit",
components: { Markdown },
props: {},
computed: {},
data() {
return {
sources,
header: "文章编辑",
loading: false,
hasLoad: false,
labelList: [],
info: {},
id: "", // 文章id
rules: {
type: [
{
required: true,
message: "请选择至少选择一个文章类型",
trigger: "change",
type: "array",
},
],
title: [{ required: true, message: "请填写文章标题", trigger: "blur" }],
desc: [{ required: true, message: "请填写文章描述", trigger: "blur" }],
markdown: [
{ required: true, message: "请填写文章内容", trigger: "blur" },
],
fileCoverImgUrl: [
{ required: true, message: "请上传文章封面", trigger: "change" },
],
},
};
},
watch: {},
async created() {
this.id = this.$route.query["id"];
await this.getLabelList();
await this.getBlogDetail();
},
mounted() { },
beforeDestroy() { },
methods: {
handleSubmit(formName) {
this.loading = true;
this.$refs[formName].validate((valid) => {
if (valid) {
this.info.releaseTime = new Date().getTime() + "";
this.info.html = this.info.markdown;
this.handleUpdateBlog();
} else {
console.log("error submit!!");
return false;
}
});
},
getBlogDetail() {
return apiGetBlogDetail({ _id: this.id })
.then((res) => {
this.info = res.data;
})
.catch((err) => {
console.log(err);
this.$message.info("获取文章详情失败");
})
.finally(() => {
this.hasLoad = true;
});
},
handleUpdateBlog() {
return apiUpdateBlog(this.info)
.then((res) => {
this.$message.success("修改文章成功");
this.$router.push("/article/list");
})
.catch((err) => {
console.log(err);
this.$message.info("修改文章失败");
})
.finally(() => {
this.loading = false;
});
},
getLabelList() {
let params = {
pageindex: 1,
pagesize: 50,
};
return apiGetLabelList(params)
.then((res) => {
let { list } = res.data;
this.labelList = list;
})
.catch((err) => {
console.log(err);
})
.finally(() => {
});
},
},
};
</script>
<style lang="less" scoped>
/deep/ .markdown {
.el-form-item__content {
width: 1400px;
}
}
</style>
<template>
<div class="article-list">
<zp-page-list>
<template v-slot:header>
<span>文章列表</span>
</template>
<!-- filter start -->
<div slot="filter">
<el-form
ref="searchForm"
class="zp-search-form"
:inline="true"
:model="searchForm"
>
<el-form-item label="关键词:" prop="keyword">
<el-input
placeholder="请输入类型、标题"
v-model="searchForm.keyword"
@keydown.enter.native="getBlogList(true)"
></el-input>
</el-form-item>
<el-button type="primary" @click="getBlogList(true)">
查询
</el-button>
</el-form>
</div>
<!-- filter end -->
<!-- list start -->
<div slot="list">
<zp-table-list
ref="sensitiveBehaviorRecordList"
:loading="listLoading"
:source="blogList"
:count="pageInfo.total"
:columns="columns"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template slot="_id" slot-scope="scope">
<el-button
icon="el-icon-document-copy"
type="primary"
size="mini"
class="clipboardBtn"
:data-clipboard-text="scope.row._id"
@click="handleCopyId"
>复制
</el-button>
</template>
<template slot="isVisible" slot-scope="scope">
{{ scope.row.isVisible ? "是" : "否" }}
</template>
<template slot="fileCoverImgUrl" slot-scope="scope">
<img
:src="scope.row.fileCoverImgUrl"
style="width: 60px; object-fit: contain"
/>
</template>
<template slot="source" slot-scope="scope">
{{
scope.row.source === 1
? "原创"
: scope.row.source === 2
? "转载"
: "翻译"
}}
</template>
<template slot="releaseTime" slot-scope="scope">
{{ scope.row.releaseTime | formatTime("yyyy-MM-dd hh:mm:ss") }}
</template>
<template slot="type" slot-scope="scope">
<el-tag
class="tag"
type="primary"
close-transition
v-for="(tag, index) in scope.row.type"
:key="index"
>{{ tag }}</el-tag
>
</template>
<template slot="operate" slot-scope="scope">
<el-button size="mini" type="primary" @click="handleEdit(scope.row)"
>编辑</el-button
>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row)"
>删除</el-button
>
</template>
</zp-table-list>
</div>
<!-- list end -->
</zp-page-list>
</div>
</template>
<script>
import Clipboard from "clipboard";
import { apiGetBlogList, apiDelBlog } from "src/api/blog";
import { apiDelUploadImg } from "src/api/upload";
export default {
name: "articleList",
components: {},
computed: {},
data() {
return {
listLoading: false,
searchForm: {
keyword: "",
},
blogList: [],
columns: [
{
label: "_id",
prop: "_id",
slot: "_id",
},
{
label: "类型",
prop: "type",
slot: "type",
showTooltip: true,
width: "120",
},
{
label: "标题",
prop: "title",
width: "160",
},
{
label: "描述",
prop: "desc",
width: "160",
showTooltip: true,
},
{
label: "封面",
slot: "fileCoverImgUrl",
prop: "fileCoverImgUrl",
},
{
label: "来源",
prop: "source",
slot: "source",
},
{
label: "级别",
prop: "level",
},
{
label: "发布时间",
prop: "releaseTime",
slot: "releaseTime",
width: "160",
},
{
label: "是否可见",
prop: "isVisible",
slot: "isVisible",
},
{
label: "作者",
prop: "auth",
},
{
label: "浏览量",
prop: "pv",
},
{
label: "点赞数",
prop: "likes",
},
{
label: "评论数",
prop: "comments",
},
{
label: "操作",
slot: "operate",
width: "150",
},
],
};
},
created() {
this.requestPageData = this.getBlogList;
this.getBlogList();
},
mounted() { },
methods: {
handleCopyId() {
let clipboard = new Clipboard(".clipboardBtn");
clipboard.on("success", (e) => {
this.$message.success("复制成功");
clipboard.destroy();
});
clipboard.on("error", (e) => {
this.$message.error("该浏览器不支持自动复制");
clipboard.destroy();
});
},
getBlogList() {
let params = {
keyword: this.searchForm.keyword,
pageindex: this.pageInfo.pageNum,
pagesize: this.pageInfo.pageSize,
};
this.listLoading = true;
return apiGetBlogList(params)
.then((res) => {
let { list, total } = res.data;
this.blogList = list;
this.pageInfo.total = total;
})
.catch((err) => {
this.listLoading = false;
console.log(err);
})
.finally(() => {
this.listLoading = false;
});
},
handleDeleteBlog(id) {
return apiDelBlog({ id })
.then((res) => {
this.$message.success("删除成功");
this.getBlogList(true);
})
.catch((err) => {
console.log(err);
this.$message.info("删除失败");
})
.finally(() => { });
},
// 删除本地图片
handleDeleteImg(fileName) {
return apiDelUploadImg({ fileName })
.then((res) => { })
.catch((err) => {
console.log(err);
});
},
handleDelete(row) {
this.$confirm("此操作将永久删除该数据, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await this.handleDeleteBlog(row._id);
let index = row.fileCoverImgUrl.lastIndexOf('/');
let fileName = row.fileCoverImgUrl.substring(index + 1);
this.handleDeleteImg(fileName);
})
.catch(() => {
this.$message.info("已取消删除");
});
},
handleEdit(row) {
this.$router.push({ path: "edit", query: { id: row._id } });
},
},
};
</script>
<style lang="less" scoped>
.tag {
margin: 0 10px;
}
</style>
<template>
<div class="home-container">home</div>
</template>
<script>
export default {
name: "home",
components: {},
computed: {},
data() {
return {};
},
created() {},
mounted() {},
methods: {},
};
</script>
<style lang="less" scoped>
</style>
<template>
<div class="label-add">
<zp-page-edit :back="true" @back="$router.back()">
<div slot="header">
{{ header }}
</div>
<el-form
:model="info"
:rules="rules"
ref="form"
label-width="100px"
class="form"
>
<el-form-item label="标签:" prop="label">
<el-input type="text" v-model="info.label"></el-input>
</el-form-item>
<el-form-item label="背景色:" prop="bgColor">
<el-input readonly :value="currentColor"></el-input>
</el-form-item>
<el-form-item>
<sketch-picker
v-model="currentColor"
@input="colorValueChange"
></sketch-picker>
</el-form-item>
<el-form-item label="预览:" v-if="info.label">
<div class="label-box" :style="{ backgroundColor: currentColor }">
{{ info.label }}
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleSubmit('form')"
:loading="loading"
>立即创建</el-button
>
</el-form-item>
</el-form>
</zp-page-edit>
</div>
</template>
<script>
import { Sketch } from 'vue-color'
import { apiAddLabel } from "src/api/label";
export default {
name: "permissionAdd",
components: {
'sketch-picker': Sketch
},
data() {
return {
header: "添加标签",
info: {
label: "",
bgColor: "rgba(70, 70, 70, 0.9)",
},
currentColor: 'rgba(70, 70, 70, 0.9)',
loading: false,
rules: {
label: [
{ required: true, message: "请填写标签", trigger: "blur" },
],
bgColor: [{ required: true, message: "请填写背景色", trigger: "blur" }],
},
};
},
methods: {
handleSubmit(formName) {
this.loading = true;
this.$refs[formName].validate(async (valid) => {
if (valid) {
return apiAddLabel(this.info)
.then((res) => {
this.$message.success("新增成功");
this.$router.push("/label/list");
})
.catch((err) => {
console.log(err);
this.$message.info("新增失败");
})
.finally(() => {
this.loading = false;
});
} else {
console.log("error submit!!");
this.loading = false;
return false;
}
});
},
// 颜色值改变事件处理
colorValueChange(fmtObj) {
// 取颜色对象的 rgba 值
const { r, g, b, a } = fmtObj.rgba;
this.currentColor = `rgba(${r}, ${g}, ${b}, ${a})`;
this.info.bgColor = this.currentColor;
}
},
};
</script>
<style lang="less" scoped>
.label-box {
color: #fff;
border-radius: 12px;
font-size: 14px;
text-align: center;
max-width: 150px;
}
</style>
\ No newline at end of file
<template>
<zp-dialog
title="标签编辑"
:visible.sync="dialogTableVisible"
@close="close"
width="480px"
:destroy-on-close="true"
v-bind="$attrs"
v-on="$listeners"
>
<el-form :model="info" :rules="rules" ref="form">
<el-form-item label="标签:" prop="label">
<el-input type="text" v-model="info.label"></el-input>
</el-form-item>
<el-form-item label="背景色:" prop="bgColor">
<el-input readonly :value="currentColor"></el-input>
</el-form-item>
<el-form-item style="margin-left: 80px">
<sketch-picker
v-model="currentColor"
@input="colorValueChange"
></sketch-picker>
</el-form-item>
<el-form-item label="预览:" v-if="info.label">
<div class="label-box" :style="{ backgroundColor: currentColor }">
{{ info.label }}
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button
type="primary"
:loading="loading"
@click="handleSubmit('form')"
>
确定
</el-button>
</div>
</zp-dialog>
</template>
<script>
import { Sketch } from 'vue-color'
import { apiUpdateLabel } from "src/api/label";
export default {
props: ["info"],
components: {
'sketch-picker': Sketch
},
data() {
return {
loading: false,
dialogTableVisible: true,
currentColor: '',
rules: {
label: [
{ required: true, message: "请填写标签名", trigger: "blur" },
],
bgColor: [{ required: true, message: "请填写背景色", trigger: "blur" }],
},
};
},
created() {
this.currentColor = this.info.bgColor;
},
methods: {
// 颜色值改变事件处理
colorValueChange(fmtObj) {
// 取颜色对象的 rgba 值
const { r, g, b, a } = fmtObj.rgba;
this.currentColor = `rgba(${r}, ${g}, ${b}, ${a})`;
this.info.bgColor = this.currentColor;
},
close() {
this.$emit("close");
},
handleSubmit(formName) {
this.loading = true;
this.$refs[formName].validate(async (valid) => {
if (valid) {
return apiUpdateLabel(this.info)
.then((res) => {
this.$message.success("编辑成功");
this.$emit("close");
})
.catch((err) => {
console.log(err);
this.$message.info("编辑失败");
})
.finally(() => {
this.loading = false;
});
} else {
console.log("error submit!!");
this.loading = false;
return false;
}
});
},
},
};
</script>
<style lang="less" scoped>
.el-form {
.el-form-item {
display: flex;
align-items: center;
/deep/ .el-form-item__label {
min-width: 80px;
}
/deep/.el-form-item__content {
margin-left: 0 !important;
width: 260px;
.el-select {
width: 260px;
}
}
}
}
.label-box {
color: #fff;
border-radius: 12px;
font-size: 14px;
text-align: center;
max-width: 150px;
}
</style>
\ No newline at end of file
<template>
<div class="label-list">
<zp-page-list>
<template v-slot:header>
<span>标签列表</span>
</template>
<!-- filter start -->
<div slot="filter">
<el-form
ref="searchForm"
class="zp-search-form"
:inline="true"
:model="searchForm"
>
<el-form-item label="关键词:" prop="keyword">
<el-input
placeholder="请输入标签、背景色"
v-model="searchForm.keyword"
@keydown.enter.native="getLabelList"
></el-input>
</el-form-item>
<el-button type="primary" @click="getLabelList"> 查询 </el-button>
</el-form>
</div>
<!-- filter end -->
<!-- list start -->
<div slot="list">
<zp-table-list
:loading="listLoading"
:source="labelList"
:count="pageInfo.total"
:columns="columns"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template slot="_id" slot-scope="scope">
<el-button
icon="el-icon-document-copy"
type="primary"
size="mini"
class="clipboardBtn"
:data-clipboard-text="scope.row._id"
@click="handleCopyId"
>复制
</el-button>
</template>
<template slot="label" slot-scope="scope">
<div
class="label-box"
:style="{ backgroundColor: scope.row.bgColor }"
>
{{ scope.row.label }}
</div>
</template>
<template slot="operate" slot-scope="scope">
<el-button size="mini" type="primary" @click="handleEdit(scope.row)"
>编辑</el-button
>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row)"
>删除</el-button
>
</template>
</zp-table-list>
</div>
<!-- list end -->
</zp-page-list>
<editDialog
v-if="editShow"
:info="labelInfo"
@close="handleClose"
></editDialog>
</div>
</template>
<script>
import Clipboard from "clipboard";
import editDialog from "./components/editDialog";
import { apiGetLabelList, apiDelLabel } from "src/api/label";
export default {
components: {
editDialog,
},
computed: {
},
data() {
return {
listLoading: false,
editShow: false,
searchForm: {
keyword: "",
},
labelList: [],
columns: [
{
label: "_id",
prop: "_id",
slot: "_id",
},
{
label: "标签",
prop: "label",
slot: "label",
},
{
label: "背景色",
prop: "bgColor",
},
{
label: "操作",
slot: "operate",
width: "150",
},
],
};
},
mounted() {
this.requestPageData = this.getLabelList;
this.getLabelList();
},
methods: {
getLabelList() {
let params = {
keyword: this.searchForm.keyword,
pageindex: this.pageInfo.pageNum,
pagesize: this.pageInfo.pageSize,
};
this.listLoading = true;
return apiGetLabelList(params)
.then((res) => {
let { list, total } = res.data;
this.labelList = list;
this.pageInfo.total = total;
})
.catch((err) => {
this.listLoading = false;
console.log(err);
})
.finally(() => {
this.listLoading = false;
});
},
handleCopyId() {
let clipboard = new Clipboard(".clipboardBtn");
clipboard.on("success", (e) => {
this.$message.success("复制成功");
clipboard.destroy();
});
clipboard.on("error", (e) => {
this.$message.error("该浏览器不支持自动复制");
clipboard.destroy();
});
},
handleClose() {
this.editShow = false;
},
handleDelete(row) {
this.$confirm("此操作将永久删除该数据, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
await this.handleDeleteLabel(row._id);;
} catch (e) {
this.$message.error("删除失败");
console.log(e);
}
})
.catch(() => {
this.$message.info("已取消删除");
});
},
handleDeleteLabel(id) {
return apiDelLabel({ id })
.then((res) => {
this.$message.success("删除成功");
this.getLabelList(true);
})
.catch((err) => {
console.log(err);
this.$message.info("删除失败");
})
.finally(() => { });
},
handleEdit(row) {
this.editShow = true;
this.labelInfo = row;
},
},
};
</script>
<style lang="less" scoped>
.label-box {
color: #fff;
padding: 4px 0;
border-radius: 12px;
font-size: 14px;
text-align: center;
max-width: 150px;
}
</style>
<template>
<div class="sign_out">
<el-dropdown>
<div class="el-dropdown-link">
<Icon name="avatar" class="avatar"></Icon>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>{{ userName }}</el-dropdown-item>
<el-dropdown-item @click.native="removeCookie">退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
import { removeToken } from "../../utils/auth";
import { mapGetters } from "vuex";
export default {
methods: {
removeCookie() {
removeToken();
this.$store.dispatch("clearInfo");
this.$router.push("/login");
},
},
computed: {
...mapGetters(["userName"]),
},
};
</script>
<style lang="less" scoped>
.sign_out {
float: right;
margin-right: 20px;
> * {
display: inline-block;
vertical-align: middle;
}
.avatar {
font-size: 40px;
margin-top: 5px;
color: @highlightColor;
}
img {
width: 40px;
height: 40px;
border-radius: 10px;
margin-top: 5px;
}
.sign_out_icon {
color: #5a5e66;
font-size: 20px;
cursor: pointer;
&:hover {
color: #aaa;
}
}
}
</style>
\ No newline at end of file
<template>
<div class="content-wrapper">
<transition>
<router-view></router-view>
</transition>
</div>
</template>
export { default as NavBar } from './navBar'
export { default as SlideBar } from './slideBar'
export { default as ContentView } from './content'
<template>
<div class="app-wrapper" :class="{hideSidebar: $store.state.app.slideBar.opened}">
<SlideBar class="slidebar-container"></SlideBar>
<div class="main-container">
<NavBar></NavBar>
<ContentView></ContentView>
</div>
</div>
</template>
<script>
import { SlideBar, NavBar, ContentView } from './index'
export default {
name: 'layout',
components: {
SlideBar,
NavBar,
ContentView
}
}
</script>
<style lang="less" scoped>
.app-wrapper {
&.hideSidebar {
.slidebar-container{
width: 64px;
overflow: inherit;
}
.main-container {
margin-left: 64px;
}
}
.slidebar-container {
height: 100%;
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow-y: auto;
transition: width 0.28s ease-out;
}
.main-container {
min-height: 100%;
margin-left: 200px;
transition: margin-left 0.28s ease-out;
}
}
</style>
<template>
<el-breadcrumb class="levelbar-wrapper" separator="/">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="index">
<router-link :to="item.path">{{ item.name }}</router-link>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script>
export default {
created() {
this.getBreadcrumb();
},
data() {
return {
levelList: []
};
},
methods: {
getBreadcrumb() {
let matched = this.$route.matched.filter(item => item.name);
let first = matched[0],
second = matched[1];
if (first && first.name !== '首页' && first.name !== '') {
matched = [{ name: '首页', path: '/' }].concat(matched);
}
if (second && second.name === '首页') {
this.levelList = [second];
} else {
this.levelList = matched;
}
}
},
watch: {
$route() {
this.getBreadcrumb();
}
}
};
</script>
<style lang="less" scoped>
.levelbar-wrapper.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 10px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>
\ No newline at end of file
<template>
<div class="nav-bar-wrapper" separator="/">
<Hamburger
class="hamburger"
:toggleClick="toggleSlideBar"
:isActive="app.slideBar.opened"
></Hamburger>
<Levelbar class="levelbar"></Levelbar>
<TabsView class="tabsview"></TabsView>
<Account></Account>
</div>
</template>
<script>
import { mapState } from "vuex";
import Hamburger from "components/hamburger";
import Levelbar from "./levelbar";
import TabsView from "./tabsView";
import Account from "./account";
export default {
components: {
Hamburger,
Levelbar,
TabsView,
Account,
},
methods: {
toggleSlideBar() {
this.$store.dispatch("toggleSideBar");
},
},
computed: {
...mapState(["app"]),
},
};
</script>
<style lang="less" scoped>
.nav-bar-wrapper {
height: 50px;
// line-height: 50px;
background: #eef1f6;
> * {
display: inline-block;
vertical-align: middle;
}
.hamburger {
line-height: 58px;
width: 40px;
height: 50px;
padding: 0 10px;
}
.levelbar {
font-size: 14px;
line-height: 50px;
margin-left: 10px;
}
.tabsview {
margin-left: 10px;
}
}
</style>
<template>
<div class="silde-bar-wrapper">
<el-menu
class="el-menu-vertical"
:default-active="$route.path"
unique-opened
router
:collapse="$store.state.app.slideBar.opened"
>
<template v-for="item in $store.state.permission.routes">
<el-menu-item
v-if="!item.hidden && !item.dropdown"
:index="
(item.path === '/' ? item.path : item.path + '/') +
item.children[0].path
"
:key="item.path"
>
<Icon :name="item.icon" class="slide-icon"></Icon>
<span slot="title">{{ item.name }}</span>
</el-menu-item>
<el-submenu
v-if="!item.hidden && item.dropdown"
:index="item.path"
:key="item.path"
>
<template slot="title">
<Icon :name="item.icon" class="slide-icon"></Icon>
<span>{{ item.name }}</span>
</template>
<template v-for="child in getSubMenuHiddleList(item.children)">
<el-menu-item
:index="item.path + '/' + child.path"
:key="child.path"
>{{ child.name }}</el-menu-item
>
</template>
</el-submenu>
</template>
</el-menu>
</div>
</template>
<script>
export default {
computed: {
getSubMenuHiddleList() {
return (list) =>
list.filter(item => !item.hidden)
},
},
data() {
return {
}
},
methods: {
}
};
</script>
<style lang="less" scoped>
.silde-bar-wrapper {
.el-menu-vertical:not(.el-menu--collapse) {
width: 200px;
height: 100%;
}
.el-menu-vertical {
height: 100%;
}
.el-menu {
border-right: none;
}
.slide-icon {
width: 24px;
font-size: 20px;
text-align: center;
vertical-align: middle;
}
}
</style>
\ No newline at end of file
<template>
<div class="tabs-wwrapper">
<router-link
class="tab-view"
:to="tag.path"
v-for="tag in getTags"
:key="tag.name"
>
<el-tag
size="small"
closable
:type="getIsActive(tag) ? '' : 'info'"
@close="closeTagView(tag, $event)"
>
{{ tag.name }}
</el-tag>
</router-link>
</div>
</template>
<script>
export default {
computed: {
getTags() {
let tagArr = this.$store.state.app.tagViews;
return tagArr.filter((item) => item.path !== "/article/edit").slice(-6);
},
},
watch: {
$route() {
this.addTagView();
},
},
mounted() {
this.getIsActive();
},
methods: {
getIsActive(tag) {
if (!tag) return;
return tag.path === this.$route.path;
},
closeTagView(tag, e) {
this.$store.dispatch("delTagView", tag);
e.preventDefault();
},
generateRoute() {
if (this.$route.matched[this.$route.matched.length - 1].name) {
return this.$route.matched[this.$route.matched.length - 1];
}
return this.$route.matched[0];
},
addTagView() {
this.$store.dispatch("addTagView", this.generateRoute());
},
},
};
</script>
<style lang="less" scoped>
.tab-view {
margin-left: 10px;
}
</style>
\ No newline at end of file
<template>
<div class="login-wrapper">
<el-form class="login-form">
<h3>博客系统登录</h3>
<el-form-item prop="username">
<Icon name="user" class="icon-user"></Icon>
<el-input
type="text"
placeholder="请输入用户名"
class="username"
v-model="loginInfo.username"
@keydown.enter.native="login"
/>
</el-form-item>
<el-form-item prop="password">
<Icon name="password" class="icon-pwd"></Icon>
<el-input
type="password"
placeholder="请输入密码"
class="pwd"
v-model="loginInfo.pwd"
@keydown.enter.native="login"
/>
</el-form-item>
<el-button type="primary" class="submit" @click="login" :loading="loading"
>登录</el-button
>
</el-form>
</div>
</template>
<script>
export default {
name: "login",
data() {
return {
loading: false,
msg: "",
loginInfo: {
username: "",
pwd: "",
},
};
},
methods: {
async login() {
this.loading = true;
if (!this.loginInfo.username) {
this.msg = "请输入用户名";
} else if (!this.loginInfo.pwd) {
this.msg = "请输入密码";
}
if (this.msg) {
this.$message({
message: this.msg,
type: "warning",
});
this.msg = "";
this.loading = false;
return;
}
try {
await this.$store.dispatch("userLogin", this.loginInfo);
this.$router.push("/article/list");
} catch (e) {
console.log(e);
}
this.loading = false;
},
},
};
</script>
<style lang="less">
.login-wrapper {
width: 100%;
height: 100%;
position: fixed;
background: #2d3a4b;
.login-form {
width: 400px;
padding: 35px;
position: absolute;
left: 0%;
right: 0%;
top: 20%;
margin: auto;
}
.el-form-item {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.1);
border-radius: 5px;
color: #454545;
}
h3 {
font-size: 26px;
color: #fff;
margin-bottom: 50px;
text-align: center;
}
.icon {
color: #889aa4;
vertical-align: middle;
width: 1em;
height: 1em;
display: inline-block;
margin-left: 10px;
}
.icon-user,
.icon-pwd {
width: 1.5em;
height: 1.5em;
margin-left: 8px;
}
input {
background: transparent;
border: 0px;
-webkit-appearance: none;
border-radius: 0px;
padding: 12px 5px 12px 15px;
color: #889aa4;
height: 47px;
vertical-align: middle;
color: #eee;
}
.username input {
padding: 12px 5px 12px 10px;
}
.el-input {
display: inline-block;
height: 47px;
width: 85%;
}
.submit {
width: 100%;
}
}
</style>
\ No newline at end of file
<template>
<div class="message-list">
<zp-page-list>
<template v-slot:header>
<span>留言列表</span>
</template>
<!-- filter start -->
<div slot="filter">
<el-form
ref="searchForm"
class="zp-search-form"
:inline="true"
:model="searchForm"
>
<el-form-item label="关键词:" prop="keyword">
<el-input
placeholder="请输入内容、昵称"
v-model="searchForm.keyword"
@keydown.enter.native="getMessageList(true)"
></el-input>
</el-form-item>
<el-button type="primary" @click="getMessageList(true)">
查询
</el-button>
</el-form>
</div>
<!-- filter end -->
<!-- list start -->
<div slot="list">
<zp-table-list
ref="sensitiveBehaviorRecordList"
:loading="listLoading"
:source="messageList"
:count="pageInfo.total"
:columns="columns"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template slot="_id" slot-scope="scope">
<el-button
icon="el-icon-document-copy"
type="primary"
size="mini"
class="clipboardBtn"
:data-clipboard-text="scope.row._id"
@click="handleCopyId"
>复制
</el-button>
</template>
<template slot="content" slot-scope="scope">
<div v-html="scope.row.content"></div>
</template>
<template slot="createTime" slot-scope="scope">
{{ scope.row.createTime | formatTime("yyyy-MM-dd hh:mm:ss") }}
</template>
<template slot="operate" slot-scope="scope">
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row)"
>删除</el-button
>
</template>
</zp-table-list>
</div>
<!-- list end -->
</zp-page-list>
</div>
</template>
<script>
import Clipboard from "clipboard";
import { apiGetMessageList, apiDelMessage } from "src/api/message";
export default {
name: "messageList",
components: {},
computed: {},
data() {
return {
listLoading: false,
searchForm: {
keyword: "",
},
messageList: [],
columns: [
{
label: "_id",
prop: "_id",
slot: "_id",
},
{
label: "昵称",
prop: "nickname",
},
{
label: "头像颜色",
prop: "headerColor",
},
{
label: "评论内容",
prop: "content",
slot: "content",
showTooltip: true,
},
{
label: "时间",
prop: "createTime",
slot: "createTime",
},
{
label: "点赞数",
prop: "likes",
},
{
label: "操作",
slot: "operate",
width: "150",
},
],
};
},
created() {
this.requestPageData = this.getMessageList;
this.getMessageList();
},
mounted() { },
methods: {
getMessageList() {
let params = {
keyword: this.searchForm.keyword,
pageindex: this.pageInfo.pageNum,
pagesize: this.pageInfo.pageSize,
};
this.listLoading = true;
return apiGetMessageList(params)
.then((res) => {
let { list, total } = res.data;
this.messageList = list;
this.pageInfo.total = total;
})
.catch((err) => {
this.listLoading = false;
console.log(err);
})
.finally(() => {
this.listLoading = false;
this.hasLoad = true;
});
},
handleCopyId() {
let clipboard = new Clipboard(".clipboardBtn");
clipboard.on("success", (e) => {
this.$message.success("复制成功");
clipboard.destroy();
});
clipboard.on("error", (e) => {
this.$message.error("该浏览器不支持自动复制");
clipboard.destroy();
});
},
handleDeleteMessage(id) {
return apiDelMessage({ id })
.then((res) => {
this.$message.success("删除成功");
this.getMessageList(true);
})
.catch((err) => {
console.log(err);
this.$message.info("删除失败");
})
.finally(() => { });
},
handleDelete(row) {
this.$confirm("此操作将永久删除该数据, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.handleDeleteMessage(row._id);
})
.catch(() => {
this.$message.info("已取消删除");
});
},
},
};
</script>
<style lang="less" scoped>
</style>
<template>
<div class="reply-list">
<zp-page-list>
<template v-slot:header>
<span>回复列表</span>
</template>
<!-- list start -->
<div slot="list">
<zp-table-list
ref="sensitiveBehaviorRecordList"
:loading="listLoading"
:source="replyList"
:count="pageInfo.total"
:columns="columns"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template slot="_id" slot-scope="scope">
<el-button
icon="el-icon-document-copy"
type="primary"
size="mini"
class="clipboardBtn"
:data-clipboard-text="scope.row._id"
@click="handleCopyId"
>复制
</el-button>
</template>
<template slot="replyContent" slot-scope="scope">
<div v-html="scope.row.replyContent"></div>
</template>
<template slot="replyTime" slot-scope="scope">
{{ scope.row.replyTime | formatTime("yyyy-MM-dd hh:mm:ss") }}
</template>
<template slot="operate" slot-scope="scope">
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row)"
>删除</el-button
>
</template>
</zp-table-list>
</div>
<!-- list end -->
</zp-page-list>
</div>
</template>
<script>
import Clipboard from "clipboard";
import { apiGetMessageList, apiDelReply } from "src/api/message";
export default {
name: "replyList",
components: {},
computed: {},
data() {
return {
listLoading: false,
searchForm: {
keyword: "",
},
messageList: [],
replyList: [],
columns: [
{
label: "留言_id",
prop: "_id",
slot: "_id",
},
{
label: "回复用户",
prop: "replyUser",
},
{
label: "被回复用户",
prop: "byReplyUser",
},
{
label: "头像颜色",
prop: "replyHeaderColor",
},
{
label: "回复内容",
prop: "replyContent",
slot: "replyContent",
showTooltip: true,
},
{
label: "回复时间",
prop: "replyTime",
slot: "replyTime",
},
{
label: "操作",
slot: "operate",
width: "150",
},
],
};
},
created() {
this.requestPageData = this.getMessageList;
this.getMessageList();
},
mounted() { },
methods: {
getMessageList() {
let params = {
keyword: this.searchForm.keyword,
pageindex: this.pageInfo.pageNum,
pagesize: this.pageInfo.pageSize,
};
this.listLoading = true;
return apiGetMessageList(params)
.then((res) => {
let { list } = res.data;
this.messageList = list;
this.replyList = this.messageList
.map((item) => item.replyList)
.flat();
this.pageInfo.total = this.replyList.length;
})
.catch((err) => {
this.listLoading = false;
console.log(err);
})
.finally(() => {
this.listLoading = false;
this.hasLoad = true;
});
},
handleCopyId() {
let clipboard = new Clipboard(".clipboardBtn");
clipboard.on("success", (e) => {
this.$message.success("复制成功");
clipboard.destroy();
});
clipboard.on("error", (e) => {
this.$message.error("该浏览器不支持自动复制");
clipboard.destroy();
});
},
handleDeleteReply(_id) {
return apiDelReply({ _id })
.then((res) => {
this.$message.success("删除成功");
this.getMessageList(true);
})
.catch((err) => {
console.log(err);
this.$message.info("删除失败");
})
.finally(() => { });
},
handleDelete(row) {
this.$confirm("此操作将永久删除该数据, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.handleDeleteReply(row._id);
})
.catch(() => {
this.$message.info("已取消删除");
});
},
},
};
</script>
<style lang="less" scoped>
</style>
<template>
<div class="permission-add">
<zp-page-edit :back="true" @back="$router.back()">
<div slot="header">
{{ header }}
</div>
<el-form
:model="info"
:rules="rules"
ref="form"
label-width="100px"
class="form"
>
<el-form-item label="用户名:" prop="username">
<el-input type="text" v-model="info.username"></el-input>
</el-form-item>
<el-form-item label="密码:" prop="pwd">
<el-input type="password" v-model="info.pwd"></el-input>
</el-form-item>
<el-form-item label="权限:" prop="roles">
<el-select
v-model="info.roles"
multiple
placeholder="请选择"
class="block"
>
<el-option
v-for="item in roles"
:key="item.value"
:label="item.label"
:value="item.value"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleSubmit('form')"
:loading="loading"
>立即创建</el-button
>
</el-form-item>
</el-form>
</zp-page-edit>
</div>
</template>
<script>
export default {
name: "permissionAdd",
components: {},
data() {
return {
header: "添加管理员",
info: {
username: "",
pwd: "",
avatar: "",
roles: ["default"],
},
roles: [
{ label: "超级管理员", value: "admin" },
{ label: "普通管理员", value: "default" },
],
loading: false,
rules: {
username: [
{ required: true, message: "请填写用户名", trigger: "blur" },
],
pwd: [{ required: true, message: "请填写密码", trigger: "blur" }],
roles: [
{
required: true,
message: "请选择权限",
trigger: "change",
type: "array",
},
],
},
};
},
methods: {
handleSubmit(formName) {
this.loading = true;
this.$refs[formName].validate(async (valid) => {
if (valid) {
try {
await this.$store.dispatch("addUser", this.info);
this.loading = false;
this.$router.push("/permission/list");
this.$message.success("新增成功");
} catch (e) {
this.$message.error("新增失败");
this.loading = false;
}
} else {
console.log("error submit!!");
this.loading = false;
return false;
}
});
},
},
};
</script>
<style lang="less" scoped>
</style>
\ No newline at end of file
<template>
<zp-dialog
title="管理员编辑"
:visible.sync="dialogTableVisible"
@close="close"
width="480px"
:destroy-on-close="true"
v-bind="$attrs"
v-on="$listeners"
>
<el-form :model="info" :rules="rules" ref="form">
<el-form-item label="用户名:" prop="username">
<el-input type="text" v-model="info.username"></el-input>
</el-form-item>
<el-form-item label="原密码:" prop="old_pwd">
<el-input type="password" v-model="info.old_pwd"></el-input>
</el-form-item>
<el-form-item label="新密码:" prop="pwd">
<el-input type="password" v-model="info.pwd"></el-input>
</el-form-item>
<el-form-item label="权限:" prop="roles">
<el-select
v-model="info.roles"
multiple
placeholder="请选择"
class="block"
>
<el-option
v-for="item in roles"
:key="item.value"
:label="item.label"
:value="item.value"
>
</el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button
type="primary"
:loading="loading"
@click="handleSubmit('form')"
>
确定
</el-button>
</div>
</zp-dialog>
</template>
<script>
export default {
props: ["info"],
components: {},
data() {
return {
roles: [
{ label: "超级管理员", value: "admin" },
{ label: "普通管理员", value: "default" },
],
loading: false,
dialogTableVisible: true,
rules: {
username: [
{ required: true, message: "请填写用户名", trigger: "blur" },
],
old_pwd: [{ required: true, message: "请填写原密码", trigger: "blur" }],
pwd: [{ required: true, message: "请填写密码", trigger: "blur" }],
roles: [
{
required: true,
message: "请选择权限",
trigger: "change",
type: "array",
},
],
},
};
},
methods: {
close() {
this.$emit("close");
},
handleSubmit(formName) {
this.loading = true;
this.$refs[formName].validate(async (valid) => {
if (valid) {
try {
delete this.info.createTime;
delete this.info.releaseTime;
await this.$store.dispatch("updateUser", this.info);
this.loading = false;
this.$message.success("编辑成功");
this.close();
} catch (e) {
this.info.pwd = "";
this.info.old_pwd = "";
this.loading = false;
this.$message.error("编辑失败");
}
} else {
console.log("error submit!!");
this.loading = false;
return false;
}
});
},
},
};
</script>
<style lang="less" scoped>
.el-form {
.el-form-item {
display: flex;
align-items: center;
/deep/ .el-form-item__label {
min-width: 80px;
}
/deep/.el-form-item__content {
margin-left: 0 !important;
width: 260px;
.el-select {
width: 260px;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="article-list">
<zp-page-list>
<template v-slot:header>
<span>管理员列表</span>
</template>
<!-- filter start -->
<div slot="filter">
<el-form
ref="searchForm"
class="zp-search-form"
:inline="true"
:model="searchForm"
>
<el-form-item label="关键词:" prop="keyword">
<el-input
placeholder="请输入用户名"
v-model="searchForm.keyword"
@keydown.enter.native="getUserList"
></el-input>
</el-form-item>
<el-button type="primary" @click="getUserList"> 查询 </el-button>
</el-form>
</div>
<!-- filter end -->
<!-- list start -->
<div slot="list">
<zp-table-list
:loading="listLoading"
:source="userList"
:count="userTotal"
:columns="columns"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template slot="_id" slot-scope="scope">
<el-button
icon="el-icon-document-copy"
type="primary"
size="mini"
class="clipboardBtn"
:data-clipboard-text="scope.row._id"
@click="handleCopyId"
>复制
</el-button>
</template>
<template slot="roles" slot-scope="scope">
<el-tag
class="tag"
type="primary"
close-transition
v-for="(tag, index) in scope.row.roles"
:key="index"
>{{ tag }}</el-tag
>
</template>
<template slot="operate" slot-scope="scope">
<el-button size="mini" type="primary" @click="handleEdit(scope.row)"
>编辑</el-button
>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row)"
>删除</el-button
>
</template>
</zp-table-list>
</div>
<!-- list end -->
</zp-page-list>
<editDialog
v-if="editShow"
:info="userInfo"
@close="handleClose"
></editDialog>
</div>
</template>
<script>
import Clipboard from "clipboard";
import editDialog from "./components/editDialog";
import { mapGetters } from "vuex";
export default {
components: {
editDialog,
},
computed: {
...mapGetters(["userList", "userTotal"]),
},
data() {
return {
listLoading: false,
editShow: false,
searchForm: {
keyword: "",
},
userInfo: {},
columns: [
{
label: "_id",
prop: "_id",
slot: "_id",
},
{
label: "用户名",
prop: "username",
},
{
label: "权限",
prop: "roles",
slot: "roles",
},
{
label: "操作",
slot: "operate",
width: "150",
},
],
};
},
mounted() {
this.requestPageData = this.getUserList;
this.getUserList();
},
methods: {
async getUserList() {
this.listLoading = true;
try {
await this.$store.dispatch("getUserList", {
keyword: this.searchForm.keyword,
pageindex: this.pageInfo.pageNum,
pagesize: this.pageInfo.pageSize,
});
this.listLoading = false;
} catch (e) {
this.listLoading = false;
}
},
handleCopyId() {
let clipboard = new Clipboard(".clipboardBtn");
clipboard.on("success", (e) => {
this.$message.success("复制成功");
clipboard.destroy();
});
clipboard.on("error", (e) => {
this.$message.error("该浏览器不支持自动复制");
clipboard.destroy();
});
},
handleClose() {
this.editShow = false;
},
handleDelete(row) {
this.$confirm("此操作将永久删除该数据, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
await this.$store.dispatch("delUser", row._id);
this.$message.success("删除成功");
this.getUserList();
} catch (e) {
this.$message.error("删除失败");
console.log(e);
}
})
.catch(() => {
this.$message.info("已取消删除");
});
},
handleEdit(row) {
this.editShow = true;
row.releaseTime = new Date(row.releaseTime);
this.userInfo = row;
},
},
};
</script>
<style lang="less" scoped>
.tag {
margin: 0 10px;
}
</style>
import Koa from 'koa'
import ip from 'ip'
import conf from './config'
import router from './router'
import middleware from './middleware'
import './mongodb'
const app = new Koa()
middleware(app)
router(app)
app.listen(conf.port, '0.0.0.0', () => {
console.log(`server is running at http://${ip.address()}:${conf.port}`)
})
\ No newline at end of file
import path from "path";
const auth = {
admin_secret: "admin-token",
tokenKey: "Token-Auth",
whiteList: ["login", "client_api"],
blackList: ["admin_api"],
};
const log = {
logLevel: "debug", // 指定记录的日志级别
dir: path.resolve(__dirname, "../../logs"), // 指定日志存放的目录名
projectName: "blog", // 项目名,记录在日志中的项目信息
ip: "0.0.0.0", // 默认情况下服务器 ip 地址
};
const port = process.env.NODE_ENV === "production" ? "80" : "3000";
export default {
env: process.env.NODE_ENV,
port,
auth,
log,
mongodb: {
username: "rf",
pwd: 123456,
address: "localhost:27017",
db: "rfBlog",
},
};
import blogModel from "../../models/blog";
import marked from "marked";
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true, //允许 Git Hub标准的markdown.
tables: true, //允许支持表格语法。该选项要求 gfm 为true。
breaks: true, //允许回车换行。该选项要求 gfm 为true。
pedantic: false, //尽可能地兼容 markdown.pl的晦涩部分。不纠正原始模型任何的不良行为和错误。
sanitize: true, //对输出进行过滤(清理),将忽略任何已经输入的html代码(标签)
smartLists: true, //使用比原生markdown更时髦的列表。 旧的列表将可能被作为pedantic的处理内容过滤掉.
smartypants: false, //使用更为时髦的标点,比如在引用语法中加入破折号。
highlight: function(code) {
return require("highlight.js").highlightAuto(code).value;
},
});
module.exports = {
async list(ctx, next) {
console.log(
"----------------获取博客列表 blog/list-----------------------"
);
let { keyword, pageindex = 1, pagesize = 10 } = ctx.request.query;
console.log("ctx.request =>", ctx.request);
console.log(
"keyword:" +
keyword +
"," +
"pageindex:" +
pageindex +
"," +
"pagesize:" +
pagesize
);
try {
let reg = new RegExp(keyword, "i");
let data = await ctx.findPage(
blogModel,
{
$or: [{ type: { $regex: reg } }, { title: { $regex: reg } }],
},
null,
{ limit: pagesize * 1, skip: (pageindex - 1) * pagesize }
);
ctx.send(data);
} catch (e) {
console.log(e);
ctx.sendError(e);
}
},
async add(ctx, next) {
console.log("----------------添加博客 blog/add-----------------------");
let paramsData = ctx.request.body;
try {
let data = await ctx.findOne(blogModel, { title: paramsData.title });
if (data) {
ctx.sendError("数据已经存在, 请重新添加!");
} else {
paramsData.html = marked(paramsData.html);
let data = await ctx.add(blogModel, paramsData);
ctx.send(paramsData);
}
} catch (e) {
ctx.sendError(e);
}
},
async update(ctx, next) {
console.log("----------------更新博客 blog/update-----------------------");
let paramsData = ctx.request.body;
try {
paramsData.html = marked(paramsData.html);
let data = await ctx.update(
blogModel,
{ _id: paramsData._id },
paramsData
);
ctx.send();
} catch (e) {
if (e === "暂无数据") {
ctx.sendError(e);
}
}
},
async del(ctx, next) {
console.log("----------------删除博客 blog/del-----------------------");
let id = ctx.request.query.id;
try {
ctx.remove(blogModel, { _id: id });
ctx.send();
} catch (e) {
ctx.sendError(e);
}
},
async info(ctx, next) {
console.log(
"----------------获取博客信息 blog/info-----------------------"
);
let _id = ctx.request.query._id;
try {
let data = await ctx.findOne(blogModel, { _id });
return ctx.send(data);
} catch (e) {
return ctx.sendError(e);
}
},
};
import labelModel from "../../models/label";
module.exports = {
async list(ctx, next) {
console.log(
"----------------获取标签列表 label/list-----------------------"
);
let { keyword, pageindex = 1, pagesize = 50 } = ctx.request.query;
try {
let reg = new RegExp(keyword, "i");
let data = await ctx.findPage(
labelModel,
{
$or: [{ label: { $regex: reg } }, { bgColor: { $regex: reg } }],
},
null,
{ limit: pagesize * 1, skip: (pageindex - 1) * pagesize }
);
ctx.send(data);
} catch (e) {
console.log(e);
ctx.sendError(e);
}
},
async add(ctx, next) {
console.log("----------------添加标签 label/add-----------------------");
let paramsData = ctx.request.body;
try {
let data = await ctx.findOne(labelModel, { label: paramsData.label });
if (data) {
ctx.sendError("数据已经存在, 请重新添加!");
} else {
let result = await ctx.add(labelModel, paramsData);
ctx.send(result);
}
} catch (e) {
ctx.sendError(e);
}
},
async update(ctx, next) {
console.log("----------------更新标签 label/update-----------------------");
let paramsData = ctx.request.body;
try {
let data = await ctx.update(
labelModel,
{ _id: paramsData._id },
paramsData
);
ctx.send(data);
} catch (e) {
if (e === "暂无数据") {
ctx.sendError(e);
}
}
},
async del(ctx, next) {
console.log("----------------删除标签 label/del-----------------------");
let id = ctx.request.query.id;
try {
let data = await ctx.remove(labelModel, { _id: id });
ctx.send(data);
} catch (e) {
ctx.sendError(e);
}
},
};
import messageModel from '../../models/message';
module.exports = {
async list(ctx, next) {
console.log(
'----------------获取留言列表 admin_api/message/list-----------------------'
);
let { keyword, pageindex = 1, pagesize = 10 } = ctx.request.query;
let reg = new RegExp(keyword, 'i');
let conditions = {
$or: [{ nickname: { $regex: reg } }, { content: { $regex: reg } }],
};
// 排序参数
let sortParams = {
createTime: -1,
};
let options = {
limit: pagesize * 1,
skip: (pageindex - 1) * pagesize,
sort: sortParams,
};
try {
let data = await ctx.find(messageModel, conditions, null, options);
return ctx.send(data);
} catch (e) {
console.log(e);
return ctx.sendError(e);
}
},
async del(ctx, next) {
console.log(
'----------------删除留言 admin_api/message/del-----------------------'
);
let id = ctx.request.query.id;
try {
ctx.remove(messageModel, { _id: id });
ctx.send();
} catch (e) {
ctx.sendError(e);
}
},
async delReply(ctx, next) {
console.log(
'----------------删除回复 admin_api/message/delReply-----------------------'
);
let { _id } = ctx.request.body;
let options = {
$pull: { replyList: { _id } },
};
try {
let data = await ctx.update(messageModel, { _id }, options);
ctx.send();
} catch (e) {
ctx.sendError(e);
}
},
};
const path = require("path");
module.exports = {
async uploadImage(ctx, next) {
console.log("----------------添加图片 uploadImage-----------------------");
try {
let opts = {
path: path.resolve(__dirname, "../../../../public"),
};
let result = await ctx.uploadFile(ctx, opts);
ctx.send(result);
} catch (e) {
ctx.sendError(e);
}
},
async delUploadImage(ctx, next) {
console.log(
"----------------删除图片 delUploadImage-----------------------"
);
let fileName = ctx.request.body.fileName;
let fileCoverImgUrl = `public/images/${fileName}`;
try {
ctx.removeFile(fileCoverImgUrl);
ctx.send();
} catch (e) {
ctx.sendError(e);
}
},
};
import jwt from "jsonwebtoken";
import conf from "../../config";
import userModel from "../../models/user";
module.exports = {
async login(ctx, next) {
console.log("----------------登录接口 user/login-----------------------");
let { username, pwd } = ctx.request.body;
try {
let data = await ctx.findOne(userModel, { username: username });
console.log(data);
if (!data) {
return ctx.sendError("用户名不存在!");
}
if (pwd !== data.pwd) {
return ctx.sendError("密码错误,请重新输入!");
}
await ctx.update(
userModel,
{ _id: data._id },
{ $set: { loginTime: new Date() } }
); //更新登陆时间
let payload = {
_id: data._id,
username: data.username,
roles: data.roles,
};
// token签名 有效期为24小时
let token = jwt.sign(payload, conf.auth.admin_secret, {
expiresIn: "24h",
});
// 是否只用于http请求中获取
ctx.cookies.set(conf.auth.tokenKey, token, {
httpOnly: false,
});
ctx.send({ message: "登录成功" });
} catch (e) {
if (e === "暂无数据") {
console.log("用户名不存在");
return ctx.sendError("用户名不存在");
}
ctx.throw(e);
ctx.sendError(e);
}
},
async info(ctx, next) {
console.log(
"----------------获取用户信息接口 user/getUserInfo-----------------------"
);
let token = ctx.request.query.token;
try {
let tokenInfo = jwt.verify(token, conf.auth.admin_secret);
console.log("log tokenInfo =>", tokenInfo);
ctx.send({
username: tokenInfo.username,
_id: tokenInfo._id,
roles: tokenInfo.roles,
});
} catch (e) {
if ("TokenExpiredError" === e.name) {
ctx.sendError("鉴权失败, 请重新登录!");
ctx.throw(401, "token验证失败, 请重新登录!");
}
ctx.throw(401, "invalid token");
ctx.sendError("系统异常!");
}
},
async list(ctx, next) {
console.log(
"----------------获取用户信息列表接口 user/getUserList-----------------------"
);
let { keyword, pageindex = 1, pagesize = 10 } = ctx.request.query;
console.log(
"keyword:" +
keyword +
"," +
"pageindex:" +
pageindex +
"," +
"pagesize:" +
pagesize
);
try {
let reg = new RegExp(keyword, "i");
let data = await ctx.findPage(
userModel,
{
$or: [{ username: { $regex: reg } }],
},
{ pwd: 0 },
{ limit: pagesize * 1, skip: (pageindex - 1) * pagesize }
);
ctx.send(data);
} catch (e) {
console.log(e);
ctx.sendError(e);
}
},
async add(ctx, next) {
console.log("----------------添加管理员 user/add-----------------------");
let paramsData = ctx.request.body;
try {
let data = await ctx.findOne(userModel, {
username: paramsData.username,
});
if (data) {
ctx.sendError("数据已经存在, 请重新添加!");
} else {
await ctx.add(userModel, paramsData);
ctx.send(paramsData);
}
} catch (e) {
ctx.sendError(e);
}
},
async update(ctx, next) {
console.log(
"----------------更新管理员 user/update-----------------------"
);
let paramsData = ctx.request.body;
console.log(paramsData);
try {
let data = await ctx.findOne(userModel, {
username: paramsData.username,
});
if (paramsData.old_pwd !== data.pwd) {
return ctx.sendError("密码不匹配!");
}
delete paramsData.old_pwd;
await ctx.update(userModel, { _id: paramsData._id }, paramsData);
ctx.send();
} catch (e) {
if (e === "暂无数据") {
ctx.sendError(e);
}
}
},
async del(ctx, next) {
console.log("----------------删除管理员 user/del-----------------------");
let id = ctx.request.query.id;
try {
ctx.remove(userModel, { _id: id });
ctx.send();
} catch (e) {
ctx.sendError(e);
}
},
};
require('babel-core/register') // babel编译
module.exports = require('./app.js')
\ No newline at end of file
import jwt from 'jsonwebtoken';
import conf from '../../config';
export default () => {
return async (ctx, next) => {
// 白名单就不需要走 jwt 鉴权
if (!conf.auth.whiteList.some((v) => ctx.path.includes(v))) {
let token = ctx.cookies.get(conf.auth.tokenKey);
try {
jwt.verify(token, conf.auth.admin_secret);
} catch (e) {
if ('TokenExpiredError' === e.name) {
ctx.sendError('token已过期, 请重新登录!');
ctx.throw(401, 'token已过期, 请重新登录!');
}
ctx.sendError('token验证失败, 请重新登录!');
ctx.throw(401, 'token验证失败, 请重新登录!');
}
console.log('鉴权成功');
}
await next();
};
};
/*
* 公共Add方法
* @param model 要操作数据库的模型
* @param conditions 增加的条件,如{id:xxx}
*/
export const add = (model, conditions) => {
return new Promise((resolve, reject) => {
model.create(conditions, (err, res) => {
if (err) {
console.error("Error: " + JSON.stringify(err));
reject(err);
return false;
}
console.log("save success!");
resolve(res);
});
});
};
/*
* 公共update方法
* @param model 要操作数据库的模型
* @param conditions 增加的条件,如{id:xxx}
* @param update 更新条件{set{id:xxx}}
* @param options
*/
export const update = (model, conditions, update, options) => {
return new Promise((resolve, reject) => {
model.update(conditions, update, options, (err, res) => {
if (err) {
console.error("Error: " + JSON.stringify(err));
reject(err);
return false;
}
if (res.n !== 0) {
console.log("update success!");
} else {
console.log("update fail:no this data!");
return reject("update fail:no this data!");
}
resolve(res);
});
});
};
/**
* 公共remove方法
* @param model
* @param conditions
*/
export const remove = (model, conditions) => {
return new Promise((resolve, reject) => {
model.remove(conditions, function(err, res) {
if (err) {
console.error("Error: " + JSON.stringify(err));
reject(err);
return false;
} else {
if (res.result.n !== 0) {
console.log("remove success!");
} else {
console.log("remove fail:no this data!");
}
resolve(res);
}
});
});
};
/**
* 公共find方法 非关联查找
* @param model
* @param conditions
* @param fields 查找时限定的条件,如顺序,某些字段不查找等
* @param options
* @param callback
*/
export const find = async (model, conditions, fields, options = {}) => {
let { sort } = options;
delete options.sort;
const getCount = () => {
return new Promise((resolve, reject) => {
model.find(conditions, fields).count({}, (err, res) => {
if (err) {
console.log("查询长度错误");
return reject(err);
}
resolve(res);
});
});
};
const count = await getCount();
return new Promise((resolve, reject) => {
model
.find(conditions, fields, options, function(err, res) {
if (err) {
console.error("Error: " + JSON.stringify(err));
reject(err);
return false;
} else {
if (res.length !== 0) {
resolve({
list: res,
total: count,
});
console.log("find success!");
} else {
console.log("find fail:no this data!");
}
// resolve(res);
resolve({
list: res,
total: count,
});
}
})
.sort(sort);
});
};
/**
* 公共findOne方法 非关联查找
* @param model
* @param conditions
* @param fields 查找时限定的条件,如顺序,某些字段不查找等
* @param options
* @param callback
*/
export const findOne = (model, conditions, fields, options = {}) => {
let { sort } = options;
delete options.sort;
return new Promise((resolve, reject) => {
model
.findOne(conditions, fields, options, function(err, res) {
if (err) {
console.error("Error: " + JSON.stringify(err));
reject(err);
return false;
} else {
if (res) {
console.log("find success!");
} else {
console.log("find fail:no this data!");
}
resolve(res);
}
})
.sort(sort);
});
};
export const findPage = async (model, conditions, fields, options = {}) => {
let { sort } = options;
delete options.sort;
const getCount = () => {
return new Promise((resolve, reject) => {
model.find(conditions, fields).count({}, (err, res) => {
if (err) {
console.log("查询长度错误");
return reject(err);
}
resolve(res);
});
});
};
const count = await getCount();
return new Promise((resolve, reject) => {
model.find(conditions, fields, options, function(err, res) {
if (err) {
console.error("Error: " + JSON.stringify(err));
reject(err);
return false;
} else {
if (res.length !== 0) {
console.log("find success!");
resolve({
list: res,
total: count,
});
} else {
console.log("find fail:no this data!");
resolve({
list: res,
total: count,
});
}
}
});
});
};
/*
* 公共aggregate方法
* @param model 要操作数据库的模型
* @param conditions 增加的条件,如{id:xxx}
*/
export const aggregate = (model, conditions) => {
return new Promise((resolve, reject) => {
model.aggregate(conditions, (err, res) => {
if (err) {
console.error("Error: " + JSON.stringify(err));
reject(err);
return false;
}
console.log("aggregate success!");
resolve(res);
});
});
};
import Busboy from "busboy";
import fs from "fs";
import path from "path";
//检测文件并创建文件
const mkdirSync = (dirname) => {
if (fs.existsSync(dirname)) {
return true;
} else {
if (mkdirSync(path.dirname(dirname))) {
fs.mkdirSync(dirname);
return true;
}
}
};
// 删除本地图片
export const removeFile = (filePath) => {
fs.unlink(filePath, function(err) {
if (err) {
throw err;
}
console.log("文件:" + filePath + "删除成功!");
});
};
export const uploadFile = (ctx, opts) => {
//重命名
function rename(fileName) {
return (
Math.random()
.toString(16)
.substr(2) +
"." +
fileName.split(".").pop()
);
}
let busboy = new Busboy({ headers: ctx.req.headers });
console.log("start uploading...");
/*
filename: 字段名,
file: 文件流,
filename: 文件名
*/
return new Promise((resolve, reject) => {
var fileObj = {};
busboy.on("file", async (fieldname, file, filename, encoding, mimetype) => {
let filePath = "",
imgPrefix = "";
filePath = path.join(opts.path, mimetype.split("/")[0] + "s");
// 现网图片路径不一样
imgPrefix = `${ctx.protocol}://${ctx.host}/${mimetype.split("/")[0]}s`;
if (!mkdirSync(filePath)) {
throw new Error("没找到目录");
}
let fName = rename(filename),
fPath = path.join(path.join(filePath, fName));
file.pipe(fs.createWriteStream(fPath));
console.log("fName =>", fName);
console.log("fPath =>", fPath);
file.on("end", () => {
fileObj[fieldname] = `${imgPrefix}/${fName}`;
});
});
busboy.on(
"field",
(
fieldname,
val,
fieldnameTruncated,
valTruncated,
encoding,
mimetype
) => {
fileObj[fieldname] = val;
}
);
busboy.on("finish", async () => {
resolve(fileObj);
console.log("finished...", fileObj);
});
busboy.on("error", function(err) {
console.log("err:" + err);
reject(err);
});
ctx.req.pipe(busboy);
});
};
export const get_client_ip = (ctx) => {
return (
ctx.request.headers["x-forwarded-for"] ||
(ctx.request.connection && ctx.request.connection.remoteAddress) ||
ctx.request.socket.remoteAddress ||
(ctx.request.connection.socket &&
ctx.request.connection.socket.remoteAddress) ||
null
);
};
import * as get_Info_func from "./get_info";
import * as db_func from "./db";
import * as file_func from "./file";
export default () => {
const func = Object.assign({}, get_Info_func, db_func, file_func);
return async (ctx, next) => {
for (let v in func) {
if (func.hasOwnProperty(v)) ctx[v] = func[v];
}
await next();
};
};
import path from "path";
import bodyParser from "koa-bodyparser";
import staticFiles from "koa-static";
import Rule from "./rule";
import Send from "./send";
import Auth from "./auth";
import Log from "./log";
import Func from "./func";
export default (app) => {
//缓存拦截器
app.use(async (ctx, next) => {
if (ctx.url == "/favicon.ico") return;
await next();
ctx.status = 200;
ctx.set("Cache-Control", "must-revalidation");
if (ctx.fresh) {
ctx.status = 304;
return;
}
});
// 日志中间件
app.use(Log());
// 数据返回的封装
app.use(Send());
// 方法封装
app.use(Func());
//权限中间件
app.use(Auth());
//post请求中间件
app.use(bodyParser());
//静态文件中间件
app.use(staticFiles(path.resolve(__dirname, "../../../public")));
// 规则中间件
Rule({
app,
rules: [
{
path: path.join(__dirname, "../controller/admin"),
name: "admin",
},
{
path: path.join(__dirname, "../controller/client"),
name: "client",
},
],
});
// 增加错误的监听处理
app.on("error", (err, ctx) => {
if (ctx && !ctx.headerSent && ctx.status < 500) {
ctx.status = 500;
}
if (ctx && ctx.log && ctx.log.error) {
if (!ctx.state.logged) {
ctx.log.error(err.stack);
}
}
});
};
export default (ctx, msg, commonInfo) => {
const {
method, // 请求方法 get post或其他
url, // 请求链接
host, // 发送请求的客户端的host
headers // 请求中的headers
} = ctx.request;
const client = {
method,
url,
host,
msg,
ip: ctx.get_client_ip(ctx),
referer: headers['referer'], // 请求的源地址
userAgent: headers['user-agent'] // 客户端信息 设备及浏览器信息
}
return JSON.stringify(Object.assign(commonInfo, client));
}
\ No newline at end of file
import logger from './log'
export default opts => {
let loggerMiddleware = logger(opts);
return async (ctx, next) => {
return loggerMiddleware(ctx, next)
.catch( e => {
if (ctx.status < 500) {
ctx.status = 500;
}
ctx.log.error(e.stack);
ctx.state.logged = true;
ctx.throw(e);
})
}
}
\ No newline at end of file
import log4js from 'log4js'
import access from './access' // 引入日志输出信息的封装文件
import config from '../../config'
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"];
// 提取默认公用参数对象
const baseInfo = config.log
export default (options = {}) => {
let contextLogger = {}, //错误日志等级对象,最后会赋值给ctx上,用于打印各种日志
appenders = {}, //日志配置
opts = Object.assign({}, baseInfo, options), //系统配置
{
logLevel,
dir,
ip,
projectName
} = opts,
commonInfo = {
projectName,
ip
}; //存储公用的日志信息
//指定要记录的日志分类
appenders.all = {
type: 'dateFile', //日志文件类型,可以使用日期作为文件名的占位符
filename: `${dir}/all/`, //日志文件名,可以设置相对路径或绝对路径
pattern: 'task-yyyy-MM-dd.log', //占位符,紧跟在filename后面
alwaysIncludePattern: true //是否总是有后缀名
}
// 环境变量为dev local development 认为是开发环境
if (config.env === "dev" || config.env === "local" || config.env === "development") {
appenders.out = {
type: "console"
}
}
let logConfig = {
appenders,
/**
* 指定日志的默认配置项
* 如果 log4js.getLogger 中没有指定,默认为 cheese 日志的配置项
*/
categories: {
default: {
appenders: Object.keys(appenders),
level: logLevel
}
}
}
let logger = log4js.getLogger('cheese');
return async (ctx, next) => {
const start = Date.now() // 记录请求开始的时间
// 循环methods将所有方法挂载到ctx 上
methods.forEach((method, i) => {
contextLogger[method] = message => {
logConfig.appenders.cheese = {
type: 'dateFile', //日志文件类型,可以使用日期作为文件名的占位符
filename: `${dir}/${method}/`,
pattern: `${method}-yyyy-MM-dd.log`,
alwaysIncludePattern: true //是否总是有后缀名
}
log4js.configure(logConfig)
logger[method](access(ctx, message, commonInfo))
}
})
ctx.log = contextLogger
await next()
// 记录完成的时间 作差 计算响应时间
const responseTime = Date.now() - start
ctx.log.info(access(ctx, {
responseTime: `响应时间为${responseTime/1000}s`
}, commonInfo))
}
}
\ No newline at end of file
import Path from "path";
import fs from "fs";
export default (opts) => {
let { app, rules = [] } = opts;
if (!app) {
throw new Error("the app params is necessary!");
}
app.router = {};
const appKeys = Object.keys(app);
rules.forEach((item) => {
let { path, name } = item;
if (appKeys.includes(name)) {
throw new Error(`the name of ${name} already exists!`);
}
let content = {};
//readdirSync: 方法将返回一个包含“指定目录下所有文件名称”的数组对象。
//extname: 返回path路径文件扩展名,如果path以 ‘.' 为结尾,将返回 ‘.',如果无扩展名 又 不以'.'结尾,将返回空值。
//basename: path.basename(p, [ext]) p->要处理的path ext->要过滤的字符
fs.readdirSync(path).forEach((filename) => {
let extname = Path.extname(filename);
if (extname === ".js") {
let name = Path.basename(filename, extname);
content[name] = require(Path.join(path, filename));
content[name].filename = name;
}
});
app[name] = content;
});
};
export default () => {
let render = (ctx) => {
return (json, msg) => {
ctx.set("Content-Type", "application/json");
ctx.body = JSON.stringify({
code: 1,
data: json || {},
msg: msg || "success",
});
};
};
let renderError = (ctx) => {
return (msg) => {
ctx.set("Content-Type", "application/json");
ctx.body = JSON.stringify({
code: 0,
data: {},
msg: msg.toString(),
});
};
};
return async (ctx, next) => {
ctx.send = render(ctx);
ctx.sendError = renderError(ctx);
await next();
};
};
import db from "../mongodb";
let blogSchema = db.Schema({
type: Array,
title: String,
desc: String,
fileCoverImgUrl: String,
html: String,
markdown: String,
level: Number,
github: String,
auth: String,
source: Number,
isVisible: Boolean,
releaseTime: String,
pv: { type: Number, default: 0 },
likes: { type: Number, default: 0 },
comments: { type: Number, default: 0 },
});
export default db.model("blog", blogSchema);
import db from "../mongodb";
let labelSchema = db.Schema({
label: String,
bgColor: String,
createTime: { type: Date, default: Date.now },
});
export default db.model("label", labelSchema);
import db from "../mongodb";
let messageSchema = db.Schema({
content: String,
headerColor: { type: String, default: "#ff6c1a" },
nickname: { type: String, default: "匿名网友" },
createTime: String,
likes: { type: Number, default: 0 },
comments: { type: Number, default: 0 },
replyList: [
{
replyHeaderColor: { type: String, default: "#009688" },
replyContent: String,
replyUser: { type: String, default: "匿名网友" },
byReplyUser: String,
replyTime: String,
},
],
});
export default db.model("message", messageSchema);
import db from "../mongodb";
let userSchema = db.Schema({
username: String,
pwd: String,
avatar: String,
roles: Array,
createTime: { type: Date, default: Date.now },
loginTime: Date,
});
export default db.model("user", userSchema);
import mongoose from "mongoose";
import conf from "./config";
const DB_URL = `mongodb://${conf.mongodb.username}:${conf.mongodb.pwd}@${conf.mongodb.address}/${conf.mongodb.db}`; // 账号登陆
mongoose.Promise = global.Promise;
mongoose.connect(DB_URL, { useMongoClient: true }, (err) => {
if (err) {
console.log("数据库连接失败!");
} else {
console.log("数据库连接成功!");
}
});
export default mongoose;
import koaRouter from "koa-router";
const router = koaRouter();
export default (app) => {
/*----------------------admin-------------------------------*/
// 用户请求
router.post("/admin_api/user/login", app.admin.user.login);
router.get("/admin_api/user/info", app.admin.user.info);
router.get("/admin_api/user/list", app.admin.user.list);
router.post("/admin_api/user/add", app.admin.user.add);
router.post("/admin_api/user/update", app.admin.user.update);
router.get("/admin_api/user/del", app.admin.user.del);
// 文章请求
router.get("/admin_api/blog/list", app.admin.blog.list);
router.post("/admin_api/blog/add", app.admin.blog.add);
router.post("/admin_api/blog/update", app.admin.blog.update);
router.get("/admin_api/blog/del", app.admin.blog.del);
router.get("/admin_api/blog/info", app.admin.blog.info);
// 标签请求
router.get("/admin_api/label/list", app.admin.label.list);
router.post("/admin_api/label/add", app.admin.label.add);
router.post("/admin_api/label/update", app.admin.label.update);
router.get("/admin_api/label/del", app.admin.label.del);
// 留言请求
router.get("/admin_api/message/list", app.admin.message.list);
router.get("/admin_api/message/del", app.admin.message.del);
router.post("/admin_api/message/delReply", app.admin.message.delReply);
// 图片请求
router.post("/admin_api/uploadImage", app.admin.upload.uploadImage);
router.post("/admin_api/delUploadImage", app.admin.upload.delUploadImage);
/*----------------------client-------------------------------*/
// 文章请求
router.get("/client_api/blog/list", app.client.blog.list);
router.get("/client_api/blog/info", app.client.blog.info);
router.post("/client_api/blog/updateLikes", app.client.blog.updateLikes);
router.post("/client_api/blog/updatePV", app.client.blog.updatePV);
// 标签请求
router.get("/client_api/label/list", app.client.label.list);
// 留言请求
router.post("/client_api/message/add", app.client.message.add);
router.get("/client_api/message/list", app.client.message.list);
router.get("/client_api/message/replyCount", app.client.message.replyCount);
router.post(
"/client_api/message/updateLikes",
app.client.message.updateLikes
);
router.post(
"/client_api/message/updateReplys",
app.client.message.updateReplys
);
app.use(router.routes()).use(router.allowedMethods());
};