This commit is contained in:
Pan
2017-04-18 15:09:13 +08:00
parent 304b17520c
commit d10370086e
145 changed files with 61322 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
<template>
<div class="app-container">
<h1 class="page-heading">
创建后台用户
</h1>
<el-form ref="createForm" :rules="createRules" label-position="left" style='width:80%' :model="form" label-width="100px">
<el-form-item label="用户邮箱" prop="email">
<el-input v-model="form.email" placeholder="公司邮箱"></el-input>
</el-form-item>
<el-form-item label="权限选择" >
<el-select style="width: 100%" v-model="form.role" multiple placeholder="请选择">
<el-option
v-for="item in roleList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click.native.prevent="onSubmit">立即创建</el-button>
<el-button>
<router-link class="normal_link" to="/index">
取消
</router-link>
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { createNewUser, getRoleList } from 'api/adminUser';
import { isWscnEmail } from 'utils/validate';
export default{
name: 'createUser',
data() {
const validateEmail = (rule, value, callback) => {
if (!isWscnEmail(value)) {
callback(new Error('邮箱错误'));
} else {
callback();
}
};
return {
roleList: [],
loading: false,
form: {
email: '',
role: ''
},
createRules: {
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ validator: validateEmail }
]
}
};
},
methods: {
onSubmit() {
this.$refs.createForm.validate(valid => {
if (valid) {
this.loading = true;
const data = {
email: this.form.email,
roles: this.form.role
};
createNewUser(data).then(() => {
this.$message.success('创建成功');
});
} else {
this.$message.error('error submit!!');
}
this.loading = false;
});
}
},
created() {
getRoleList().then(response => {
const roleMap = response.data.role_map;
const keyArr = Object.keys(roleMap);
this.roleList = keyArr.map(v => ({ value: v, label: roleMap[v] }));
});
}
};
</script>

404
src/views/admin/profile.vue Normal file
View File

@@ -0,0 +1,404 @@
<template>
<div class="profile-container clearfix">
<div style="position: relative;margin-left: 30px;">
<PanThumb :image="avatar"> 你的权限:
<span class="pan-info-roles" v-for="item in roles">{{item}}</span>
</PanThumb>
<el-button type="primary" icon="upload" style="position: absolute;bottom: 15px;margin-left: 40px;"
@click="handleImagecropper">修改头像
</el-button>
</div>
<!--popover-->
<el-popover
ref="popoverWX"
placement="top"
width="160"
trigger="click"
v-model="popoverVisibleWX">
<p>你确定要解绑么?</p>
<div style="text-align: right; margin: 0">
<el-button size="mini" type="text" @click="popoverVisibleWX = false">取消</el-button>
<el-button type="primary" size="mini" @click="handleUnbind('wechat')">确定</el-button>
</div>
</el-popover>
<el-popover
ref="popoverQQ"
placement="top"
width="160"
trigger="click"
v-model="popoverVisibleQQ">
<p>你确定要解绑么?</p>
<div style="text-align: right; margin: 0">
<el-button size="mini" type="text" @click="popoverVisibleQQ = false">取消</el-button>
<el-button type="primary" size="mini" @click="handleUnbind('tencent')">确定</el-button>
</div>
</el-popover>
<!--popover End-->
<el-card class="box-card">
<div slot="header" class="clearfix">
<span style="line-height: 36px;">个人资料</span>
</div>
<div class="box-item">
<span class="field-label">昵称</span>
<div class="field-content">
{{name}}
<el-button class="edit-btn" @click="handleEditName" type="primary" icon="edit"
size="small"></el-button>
</div>
</div>
<div class="box-item">
<span class="field-label">简介</span>
<div class="field-content">
{{introduction.length==0?'未填写':introduction}}
<el-button class="edit-btn" @click="handleIntroductionName" type="primary" icon="edit"
size="small"></el-button>
</div>
</div>
<div class="box-item" style="margin-bottom: 10px;">
<span class="field-label">密码</span>
<div class="field-content">
<el-button type="primary" @click="resetPSWdialogVisible=true">修改密码</el-button>
</div>
</div>
<div class="box-item" style="margin-top: 5px;">
<div class="field-content">
<span class="wx-svg-container"><wscn-icon-svg icon-class="weixin" class="icon"/></span>
<el-button class="unbind-btn" v-popover:popoverWX type="danger">解绑微信</el-button>
</div>
</div>
<div class="box-item">
<div class="field-content">
<span class="qq-svg-container"><wscn-icon-svg icon-class="QQ" class="icon"/></span>
<el-button class="unbind-btn" v-popover:popoverQQ style="padding: 10px 18px" type="danger">
解绑QQ
</el-button>
</div>
</div>
</el-card>
<el-card class="box-card">
<div slot="header" class="clearfix">
<span style="line-height: 36px;">偏好设置</span>
<el-button @click="updateSetting" style="float: right;margin-top: 5px;" size="small" type="success">
更新偏好
</el-button>
</div>
<div>
<div class="box-item">
<span class="field-label">文章平台默认项选择:</span>
<el-checkbox-group v-model="articlePlatform">
<el-checkbox label="wscn-platform">见闻</el-checkbox>
<el-checkbox label="gold-platform">黄金头条</el-checkbox>
<el-checkbox label="weex-platform">WEEX</el-checkbox>
</el-checkbox-group>
<span class="field-label">使用自定义主题:</span>
<el-switch
v-model="theme"
on-text=""
off-text="">
</el-switch>
</div>
</div>
</el-card>
<ImageCropper field="img"
:width="300"
:height="300"
url="/upload"
@crop-upload-success="cropSuccess"
:key="imagecropperKey"
v-show="imagecropperShow"></ImageCropper>
<el-dialog title="昵称" v-model="nameDialogFormVisible">
<el-form label-position="left" label-width="50px">
<el-form-item label="昵称" style="width: 300px;">
<input class="input" ref="nameInput" :value="name" autocomplete="off" :maxlength=10>
<span>(最多填写十个字符)</span>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="nameDialogFormVisible = false"> </el-button>
<el-button type="primary" @click="setName"> </el-button>
</div>
</el-dialog>
<el-dialog title="简介" v-model="introductionDialogFormVisible">
<el-form label-position="left" label-width="50px">
<el-form-item label="简介" style="width: 500px;">
<textarea :row=3 class="textarea" ref="introductionInput" :value="introduction"></textarea>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="introductionDialogFormVisible = false"> </el-button>
<el-button type="primary" @click="setIntroduction"> </el-button>
</div>
</el-dialog>
<el-dialog title="提示" v-model="resetPSWdialogVisible" size="tiny">
<span>你确定要重设密码么? <strong>&nbsp&nbsp&nbsp&nbsp&nbsp( :重设密码将会登出,请注意!!! )</strong></span>
<span slot="footer" class="dialog-footer">
<el-button @click="resetPSWdialogVisible = false"> </el-button>
<el-button type="primary" @click="resetPSW"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { updateInfo, unbind, updateSetting } from 'api/adminUser';
import ImageCropper from 'components/ImageCropper';
import PanThumb from 'components/PanThumb';
import { toggleClass } from 'utils'
export default{
components: { ImageCropper, PanThumb },
data() {
return {
nameDialogFormVisible: false,
introductionDialogFormVisible: false,
resetPSWdialogVisible: false,
popoverVisibleQQ: false,
popoverVisibleWX: false,
imagecropperShow: false,
imagecropperKey: 0,
articlePlatform: [],
theme: false
}
},
created() {
if (this.setting.articlePlatform) {
this.articlePlatform = this.setting.articlePlatform
}
},
computed: {
...mapGetters([
'name',
'avatar',
'email',
'introduction',
'roles',
'uid',
'setting'
])
},
watch: {
theme() {
toggleClass(document.body, 'custom-theme')
// this.$store.dispatch('setTheme', value);
}
},
methods: {
resetPSW() {
this.$store.dispatch('LogOut').then(() => {
this.$router.push({ path: '/sendpwd' })
});
},
toggleResetDialog(state) {
this.resetDialogVisible = state;
},
handleEditName() {
this.nameDialogFormVisible = true;
},
handleIntroductionName() {
this.introductionDialogFormVisible = true;
},
setName() {
const displayName = this.$refs.nameInput.value;
const data = {
display_name: displayName,
uid: this.uid
};
updateInfo(data).then(() => {
this.$store.commit('SET_NAME', displayName);
this.$notify({
title: '成功',
message: '昵称修改成功',
type: 'success'
});
});
this.nameDialogFormVisible = false;
},
setIntroduction() {
const introduction = this.$refs.introductionInput.value;
const data = {
introduction,
uid: this.uid
};
updateInfo(data).then(() => {
this.$store.commit('SET_INTRODUCTION', introduction);
this.$notify({
title: '成功',
message: '简介修改成功',
type: 'success'
});
});
this.introductionDialogFormVisible = false;
},
handleUnbind(unbindType) {
const data = {
unbind_type: unbindType
};
unbind(data).then(() => {
this.$notify({
title: '成功',
message: '解绑成功,即将登出',
type: 'success'
});
setTimeout(() => {
this.$store.dispatch('LogOut').then(() => {
this.$router.push({ path: '/login' })
});
}, 3000)
});
this.popoverVisibleQQ = false;
this.popoverVisibleWX = false;
},
handleImagecropper() {
this.imagecropperShow = true;
this.imagecropperKey = this.imagecropperKey + 1;
},
cropSuccess(url) {
this.imagecropperShow = false;
const data = {
image: url,
uid: this.uid
};
updateInfo(data).then(() => {
this.$store.commit('SET_AVATAR', url);
this.$notify({
title: '成功',
message: '头像修改成功',
type: 'success'
});
});
},
updateSetting() {
const obj = Object.assign(this.setting, { articlePlatform: this.articlePlatform });
updateSetting({ setting: JSON.stringify(obj) }).then(() => {
this.$store.commit('SET_SETTING', this.setting);
this.$notify({
title: '成功',
message: '更新偏好成功',
type: 'success'
});
});
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.input {
-moz-appearance: none;
appearance: none;
background-color: #fff;
border-radius: 4px;
border: 1px solid #bfcbd9;
color: #1f2d3d;
display: block;
font-size: inherit;
height: 36px;
line-height: 1;
padding: 3px 10px;
transition: border-color .2s cubic-bezier(.645, .045, .355, 1);
width: 100%;
}
.textarea {
height: 90px;
display: block;
resize: vertical;
padding: 5px 7px;
line-height: 1.5;
box-sizing: border-box;
width: 100%;
font-size: 14px;
color: #1f2d3d;
background-color: #fff;
background-image: none;
border: 1px solid #bfcbd9;
border-radius: 4px;
transition: border-color .2s cubic-bezier(.645, .045, .355, 1);
}
.icon {
color: #fff;
font-size: 30px;
margin-top: 6px;
}
.wx-svg-container, .qq-svg-container {
display: inline-block;
width: 40px;
height: 40px;
line-height: 40px;
text-align: center;
padding-top: 1px;
border-radius: 4px;
margin-bottom: 10px;
margin-right: 55px;
}
.wx-svg-container {
background-color: #8dc349;
}
.qq-svg-container {
background-color: #6BA2D6;
}
.unbind-btn {
position: absolute;
right: -60px;
top: 2px;
}
.profile-container {
padding: 20px;
.box-card {
width: 400px;
margin: 30px;
float: left;
.field-label {
font-size: 16px;
font-weight: 700;
line-height: 36px;
color: #333;
padding-right: 30px;
}
.box-item {
.field-content {
display: inline-block;
position: relative;
cursor: pointer;
.edit-btn {
display: none;
position: absolute;
right: -50px;
top: -5px;
}
}
&:hover {
.edit-btn {
display: block;
}
}
}
}
}
.pan-info-roles {
font-size: 12px;
font-weight: 700;
color: #333;
display: block;
}
</style>

View File

@@ -0,0 +1,92 @@
<template>
<div class="app-container quicklyCreateUser-container">
<el-form ref="form" :rules="rules" :model="form" label-position="left" label-width="60px">
<el-card style=" margin-top: 50px;width: 600px;">
<div slot="header" class="clearfix">
<el-row :gutter="20">
<el-col :span="20">
<el-form-item label="昵称" prop="display_name">
<el-input v-model="form.display_name"></el-input>
</el-form-item>
</el-col>
<el-col :span="4">
<el-button type="success" @click="onSubmit">立即创建</el-button>
</el-col>
</el-row>
</div>
<el-row>
<el-col :span="12">
<el-button style="height: 150px;width: 150px;" @click="handleImagecropper" type="primary">上传头像
</el-button>
</el-col>
<el-col :span="12">
<img style=" float:right;width: 150px;height: 150px;border-radius: 50%;margin-left: 50px;"
:src="form.image">
</el-col>
</el-row>
</el-card>
</el-form>
<el-tooltip style="position: absolute;margin-left: 750px;top: 380px" placement="top">
<el-button>Tooltip</el-button>
<div slot="content">昵称为必填项<br/><br/>一键创建只能创建后台虚拟账号<br/><br/>没有任何实际操作能力</div>
</el-tooltip>
<ImageCropper field="img"
:width="300"
:height="300"
url="/upload"
@crop-upload-success="cropSuccess"
:key="imagecropperKey"
v-show="imagecropperShow">
</ImageCropper>
</div>
</template>
<script>
import { createNewUser } from 'api/adminUser';
import ImageCropper from 'components/ImageCropper';
export default{
name: 'quicklyCreateUser',
components: { ImageCropper },
data() {
return {
form: {
display_name: '',
image: '',
role: ['virtual_editor']
},
imagecropperShow: false,
imagecropperKey: 0,
rules: {
display_name: [{ required: true, message: '昵称必填', trigger: 'blur' }]
}
}
},
methods: {
onSubmit() {
this.$refs.form.validate(valid => {
if (valid) {
createNewUser(this.form).then(() => {
this.$message.success('创建成功')
});
} else {
console.log('error submit!!');
return false;
}
});
},
handleImagecropper() {
this.imagecropperShow = true;
this.imagecropperKey = this.imagecropperKey + 1;
},
cropSuccess(url) {
this.imagecropperShow = false;
this.form.image = url
}
}
}
</script>

View File

@@ -0,0 +1,241 @@
<template>
<div class="app-container adminUsers-list-container">
<div class="filter-container">
<el-input @keyup.enter.native="handleFilter" style="width:135px;" class="filter-item" placeholder="ID" type="number" v-model="listQuery.uid">
</el-input>
<el-input style="width:135px;" class="filter-item" placeholder="Name" @keyup.enter.native="handleFilter" v-model="listQuery.display_name">
</el-input>
<el-input class="filter-item" style="width:300px;display: inline-table" placeholder="email" @keyup.enter.native="handleFilter"
v-model="listQuery.email">
<template slot="append">@wallstreetcn.com</template>
</el-input>
</el-input>
<el-select style="width: 250px" class="filter-item" v-model="listQuery.role" clearable placeholder="权限">
<el-option v-for="item in roleOptions" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
<el-button class="filter-item" type="primary" icon="search" @click="handleFilter">
搜索
</el-button>
<el-button class="filter-item" type="danger" @click="resetFilter">重置筛选项</el-button>
</div>
<el-table :data="list" v-loading.body="listLoading" border fit highlight-current-row>
<el-table-column label="ID" width="130">
<template scope="scope">
<span>{{scope.row.uid}}</span>
</template>
</el-table-column>
<el-table-column label="Name">
<template scope="scope">
<span>{{scope.row.display_name}}</span>
</template>
</el-table-column>
<el-table-column label="Email">
<template scope="scope">
<span>{{scope.row.email}}</span>
</template>
</el-table-column>
<el-table-column label="Role">
<template scope="scope">
<el-tag style="margin-right: 5px;" v-for='item in scope.row.roles' :key='item+scope.row.uid' type="primary">
{{item}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="170">
<template scope="scope">
<el-button size="small" type="success" @click="handleEdit(scope.row)">编辑权限</el-button>
<el-button size="small" v-if='scope.row.roles.indexOf("virtual_editor")>=0||hasRoleEdit' type="primary" @click="handleInfo(scope.row)">修改</el-button>
</template>
</el-table-column>
</el-table>
<div v-show="!listLoading" class="pagination-container">
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="listQuery.page" :page-sizes="[10,20,30, 50]"
:page-size="listQuery.limit" layout="total, sizes, prev, pager, next, jumper" :total="total">
</el-pagination>
</div>
<el-dialog title="编辑" v-model="dialogFormVisible" size='small'>
<el-form :model="tempForm" label-position="left" label-width="70px">
<el-form-item label="权限">
<el-select style="width: 100%" v-model="tempForm.roles" multiple placeholder="请选择">
<el-option v-for="item in roleOptions" :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="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="updateUser">确 定</el-button>
</div>
</el-dialog>
<el-dialog title="编辑简介" v-model="dialogInfoVisible">
<el-form :model="infoForm">
<el-form-item label="昵称">
<el-input v-model="infoForm.display_name"></el-input>
</el-form-item>
<el-form-item label="简介">
<el-input type="textarea" :autosize="{ minRows: 2}" v-model="infoForm.introduction"></el-input>
</el-form-item>
<el-form-item label="头像">
</el-form-item>
<div style='width:200px;height:200px;'>
<singleImageUpload2 v-model="infoForm.image"></singleImageUpload2>
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogInfoVisible = false">取 消</el-button>
<el-button type="primary" @click="submitInfo">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { getRoleList, updateInfo } from 'api/adminUser';
import { getUserList } from 'api/remoteSearch';
import { objectMerge } from 'utils';
import singleImageUpload2 from 'components/Upload/singleImage2';
export default {
name: 'adminUsersList',
components: { singleImageUpload2 },
data() {
return {
list: null,
total: null,
listLoading: true,
listQuery: {
page: 1,
limit: 20,
role: '',
uid: undefined,
display_name: ''
},
roleOptions: [],
dialogFormVisible: false,
tempForm: {
uid: null,
roles: []
},
dialogInfoVisible: false,
infoForm: {
uid: null,
image: '',
display_name: '',
introduction: ''
}
}
},
computed: {
...mapGetters([
'roles'
]),
hasRoleEdit() {
if (this.roles.indexOf('admin') >= 0) {
return true;
} else {
return false;
}
}
},
created() {
this.getList();
this.getAdminRoleList();
},
methods: {
getList() {
getUserList(this.listQuery).then(response => {
const data = response.data;
this.list = data.items;
this.total = data.count;
this.listLoading = false;
})
},
resetFilter() {
this.listQuery = {
page: 1,
limit: 20,
role: '',
uid: undefined,
display_name: ''
}
this.getList();
},
handleSizeChange(val) {
this.listQuery.limit = val;
this.getList();
},
handleCurrentChange(val) {
this.listQuery.page = val;
this.getList();
},
handleFilter() {
this.getList();
},
getAdminRoleList() {
getRoleList().then(response => {
const roleMap = response.data.role_map;
const keyArr = Object.keys(roleMap);
this.roleOptions = keyArr.map(v => ({ value: v, label: roleMap[v] }));
})
},
handleEdit(row) {
this.dialogFormVisible = true;
objectMerge(this.tempForm, row);
},
updateUser() {
updateInfo(this.tempForm).then(() => {
this.$notify({
title: '成功',
message: '修改成功',
type: 'success'
});
for (const item of this.list) {
if (item.uid === this.tempForm.uid) {
const index = this.list.indexOf(item);
this.list[index] = objectMerge(this.list[index], this.tempForm);
break
}
}
this.dialogFormVisible = false;
});
},
handleInfo(row) {
this.dialogInfoVisible = true;
objectMerge(this.infoForm, row);
},
submitInfo() {
updateInfo(this.infoForm).then(() => {
this.$notify({
title: '成功',
message: '修改成功',
type: 'success'
});
for (const item of this.list) {
if (item.uid === this.infoForm.uid) {
const index = this.list.indexOf(item);
this.list[index] = objectMerge(this.list[index], this.infoForm);
break
}
}
this.dialogInfoVisible = false;
});
}
}
}
</script>

View File

@@ -0,0 +1,61 @@
<template>
<div class="errorpage-container"> 404
<splitPane v-on:resize="resize" split="vertical">
<template slot="paneL">
<div class="left-container"></div>
</template>
<template slot="paneR">
<splitPane split="horizontal">
<template slot="paneL">
<div class="top-container"></div>
</template>
<template slot="paneR">
<div class="bottom-container">
</div>
</template>
</splitPane>
</template>
</splitPane>
</div>
</template>
<script>
import splitPane from 'components/SplitPane/SplitPane'
export default {
components: { splitPane },
methods: {
resize() {
console.log('resize')
}
}
};
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.errorpage-container{
position: relative;
height: 100vh;
}
.left-container {
background-color: #F38181;
height:100%;
}
.right-container {
background-color: #FCE38A;
height: 200px;
}
.top-container {
background-color: #FCE38A;
width: 100%;
height: 100%;
}
.bottom-container {
width: 100%;
background-color: #95E1D3;
height: 100%;
}
</style>

View File

@@ -0,0 +1,22 @@
<template>
<div class="components-container">
<code>公司做的后台主要是一个cms系统公司也是已自媒体为核心的所以富文本是后台很核心的功能在选择富文本的过程中也走了不少的弯路市面上常见的富文本都基本用过了最终选择了tinymce</code>
<div class="editor-container">
<MdEditor id='contentEditor' ref="contentEditor" v-model='content' :height="150"></MdEditor>
</div>
</div>
</template>
<script>
import MdEditor from 'components/MdEditor';
export default {
components: { MdEditor },
data() {
return {
content: 'Simplemde'
}
}
};
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="components-container">
<code>公司做的后台主要是一个cms系统公司也是已自媒体为核心的所以富文本是后台很核心的功能在选择富文本的过程中也走了不少的弯路市面上常见的富文本都基本用过了最终选择了tinymce</code>
<div class="editor-container">
<Tinymce :height=200 ref="editor" v-model="content"></Tinymce>
</div>
<!--<div class='editor-content'>
{{content}}
</div>-->
</div>
</template>
<script>
import Tinymce from 'components/Tinymce';
export default {
components: { Tinymce },
data() {
return {
content: 'Tinymce'
}
},
methods: {
}
};
</script>

View File

@@ -0,0 +1,75 @@
<template>
<div class="dashboard-editor-container">
<div class=" clearfix">
<PanThumb style="float: left" :image="avatar"> 你的权限:
<span class="pan-info-roles" v-for="item in roles">{{item}}</span>
</PanThumb>
<div class="info-container">
<span class="display_name">{{name}}</span>
<span style='font-size:20px;padding-top:20px;display:inline-block;'>赶紧把你们想要的快捷键报给产品锦鲤!</span>
</div>
</div>
<div>
<img class='emptyGif' :src="emptyGif" >
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import PanThumb from 'components/PanThumb';
import emptyGif from 'assets/gifs/business_fella.gif';
export default {
name: 'dashboard-default',
components: { PanThumb },
data() {
return {
emptyGif
}
},
computed: {
...mapGetters([
'name',
'avatar',
'email',
'uid',
'introduction',
'roles'
])
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.emptyGif {
display: block;
width: 45%;
margin: 0 auto;
}
.dashboard-editor-container {
background-color: #e3e3e3;
min-height: 100vh;
margin-top: -50px;
padding: 100px 60px 0px;
.pan-info-roles {
font-size: 12px;
font-weight: 700;
color: #333;
display: block;
}
.info-container {
position: relative;
margin-left: 190px;
height: 150px;
line-height: 200px;
.display_name {
font-size: 48px;
line-height: 48px;
color: #212121;
position: absolute;
top: 25px;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="articlesChart-container">
<span class="articlesChart-container-title">每天撸文</span>
<line-chart :listData='listData' ></line-chart>
</div>
</template>
<script>
import LineChart from 'components/Charts/line';
export default {
name: 'articlesChart',
components: { LineChart },
props: {
listData: {
type: Array,
default: [],
require: true
}
},
data() {
return {}
}
}
</script>
<style>
.articlesChart-container {
width: 100%;
}
.articlesChart-container-title {
color: #7F8C8D;
font-size: 16px;
display: inline-block;
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,284 @@
<template>
<div class="dashboard-editor-container">
<div class=" clearfix">
<PanThumb style="float: left" :image="avatar"> 你的权限:
<span class="pan-info-roles" v-for="item in roles">{{item}}</span>
</PanThumb>
<div class="info-container">
<span class="display_name">{{name}}</span>
<div class="info-wrapper">
<router-link class="info-item" :to="'/article/wscnlist?uid='+uid">
<span class="info-item-num">{{statisticsData.article_count | toThousandslsFilter}}</span>
<span class="info-item-text">文章</span>
<wscn-icon-svg icon-class="a" class="dashboard-editor-icon"/>
</router-link>
<div class="info-item" style="cursor: auto">
<span class="info-item-num"> {{statisticsData.pageviews_count | toThousandslsFilter}}</span>
<span class="info-item-text">浏览量</span>
<wscn-icon-svg icon-class="b" class="dashboard-editor-icon"/>
</div>
<router-link class="info-item" :to="'/comment/commentslist?res_author_id='+uid">
<span class="info-item-num">{{statisticsData.comment_count | toThousandslsFilter}}</span>
<span class="info-item-text">评论</span>
<wscn-icon-svg icon-class="c" class="dashboard-editor-icon"/>
</router-link>
</div>
</div>
</div>
<div class="btn-group">
<router-link class="pan-btn blue-btn" to="/article/create">发表文章</router-link>
<router-link class="pan-btn light-blue-btn" to="/livenews/create">发布快讯</router-link>
<router-link class="pan-btn red-btn" to="/push/create">推送</router-link>
<router-link class="pan-btn pink-btn" to="/comment/commentslist">评论管理</router-link>
<router-link class="pan-btn green-btn" to="/article/wscnlist">文章列表</router-link>
<router-link class="pan-btn tiffany-btn" to="/livenews/list">实时列表</router-link>
</div>
<div class="clearfix main-dashboard-container">
<div class="chart-container">
<MonthKpi style="border-bottom: 1px solid #DEE1E2;"
:articlesComplete='statisticsData.month_article_count'></MonthKpi>
<ArticlesChart :listData='statisticsData.week_article'></ArticlesChart>
</div>
<div class="recent-articles-container">
<div class="recent-articles-title">最近撸了</div>
<div class="recent-articles-wrapper">
<template v-if="recentArticles.length!=0">
<div class="recent-articles-item" v-for="item in recentArticles">
<span class="recent-articles-status">{{item.status | statusFilter}}</span>
<router-link class="recent-articles-content" :to="'/article/edit/'+item.id">
<span>{{item.title}}</span>
</router-link>
<span class="recent-articles-time"><i style="padding-right: 4px;" class="el-icon-time"></i>{{item.display_time | parseTime('{m}-{d} {h}:{i}')}}</span>
</div>
</template>
<template v-else>
<div class="recent-articles-emptyTitle">你太懒了最近都没有撸</div>
<!--<img class="emptyGif" :src="emptyGif">-->
</template>
</div>
<router-link class="recent-articles-more" :to="'/article/wscnlist?uid='+uid">
Show more
</router-link>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import PanThumb from 'components/PanThumb';
import MonthKpi from './monthKpi';
import ArticlesChart from './articlesChart';
// import { getStatistics } from 'api/article';
import emptyGif from 'assets/compbig.gif';
export default {
name: 'dashboard-editor',
components: { PanThumb, MonthKpi, ArticlesChart },
data() {
return {
chart: null,
statisticsData: {
article_count: undefined,
comment_count: undefined,
latest_article: [],
month_article_count: undefined,
pageviews_count: undefined,
week_article: []
},
emptyGif
}
},
created() {
this.fetchData();
},
computed: {
...mapGetters([
'name',
'avatar',
'email',
'uid',
'introduction',
'roles'
]),
recentArticles() {
return this.statisticsData.latest_article.slice(0, 7)
}
},
methods: {
fetchData() {
// getStatistics().then(response => {
// this.statisticsData = response.data;
// this.statisticsData.week_article = this.statisticsData.week_article.slice().reverse();
// })
}
},
filters: {
statusFilter(status) {
const statusMap = {
published: '已发布',
draft: '草稿中',
deleted: '已删除'
};
return statusMap[status];
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.emptyGif {
width: 100%;
height: 100%;
}
.recent-articles-emptyTitle {
font-size: 16px;
color: #95A5A6;
padding-top: 20px;
text-align: center;
}
.dashboard-editor-container {
padding: 30px 50px;
.pan-info-roles {
font-size: 12px;
font-weight: 700;
color: #333;
display: block;
}
.info-container {
position: relative;
margin-left: 190px;
height: 150px;
line-height: 200px;
.display_name {
font-size: 48px;
line-height: 48px;
color: #212121;
position: absolute;
top: 25px;
}
.info-wrapper {
line-height: 40px;
position: absolute;
bottom: 0px;
.info-item {
cursor: pointer;
display: inline-block;
margin-right: 95px;
.info-item-num {
color: #212121;
font-size: 24px;
display: inline-block;
padding-right: 5px;
}
.info-item-text {
color: #727272;
font-size: 14px;
padding-right: 5px;
display: inline-block;
}
}
}
.dashboard-editor-icon {
width: 22px;
height: 22px;
}
}
.btn-group {
margin: 30px 36px 30px 0;
}
.main-dashboard-container {
width: 100%;
position: relative;
border: 1px solid #DEE1E2;
padding: 0 10px;
}
.chart-container {
float: left;
position: relative;
padding-right: 10px;
width: 40%;
border-right: 1px solid #DEE1E2;
}
.recent-articles-container {
padding: 12px 12px 0px;
float: left;
width: 60%;
position: relative;
.recent-articles- {
&title {
font-size: 16px;
color: #95A5A6;
letter-spacing: 1px;
padding-left: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #DEE1E2;
}
&more {
color: #2C3E50;
font-size: 12px;
float: right;
margin-right: 25px;
line-height: 40px;
&:hover {
color: #3A71A8;
}
}
&wrapper {
padding: 0 20px 0 22px;
.recent-articles- {
&item {
cursor: pointer;
padding: 16px 100px 16px 16px;
border-bottom: 1px solid #DEE1E2;
position: relative;
&:before {
content: "";
height: 104%;
width: 0px;
background: #30B08F;
display: inline-block;
position: absolute;
opacity: 0;
left: 0px;
top: -2px;
transition: 0.3s ease all;
}
&:hover {
&:before {
opacity: 1;
width: 3px;
}
}
}
&status {
font-size: 12px;
display: inline-block;
color: #9B9B9B;
padding-right: 6px;
}
&content {
font-size: 13px;
color: #2C3E50;
&:hover {
color: #3A71A8;
}
}
&time {
position: absolute;
right: 16px;
top: 16px;
color: #9B9B9B;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<div class="monthKpi-container">
<span class="monthKpi-container-title">{{month}}</span>
<BarPercent class="monthKpi-container-chart" :dataNum='articlesComplete'></BarPercent>
<span class="monthKpi-container-text">文章完成比例</span>
<span class="monthKpi-container-count">{{articlesComplete}}<b></b></span>
</div>
</template>
<script>
import BarPercent from 'components/Charts/barPercent';
export default {
name: 'monthKpi',
components: { BarPercent },
props: {
articlesComplete: {
type: Number
}
},
data() {
return {
month: new Date().getMonth() + 1
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.monthKpi-container{
width: 100%;
}
.monthKpi-container-title {
color: #7F8C8D;
font-size: 16px;
display: inline-block;
margin-top: 10px;
}
.monthKpi-container-chart {
margin-left: 100px;
margin-bottom: 4px;
}
.monthKpi-container-text {
margin-left: 112px;
color: #9EA7B3;
font-size: 12px;
}
.monthKpi-container-count {
color: #30B08F;
font-size: 34px;
position: absolute;
left: 260px;
top: 60px;
b {
padding-left: 10px;
color: #9EA7B3;
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<div class="dashboard-container">
<component v-bind:is="currentRole"> </component>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import EditorDashboard from './editor/index';
import DefaultDashboard from './default/index';
export default {
name: 'dashboard',
components: { EditorDashboard, DefaultDashboard },
data() {
return {
currentRole: 'EditorDashboard'
}
},
computed: {
...mapGetters([
'name',
'avatar',
'email',
'introduction',
'roles'
])
},
created() {
if (this.roles.indexOf('admin') >= 0) {
return;
}
const isEditor = this.roles.some(v => v.indexOf('editor') >= 0)
if (!isEditor) {
this.currentRole = 'DefaultDashboard';
}
}
}
</script>

82
src/views/error/401.vue Normal file
View File

@@ -0,0 +1,82 @@
<template>
<div class="errPage-container">
<el-button @click="back" icon='arrow-left' class="pan-back-btn">返回</el-button>
<el-row>
<el-col :span="12">
<h1 class="text-jumbo text-ginormous">Oops!</h1>
<h2>你没有权限去该页面</h2>
<h6>如有不满请联系你领导</h6>
<ul class="list-unstyled">
<li>或者你可以去:</li>
<li class="link-type">
<router-link to="/dashboard">回首页</router-link>
</li>
<li class="link-type"><a href="https://www.taobao.com/">随便看看</a></li>
<li><a @click="dialogVisible=true" href="#">点我看图</a></li>
</ul>
</el-col>
<el-col :span="12">
<img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream.">
</el-col>
</el-row>
<el-dialog title="随便看" v-model="dialogVisible" size="large">
<img class="pan-img" :src="ewizardClap">
</el-dialog>
</div>
</template>
<script>
import errGif from 'assets/401.gif';
import ewizardClap from 'assets/gifs/wizard_clap.gif';
export default {
data() {
return {
errGif: errGif + '?' + +new Date(),
ewizardClap,
dialogVisible: false
}
},
methods: {
back() {
this.$router.go(-1)
}
}
};
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.errPage-container {
width: 800px;
margin: 100px auto;
.pan-back-btn {
background: #008489;
color: #fff;
}
.pan-gif {
margin: 0 auto;
display: block;
}
.pan-img{
display: block;
margin: 0 auto;
}
.text-jumbo {
font-size: 60px;
font-weight: 700;
color: #484848;
}
.list-unstyled {
font-size: 14px;
li {
padding-bottom: 5px;
}
a {
color: #008489;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
</style>

61
src/views/error/404.vue Normal file
View File

@@ -0,0 +1,61 @@
<template>
<div class="errorpage-container"> 404
<splitPane v-on:resize="resize" split="vertical">
<template slot="paneL">
<div class="left-container"></div>
</template>
<template slot="paneR">
<splitPane split="horizontal">
<template slot="paneL">
<div class="top-container"></div>
</template>
<template slot="paneR">
<div class="bottom-container">
</div>
</template>
</splitPane>
</template>
</splitPane>
</div>
</template>
<script>
import splitPane from 'components/SplitPane/SplitPane'
export default {
components: { splitPane },
methods: {
resize() {
console.log('resize')
}
}
};
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.errorpage-container{
position: relative;
height: 100vh;
}
.left-container {
background-color: #F38181;
height:100%;
}
.right-container {
background-color: #FCE38A;
height: 200px;
}
.top-container {
background-color: #FCE38A;
width: 100%;
height: 100%;
}
.bottom-container {
width: 100%;
background-color: #95E1D3;
height: 100%;
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<section class="app-main" style="min-height: 100%">
<transition name="fade" mode="out-in">
<router-view :key="key"></router-view>
</transition>
</section>
</template>
<script>
export default {
name: 'AppMain',
computed: {
key() {
return this.$route.name !== undefined
? this.$route.name + +new Date()
: this.$route + +new Date()
}
}
}
</script>

View File

@@ -0,0 +1,98 @@
<template>
<div class="app-wrapper" :class="{hideSidebar:!sidebar.opened}">
<div class="sidebar-wrapper">
<Sidebar class="sidebar-container"/>
</div>
<div class="main-container">
<Navbar/>
<App-main/>
</div>
</div>
</template>
<script>
import { Navbar, Sidebar, AppMain } from 'views/layout';
import store from 'store';
import router from 'router';
import permission from 'store/permission';
// import { Loading } from 'element-ui';
// let loadingInstance;
export default {
name: 'layout',
components: {
Navbar,
Sidebar,
AppMain
},
computed: {
sidebar() {
return this.$store.state.app.sidebar;
}
},
beforeRouteEnter: (to, from, next) => {
console.log('b')
const roles = store.getters.roles;
if (roles.length !== 0) {
next();
return
}
// loadingInstance = Loading.service({ fullscreen: true, text: '玩命加载中' });
store.dispatch('GetInfo').then(() => {
permission.init({
roles: store.getters.roles,
router: router.options.routes
});
// loadingInstance.close();
next();
}).catch(err => {
// loadingInstance.close();
console.log(err);
});
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
@import "src/styles/mixin.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
padding-left: 180px;
&.hideSidebar {
padding-left: 40px;
.sidebar-wrapper {
transform: translate(-140px, 0);
.sidebar-container {
transform: translate(132px, 0);
}
&:hover {
transform: translate(0, 0);
.sidebar-container {
transform: translate(0, 0);
}
}
}
}
.sidebar-wrapper {
width: 180px;
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow-x: hidden;
transition: all .28s ease-out;
@include scrollBar;
}
.sidebar-container {
transition: all .28s ease-out;
}
.main-container {
width: 100%;
min-height: 100%;
transition: all .28s ease-out;
}
}
</style>

View File

@@ -0,0 +1,48 @@
<template>
<el-breadcrumb class="app-levelbar" separator="/">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item">
<router-link v-if='item.redirect==="noredirect"||index==levelList.length-1' to="" class="no-redirect">{{item.name}}</router-link>
<router-link v-else :to="item.path">{{item.name}}</router-link>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script>
export default {
created() {
this.getBreadcrumb()
},
data() {
return {
levelList: null
}
},
methods: {
getBreadcrumb() {
let matched = this.$route.matched.filter(item => item.name);
const first = matched[0];
if (first && (first.name !== '首页' || first.path !== '')) {
matched = [{ name: '首页', path: '/' }].concat(matched)
}
this.levelList = matched;
}
},
watch: {
$route() {
this.getBreadcrumb();
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.app-levelbar.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 10px;
.no-redirect{
color: #97a8be;
cursor:text;
}
}
</style>

107
src/views/layout/Navbar.vue Normal file
View File

@@ -0,0 +1,107 @@
<template>
<el-menu class="navbar" mode="horizontal">
<Hamburger class="hamburger-container" :toggleClick="toggleSideBar" :isActive="sidebar.opened"></Hamburger>
<levelbar></levelbar>
<ErrLog v-if="log.length>0" class="errLog-container" :logsList="log"></ErrLog>
<el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper">
<img class="user-avatar" :src="avatar+'?imageView2/1/w/80/h/80'">
<i class="el-icon-caret-bottom"/>
</div>
<el-dropdown-menu class="user-dropdown" slot="dropdown">
<router-link class='inlineBlock' to="/">
<el-dropdown-item>
首页
</el-dropdown-item>
</router-link>
<router-link class='inlineBlock' to="/admin/profile">
<el-dropdown-item>
设置
</el-dropdown-item>
</router-link>
<el-dropdown-item divided><span @click="logout" style="display:block;">退出登录</span></el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-menu>
</template>
<script>
import { mapGetters } from 'vuex'
import Levelbar from './Levelbar';
import Hamburger from 'components/Hamburger';
import ErrLog from 'components/ErrLog';
import errLogStore from 'store/errLog';
export default {
components: {
Levelbar,
Hamburger,
ErrLog
},
data() {
return {
log: errLogStore.state.errLog
}
},
computed: {
...mapGetters([
'sidebar',
'name',
'avatar'
])
},
methods: {
toggleSideBar() {
this.$store.dispatch('ToggleSideBar')
},
logout() {
this.$store.dispatch('LogOut').then(() => {
this.$router.push({ path: '/login' })
});
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.navbar {
height: 50px;
line-height: 50px;
border-radius: 0px !important;
.hamburger-container {
line-height: 58px;
height: 50px;
float: left;
padding: 0 10px;
}
.errLog-container {
display: inline-block;
position: absolute;
right: 150px;
}
.avatar-container {
height: 50px;
display: inline-block;
position: absolute;
right: 35px;
.avatar-wrapper {
cursor: pointer;
margin-top:5px;
position: relative;
.user-avatar {
width: 40px;
height: 40px;
border-radius: 10px;
}
.el-icon-caret-bottom {
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
</style>

View File

@@ -0,0 +1,48 @@
<template>
<el-menu mode="vertical" theme="dark" :default-active="$route.path">
<template v-for="item in permissionRoutes" v-if="!item.hidden">
<el-submenu :index="item.name" v-if="!item.noDropdown">
<template slot="title">
<wscn-icon-svg :icon-class="item.icon||'wenzhang1'"/>
{{item.name}}
</template>
<router-link v-for="child in item.children" :key="child.path" v-if="!child.hidden"
class="title-link" :to="item.path+'/'+child.path + '#'+ +new Date()">
<el-menu-item :index="item.path+'/'+child.path">
{{child.name}}
</el-menu-item>
</router-link>
</el-submenu>
<router-link v-if="item.noDropdown&&item.children.length>0" class="title-link"
:to="item.path+'/'+item.children[0].path">
<el-menu-item
:index="item.path+'/'+item.children[0].path +'#'+ +new Date()">
<wscn-icon-svg :icon-class="item.icon||'geren1'"/>
{{item.children[0].name}}
</el-menu-item>
</router-link>
</template>
</el-menu>
</template>
<script>
import permissionRoutes from 'store/permission';
export default {
name: 'Sidebar',
data() {
return {
permissionRoutes: permissionRoutes.get()
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.el-menu {
min-height: 100%;
}
.wscn-icon {
margin-right: 10px;
}
</style>

View File

@@ -0,0 +1,7 @@
export { default as Navbar } from './Navbar';
export { default as Sidebar } from './Sidebar';
export { default as Levelbar } from './Sidebar';
export { default as AppMain } from './AppMain';

View File

@@ -0,0 +1,10 @@
<script>
export default {
name: 'authredirect',
created() {
const hash = window.location.search.slice(1);
window.opener.location.href = window.location.origin + '/login#' + hash;
window.close();
}
}
</script>

188
src/views/login/index.vue Normal file
View File

@@ -0,0 +1,188 @@
<template>
<div class="login-container">
<el-form autoComplete="on" :model="loginForm" :rules="loginRules" ref="loginForm" label-position="left"
label-width="0px"
class="card-box login-form">
<h3 class="title">系统登录</h3>
<el-form-item prop="email">
<span class="svg-container"><wscn-icon-svg icon-class="jiedianyoujian"/></span>
<el-input name="email" type="text" v-model="loginForm.email" autoComplete="on"
placeholder="邮箱"></el-input>
</el-form-item>
<el-form-item prop="password">
<span class="svg-container"><wscn-icon-svg icon-class="mima"/></span>
<el-input name="password" type="password" @keyup.enter.native="handleLogin" v-model="loginForm.password"
autoComplete="on" placeholder="密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width:100%;" :loading="loading" @click.native.prevent="handleLogin">
登录
</el-button>
</el-form-item>
<router-link to="/sendpwd" class="forget-pwd">
忘记密码?(或首次登录)
</router-link>
</el-form>
<el-dialog title="第三方验证" v-model="showDialog">
邮箱登录成功,请选择第三方验证
<socialSign></socialSign>
</el-dialog>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { isWscnEmail } from 'utils/validate';
import { getQueryObject } from 'utils';
import socialSign from './socialsignin';
export default {
components: { socialSign },
name: 'login',
data() {
const validateEmail = (rule, value, callback) => {
if (!isWscnEmail(value)) {
callback(new Error('请输入正确的合法邮箱'));
} else {
callback();
}
};
const validatePass = (rule, value, callback) => {
if (value.length < 6) {
callback(new Error('密码不能小于6位'));
} else {
callback();
}
};
return {
loginForm: {
email: '',
password: ''
},
loginRules: {
email: [
{ required: true, trigger: 'blur', validator: validateEmail }
],
password: [
{ required: true, trigger: 'blur', validator: validatePass }
]
},
loading: false,
showDialog: false
}
},
computed: {
...mapGetters([
'status',
'auth_type'
])
},
methods: {
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true;
this.$store.dispatch('LoginByEmail', this.loginForm).then(() => {
this.loading = false;
this.$router.push({ path: '/' });
// this.showDialog = true;
}).catch(err => {
this.$message.error(err);
this.loading = false;
});
} else {
console.log('error submit!!');
return false;
}
});
},
afterQRScan() {
const hash = window.location.hash.slice(1);
const hashObj = getQueryObject(hash);
const originUrl = window.location.origin;
history.replaceState({}, '', originUrl);
const codeMap = {
wechat: 'code',
tencent: 'code'
};
const codeName = hashObj[codeMap[this.auth_type]];
if (!codeName) {
alert('第三方登录失败');
} else {
this.$store.dispatch('LoginByThirdparty', codeName).then(() => {
this.$router.push({ path: '/' });
});
}
}
},
created() {
window.addEventListener('hashchange', this.afterQRScan);
},
destroyed() {
window.removeEventListener('hashchange', this.afterQRScan);
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoprd>
@import "src/styles/mixin.scss";
.login-container {
@include relative;
height: 100vh;
background-color: #2d3a4b;
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px #293444 inset !important;
-webkit-text-fill-color: #fff !important;
}
input {
background: transparent;
border: 0px;
-webkit-appearance: none;
border-radius: 0px;
padding: 12px 5px 12px 15px;
color: #eeeeee;
height: 47px;
}
.el-input {
display: inline-block;
height: 47px;
width: 85%;
}
.svg-container {
padding: 6px 5px 6px 15px;
color: #889aa4;
}
.title {
font-size: 26px;
font-weight: 400;
color: #eeeeee;
margin: 0px auto 40px auto;
text-align: center;
font-weight: bold;
}
.login-form {
position: absolute;
left: 0;
right: 0;
width: 400px;
padding: 35px 35px 15px 35px;
margin: 120px 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;
}
.forget-pwd {
color: #fff;
}
}
</style>

178
src/views/login/reset.vue Normal file
View File

@@ -0,0 +1,178 @@
<template>
<div class="reset-container">
<el-form autoComplete="on" :model="resetForm" :rules="resetRules" ref="resetForm" label-position="left"
label-width="0px"
class="card-box reset-form">
<div>
<router-link to="/login" class="back-icon">
<i class="el-icon-arrow-left"></i>
</router-link>
<h3 class="title">重设密码</h3>
</div>
<el-form-item prop="email">
<el-input name="email" type="text" v-model="resetForm.email"
placeholder="邮箱"></el-input>
</el-form-item>
<el-form-item prop="code">
<el-input name="code" type="text" v-model="resetForm.code"
placeholder="验证码"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input name="password" :type="passwordType" v-model="resetForm.password"
placeholder="密码"></el-input>
</el-form-item>
<el-form-item prop="checkPass">
<el-input name="checkPass" :type="passwordType"
v-model="resetForm.checkPass"
placeholder="确认密码"></el-input>
<i @click="togglePasswordType" class="el-icon-information"></i>
</el-form-item>
<el-form-item style="width:100%;">
<el-button type="primary" style="width:100%;" :loading="loading" @click.native.prevent="setPWD">
修改密码
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { isWscnEmail } from 'utils/validate';
// import { restPWD } from 'api/login';
export default {
name: 'reset',
data() {
const validateEmail = (rule, value, callback) => {
if (!isWscnEmail(value)) {
callback(new Error('邮箱错误'));
} else {
callback();
}
};
const validaePass = (rule, value, callback) => {
if (value.length < 6) {
callback(new Error('密码不能小于6位'));
} else {
callback();
}
};
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== this.resetForm.password) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
};
return {
resetForm: {
email: '',
password: '',
checkPass: '',
code: ''
},
passwordType: 'password',
resetRules: {
email: [
{ required: true, trigger: 'blur', validator: validateEmail }
],
password: [
{ required: true, trigger: 'blur', validator: validaePass }
],
checkPass: [
{ required: true, trigger: 'blur', validator: validatePass2 }
],
code: [
{ required: true, message: '必填项', trigger: 'blur' }
]
},
loading: false
}
},
methods: {
setPWD() {
this.loading = true;
const _this = this;
this.$refs.resetForm.validate(valid => {
if (valid) {
const data = {
email: this.resetForm.email,
code: this.resetForm.code,
new_password: this.resetForm.checkPass
};
// restPWD(data).then(() => {
// this.$message.success('密码设置成功,五秒后调整到登录页');
// setTimeout(() => {
// _this.$router.push({ path: '/login' })
// }, 5 * 1000)
// });
} else {
this.$message.error('error submit!!');
}
this.loading = false;
});
},
togglePasswordType() {
if (this.passwordType === 'text') {
this.passwordType = 'password'
} else {
this.passwordType = 'text'
}
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss">
@import "src/styles/mixin.scss";
.reset-container {
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px #293444 inset !important;
-webkit-text-fill-color: #3E3E3E !important;
}
@include relative;
height: 100vh;
background-color: #324057;
.back-icon {
float: left;
margin-top: 5px;
}
.el-icon-information {
position: absolute;
right: -18px;
top: 10px;
}
.reset-form {
position: absolute;
left: 0;
right: 0;
width: 350px;
padding: 35px 35px 15px 35px;
margin: 120px auto;
}
.card-box {
padding: 20px;
box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.06), 0 1px 0px 0 rgba(0, 0, 0, 0.02);
-webkit-border-radius: 5px;
border-radius: 5px;
-moz-border-radius: 5px;
background-clip: padding-box;
margin-bottom: 20px;
background-color: #F9FAFC;
width: 400px;
border: 2px solid #8492A6;
}
.title {
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
}
</style>

117
src/views/login/sendpwd.vue Normal file
View File

@@ -0,0 +1,117 @@
<template>
<div class="sendpwd-container">
<el-form autoComplete="on" :model="resetForm" :rules="resetRules" ref="resetForm" label-position="left"
label-width="0px"
class="card-box reset-form">
<div>
<router-link to="/login" class="back-icon">
<i class="el-icon-arrow-left"></i>
</router-link>
<h3 class="title">发送验证码至邮箱</h3>
</div>
<el-form-item prop="email">
<el-input name="email" type="text" v-model="resetForm.email"
placeholder="邮箱"></el-input>
</el-form-item>
<el-form-item style="width:100%;">
<el-button type="primary" style="width:100%;" :loading="loading" @click.native.prevent="handleSendPWD">
发送验证码至邮箱
</el-button>
</el-form-item>
<router-link to="/reset">
<el-button type="info" style="width:100%;">
已收到验证码,去重设密码
</el-button>
</router-link>
</el-form>
</div>
</template>
<script>
import { isWscnEmail } from 'utils/validate';
// import { sendPWD2Email } from 'api/login';
export default {
name: 'reset',
data() {
const validateEmail = (rule, value, callback) => {
if (!isWscnEmail(value)) {
callback(new Error('请输入正确的邮箱'));
} else {
callback();
}
};
return {
resetForm: {
email: ''
},
resetRules: {
email: [
{ required: true, trigger: 'blur' },
{ validator: validateEmail }
]
},
loading: false
}
},
methods: {
handleSendPWD() {
this.loading = true;
this.$refs.resetForm.validate(valid => {
if (valid) {
// sendPWD2Email(this.resetForm.email).then(() => {
// this.$message.success('密码有发送至邮箱,请查收')
// });
} else {
this.$message.error('错误提交!!');
}
this.loading = false;
});
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss">
.sendpwd-container {
height: 100vh;
background-color: #2d3a4b;
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px #293444 inset !important;
-webkit-text-fill-color: #3E3E3E !important;
}
.back-icon{
float: left;
margin-top: 5px;
}
.reset-form {
position: absolute;
left: 0;
right: 0;
width: 350px;
padding: 35px 35px 15px 35px;
margin: 120px auto;
}
.card-box {
padding: 20px;
box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.06), 0 1px 0px 0 rgba(0, 0, 0, 0.02);
-webkit-border-radius: 5px;
border-radius: 5px;
-moz-border-radius: 5px;
background-clip: padding-box;
margin-bottom: 20px;
background-color: #F9FAFC;
width: 400px;
border: 2px solid #8492A6;
}
.title {
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<div class="social-signup-container">
<div class="sign-btn" @click="wechatHandleClick('wechat')">
<span class="wx-svg-container"><wscn-icon-svg icon-class="weixin" class="icon"/></span>
微信
</div>
<div class="sign-btn" @click="tencentHandleClick('tencent')">
<span class="qq-svg-container"><wscn-icon-svg icon-class="QQ" class="icon"/></span>
QQ
</div>
</div>
</template>
<script>
import openWindow from 'utils/openWindow';
export default {
name: 'social-signin',
methods: {
wechatHandleClick(thirdpart) {
this.$store.commit('SET_AUTH_TYPE', thirdpart);
const appid = 'wxff5aaaad72308d46';
const redirect_uri = encodeURIComponent('http://wallstreetcn.com/auth/redirect?redirect=' + window.location.origin + '/authredirect');
const url = 'https://open.weixin.qq.com/connect/qrconnect?appid=' + appid + '&redirect_uri=' + redirect_uri + '&response_type=code&scope=snsapi_login#wechat_redirect';
openWindow(url, thirdpart, 540, 540);
},
tencentHandleClick(thirdpart) {
this.$store.commit('SET_AUTH_TYPE', thirdpart);
const client_id = '101150108';
const redirect_uri = encodeURIComponent('http://wallstreetcn.com/auth/redirect?redirect=' + window.location.origin + '/authredirect');
const url = 'https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=' + client_id + '&redirect_uri=' + redirect_uri;
openWindow(url, thirdpart, 540, 540);
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.social-signup-container {
margin: 20px 0;
.sign-btn {
display: inline-block;
cursor: pointer;
}
.icon {
color: #fff;
font-size: 30px;
margin-top: 6px;
}
.wx-svg-container, .qq-svg-container {
display: inline-block;
width: 40px;
height: 40px;
line-height: 40px;
text-align: center;
padding-top: 1px;
border-radius: 4px;
margin-bottom: 20px;
margin-right: 5px;
}
.wx-svg-container {
background-color: #8dc349;
}
.qq-svg-container {
background-color: #6BA2D6;
margin-left: 50px;
}
}
</style>

View File

@@ -0,0 +1,102 @@
<template>
<div class="app-container mediaUpload-container">
<el-upload
class="upload-container"
action="https://upload.qbox.me"
:data="dataObj"
drag
:multiple="true"
:on-preview="handlePreview"
:on-remove="handleRemove"
:on-success="handleSuccess"
:before-upload="beforeUpload">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
</el-upload>
<template v-if='fileList.length!=0'>
<el-table
:data="fileList"
border
style="width: 100%">
<el-table-column label="名字">
<template scope="scope">
{{scope.row.name}}
</template>
</el-table-column>
<el-table-column label="url">
<template scope="scope">
{{scope.row.url}}
</template>
</el-table-column>
</el-table>
</template>
</div>
</template>
<script>
import { getToken } from 'api/qiniu';
export default{
data() {
return {
image_uri: [],
dataObj: { token: '', key: '' },
fileList: []
}
},
computed: {
token() {
return this.$store.getters.token
}
},
methods: {
beforeUpload() {
const _self = this;
return new Promise((resolve, reject) => {
getToken(this.token).then(response => {
const key = response.data.qiniu_key;
const token = response.data.qiniu_token;
this.addFile(key, response.data.qiniu_url);
_self._data.dataObj.token = token;
_self._data.dataObj.key = key;
resolve(true);
}).catch(err => {
console.log(err)
reject(false)
});
});
},
handleRemove(file, fileList) {
console.log(file, fileList);
},
handlePreview(file) {
console.log(file);
},
handleSuccess(response, file) {
const key = response.key;
for (let i = this.fileList.length; i--;) {
const item = this.fileList[i];
if (item.key === key) {
this.fileList[i].name = file.name;
return
}
}
},
addFile(key, url) {
this.fileList.push({
key,
url,
name: ''
})
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.mediaUpload-container {
.upload-container {
margin: 30px;
}
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<section class="app-main" style="min-height: 100%">
<transition name="fade" mode="out-in">
<router-view :key="key"></router-view>
</transition>
</section>
</template>
<script>
export default {
name: 'AppMain',
computed: {
key() {
return this.$route.name !== undefined
? this.$route.name
: this.$route
}
}
}
</script>

View File

@@ -0,0 +1,118 @@
<template>
<div class="fedUser-info-container">
<el-button type="success" v-if="hasPermission" style='position: absolute; top: 20px;left: 50%'
@click="updateForm">更新
</el-button>
<el-form style="margin:0 50px;width: 400px" label-position="left"
label-width="100px">
<el-form-item label="昵称">
<el-input v-model="form.base_info.display_name"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.base_info.new_password"></el-input>
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.base_info.email"></el-input>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.base_info.mobile"></el-input>
</el-form-item>
<el-form-item label="真实姓名">
<el-input v-model="form.extented_info.real_name"></el-input>
</el-form-item>
<el-form-item label="生日">
<el-input v-model="form.extented_info.birthday"></el-input>
</el-form-item>
<el-form-item label="性别">
<el-select v-model="form.base_info.gender" placeholder="请选择">
<el-option
v-for="item in genderOptions"
:key="item"
:label="item"
:value="item">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="教育背景">
<el-select v-model="form.extented_info.education" placeholder="请选择">
<el-option
v-for="item in educationOptions"
:key="item"
:label="item"
:value="item">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="收入">
<el-select v-model="form.extented_info.income" placeholder="请选择">
<el-option
v-for="item in incomeOptions"
:key="item"
:label="item"
:value="item">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="自我介绍">
<el-input
type="textarea"
:autosize="{ minRows: 4}"
placeholder="请输入内容"
v-model=" form.extented_info.introduction">
</el-input>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { updateUserInfo } from 'api/user';
export default {
name: 'fedUser-info',
props: ['info'],
data() {
return {
genderOptions: ['male', 'female', 'other'],
educationOptions: ['high_school', 'bachelor', 'master', 'Ph.D.', 'other'],
incomeOptions: ['3000', '5000', '8000', 'other']
}
},
computed: {
form() {
return this.info
},
hasPermission() {
return ~this.$store.getters.roles.indexOf('admin')
}
},
methods: {
updateForm() {
updateUserInfo(this.form).then(() => {
this.$notify({
title: '成功',
message: '更新成功',
type: 'success'
});
}).catch(err => {
console.log(err);
});
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.fedUser-detail-container {
}
</style>

125
src/views/user/detail.vue Normal file
View File

@@ -0,0 +1,125 @@
<template>
<div class="fedUser-detail-container" v-loading="!fetchSuccess">
<div v-if="fetchSuccess" class="top-container clearfix">
<el-col :span="5">
<img class="info-avatar" :src="userInfo.base_info.image">
</el-col>
<el-col :span="16" :offset="2">
<div class="info-item">
<span class="info-label">用户名</span>
<span class="info-text">{{userInfo.base_info.username}}</span>
</div>
<div class="info-item">
<span class="info-label">昵称</span>
<span class="info-text">{{userInfo.base_info.display_name}}</span>
</div>
<div class="info-item">
<span class="info-label">手机号</span>
<span class="info-text">{{userInfo.base_info.mobile}}</span>
</div>
<div class="info-item">
<span class="info-label">余额</span>
<span class="info-text">{{userInfo.banance}}</span>
</div>
<div class="info-item">
<span class="info-label">ios余额</span>
<span class="info-text">{{userInfo.ios_banance}}</span>
</div>
<div class="info-item">
<span class="info-label">注册日期</span>
<span class="info-text">{{userInfo.created_at | parseTime('{y}-{m}-{d} {h}:{i}')}} 注册渠道:{{userInfo.signup_method}}</span>
</div>
<div class="info-item">
<span class="info-label">最后登录</span>
<span class="info-text">{{userInfo.last_signin_time | parseTime('{y}-{m}-{d} {h}:{i}')}}</span>
</div>
</el-col>
</div>
<el-tabs v-if="fetchSuccess" v-model="activeTab">
<el-tab-pane label="基本信息" name="info">
<Info :info="userInfo"></Info>
</el-tab-pane>
<el-tab-pane label="评论记录" name="information">
<Comment :user_id="userInfo.uid"></Comment>
</el-tab-pane>
<!--<el-tab-pane label="消费记录" name="stream">
</el-tab-pane>-->
</el-tabs>
</div>
</template>
<script>
import { userInfo } from 'api/user';
import Info from './components/info';
import Comment from '../comment/commentsList'
export default {
name: 'fedUser-detail',
components: { Info, Comment },
data() {
return {
userInfo: {},
activeTab: 'info',
fetchSuccess: false
}
},
created() {
this.fetchData();
},
methods: {
fetchData() {
userInfo(this.$route.params.id).then(response => {
this.userInfo = response.data;
this.fetchSuccess = true;
}).catch(err => {
this.fetchSuccess = true;
console.log(err);
});
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.fedUser-detail-container {
padding: 30px;
.top-container {
margin-bottom: 30px;
.info-item {
line-height: 14px;
padding-bottom: 18px;
.info-label {
display: inline-block;
color: #1f2f3d;
font-size: 16px;
position: absolute;
}
.info-text {
margin-left: 120px;
font-size: 14px;
color: #5e6d82;
}
}
}
.info-avatar {
width: 200px;
height: 200px;
border-radius: 100%;
}
}
</style>

183
src/views/user/list.vue Normal file
View File

@@ -0,0 +1,183 @@
<template>
<div class="app-container topic-list-container">
<div class="filter-container">
<el-input
style="width:200px"
@keyup.enter.native="handleFilter"
class="filter-item"
placeholder="display_name"
v-model="display_name">
</el-input>
<el-input
style="width:200px"
@keyup.enter.native="handleFilter"
class="filter-item"
placeholder="username"
v-model="username">
</el-input>
<el-select v-model="status" placeholder="状态" >
<el-option
v-for="item in statusOptions"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
<el-button class="filter-item" style="margin-left: 30px;" type="primary" icon="search"
@click="handleFilter">搜索
</el-button>
</div>
<el-table :data="list" v-loading.body="listLoading" border fit highlight-current-row>
<el-table-column header prop="id" label="uid" width="160">
<template scope="scope">
<span style="margin-left: 10px">{{scope.row.uid}}</span>
</template>
</el-table-column>
<el-table-column label="display_name" show-overflow-tooltip>
<template scope="scope">
<router-link class="link-type" :to="'/user/'+scope.row.uid">
{{scope.row.display_name}}
</router-link>
</template>
</el-table-column>
<el-table-column label="username" show-overflow-tooltip>
<template scope="scope">
<router-link class="link-type" :to="'/user/'+scope.row.uid">
{{scope.row.username}}
</router-link>
</template>
</el-table-column>
<el-table-column label="手机号" width="150">
<template scope="scope">
<span>{{scope.row.mobile}}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align='center'>
<template scope="scope">
<el-button v-if='condition.status==""' size="small" type="warning" @click="handleModifyUserStatus('frozen',scope.row)">
注销用户
</el-button>
<el-button v-else type="info" size="small" @click="handleModifyUserStatus('',scope.row)">解禁用户
</el-button>
</template>
</el-table-column>
</el-table>
<div v-show="!listLoading" class="pagination-container">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="listQuery.page"
:page-sizes="[10,20,30, 50]"
:page-size="listQuery.limit"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</template>
<script>
import { usersList, modifyStatus } from 'api/user';
export default {
name: 'fedUserList',
data() {
return {
list: null,
total: null,
listLoading: true,
listQuery: {
page: 1,
limit: 20,
app_type: 'wscn',
condition: ''
},
display_name: undefined,
username: undefined,
status: '',
statusOptions: [{ label: '正常', value: '' }, { label: '已删除', value: 'frozen' }],
condition: {
status: ''
}
}
},
created() {
this.fetchList();
},
watch: {
display_name(value) {
if (!value) return;
this.condition = {
display_name: value
};
this.username = '';
},
username(value) {
if (!value) return;
this.condition = {
username: value
};
this.display_name = '';
}
},
methods: {
fetchList() {
this.condition.status = this.status;
this.listQuery.condition = JSON.stringify(this.condition);
usersList(this.listQuery).then(response => {
const data = response.data;
this.list = data.items;
this.total = data.total_count;
this.listLoading = false;
})
},
handleSizeChange(val) {
this.listQuery.limit = val;
this.fetchList();
},
handleCurrentChange(val) {
this.listQuery.page = val;
this.fetchList();
},
handleFilter() {
this.fetchList();
},
handleModifyUserStatus(status, row) {
const msg = status === 'frozen' ? '注销' : '恢复';
this.$confirm('是否确' + msg + '用户:' + row.display_name || row.username, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
modifyStatus(status, [row.uid]).then(() => {
this.$notify({
title: '成功',
message: msg + '成功',
type: 'success',
duration: 1600
});
for (const i of this.list) {
if (i.uid === row.uid) {
const index = this.list.indexOf(i);
this.list.splice(index, 1);
break;
}
}
done();
}).catch(() => {
done();
});
} else {
done();
}
}
})
}
}
}
</script>