init
This commit is contained in:
11
src/App.vue
Normal file
11
src/App.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default{
|
||||
name: 'APP'
|
||||
}
|
||||
</script>
|
28
src/api/qiniu.js
Normal file
28
src/api/qiniu.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import fetch, { tpFetch } from 'utils/fetch';
|
||||
|
||||
export function getToken() {
|
||||
return fetch({
|
||||
url: '/qiniu/upload/token',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
export function upload(data) {
|
||||
return tpFetch({
|
||||
url: 'https://upload.qbox.me',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* 外部uri转七牛uri*/
|
||||
export function netUpload(token, net_url) {
|
||||
const imgData = {
|
||||
net_url
|
||||
};
|
||||
return fetch({
|
||||
url: '/qiniu/upload/net/async',
|
||||
method: 'post',
|
||||
data: imgData
|
||||
});
|
||||
}
|
BIN
src/assets/401.gif
Normal file
BIN
src/assets/401.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 160 KiB |
BIN
src/assets/compbig.gif
Normal file
BIN
src/assets/compbig.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 793 KiB |
BIN
src/assets/custom-theme/fonts/element-icons.ttf
Normal file
BIN
src/assets/custom-theme/fonts/element-icons.ttf
Normal file
Binary file not shown.
BIN
src/assets/custom-theme/fonts/element-icons.woff
Normal file
BIN
src/assets/custom-theme/fonts/element-icons.woff
Normal file
Binary file not shown.
23959
src/assets/custom-theme/index.css
Normal file
23959
src/assets/custom-theme/index.css
Normal file
File diff suppressed because it is too large
Load Diff
126
src/assets/iconfont/iconfont.js
Normal file
126
src/assets/iconfont/iconfont.js
Normal file
@@ -0,0 +1,126 @@
|
||||
;(function(window) {
|
||||
|
||||
var svgSprite = '<svg>' +
|
||||
'' +
|
||||
'<symbol id="icon-zujian" viewBox="0 0 1024 1024">' +
|
||||
'' +
|
||||
'<path d="M568.6 0h454.9v454.9H568.6V0z m0 568.6h454.9v454.9H568.6V568.6zM0 568.6h454.9v454.9H0V568.6zM0 0h454.9v454.9H0V0z" fill="" ></path>' +
|
||||
'' +
|
||||
'</symbol>' +
|
||||
'' +
|
||||
'</svg>'
|
||||
var script = function() {
|
||||
var scripts = document.getElementsByTagName('script')
|
||||
return scripts[scripts.length - 1]
|
||||
}()
|
||||
var shouldInjectCss = script.getAttribute("data-injectcss")
|
||||
|
||||
/**
|
||||
* document ready
|
||||
*/
|
||||
var ready = function(fn) {
|
||||
if (document.addEventListener) {
|
||||
if (~["complete", "loaded", "interactive"].indexOf(document.readyState)) {
|
||||
setTimeout(fn, 0)
|
||||
} else {
|
||||
var loadFn = function() {
|
||||
document.removeEventListener("DOMContentLoaded", loadFn, false)
|
||||
fn()
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", loadFn, false)
|
||||
}
|
||||
} else if (document.attachEvent) {
|
||||
IEContentLoaded(window, fn)
|
||||
}
|
||||
|
||||
function IEContentLoaded(w, fn) {
|
||||
var d = w.document,
|
||||
done = false,
|
||||
// only fire once
|
||||
init = function() {
|
||||
if (!done) {
|
||||
done = true
|
||||
fn()
|
||||
}
|
||||
}
|
||||
// polling for no errors
|
||||
var polling = function() {
|
||||
try {
|
||||
// throws errors until after ondocumentready
|
||||
d.documentElement.doScroll('left')
|
||||
} catch (e) {
|
||||
setTimeout(polling, 50)
|
||||
return
|
||||
}
|
||||
// no errors, fire
|
||||
|
||||
init()
|
||||
};
|
||||
|
||||
polling()
|
||||
// trying to always fire before onload
|
||||
d.onreadystatechange = function() {
|
||||
if (d.readyState == 'complete') {
|
||||
d.onreadystatechange = null
|
||||
init()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert el before target
|
||||
*
|
||||
* @param {Element} el
|
||||
* @param {Element} target
|
||||
*/
|
||||
|
||||
var before = function(el, target) {
|
||||
target.parentNode.insertBefore(el, target)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend el to target
|
||||
*
|
||||
* @param {Element} el
|
||||
* @param {Element} target
|
||||
*/
|
||||
|
||||
var prepend = function(el, target) {
|
||||
if (target.firstChild) {
|
||||
before(el, target.firstChild)
|
||||
} else {
|
||||
target.appendChild(el)
|
||||
}
|
||||
}
|
||||
|
||||
function appendSvg() {
|
||||
var div, svg
|
||||
|
||||
div = document.createElement('div')
|
||||
div.innerHTML = svgSprite
|
||||
svgSprite = null
|
||||
svg = div.getElementsByTagName('svg')[0]
|
||||
if (svg) {
|
||||
svg.setAttribute('aria-hidden', 'true')
|
||||
svg.style.position = 'absolute'
|
||||
svg.style.width = 0
|
||||
svg.style.height = 0
|
||||
svg.style.overflow = 'hidden'
|
||||
prepend(svg, document.body)
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldInjectCss && !window.__iconfont__svg__cssinject__) {
|
||||
window.__iconfont__svg__cssinject__ = true
|
||||
try {
|
||||
document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>");
|
||||
} catch (e) {
|
||||
console && console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
ready(appendSvg)
|
||||
|
||||
|
||||
})(window)
|
103
src/components/Charts/barPercent.vue
Normal file
103
src/components/Charts/barPercent.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div :class="className" :id="id" :style="{height:height,width:width}"></div>
|
||||
</template>
|
||||
<script>
|
||||
// 引入 ECharts 主模块
|
||||
const echarts = require('echarts/lib/echarts');
|
||||
// 引入柱状图
|
||||
require('echarts/lib/chart/bar');
|
||||
// 引入提示框和标题组件
|
||||
require('echarts/lib/component/tooltip');
|
||||
export default {
|
||||
name: 'barPercent',
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'bar-percent-chart'
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: 'bar-percent-chart'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '80px'
|
||||
},
|
||||
dataNum: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
dataNum() {
|
||||
this.setOptions()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initBar();
|
||||
},
|
||||
methods: {
|
||||
initBar() {
|
||||
this.chart = echarts.init(document.getElementById(this.id));
|
||||
},
|
||||
setOptions() {
|
||||
this.chart.setOption({
|
||||
tooltip: {
|
||||
show: true,
|
||||
formatter(params) {
|
||||
return '已完成' + params.value + '篇<br/>目标90篇<br/>完成进度' + Math.round((params.value / 90) * 100) + '%'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
containLabel: false
|
||||
},
|
||||
xAxis: [{
|
||||
type: 'category',
|
||||
data: ['文章完成比例']
|
||||
}],
|
||||
yAxis: [{
|
||||
type: 'value',
|
||||
data: [],
|
||||
show: false
|
||||
}],
|
||||
animationDelay: 1000,
|
||||
series: [{
|
||||
type: 'bar',
|
||||
name: '初诊',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: '#e5e5e5'
|
||||
}
|
||||
},
|
||||
silent: true,
|
||||
barGap: '-100%', // Make series be overlap
|
||||
data: [150]
|
||||
}, {
|
||||
type: 'bar',
|
||||
name: 'app',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: '#30b08f'
|
||||
}
|
||||
},
|
||||
z: 10,
|
||||
data: [this.dataNum]
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
113
src/components/Charts/keyboard.vue
Normal file
113
src/components/Charts/keyboard.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div :class="className" :id="id" :style="{height:height,width:width}"></div>
|
||||
</template>
|
||||
<script>
|
||||
// 引入 ECharts 主模块
|
||||
const echarts = require('echarts/lib/echarts');
|
||||
// 引入柱状图
|
||||
require('echarts/lib/chart/bar');
|
||||
require('echarts/lib/chart/line');
|
||||
// 引入提示框和标题组件
|
||||
require('echarts/lib/component/tooltip');
|
||||
require('echarts/lib/component/title');
|
||||
|
||||
require('echarts/lib/component/visualMap');
|
||||
export default {
|
||||
name: 'barPercent',
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'bar-percent-chart'
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: 'bar-percent-chart'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
mounted() {
|
||||
this.initBar();
|
||||
},
|
||||
methods: {
|
||||
initBar() {
|
||||
this.chart = echarts.init(document.getElementById(this.id));
|
||||
|
||||
const xAxisData = [];
|
||||
const data = [];
|
||||
for (let i = 0; i < 30; i++) {
|
||||
xAxisData.push(i + '号');
|
||||
data.push(Math.round(Math.random() * 2 + 3))
|
||||
}
|
||||
|
||||
this.chart.setOption(
|
||||
{
|
||||
backgroundColor: '#08263a',
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
show: false,
|
||||
data: xAxisData
|
||||
},
|
||||
visualMap: {
|
||||
show: false,
|
||||
min: 0,
|
||||
max: 50,
|
||||
dimension: 0,
|
||||
inRange: {
|
||||
color: ['#4a657a', '#308e92', '#b1cfa5', '#f5d69f', '#f5898b', '#ef5055']
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
color: '#4a657a'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#08263f'
|
||||
}
|
||||
},
|
||||
axisTick: {}
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data,
|
||||
name: '撸文数',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
barBorderRadius: 5,
|
||||
shadowBlur: 10,
|
||||
shadowColor: '#111'
|
||||
}
|
||||
},
|
||||
animationEasing: 'elasticOut',
|
||||
animationEasingUpdate: 'elasticOut',
|
||||
animationDelay(idx) {
|
||||
return idx * 20;
|
||||
},
|
||||
animationDelayUpdate(idx) {
|
||||
return idx * 20;
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
145
src/components/Charts/line.vue
Normal file
145
src/components/Charts/line.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div :class="className" :id="id" :style="{height:height,width:width}"></div>
|
||||
</template>
|
||||
<script>
|
||||
// 引入 ECharts 主模块
|
||||
const echarts = require('echarts/lib/echarts');
|
||||
// 引入图
|
||||
require('echarts/lib/chart/line');
|
||||
// 引入提示框和标题组件
|
||||
require('echarts/lib/component/markLine');
|
||||
require('echarts/lib/component/markPoint');
|
||||
require('echarts/lib/component/tooltip');
|
||||
export default {
|
||||
name: 'lineChart',
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'line-chart'
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: 'line-chart'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '280px'
|
||||
},
|
||||
listData: {
|
||||
type: Array,
|
||||
require: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
listData(dataList) {
|
||||
this.setLine(dataList)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.chart = echarts.init(document.getElementById(this.id));
|
||||
},
|
||||
methods: {
|
||||
setLine(dataList) {
|
||||
const xAxisData = [];
|
||||
const data = [];
|
||||
for (let i = 0; i < dataList.length; i++) {
|
||||
const item = dataList[i]
|
||||
xAxisData.push(item.week.substring(item.week.length - 2) + '周');
|
||||
data.push(item.count)
|
||||
}
|
||||
const markLineData = [];
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
markLineData.push([{
|
||||
xAxis: i - 1,
|
||||
yAxis: data[i - 1],
|
||||
value: data[i] - data[i - 1]
|
||||
}, {
|
||||
xAxis: i,
|
||||
yAxis: data[i]
|
||||
}]);
|
||||
}
|
||||
this.chart.setOption({
|
||||
title: {
|
||||
text: 'Awesome Chart'
|
||||
},
|
||||
grid: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 20,
|
||||
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
animationDelay: 1000,
|
||||
xAxis: {
|
||||
data: xAxisData,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
// axisLabel:{
|
||||
// show:false
|
||||
// },
|
||||
},
|
||||
|
||||
yAxis: {
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
show: false
|
||||
// min: 90
|
||||
},
|
||||
series: [{
|
||||
name: '撸文数',
|
||||
type: 'line',
|
||||
data,
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: '最大值' },
|
||||
{ type: 'min', name: '最小值' }
|
||||
]
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: '#30b08f'
|
||||
}
|
||||
},
|
||||
markLine: {
|
||||
silent: true,
|
||||
smooth: true,
|
||||
effect: {
|
||||
show: true
|
||||
},
|
||||
animationDuration(idx) {
|
||||
return idx * 100;
|
||||
},
|
||||
animationDelay: 1000,
|
||||
animationEasing: 'quadraticInOut',
|
||||
distance: 1,
|
||||
label: {
|
||||
normal: {
|
||||
position: 'middle'
|
||||
}
|
||||
},
|
||||
symbol: ['none', 'none'],
|
||||
data: markLineData
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
291
src/components/Dropzone/index.vue
Normal file
291
src/components/Dropzone/index.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div :ref="id" :action="url" class="dropzone" :id="id">
|
||||
<input type="file" name="file">
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Dropzone from 'dropzone';
|
||||
import 'dropzone/dist/dropzone.css';
|
||||
import { getToken } from 'api/qiniu';
|
||||
|
||||
Dropzone.autoDiscover = false;
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
dropzone: '',
|
||||
initOnce: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const element = document.getElementById(this.id);
|
||||
const vm = this;
|
||||
this.dropzone = new Dropzone(element, {
|
||||
clickable: this.clickable,
|
||||
thumbnailWidth: this.thumbnailWidth,
|
||||
thumbnailHeight: this.thumbnailHeight,
|
||||
maxFiles: this.maxFiles,
|
||||
maxFilesize: this.maxFilesize,
|
||||
dictRemoveFile: 'Remove',
|
||||
addRemoveLinks: this.showRemoveLink,
|
||||
acceptedFiles: this.acceptedFiles,
|
||||
autoProcessQueue: this.autoProcessQueue,
|
||||
dictDefaultMessage: '<i style="margin-top: 3em;display: inline-block" class="material-icons">' + this.defaultMsg + '</i><br>Drop files here to upload',
|
||||
dictMaxFilesExceeded: '只能一个图',
|
||||
previewTemplate: '<div class="dz-preview dz-file-preview"> <div class="dz-image" style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" ><img style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" data-dz-thumbnail /></div> <div class="dz-details"><div class="dz-size"><span data-dz-size></span></div> <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div> <div class="dz-error-message"><span data-dz-errormessage></span></div> <div class="dz-success-mark"> <i class="material-icons">done</i> </div> <div class="dz-error-mark"><i class="material-icons">error</i></div></div>',
|
||||
init() {
|
||||
const val = vm.defaultImg;
|
||||
if (!val) return;
|
||||
if (Array.isArray(val)) {
|
||||
if (val.length === 0) return;
|
||||
val.map((v, i) => {
|
||||
const mockFile = { name: 'name' + i, size: 12345, url: v };
|
||||
this.options.addedfile.call(this, mockFile);
|
||||
this.options.thumbnail.call(this, mockFile, v);
|
||||
mockFile.previewElement.classList.add('dz-success');
|
||||
mockFile.previewElement.classList.add('dz-complete');
|
||||
vm.initOnce = false;
|
||||
return true;
|
||||
})
|
||||
} else {
|
||||
const mockFile = { name: 'name', size: 12345, url: val };
|
||||
this.options.addedfile.call(this, mockFile);
|
||||
this.options.thumbnail.call(this, mockFile, val);
|
||||
mockFile.previewElement.classList.add('dz-success');
|
||||
mockFile.previewElement.classList.add('dz-complete');
|
||||
vm.initOnce = false;
|
||||
}
|
||||
},
|
||||
accept: (file, done) => {
|
||||
const token = this.$store.getters.token;
|
||||
getToken(token).then(response => {
|
||||
file.token = response.data.qiniu_token;
|
||||
file.key = response.data.qiniu_key;
|
||||
file.url = response.data.qiniu_url;
|
||||
done();
|
||||
})
|
||||
},
|
||||
sending: (file, xhr, formData) => {
|
||||
formData.append('token', file.token);
|
||||
formData.append('key', file.key);
|
||||
vm.initOnce = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.couldPaste) {
|
||||
document.addEventListener('paste', this.pasteImg)
|
||||
}
|
||||
|
||||
this.dropzone.on('success', file => {
|
||||
vm.$emit('dropzone-success', file, vm.dropzone.element)
|
||||
});
|
||||
this.dropzone.on('addedfile', file => {
|
||||
vm.$emit('dropzone-fileAdded', file)
|
||||
});
|
||||
this.dropzone.on('removedfile', file => {
|
||||
vm.$emit('dropzone-removedFile', file)
|
||||
});
|
||||
this.dropzone.on('error', (file, error, xhr) => {
|
||||
vm.$emit('dropzone-error', file, error, xhr)
|
||||
});
|
||||
this.dropzone.on('successmultiple', (file, error, xhr) => {
|
||||
vm.$emit('dropzone-successmultiple', file, error, xhr)
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
removeAllFiles() {
|
||||
this.dropzone.removeAllFiles(true)
|
||||
},
|
||||
processQueue() {
|
||||
this.dropzone.processQueue()
|
||||
},
|
||||
pasteImg(event) {
|
||||
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
|
||||
if (items[0].kind === 'file') {
|
||||
this.dropzone.addFile(items[0].getAsFile())
|
||||
}
|
||||
},
|
||||
initImages(val) {
|
||||
if (!val) return;
|
||||
if (Array.isArray(val)) {
|
||||
val.map((v, i) => {
|
||||
const mockFile = { name: 'name' + i, size: 12345, url: v };
|
||||
this.dropzone.options.addedfile.call(this.dropzone, mockFile);
|
||||
this.dropzone.options.thumbnail.call(this.dropzone, mockFile, v);
|
||||
mockFile.previewElement.classList.add('dz-success');
|
||||
mockFile.previewElement.classList.add('dz-complete');
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
const mockFile = { name: 'name', size: 12345, url: val };
|
||||
this.dropzone.options.addedfile.call(this.dropzone, mockFile);
|
||||
this.dropzone.options.thumbnail.call(this.dropzone, mockFile, val);
|
||||
mockFile.previewElement.classList.add('dz-success');
|
||||
mockFile.previewElement.classList.add('dz-complete');
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener('paste', this.pasteImg);
|
||||
this.dropzone.destroy();
|
||||
},
|
||||
watch: {
|
||||
defaultImg(val) {
|
||||
if (val.length === 0) {
|
||||
this.initOnce = false;
|
||||
return;
|
||||
}
|
||||
if (!this.initOnce) return;
|
||||
this.initImages(val);
|
||||
this.initOnce = false;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
clickable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
defaultMsg: {
|
||||
type: String,
|
||||
default: '上传图片'
|
||||
},
|
||||
acceptedFiles: {
|
||||
type: String
|
||||
},
|
||||
thumbnailHeight: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
thumbnailWidth: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
showRemoveLink: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
maxFilesize: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
maxFiles: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
autoProcessQueue: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
useCustomDropzoneOptions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
defaultImg: {
|
||||
default: false
|
||||
},
|
||||
couldPaste: {
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dropzone {
|
||||
border: 2px solid #E5E5E5;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
color: #777;
|
||||
transition: background-color .2s linear;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropzone:hover {
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
|
||||
i {
|
||||
color: #CCC;
|
||||
}
|
||||
|
||||
.dropzone .dz-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dropzone input[name='file'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-image {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview:hover .dz-image img {
|
||||
transform: none;
|
||||
-webkit-filter: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-details {
|
||||
bottom: 0px;
|
||||
top: 0px;
|
||||
color: white;
|
||||
background-color: rgba(33, 150, 243, 0.8);
|
||||
transition: opacity .2s linear;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-details .dz-filename:hover span {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-remove {
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
color: white;
|
||||
margin-left: 15px;
|
||||
padding: 10px;
|
||||
top: inherit;
|
||||
bottom: 15px;
|
||||
border: 2px white solid;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1.1px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview:hover .dz-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
|
||||
margin-left: -40px;
|
||||
margin-top: -50px;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-success-mark i, .dropzone .dz-preview .dz-error-mark i {
|
||||
color: white;
|
||||
font-size: 5rem;
|
||||
}
|
||||
</style>
|
43
src/components/ErrLog/index.vue
Normal file
43
src/components/ErrLog/index.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-badge :is-dot="true" style="line-height: 30px;" @click.native="dialogTableVisible=true">
|
||||
<el-button size="small" type="primary">
|
||||
<wscn-icon-svg icon-class="bug" class="meta-item__icon"/>
|
||||
</el-button>
|
||||
</el-badge>
|
||||
<el-dialog title="bug日志" v-model="dialogTableVisible">
|
||||
<el-table :data="logsList">
|
||||
<el-table-column label="message">
|
||||
<template scope="scope">
|
||||
<div>msg:{{ scope.row.err.message }}</div>
|
||||
<br/>
|
||||
<div>url: {{scope.row.url}}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="stack">
|
||||
<template scope="scope">
|
||||
{{ scope.row.err.stack}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'errLog',
|
||||
props: {
|
||||
logsList: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogTableVisible: false
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
38
src/components/Hamburger/index.vue
Normal file
38
src/components/Hamburger/index.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div>
|
||||
<svg @click="toggleClick" class="wscn-icon hamburger" :class="{'is-active':isActive}" aria-hidden="true">
|
||||
<use xlink:href="#icon-hamburger"></use>
|
||||
</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: .38s;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
.hamburger.is-active {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
</style>
|
11
src/components/Icon-svg/index.js
Normal file
11
src/components/Icon-svg/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
function registerAllComponents(requireContext) {
|
||||
return requireContext.keys().forEach(comp => {
|
||||
const vueComp = requireContext(comp)
|
||||
const compName = vueComp.name ? vueComp.name.toLowerCase() : /\/([\w-]+)\.vue$/.exec(comp)[1]
|
||||
Vue.component(compName, vueComp)
|
||||
})
|
||||
}
|
||||
|
||||
registerAllComponents(require.context('./', false, /\.vue$/))
|
52
src/components/Icon-svg/wscn-icon-stack.vue
Normal file
52
src/components/Icon-svg/wscn-icon-stack.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="icon-container" :style="containerStyle">
|
||||
<slot class="icon"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'wscn-icon-stack',
|
||||
props: {
|
||||
width: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
shape: {
|
||||
type: String,
|
||||
default: 'circle',
|
||||
validator: val => {
|
||||
const validShapes = ['circle', 'square']
|
||||
return validShapes.indexOf(val) > -1
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
containerStyle() {
|
||||
return {
|
||||
width: `${this.width}px`,
|
||||
height: `${this.width}px`,
|
||||
fontSize: `${this.width * 0.6}px`,
|
||||
borderRadius: `${this.shape === 'circle' && '50%'}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #1482F0;
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
color: #ffffff;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
</style>
|
26
src/components/Icon-svg/wscn-icon-svg.vue
Normal file
26
src/components/Icon-svg/wscn-icon-svg.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<svg class="wscn-icon" aria-hidden="true">
|
||||
<use :xlink:href="iconName"></use>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'wscn-icon-svg',
|
||||
props: {
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
return `#icon-${this.iconClass}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
676
src/components/ImageCropper/index.vue
Normal file
676
src/components/ImageCropper/index.vue
Normal file
@@ -0,0 +1,676 @@
|
||||
<template>
|
||||
<div class="vue-image-crop-upload" v-show="show">
|
||||
<div class="vicp-wrap">
|
||||
<div class="vicp-close" @click="off">
|
||||
<i class="vicp-icon4"></i>
|
||||
</div>
|
||||
|
||||
<div class="vicp-step1" v-show="step == 1">
|
||||
<div class="vicp-drop-area"
|
||||
@dragleave="preventDefault"
|
||||
@dragover="preventDefault"
|
||||
@dragenter="preventDefault"
|
||||
@click="handleClick"
|
||||
@drop="handleChange">
|
||||
<i class="vicp-icon1" v-show="loading != 1">
|
||||
<i class="vicp-icon1-arrow"></i>
|
||||
<i class="vicp-icon1-body"></i>
|
||||
<i class="vicp-icon1-bottom"></i>
|
||||
</i>
|
||||
<span class="vicp-hint" v-show="loading !== 1">{{ lang.hint }}</span>
|
||||
<span class="vicp-no-supported-hint" v-show="!isSupported">{{ lang.noSupported }}</span>
|
||||
<input type="file" v-show="false" @change="handleChange" ref="fileinput">
|
||||
</div>
|
||||
<div class="vicp-error" v-show="hasError">
|
||||
<i class="vicp-icon2"></i> {{ errorMsg }}
|
||||
</div>
|
||||
<div class="vicp-operate">
|
||||
<a @click="off" @mousedown="ripple">{{ lang.btn.off }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vicp-step2" v-if="step == 2">
|
||||
<div class="vicp-crop">
|
||||
<div class="vicp-crop-left" v-show="true">
|
||||
<div class="vicp-img-container">
|
||||
<img :src="sourceImgUrl"
|
||||
:style="sourceImgStyle"
|
||||
class="vicp-img"
|
||||
draggable="false"
|
||||
@drag="preventDefault"
|
||||
@dragstart="preventDefault"
|
||||
@dragend="preventDefault"
|
||||
@dragleave="preventDefault"
|
||||
@dragover="preventDefault"
|
||||
@dragenter="preventDefault"
|
||||
@drop="preventDefault"
|
||||
@mousedown="imgStartMove"
|
||||
@mousemove="imgMove"
|
||||
@mouseup="createImg"
|
||||
@mouseout="createImg"
|
||||
ref="img">
|
||||
<div class="vicp-img-shade vicp-img-shade-1" :style="sourceImgShadeStyle"></div>
|
||||
<div class="vicp-img-shade vicp-img-shade-2" :style="sourceImgShadeStyle"></div>
|
||||
</div>
|
||||
<div class="vicp-range">
|
||||
<input type="range" :value="scale.range" step="1" min="0" max="100" @change="zoomChange">
|
||||
<i @mousedown="startZoomSub" @mouseout="endZoomSub" @mouseup="endZoomSub"
|
||||
class="vicp-icon5"></i>
|
||||
<i @mousedown="startZoomAdd" @mouseout="endZoomAdd" @mouseup="endZoomAdd"
|
||||
class="vicp-icon6"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vicp-crop-right" v-show="true">
|
||||
<div class="vicp-preview">
|
||||
<div class="vicp-preview-item">
|
||||
<img :src="createImgUrl" :style="previewStyle">
|
||||
<span>{{ lang.preview }}</span>
|
||||
</div>
|
||||
<div class="vicp-preview-item">
|
||||
<img :src="createImgUrl" :style="previewStyle" v-if="!noCircle">
|
||||
<span>{{ lang.preview }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vicp-operate">
|
||||
<a @click="setStep(1)" @mousedown="ripple">{{ lang.btn.back }}</a>
|
||||
<a class="vicp-operate-btn" @click="upload" @mousedown="ripple">{{ lang.btn.save }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vicp-step3" v-if="step == 3">
|
||||
<div class="vicp-upload">
|
||||
<span class="vicp-loading" v-show="loading === 1">{{ lang.loading }}</span>
|
||||
<div class="vicp-progress-wrap">
|
||||
<span class="vicp-progress" v-show="loading === 1" :style="progressStyle"></span>
|
||||
</div>
|
||||
<div class="vicp-error" v-show="hasError">
|
||||
<i class="vicp-icon2"></i> {{ errorMsg }}
|
||||
</div>
|
||||
<div class="vicp-success" v-show="loading === 2">
|
||||
<i class="vicp-icon3"></i> {{ lang.success }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vicp-operate">
|
||||
<a @click="setStep(2)" @mousedown="ripple">{{ lang.btn.back }}</a>
|
||||
<a @click="off" @mousedown="ripple">{{ lang.btn.close }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<canvas v-show="false" :width="width" :height="height" ref="canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint-disable */
|
||||
import {getToken, upload} from 'api/qiniu';
|
||||
import {effectRipple, data2blob} from './utils';
|
||||
import langBag from './lang';
|
||||
const mimes = {
|
||||
'jpg': 'image/jpeg',
|
||||
'png': 'image/png',
|
||||
'gif': 'image/gif',
|
||||
'svg': 'image/svg+xml',
|
||||
'psd': 'image/photoshop'
|
||||
};
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 域,上传文件name,触发事件会带上(如果一个页面多个图片上传控件,可以做区分
|
||||
field: {
|
||||
type: String,
|
||||
default: 'avatar'
|
||||
},
|
||||
// 上传地址
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 其他要上传文件附带的数据,对象格式
|
||||
params: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
// 剪裁图片的宽
|
||||
width: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
// 剪裁图片的高
|
||||
height: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
// 不预览圆形图片
|
||||
noCircle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 单文件大小限制
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 10240
|
||||
},
|
||||
// 语言类型
|
||||
langType: {
|
||||
type: String,
|
||||
'default': 'zh'
|
||||
},
|
||||
|
||||
},
|
||||
data() {
|
||||
let that = this,
|
||||
{
|
||||
langType,
|
||||
width,
|
||||
height
|
||||
} = that,
|
||||
isSupported = true,
|
||||
lang = langBag[langType] ? langBag[langType] : lang['zh'];
|
||||
|
||||
if (typeof FormData != 'function') {
|
||||
isSupported = false;
|
||||
}
|
||||
return {
|
||||
show: true,
|
||||
// 图片的mime
|
||||
mime:mimes['jpg'],
|
||||
// 语言包
|
||||
lang,
|
||||
// 浏览器是否支持该控件
|
||||
isSupported,
|
||||
// 步骤
|
||||
step: 1, //1选择文件 2剪裁 3上传
|
||||
// 上传状态及进度
|
||||
loading: 0, //0未开始 1正在 2成功 3错误
|
||||
progress: 0,
|
||||
// 是否有错误及错误信息
|
||||
hasError: false,
|
||||
errorMsg: '',
|
||||
// 需求图宽高比
|
||||
ratio: width / height,
|
||||
// 原图地址、生成图片地址
|
||||
sourceImg: null,
|
||||
sourceImgUrl: '',
|
||||
createImgUrl: '',
|
||||
// 原图片拖动事件初始值
|
||||
sourceImgMouseDown: {
|
||||
on: false,
|
||||
mX: 0, //鼠标按下的坐标
|
||||
mY: 0,
|
||||
x: 0, //scale原图坐标
|
||||
y: 0
|
||||
},
|
||||
// 生成图片预览的容器大小
|
||||
previewContainer: {
|
||||
width: 100,
|
||||
height: 100
|
||||
},
|
||||
// 原图容器宽高
|
||||
sourceImgContainer: { // sic
|
||||
width: 240,
|
||||
height: 180
|
||||
},
|
||||
// 原图展示属性
|
||||
scale: {
|
||||
zoomAddOn: false, //按钮缩放事件开启
|
||||
zoomSubOn: false, //按钮缩放事件开启
|
||||
range: 1, //最大100
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
maxWidth: 0,
|
||||
maxHeight: 0,
|
||||
minWidth: 0, //最宽
|
||||
minHeight: 0,
|
||||
naturalWidth: 0, //原宽
|
||||
naturalHeight: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 进度条样式
|
||||
progressStyle() {
|
||||
let {
|
||||
progress
|
||||
} = this;
|
||||
return {
|
||||
width: progress + '%'
|
||||
}
|
||||
},
|
||||
// 原图样式
|
||||
sourceImgStyle() {
|
||||
let {
|
||||
scale,
|
||||
sourceImgMasking
|
||||
} = this;
|
||||
return {
|
||||
top: scale.y + sourceImgMasking.y + 'px',
|
||||
left: scale.x + sourceImgMasking.x + 'px',
|
||||
width: scale.width + 'px',
|
||||
height: scale.height + 'px'
|
||||
}
|
||||
},
|
||||
// 原图蒙版属性
|
||||
sourceImgMasking() {
|
||||
let {
|
||||
width,
|
||||
height,
|
||||
ratio,
|
||||
sourceImgContainer
|
||||
} = this,
|
||||
sic = sourceImgContainer,
|
||||
sicRatio = sic.width / sic.height, // 原图容器宽高比
|
||||
x = 0,
|
||||
y = 0,
|
||||
w = sic.width,
|
||||
h = sic.height,
|
||||
scale = 1;
|
||||
if (ratio < sicRatio) {
|
||||
scale = sic.height / height;
|
||||
w = sic.height * ratio;
|
||||
x = (sic.width - w) / 2;
|
||||
}
|
||||
if (ratio > sicRatio) {
|
||||
scale = sic.width / width;
|
||||
h = sic.width / ratio;
|
||||
y = (sic.height - h) / 2;
|
||||
}
|
||||
return {
|
||||
scale, // 蒙版相对需求宽高的缩放
|
||||
x,
|
||||
y,
|
||||
width: w,
|
||||
height: h
|
||||
};
|
||||
},
|
||||
// 原图遮罩样式
|
||||
sourceImgShadeStyle() {
|
||||
let sic = this.sourceImgContainer,
|
||||
sim = this.sourceImgMasking,
|
||||
w = sim.width == sic.width ? sim.width : (sic.width - sim.width) / 2,
|
||||
h = sim.height == sic.height ? sim.height : (sic.height - sim.height) / 2;
|
||||
return {
|
||||
width: w + 'px',
|
||||
height: h + 'px'
|
||||
};
|
||||
},
|
||||
previewStyle() {
|
||||
let {
|
||||
width,
|
||||
height,
|
||||
ratio,
|
||||
previewContainer
|
||||
} = this,
|
||||
pc = previewContainer,
|
||||
w = pc.width,
|
||||
h = pc.height,
|
||||
pcRatio = w / h;
|
||||
if (ratio < pcRatio) {
|
||||
w = pc.height * ratio;
|
||||
}
|
||||
if (ratio > pcRatio) {
|
||||
h = pc.width / ratio;
|
||||
}
|
||||
return {
|
||||
width: w + 'px',
|
||||
height: h + 'px'
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击波纹效果
|
||||
ripple(e) {
|
||||
effectRipple(e);
|
||||
},
|
||||
// 关闭控件
|
||||
off() {
|
||||
this.show = false;
|
||||
},
|
||||
// 设置步骤
|
||||
setStep(step) {
|
||||
let that = this;
|
||||
setTimeout(function () {
|
||||
that.step = step;
|
||||
}, 200);
|
||||
},
|
||||
/* 图片选择区域函数绑定
|
||||
---------------------------------------------------------------*/
|
||||
preventDefault(e) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
},
|
||||
handleClick(e) {
|
||||
if (this.loading !== 1) {
|
||||
if (e.target !== this.$refs.fileinput) {
|
||||
e.preventDefault();
|
||||
if (document.activeElement !== this.$refs) {
|
||||
this.$refs.fileinput.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
handleChange(e) {
|
||||
e.preventDefault();
|
||||
if (this.loading !== 1) {
|
||||
let files = e.target.files || e.dataTransfer.files;
|
||||
this.reset();
|
||||
if (this.checkFile(files[0])) {
|
||||
this.setSourceImg(files[0]);
|
||||
}
|
||||
}
|
||||
},
|
||||
/* ---------------------------------------------------------------*/
|
||||
// 检测选择的文件是否合适
|
||||
checkFile(file) {
|
||||
let that = this,
|
||||
{
|
||||
lang,
|
||||
maxSize
|
||||
} = that;
|
||||
// 仅限图片
|
||||
if (file.type.indexOf('image') === -1) {
|
||||
that.hasError = true;
|
||||
that.errorMsg = lang.error.onlyImg;
|
||||
return false;
|
||||
}
|
||||
this.mime=file.type;
|
||||
// 超出大小
|
||||
if (file.size / 1024 > maxSize) {
|
||||
that.hasError = true;
|
||||
that.errorMsg = lang.error.outOfSize + maxSize + 'kb';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 重置控件
|
||||
reset() {
|
||||
let that = this;
|
||||
that.step = 1;
|
||||
that.loading = 0;
|
||||
that.hasError = false;
|
||||
that.errorMsg = '';
|
||||
that.progress = 0;
|
||||
},
|
||||
// 设置图片源
|
||||
setSourceImg(file) {
|
||||
let that = this,
|
||||
fr = new FileReader();
|
||||
fr.onload = function (e) {
|
||||
that.sourceImgUrl = fr.result;
|
||||
that.startCrop();
|
||||
};
|
||||
fr.readAsDataURL(file);
|
||||
},
|
||||
// 剪裁前准备工作
|
||||
startCrop() {
|
||||
let that = this,
|
||||
{
|
||||
width,
|
||||
height,
|
||||
ratio,
|
||||
scale,
|
||||
sourceImgUrl,
|
||||
sourceImgMasking,
|
||||
lang
|
||||
} = that,
|
||||
sim = sourceImgMasking,
|
||||
img = new Image();
|
||||
img.src = sourceImgUrl;
|
||||
img.onload = function () {
|
||||
let nWidth = img.naturalWidth,
|
||||
nHeight = img.naturalHeight,
|
||||
nRatio = nWidth / nHeight,
|
||||
w = sim.width,
|
||||
h = sim.height,
|
||||
x = 0,
|
||||
y = 0;
|
||||
// 图片像素不达标
|
||||
// if (nWidth < width || nHeight < height) {
|
||||
// that.hasError = true;
|
||||
// that.errorMsg = lang.error.lowestPx + width + '*' + height;
|
||||
// return false;
|
||||
// }
|
||||
if (ratio > nRatio) {
|
||||
h = w / nRatio;
|
||||
y = (sim.height - h) / 2;
|
||||
}
|
||||
if (ratio < nRatio) {
|
||||
w = h * nRatio;
|
||||
x = (sim.width - w) / 2;
|
||||
}
|
||||
scale.range = 0;
|
||||
scale.x = x;
|
||||
scale.y = y;
|
||||
scale.width = w;
|
||||
scale.height = h;
|
||||
scale.minWidth = w;
|
||||
scale.minHeight = h;
|
||||
scale.maxWidth = nWidth * sim.scale;
|
||||
scale.maxHeight = nHeight * sim.scale;
|
||||
scale.naturalWidth = nWidth;
|
||||
scale.naturalHeight = nHeight;
|
||||
that.sourceImg = img;
|
||||
that.createImg();
|
||||
that.setStep(2);
|
||||
};
|
||||
},
|
||||
// 鼠标按下图片准备移动
|
||||
imgStartMove(e) {
|
||||
let {
|
||||
sourceImgMouseDown,
|
||||
scale
|
||||
} = this,
|
||||
simd = sourceImgMouseDown;
|
||||
simd.mX = e.screenX;
|
||||
simd.mY = e.screenY;
|
||||
simd.x = scale.x;
|
||||
simd.y = scale.y;
|
||||
simd.on = true;
|
||||
},
|
||||
// 鼠标按下状态下移动,图片移动
|
||||
imgMove(e) {
|
||||
let {
|
||||
sourceImgMouseDown: {
|
||||
on,
|
||||
mX,
|
||||
mY,
|
||||
x,
|
||||
y
|
||||
},
|
||||
scale,
|
||||
sourceImgMasking
|
||||
} = this,
|
||||
sim = sourceImgMasking,
|
||||
nX = e.screenX,
|
||||
nY = e.screenY,
|
||||
dX = nX - mX,
|
||||
dY = nY - mY,
|
||||
rX = x + dX,
|
||||
rY = y + dY;
|
||||
if (!on) return;
|
||||
if (rX > 0) {
|
||||
rX = 0;
|
||||
}
|
||||
if (rY > 0) {
|
||||
rY = 0;
|
||||
}
|
||||
if (rX < sim.width - scale.width) {
|
||||
rX = sim.width - scale.width;
|
||||
}
|
||||
if (rY < sim.height - scale.height) {
|
||||
rY = sim.height - scale.height;
|
||||
}
|
||||
scale.x = rX;
|
||||
scale.y = rY;
|
||||
},
|
||||
// 按钮按下开始放大
|
||||
startZoomAdd(e) {
|
||||
let that = this,
|
||||
{
|
||||
scale
|
||||
} = that;
|
||||
scale.zoomAddOn = true;
|
||||
function zoom() {
|
||||
if (scale.zoomAddOn) {
|
||||
let range = scale.range >= 100 ? 100 : ++scale.range;
|
||||
that.zoomImg(range);
|
||||
setTimeout(function () {
|
||||
zoom();
|
||||
}, 60);
|
||||
}
|
||||
}
|
||||
|
||||
zoom();
|
||||
},
|
||||
// 按钮松开或移开取消放大
|
||||
endZoomAdd(e) {
|
||||
this.scale.zoomAddOn = false;
|
||||
},
|
||||
// 按钮按下开始缩小
|
||||
startZoomSub(e) {
|
||||
let that = this,
|
||||
{
|
||||
scale
|
||||
} = that;
|
||||
scale.zoomSubOn = true;
|
||||
function zoom() {
|
||||
if (scale.zoomSubOn) {
|
||||
let range = scale.range <= 0 ? 0 : --scale.range;
|
||||
that.zoomImg(range);
|
||||
setTimeout(function () {
|
||||
zoom();
|
||||
}, 60);
|
||||
}
|
||||
}
|
||||
|
||||
zoom();
|
||||
},
|
||||
// 按钮松开或移开取消缩小
|
||||
endZoomSub(e) {
|
||||
let {
|
||||
scale
|
||||
} = this;
|
||||
scale.zoomSubOn = false;
|
||||
},
|
||||
zoomChange(e) {
|
||||
this.zoomImg(e.target.value);
|
||||
},
|
||||
// 缩放原图
|
||||
zoomImg(newRange) {
|
||||
let that = this,
|
||||
{
|
||||
sourceImgMasking,
|
||||
sourceImgMouseDown,
|
||||
scale
|
||||
} = this,
|
||||
{
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
minWidth,
|
||||
minHeight,
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
range
|
||||
} = scale,
|
||||
sim = sourceImgMasking,
|
||||
// 蒙版宽高
|
||||
sWidth = sim.width,
|
||||
sHeight = sim.height,
|
||||
// 新宽高
|
||||
nWidth = minWidth + (maxWidth - minWidth) * newRange / 100,
|
||||
nHeight = minHeight + (maxHeight - minHeight) * newRange / 100,
|
||||
// 新坐标(根据蒙版中心点缩放)
|
||||
nX = sWidth / 2 - (nWidth / width) * (sWidth / 2 - x),
|
||||
nY = sHeight / 2 - (nHeight / height) * (sHeight / 2 - y);
|
||||
// 判断新坐标是否超过蒙版限制
|
||||
if (nX > 0) {
|
||||
nX = 0;
|
||||
}
|
||||
if (nY > 0) {
|
||||
nY = 0;
|
||||
}
|
||||
if (nX < sWidth - nWidth) {
|
||||
nX = sWidth - nWidth;
|
||||
}
|
||||
if (nY < sHeight - nHeight) {
|
||||
nY = sHeight - nHeight;
|
||||
}
|
||||
// 赋值处理
|
||||
scale.x = nX;
|
||||
scale.y = nY;
|
||||
scale.width = nWidth;
|
||||
scale.height = nHeight;
|
||||
scale.range = newRange;
|
||||
setTimeout(function () {
|
||||
if (scale.range == newRange) {
|
||||
that.createImg();
|
||||
}
|
||||
}, 300);
|
||||
},
|
||||
// 生成需求图片
|
||||
createImg(e) {
|
||||
let that = this,
|
||||
{
|
||||
mime,
|
||||
sourceImg,
|
||||
scale: {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
},
|
||||
sourceImgMasking: {
|
||||
scale
|
||||
}
|
||||
} = that,
|
||||
canvas = that.$refs.canvas,
|
||||
ctx = canvas.getContext('2d');
|
||||
if (e) {
|
||||
// 取消鼠标按下移动状态
|
||||
that.sourceImgMouseDown.on = false;
|
||||
}
|
||||
ctx.drawImage(sourceImg, x / scale, y / scale, width / scale, height / scale);
|
||||
that.createImgUrl = canvas.toDataURL(mime);
|
||||
},
|
||||
// 上传图片
|
||||
upload() {
|
||||
let that = this,
|
||||
{
|
||||
lang,
|
||||
mime,
|
||||
createImgUrl
|
||||
} = this,
|
||||
formData = new FormData();
|
||||
// 上传文件
|
||||
that.loading = 1;
|
||||
that.progress = 0;
|
||||
that.setStep(3);
|
||||
formData.append('file', data2blob(createImgUrl, mime));
|
||||
const token = this.$store.getters.token;
|
||||
getToken(token).then((response)=> {
|
||||
const url = response.data.qiniu_url;
|
||||
formData.append('token', response.data.qiniu_token);
|
||||
formData.append('key', response.data.qiniu_key);
|
||||
upload(formData).then((response)=> {
|
||||
that.loading = 2;
|
||||
that.$emit('crop-upload-success', url);
|
||||
}).catch(err => {
|
||||
that.loading = 3;
|
||||
that.hasError = true;
|
||||
that.errorMsg = lang.fail;
|
||||
that.$emit('crop-upload-fail');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import "./upload.css";
|
||||
</style>
|
41
src/components/ImageCropper/lang.js
Normal file
41
src/components/ImageCropper/lang.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const langBag = {
|
||||
zh: {
|
||||
hint: '点击,或拖动图片至此处',
|
||||
loading: '正在上传……',
|
||||
noSupported: '浏览器不支持该功能,请使用IE10以上或其他现在浏览器!',
|
||||
success: '上传成功',
|
||||
fail: '图片上传失败',
|
||||
preview: '头像预览',
|
||||
btn: {
|
||||
off: '取消',
|
||||
close: '关闭',
|
||||
back: '上一步',
|
||||
save: '保存'
|
||||
},
|
||||
error: {
|
||||
onlyImg: '仅限图片格式',
|
||||
outOfSize: '单文件大小不能超过 ',
|
||||
lowestPx: '图片最低像素为(宽*高):'
|
||||
}
|
||||
},
|
||||
en: {
|
||||
hint: 'Click, or drag the file here',
|
||||
loading: 'Uploading……',
|
||||
noSupported: 'Browser does not support, please use IE10+ or other browsers',
|
||||
success: 'Upload success',
|
||||
fail: 'Upload failed',
|
||||
preview: 'Preview',
|
||||
btn: {
|
||||
off: 'Cancel',
|
||||
close: 'Close',
|
||||
back: 'Back',
|
||||
save: 'Save'
|
||||
},
|
||||
error: {
|
||||
onlyImg: 'Image only',
|
||||
outOfSize: 'Image exceeds size limit: ',
|
||||
lowestPx: 'The lowest pixel in the image: '
|
||||
}
|
||||
}
|
||||
};
|
||||
export default langBag;
|
691
src/components/ImageCropper/upload.css
Normal file
691
src/components/ImageCropper/upload.css
Normal file
@@ -0,0 +1,691 @@
|
||||
@charset "UTF-8";
|
||||
@-webkit-keyframes vicp_progress {
|
||||
0% {
|
||||
background-position-y: 0;
|
||||
}
|
||||
100% {
|
||||
background-position-y: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes vicp_progress {
|
||||
0% {
|
||||
background-position-y: 0;
|
||||
}
|
||||
100% {
|
||||
background-position-y: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes vicp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0) translatey(-60px);
|
||||
transform: scale(0) translatey(-60px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1) translatey(0);
|
||||
transform: scale(1) translatey(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes vicp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0) translatey(-60px);
|
||||
transform: scale(0) translatey(-60px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1) translatey(0);
|
||||
transform: scale(1) translatey(0);
|
||||
}
|
||||
}
|
||||
|
||||
.vue-image-crop-upload {
|
||||
position: fixed;
|
||||
display: block;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 10000;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-moz-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap {
|
||||
-webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
|
||||
position: fixed;
|
||||
display: block;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 10000;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
width: 600px;
|
||||
height: 330px;
|
||||
padding: 25px;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
-webkit-animation: vicp 0.12s ease-in;
|
||||
animation: vicp 0.12s ease-in;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-close {
|
||||
position: absolute;
|
||||
right: -30px;
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4 {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
-webkit-transition: -webkit-transform 0.18s;
|
||||
transition: -webkit-transform 0.18s;
|
||||
transition: transform 0.18s;
|
||||
transition: transform 0.18s, -webkit-transform 0.18s;
|
||||
-webkit-transform: rotate(0);
|
||||
-ms-transform: rotate(0);
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::after, .vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::before {
|
||||
-webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 4px;
|
||||
width: 20px;
|
||||
height: 3px;
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::after {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
-ms-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4:hover {
|
||||
-webkit-transform: rotate(90deg);
|
||||
-ms-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area {
|
||||
position: relative;
|
||||
padding: 35px;
|
||||
height: 200px;
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
text-align: center;
|
||||
border: 1px dashed rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 {
|
||||
display: block;
|
||||
margin: 0 auto 6px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 .vicp-icon1-arrow {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom: 14.7px solid rgba(0, 0, 0, 0.3);
|
||||
border-left: 14.7px solid transparent;
|
||||
border-right: 14.7px solid transparent;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 .vicp-icon1-body {
|
||||
display: block;
|
||||
width: 12.6px;
|
||||
height: 14.7px;
|
||||
margin: 0 auto;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 .vicp-icon1-bottom {
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
height: 12.6px;
|
||||
border: 6px solid rgba(0, 0, 0, 0.3);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-hint {
|
||||
display: block;
|
||||
padding: 15px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-no-supported-hint {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 30px;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
line-height: 30px;
|
||||
background-color: #eee;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area:hover {
|
||||
cursor: pointer;
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-img-container {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 240px;
|
||||
height: 180px;
|
||||
background-color: #e5e5e0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-img-container .vicp-img {
|
||||
position: absolute;
|
||||
display: block;
|
||||
cursor: move;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-img-container .vicp-img-shade {
|
||||
-webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
|
||||
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
|
||||
position: absolute;
|
||||
background-color: rgba(241, 242, 243, 0.8);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-img-container .vicp-img-shade.vicp-img-shade-1 {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-img-container .vicp-img-shade.vicp-img-shade-2 {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range {
|
||||
position: relative;
|
||||
margin: 30px 0;
|
||||
width: 240px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon5,
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon6 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon5:hover,
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon6:hover {
|
||||
-webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
||||
cursor: pointer;
|
||||
background-color: rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon5 {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon5::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
display: block;
|
||||
left: 3px;
|
||||
top: 8px;
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon6 {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon6::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
display: block;
|
||||
left: 3px;
|
||||
top: 8px;
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon6::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
display: block;
|
||||
top: 3px;
|
||||
left: 8px;
|
||||
width: 2px;
|
||||
height: 12px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range] {
|
||||
display: block;
|
||||
padding-top: 5px;
|
||||
margin: 0 auto;
|
||||
width: 180px;
|
||||
height: 8px;
|
||||
vertical-align: top;
|
||||
background: transparent;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
/* 滑块
|
||||
---------------------------------------------------------------*/
|
||||
/* 轨道
|
||||
---------------------------------------------------------------*/
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-webkit-slider-thumb {
|
||||
-webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
|
||||
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin-top: -3px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: #61c091;
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
-webkit-transition: 0.2s;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-moz-range-thumb {
|
||||
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: #61c091;
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
-webkit-transition: 0.2s;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-ms-thumb {
|
||||
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
|
||||
appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: #61c091;
|
||||
border: none;
|
||||
border-radius: 100%;
|
||||
-webkit-transition: 0.2s;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:active::-moz-range-thumb {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:active::-ms-thumb {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:active::-webkit-slider-thumb {
|
||||
-webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
|
||||
margin-top: -4px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-webkit-slider-runnable-track {
|
||||
-webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
border: none;
|
||||
background-color: rgba(68, 170, 119, 0.3);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-moz-range-track {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
border: none;
|
||||
background-color: rgba(68, 170, 119, 0.3);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-ms-track {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
height: 6px;
|
||||
border-radius: 2px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-ms-fill-lower {
|
||||
background-color: rgba(68, 170, 119, 0.3);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]::-ms-fill-upper {
|
||||
background-color: rgba(68, 170, 119, 0.15);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:focus::-webkit-slider-runnable-track {
|
||||
background-color: rgba(68, 170, 119, 0.5);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:focus::-moz-range-track {
|
||||
background-color: rgba(68, 170, 119, 0.5);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:focus::-ms-fill-lower {
|
||||
background-color: rgba(68, 170, 119, 0.45);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range input[type=range]:focus::-ms-fill-upper {
|
||||
background-color: rgba(68, 170, 119, 0.25);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview {
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview .vicp-preview-item {
|
||||
position: relative;
|
||||
padding: 5px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
float: left;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview .vicp-preview-item span {
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
color: #bbb;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview .vicp-preview-item img {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
padding: 3px;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview .vicp-preview-item:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview .vicp-preview-item:last-child img {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload {
|
||||
position: relative;
|
||||
padding: 35px;
|
||||
height: 200px;
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
text-align: center;
|
||||
border: 1px dashed #ddd;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-loading {
|
||||
display: block;
|
||||
padding: 15px;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-progress-wrap {
|
||||
margin-top: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-progress-wrap .vicp-progress {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background-color: #4a7;
|
||||
-webkit-box-shadow: 0 2px 6px 0 rgba(68, 170, 119, 0.3);
|
||||
box-shadow: 0 2px 6px 0 rgba(68, 170, 119, 0.3);
|
||||
-webkit-transition: width 0.15s linear;
|
||||
transition: width 0.15s linear;
|
||||
background-image: -webkit-linear-gradient(135deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);
|
||||
background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);
|
||||
background-size: 40px 40px;
|
||||
-webkit-animation: vicp_progress 0.5s linear infinite;
|
||||
animation: vicp_progress 0.5s linear infinite;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-progress-wrap .vicp-progress::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border: 1px solid rgba(245, 246, 247, 0.7);
|
||||
-webkit-box-shadow: 0 1px 4px 0 rgba(68, 170, 119, 0.7);
|
||||
box-shadow: 0 1px 4px 0 rgba(68, 170, 119, 0.7);
|
||||
border-radius: 100%;
|
||||
background-color: #4a7;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-error,
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-success {
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-operate {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-operate a {
|
||||
position: relative;
|
||||
float: left;
|
||||
display: block;
|
||||
margin-left: 10px;
|
||||
width: 100px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #4a7;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-operate a:hover {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-error,
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-success {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
height: 24px;
|
||||
color: #d10;
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-success {
|
||||
color: #4a7;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-icon3 {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-icon3::after {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 6px;
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
border-width: 0 2px 2px 0;
|
||||
border-color: #4a7;
|
||||
border-style: solid;
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-icon2 {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-icon2::after, .vue-image-crop-upload .vicp-wrap .vicp-icon2::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
left: 4px;
|
||||
width: 13px;
|
||||
height: 2px;
|
||||
background-color: #d10;
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap .vicp-icon2::after {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
-ms-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.e-ripple {
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
background-clip: padding-box;
|
||||
pointer-events: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-transform: scale(0);
|
||||
-ms-transform: scale(0);
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.e-ripple.z-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(2);
|
||||
-ms-transform: scale(2);
|
||||
transform: scale(2);
|
||||
-webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
|
||||
transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
|
||||
transition: opacity 1.2s ease-out, transform 0.6s ease-out;
|
||||
transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
|
||||
}
|
58
src/components/ImageCropper/utils.js
Normal file
58
src/components/ImageCropper/utils.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
*
|
||||
* @param e
|
||||
* @param arg_opts
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function effectRipple(e, arg_opts) {
|
||||
let opts = Object.assign({
|
||||
ele: e.target, // 波纹作用元素
|
||||
type: 'hit', // hit点击位置扩散 center中心点扩展
|
||||
bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
|
||||
}, arg_opts),
|
||||
target = opts.ele;
|
||||
if (target) {
|
||||
let rect = target.getBoundingClientRect(),
|
||||
ripple = target.querySelector('.e-ripple');
|
||||
if (!ripple) {
|
||||
ripple = document.createElement('span');
|
||||
ripple.className = 'e-ripple';
|
||||
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px';
|
||||
target.appendChild(ripple);
|
||||
} else {
|
||||
ripple.className = 'e-ripple';
|
||||
}
|
||||
switch (opts.type) {
|
||||
case 'center':
|
||||
ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px';
|
||||
ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px';
|
||||
break;
|
||||
default:
|
||||
ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px';
|
||||
ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px';
|
||||
}
|
||||
ripple.style.backgroundColor = opts.bgc;
|
||||
ripple.className = 'e-ripple z-active';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// database64文件格式转换为2进制
|
||||
/**
|
||||
*
|
||||
* @param data
|
||||
* @param mime
|
||||
* @returns {*}
|
||||
*/
|
||||
export function data2blob(data, mime) {
|
||||
// dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了
|
||||
data = data.split(',')[1];
|
||||
data = window.atob(data);
|
||||
var ia = new Uint8Array(data.length);
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
ia[i] = data.charCodeAt(i);
|
||||
}
|
||||
// canvas.toDataURL 返回的默认格式就是 image/png
|
||||
return new Blob([ia], {type: mime});
|
||||
};
|
407
src/components/MDinput/index.vue
Normal file
407
src/components/MDinput/index.vue
Normal file
@@ -0,0 +1,407 @@
|
||||
<template>
|
||||
<div class="material-input__component" :class="computedClasses">
|
||||
<input
|
||||
v-if="type === 'email'"
|
||||
type="email"
|
||||
class="material-input"
|
||||
:name="name"
|
||||
:id="id"
|
||||
:placeholder="placeholder"
|
||||
v-model="valueCopy"
|
||||
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:autocomplete="autocomplete"
|
||||
|
||||
:required="required"
|
||||
|
||||
@focus="handleFocus(true)"
|
||||
@blur="handleFocus(false)"
|
||||
@input="handleModelInput"
|
||||
>
|
||||
<input
|
||||
v-if="type === 'url'"
|
||||
type="url"
|
||||
class="material-input"
|
||||
:name="name"
|
||||
:id="id"
|
||||
:placeholder="placeholder"
|
||||
v-model="valueCopy"
|
||||
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:autocomplete="autocomplete"
|
||||
|
||||
:required="required"
|
||||
|
||||
@focus="handleFocus(true)"
|
||||
@blur="handleFocus(false)"
|
||||
@input="handleModelInput"
|
||||
>
|
||||
<input
|
||||
v-if="type === 'number'"
|
||||
type="number"
|
||||
class="material-input"
|
||||
:name="name"
|
||||
:id="id"
|
||||
:placeholder="placeholder"
|
||||
v-model="valueCopy"
|
||||
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:autocomplete="autocomplete"
|
||||
|
||||
:max="max"
|
||||
:min="min"
|
||||
:minlength="minlength"
|
||||
:maxlength="maxlength"
|
||||
:required="required"
|
||||
|
||||
@focus="handleFocus(true)"
|
||||
@blur="handleFocus(false)"
|
||||
@input="handleModelInput"
|
||||
>
|
||||
<input
|
||||
v-if="type === 'password'"
|
||||
type="password"
|
||||
class="material-input"
|
||||
:name="name"
|
||||
:id="id"
|
||||
:placeholder="placeholder"
|
||||
v-model="valueCopy"
|
||||
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:autocomplete="autocomplete"
|
||||
|
||||
:max="max"
|
||||
:min="min"
|
||||
:required="required"
|
||||
|
||||
@focus="handleFocus(true)"
|
||||
@blur="handleFocus(false)"
|
||||
@input="handleModelInput"
|
||||
>
|
||||
<input
|
||||
v-if="type === 'tel'"
|
||||
type="tel"
|
||||
class="material-input"
|
||||
:name="name"
|
||||
:id="id"
|
||||
:placeholder="placeholder"
|
||||
v-model="valueCopy"
|
||||
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:autocomplete="autocomplete"
|
||||
|
||||
:required="required"
|
||||
|
||||
@focus="handleFocus(true)"
|
||||
@blur="handleFocus(false)"
|
||||
@input="handleModelInput"
|
||||
>
|
||||
<input
|
||||
v-if="type === 'text'"
|
||||
type="text"
|
||||
class="material-input"
|
||||
:name="name"
|
||||
:id="id"
|
||||
:placeholder="placeholder"
|
||||
v-model="valueCopy"
|
||||
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:autocomplete="autocomplete"
|
||||
|
||||
:minlength="minlength"
|
||||
:maxlength="maxlength"
|
||||
:required="required"
|
||||
|
||||
@focus="handleFocus(true)"
|
||||
@blur="handleFocus(false)"
|
||||
@input="handleModelInput"
|
||||
>
|
||||
|
||||
<span class="material-input-bar"></span>
|
||||
|
||||
<label class="material-label">
|
||||
<slot></slot>
|
||||
</label>
|
||||
<div v-if="errorMessages" class="material-errors">
|
||||
<div v-for="error in computedErrors" class="material-error">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'material-input',
|
||||
computed: {
|
||||
computedErrors() {
|
||||
return typeof this.errorMessages === 'string'
|
||||
? [this.errorMessages] : this.errorMessages
|
||||
},
|
||||
computedClasses() {
|
||||
return {
|
||||
'material--active': this.focus,
|
||||
'material--disabled': this.disabled,
|
||||
'material--has-errors': Boolean(
|
||||
!this.valid ||
|
||||
(this.errorMessages && this.errorMessages.length)),
|
||||
'material--raised': Boolean(
|
||||
this.focus ||
|
||||
this.valueCopy || // has value
|
||||
(this.placeholder && !this.valueCopy)) // has placeholder
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
valueCopy: null,
|
||||
focus: false,
|
||||
valid: true
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
// Here we are following the Vue2 convention on custom v-model:
|
||||
// https://github.com/vuejs/vue/issues/2873#issuecomment-223759341
|
||||
this.copyValue(this.value)
|
||||
},
|
||||
methods: {
|
||||
handleModelInput(event) {
|
||||
this.$emit('input', event.target.value, event)
|
||||
this.handleValidation()
|
||||
},
|
||||
handleFocus(focused) {
|
||||
this.focus = focused
|
||||
},
|
||||
handleValidation() {
|
||||
this.valid = this.$el ? this.$el.querySelector(
|
||||
'.material-input').validity.valid : this.valid
|
||||
},
|
||||
copyValue(value) {
|
||||
this.valueCopy = value
|
||||
this.handleValidation()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newValue) {
|
||||
this.copyValue(newValue)
|
||||
}
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
value: {
|
||||
default: null
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
min: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
max: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
minlength: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
maxlength: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: 'off'
|
||||
},
|
||||
errorMessages: {
|
||||
type: [Array, String],
|
||||
default: null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
// Fonts:
|
||||
$font-size-base: 16px;
|
||||
$font-size-small: 18px;
|
||||
$font-size-smallest: 12px;
|
||||
$font-weight-normal: normal;
|
||||
// Utils
|
||||
$spacer: 12px;
|
||||
$transition: 0.2s ease all;
|
||||
// Base clases:
|
||||
%base-bar-pseudo {
|
||||
content: '';
|
||||
height: 1px;
|
||||
width: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
transition: $transition;
|
||||
}
|
||||
|
||||
// Mixins:
|
||||
@mixin slided-top() {
|
||||
top: -2 * $spacer;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
// Component:
|
||||
.material-input__component {
|
||||
/*margin-top: 30px;*/
|
||||
position: relative;
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.material-input {
|
||||
font-size: $font-size-base;
|
||||
padding: $spacer $spacer $spacer $spacer / 2;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid transparent; // fixes the height issue
|
||||
}
|
||||
}
|
||||
.material-label {
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-normal;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
left: 0;
|
||||
top: $spacer;
|
||||
transition: $transition;
|
||||
}
|
||||
.material-input-bar {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
&:before {
|
||||
@extend %base-bar-pseudo;
|
||||
left: 50%;
|
||||
}
|
||||
&:after {
|
||||
@extend %base-bar-pseudo;
|
||||
right: 50%;
|
||||
}
|
||||
}
|
||||
// Disabled state:
|
||||
&.material--disabled {
|
||||
.material-input {
|
||||
border-bottom-style: dashed;
|
||||
}
|
||||
}
|
||||
// Raised state:
|
||||
&.material--raised {
|
||||
.material-label {
|
||||
@include slided-top();
|
||||
}
|
||||
}
|
||||
// Active state:
|
||||
&.material--active {
|
||||
.material-input-bar {
|
||||
&:before,
|
||||
&:after {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Errors:
|
||||
.material-errors {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.material-error {
|
||||
font-size: $font-size-smallest;
|
||||
line-height: $font-size-smallest + 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 0;
|
||||
padding-top: $spacer / 2;
|
||||
padding-right: $spacer / 2;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Theme:
|
||||
$color-white: white;
|
||||
$color-grey: #9E9E9E;
|
||||
$color-grey-light: #E0E0E0;
|
||||
$color-blue: #2196F3;
|
||||
$color-red: #F44336;
|
||||
$color-black: black;
|
||||
.material-input__component {
|
||||
background: $color-white;
|
||||
.material-input {
|
||||
background: none;
|
||||
color: $color-black;
|
||||
text-indent: 30px;
|
||||
border-bottom: 1px solid $color-grey-light;
|
||||
}
|
||||
.material-label {
|
||||
color: $color-grey;
|
||||
}
|
||||
.material-input-bar {
|
||||
&:before,
|
||||
&:after {
|
||||
background: $color-blue;
|
||||
}
|
||||
}
|
||||
// Active state:
|
||||
&.material--active {
|
||||
.material-label {
|
||||
color: $color-blue;
|
||||
}
|
||||
}
|
||||
// Errors:
|
||||
&.material--has-errors {
|
||||
// These styles are required
|
||||
// for custom validation:
|
||||
&.material--active .material-label {
|
||||
color: $color-red;
|
||||
}
|
||||
.material-input-bar {
|
||||
&:before,
|
||||
&:after {
|
||||
background: $color-red;
|
||||
}
|
||||
}
|
||||
.material-errors {
|
||||
color: $color-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
108
src/components/MdEditor/index.vue
Normal file
108
src/components/MdEditor/index.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class='simplemde-container'>
|
||||
<textarea :id='id'>
|
||||
</textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import 'simplemde/dist/simplemde.min.css'
|
||||
import SimpleMDE from 'simplemde'
|
||||
export default {
|
||||
name: 'Sticky',
|
||||
props: {
|
||||
value: String,
|
||||
id: {
|
||||
type: String,
|
||||
default: 'markdown-editor'
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
toolbar: {
|
||||
type: Array
|
||||
// default() {
|
||||
// return ['bold', '|', 'link']
|
||||
// }
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
simplemde: null,
|
||||
hasChange: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
if (val === this.simplemde.value() && !this.hasChange) return;
|
||||
this.simplemde.value(val);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.simplemde = new SimpleMDE({
|
||||
element: document.getElementById(this.id),
|
||||
autofocus: this.autofocus,
|
||||
toolbar: this.toolbar,
|
||||
spellChecker: false,
|
||||
insertTexts: {
|
||||
link: ['[', ']( )']
|
||||
},
|
||||
// hideIcons: ['guide', 'heading', 'quote', 'image', 'preview', 'side-by-side', 'fullscreen'],
|
||||
placeholder: this.placeholder
|
||||
});
|
||||
if (this.value) {
|
||||
this.simplemde.value(this.value);
|
||||
}
|
||||
|
||||
this.simplemde.codemirror.on('change', () => {
|
||||
if (this.hasChange) {
|
||||
this.hasChange = true
|
||||
}
|
||||
this.$emit('input', this.simplemde.value());
|
||||
});
|
||||
},
|
||||
destroyed() {
|
||||
this.simplemde = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.simplemde-container .CodeMirror {
|
||||
height: 150px;
|
||||
min-height: 150px;
|
||||
}
|
||||
.simplemde-container .CodeMirror-scroll{
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.simplemde-container .CodeMirror-code{
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
.simplemde-container .editor-statusbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.simplemde-container .CodeMirror .CodeMirror-code .cm-link {
|
||||
color: #1482F0;
|
||||
}
|
||||
|
||||
.simplemde-container .CodeMirror .CodeMirror-code .cm-string.cm-url {
|
||||
color: #2d3b4d;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.simplemde-container .CodeMirror .CodeMirror-code .cm-formatting-link-string.cm-url {
|
||||
padding: 0 2px;
|
||||
font-weight: bold;
|
||||
color: #E61E1E;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
145
src/components/PanThumb/index.vue
Normal file
145
src/components/PanThumb/index.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="pan-item" :style="{zIndex:zIndex,height:height,width:width}">
|
||||
<div class="pan-info">
|
||||
<div class="pan-info-roles-container">
|
||||
<slot>pan</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pan-thumb" :style="{ backgroundImage: 'url('+ image+')' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'PanThumb',
|
||||
props: {
|
||||
image: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '150px'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '150px'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pan-item {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: default;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.pan-info-roles-container {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pan-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
transform-origin: 95% 40%;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.pan-thumb:after {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
top: 40%;
|
||||
left: 95%;
|
||||
margin: -4px 0 0 -4px;
|
||||
background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
|
||||
box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.pan-info {
|
||||
position: absolute;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.pan-info h3 {
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
letter-spacing: 2px;
|
||||
font-size: 18px;
|
||||
margin: 0 60px;
|
||||
padding: 22px 0 0 0;
|
||||
height: 85px;
|
||||
font-family: 'Open Sans', Arial, sans-serif;
|
||||
text-shadow: 0 0 1px #fff,
|
||||
0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.pan-info p {
|
||||
color: #fff;
|
||||
padding: 10px 5px;
|
||||
font-style: italic;
|
||||
margin: 0 30px;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.pan-info p a {
|
||||
display: block;
|
||||
color: #333;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
font-size: 9px;
|
||||
letter-spacing: 1px;
|
||||
padding-top: 24px;
|
||||
margin: 7px auto 0;
|
||||
font-family: 'Open Sans', Arial, sans-serif;
|
||||
opacity: 0;
|
||||
transition: transform 0.3s ease-in-out 0.2s,
|
||||
opacity 0.3s ease-in-out 0.2s,
|
||||
background 0.2s linear 0s;
|
||||
transform: translateX(60px) rotate(90deg);
|
||||
}
|
||||
|
||||
.pan-info p a:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.pan-item:hover .pan-thumb {
|
||||
transform: rotate(-110deg);
|
||||
}
|
||||
|
||||
.pan-item:hover .pan-info p a {
|
||||
opacity: 1;
|
||||
transform: translateX(0px) rotate(0deg);
|
||||
}
|
||||
</style>
|
61
src/components/SplitPane/Pane.vue
Normal file
61
src/components/SplitPane/Pane.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Pane',
|
||||
props: {
|
||||
// split: {
|
||||
// validator: function (value) {
|
||||
// return ['vertical', 'horizontal'].indexOf(value) >= 0
|
||||
// },
|
||||
// required: true
|
||||
// }
|
||||
},
|
||||
// computed:{
|
||||
// classes () {
|
||||
// return this.$parent.split
|
||||
// },
|
||||
// },
|
||||
data() {
|
||||
const classes = ['Pane', this.$parent.split, 'className'];
|
||||
return {
|
||||
classes: classes.join(' '),
|
||||
percent: 50
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// console.log(this.$parent.split)
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.splitter-pane.vertical.splitter-paneL{
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
}
|
||||
.splitter-pane.vertical.splitter-paneR{
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
height: 100%;
|
||||
}
|
||||
.splitter-pane.horizontal.splitter-paneL{
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
.splitter-pane.horizontal.splitter-paneR{
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
75
src/components/SplitPane/Resizer.vue
Normal file
75
src/components/SplitPane/Resizer.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div :class="classes" @mousedown="onMouseDown"></div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.Resizer {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
background: #000;
|
||||
position: absolute;
|
||||
opacity: .2;
|
||||
z-index: 1;
|
||||
/*-moz-background-clip: padding;*/
|
||||
/*-webkit-background-clip: padding;*/
|
||||
/*background-clip: padding-box;*/
|
||||
}
|
||||
|
||||
/*.Resizer:hover {*/
|
||||
/*-webkit-transition: all 2s ease;*/
|
||||
/*transition: all 2s ease;*/
|
||||
/*}*/
|
||||
|
||||
.Resizer.horizontal {
|
||||
height: 11px;
|
||||
margin: -5px 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Resizer.horizontal:hover {
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.vertical {
|
||||
width: 11px;
|
||||
height: 100%;
|
||||
/*margin: 0 -5px;*/
|
||||
|
||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.Resizer.vertical:hover {
|
||||
border-left: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-right: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
split: {
|
||||
validator(value) {
|
||||
return ['vertical', 'horizontal'].indexOf(value) >= 0
|
||||
},
|
||||
required: true
|
||||
},
|
||||
onMouseDown: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const classes = ['Resizer', this.split, 'className'];
|
||||
return {
|
||||
classes: classes.join(' ')
|
||||
}
|
||||
},
|
||||
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
175
src/components/SplitPane/SplitPane-backup.vue
Normal file
175
src/components/SplitPane/SplitPane-backup.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<!--<template>-->
|
||||
<!--<div :style="{ cursor, userSelect }" class="vue-splitter-container clearfix" @mouseup="onMouseUp"-->
|
||||
<!--@mousemove="onMouseMove">-->
|
||||
|
||||
<!--<Pane split="vertical" :style="{ width: percent+'%' }" class="left-container splitter-pane">-->
|
||||
<!--orange-->
|
||||
<!--</Pane>-->
|
||||
|
||||
<!--<Resizer split="vertical" :onMouseDown="onMouseDown" @click="onClick"></Resizer>-->
|
||||
<!--<div class="todel" :style="{ width: 100-percent+'%'}">-->
|
||||
<!--<Pane split="horizontal" class="top-container">-->
|
||||
<!--<div slot>apple banana</div>-->
|
||||
<!--</Pane>-->
|
||||
<!--<Resizer split="horizontal" :onMouseDown="onMouseDown" @click="onClick"></Resizer>-->
|
||||
<!--<Pane split="horizontal" class="bottom-container">-->
|
||||
<!--<div slot>apple banana</div>-->
|
||||
<!--</Pane>-->
|
||||
<!--</div>-->
|
||||
|
||||
<!--</div>-->
|
||||
|
||||
<!--</template>-->
|
||||
<style scoped>
|
||||
.clearfix:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
content: " ";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
.vue-splitter-container {
|
||||
height: inherit;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
/* eslint-disable */
|
||||
import Resizer from './Resizer';
|
||||
import vue from 'vue'
|
||||
export default {
|
||||
name: 'splitPane',
|
||||
components: {Resizer},
|
||||
props: {
|
||||
margin: {
|
||||
type: Number,
|
||||
default: 10
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
active: false,
|
||||
percent: 50,
|
||||
hasMoved: false,
|
||||
panes: []
|
||||
}
|
||||
},
|
||||
props: {
|
||||
split: {
|
||||
validator: function (value) {
|
||||
return ['vertical', 'horizontal'].indexOf(value) >= 0
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userSelect () {
|
||||
return this.active ? 'none' : ''
|
||||
},
|
||||
cursor () {
|
||||
return this.active ? 'col-resize' : ''
|
||||
},
|
||||
// $paneItems () {
|
||||
// return this.$children.filter(child => {
|
||||
// console.log(child)
|
||||
// })
|
||||
// }
|
||||
},
|
||||
render(h){
|
||||
const temp = [];
|
||||
this.$slots.default.map((item, i) => {
|
||||
if (item.tag && item.tag.toUpperCase().indexOf('PANE') >= 0) {
|
||||
temp.push(item)
|
||||
}
|
||||
});
|
||||
const newSlots = [];
|
||||
const length = temp.length;
|
||||
temp.map((item, index)=> {
|
||||
newSlots.push(item)
|
||||
if (index != length - 1) {
|
||||
newSlots.push(
|
||||
h('Resizer', {
|
||||
props: {
|
||||
split: this.split,
|
||||
onMouseDown: this.onMouseDown
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
return h('div', {
|
||||
on: {
|
||||
mousemove: this.onMouseMove
|
||||
}
|
||||
}, [
|
||||
h('div', {
|
||||
'class': {
|
||||
'vue-splitter-container': true
|
||||
},
|
||||
}, newSlots)
|
||||
])
|
||||
},
|
||||
// beforeMount(){
|
||||
// this.$slots.default=this.$slots.default.map((item, i) => {
|
||||
// if (item.tag&&item.tag.toUpperCase().indexOf('PANE') >= 0) {
|
||||
// return item
|
||||
// }else{
|
||||
// return null
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// },
|
||||
created(){
|
||||
|
||||
},
|
||||
mounted(){
|
||||
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
if (!this.hasMoved) {
|
||||
this.percent = 50;
|
||||
this.$emit('resize');
|
||||
}
|
||||
},
|
||||
onMouseDown () {
|
||||
this.active = true;
|
||||
this.hasMoved = false;
|
||||
},
|
||||
onMouseUp () {
|
||||
this.active = false;
|
||||
},
|
||||
onMouseMove (e) {
|
||||
if (e.buttons === 0 || e.which === 0) {
|
||||
this.active = false;
|
||||
}
|
||||
if (this.active) {
|
||||
|
||||
let offset = 0;
|
||||
let target = e.currentTarget;
|
||||
while (target) {
|
||||
offset += target.offsetLeft;
|
||||
target = target.offsetParent;
|
||||
}
|
||||
const percent = Math.floor(((e.pageX - offset) / e.currentTarget.offsetWidth) * 10000) / 100;
|
||||
if (percent > this.margin && percent < 100 - this.margin) {
|
||||
this.percent = percent;
|
||||
}
|
||||
console.log(percent)
|
||||
this.$children.map((v, i)=> {
|
||||
if (i == 0) {
|
||||
v.percent = percent
|
||||
} else {
|
||||
v.percent = 100 - percent
|
||||
}
|
||||
|
||||
})
|
||||
this.$emit('resize');
|
||||
this.hasMoved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
117
src/components/SplitPane/SplitPane.vue
Normal file
117
src/components/SplitPane/SplitPane.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div ref :style="{ cursor, userSelect}" class="vue-splitter-container clearfix" @mouseup="onMouseUp"
|
||||
@mousemove="onMouseMove">
|
||||
<Pane class="splitter-pane splitter-paneL" :split="split" :style="{ [type]: percent+'%'}">
|
||||
<slot name="paneL"></slot>
|
||||
</Pane>
|
||||
<Resizer :style="{ [resizeType]: percent+'%'}" :split="split" :onMouseDown="onMouseDown"
|
||||
@click="onClick"></Resizer>
|
||||
<Pane class="splitter-pane splitter-paneR" :split="split" :style="{ [type]: 100-percent+'%'}">
|
||||
<slot name="paneR"></slot>
|
||||
</Pane>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.clearfix:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
content: " ";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
.vue-splitter-container {
|
||||
height: 100%;
|
||||
/*display: flex;*/
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Resizer from './Resizer';
|
||||
import Pane from './Pane';
|
||||
export default {
|
||||
name: 'splitPane',
|
||||
components: { Resizer, Pane },
|
||||
props: {
|
||||
margin: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
split: {
|
||||
validator(value) {
|
||||
return ['vertical', 'horizontal'].indexOf(value) >= 0
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
active: false,
|
||||
hasMoved: false,
|
||||
height: null,
|
||||
percent: 50,
|
||||
type: this.split === 'vertical' ? 'width' : 'height',
|
||||
resizeType: this.split === 'vertical' ? 'left' : 'top'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userSelect() {
|
||||
return this.active ? 'none' : ''
|
||||
},
|
||||
cursor() {
|
||||
return this.active ? 'col-resize' : ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const element = this.$el;
|
||||
const elementOffset = element.getBoundingClientRect();
|
||||
console.log(elementOffset.height)
|
||||
// this.height = elementOffset.height+'px';
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
if (!this.hasMoved) {
|
||||
this.percent = 50;
|
||||
this.$emit('resize');
|
||||
}
|
||||
},
|
||||
onMouseDown() {
|
||||
this.active = true;
|
||||
this.hasMoved = false;
|
||||
},
|
||||
onMouseUp() {
|
||||
this.active = false;
|
||||
},
|
||||
onMouseMove(e) {
|
||||
if (e.buttons === 0 || e.which === 0) {
|
||||
this.active = false;
|
||||
}
|
||||
if (this.active) {
|
||||
let offset = 0;
|
||||
let target = e.currentTarget;
|
||||
if (this.split === 'vertical') {
|
||||
while (target) {
|
||||
offset += target.offsetLeft;
|
||||
target = target.offsetParent;
|
||||
}
|
||||
} else {
|
||||
while (target) {
|
||||
offset += target.offsetTop;
|
||||
target = target.offsetParent;
|
||||
}
|
||||
}
|
||||
|
||||
const currentPage = this.split === 'vertical' ? e.pageX : e.pageY;
|
||||
const targetOffset = this.split === 'vertical' ? e.currentTarget.offsetWidth : e.currentTarget.offsetHeight;
|
||||
const percent = Math.floor(((currentPage - offset) / targetOffset) * 10000) / 100;
|
||||
if (percent > this.margin && percent < 100 - this.margin) {
|
||||
this.percent = percent;
|
||||
}
|
||||
this.$emit('resize');
|
||||
this.hasMoved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
7
src/components/SplitPane/index.js
Normal file
7
src/components/SplitPane/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import SplitPane from './a.vue';
|
||||
import Pane from './Pane.vue';
|
||||
|
||||
export {
|
||||
SplitPane,
|
||||
Pane
|
||||
}
|
73
src/components/Sticky/index.vue
Normal file
73
src/components/Sticky/index.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div :style="{height:height+'px',zIndex:zIndex}">
|
||||
<div :class="className" :style="{top:stickyTop+'px',zIndex:zIndex,position:position,width:width,height:height+'px'}">
|
||||
<slot>
|
||||
<div>sticky</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'Sticky',
|
||||
props: {
|
||||
stickyTop: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
},
|
||||
className: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
active: false,
|
||||
position: '',
|
||||
currentTop: '',
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
child: null,
|
||||
stickyHeight: 0
|
||||
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
sticky() {
|
||||
if (this.active) {
|
||||
return
|
||||
}
|
||||
this.position = 'fixed';
|
||||
this.active = true;
|
||||
this.width = this.width + 'px';
|
||||
},
|
||||
reset() {
|
||||
if (!this.active) {
|
||||
return
|
||||
}
|
||||
this.position = '';
|
||||
this.width = 'auto'
|
||||
this.active = false
|
||||
},
|
||||
handleScroll() {
|
||||
this.width = this.$el.getBoundingClientRect().width;
|
||||
const offsetTop = this.$el.getBoundingClientRect().top;
|
||||
if (offsetTop <= this.stickyTop) {
|
||||
this.sticky();
|
||||
return
|
||||
}
|
||||
this.reset()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.height = this.$el.getBoundingClientRect().height;
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
};
|
||||
</script>
|
119
src/components/Tinymce/components/editorAudio.vue
Normal file
119
src/components/Tinymce/components/editorAudio.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-button :style="{background:color,borderColor:color}" @click=" dialogVisible=true" type="primary">上传音频
|
||||
</el-button>
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px" label-position="right">
|
||||
<el-upload
|
||||
class="editor-audio-upload"
|
||||
action="https://upload.qbox.me"
|
||||
:data="dataObj"
|
||||
:show-file-list="true"
|
||||
:file-list="audioList"
|
||||
:on-success="handleAudioScucess"
|
||||
:on-change="handleAudioChange"
|
||||
:before-upload="audioBeforeUpload">
|
||||
<el-button size="small" type="primary">上传音频</el-button>
|
||||
</el-upload>
|
||||
<el-form-item prop="url" label="音频URL">
|
||||
<el-input v-model="form.url"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="title" label="音频标题">
|
||||
<el-input v-model="form.title"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="音频文本">
|
||||
<el-input type="textarea" :autosize="{ minRows: 2}" v-model="form.text"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getToken } from 'api/qiniu';
|
||||
|
||||
export default {
|
||||
name: 'editorAudioUpload',
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: '#20a0ff'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
dataObj: { token: '', key: '' },
|
||||
audioList: [],
|
||||
tempAudioUrl: '',
|
||||
form: {
|
||||
title: '',
|
||||
url: '',
|
||||
text: ''
|
||||
},
|
||||
rules: {
|
||||
title: [
|
||||
{ required: true, trigger: 'blur' }
|
||||
],
|
||||
url: [
|
||||
{ required: true, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleSubmit() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
this.$emit('successCBK', this.form);
|
||||
this.dialogVisible = false;
|
||||
this.form = {
|
||||
title: '',
|
||||
url: '',
|
||||
text: ''
|
||||
}
|
||||
} else {
|
||||
this.$message('填写有误');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
handleAudioChange(file, fileList) {
|
||||
this.audioList = fileList.slice(-1);
|
||||
},
|
||||
handleAudioScucess() {
|
||||
this.form.url = this.tempAudioUrl
|
||||
},
|
||||
audioBeforeUpload() {
|
||||
const _self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
getToken().then(response => {
|
||||
const key = response.data.qiniu_key;
|
||||
const token = response.data.qiniu_token;
|
||||
_self._data.dataObj.token = token;
|
||||
_self._data.dataObj.key = key;
|
||||
this.tempAudioUrl = response.data.qiniu_url;
|
||||
resolve(true);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
reject(false)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.upload-container {
|
||||
.editor-audio-upload {
|
||||
button {
|
||||
float: left;
|
||||
margin-left: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
85
src/components/Tinymce/components/editorImage.vue
Normal file
85
src/components/Tinymce/components/editorImage.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-button icon='upload' :style="{background:color,borderColor:color}" @click=" dialogVisible=true" type="primary">上传图片
|
||||
</el-button>
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<el-upload
|
||||
class="editor-slide-upload"
|
||||
action="https://upload.qbox.me"
|
||||
:data="dataObj"
|
||||
:multiple="true"
|
||||
:file-list="fileList"
|
||||
:show-file-list="true"
|
||||
list-type="picture-card"
|
||||
:on-remove="handleRemove"
|
||||
:before-upload="beforeUpload">
|
||||
<el-button size="small" type="primary">点击上传</el-button>
|
||||
</el-upload>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getToken } from 'api/qiniu';
|
||||
|
||||
export default {
|
||||
name: 'editorSlideUpload',
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: '#20a0ff'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
dataObj: { token: '', key: '' },
|
||||
list: [],
|
||||
fileList: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleSubmit() {
|
||||
const arr = this.list.map(v => v.url);
|
||||
this.$emit('successCBK', arr);
|
||||
this.list = [];
|
||||
this.fileList = [];
|
||||
this.dialogVisible = false;
|
||||
},
|
||||
handleRemove(file) {
|
||||
const key = file.response.key;
|
||||
for (let i = 0, len = this.list.length; i < len; i++) {
|
||||
if (this.list[i].key === key) {
|
||||
this.list.splice(i, 1);
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUpload() {
|
||||
const _self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
getToken().then(response => {
|
||||
const key = response.data.qiniu_key;
|
||||
const token = response.data.qiniu_token;
|
||||
_self._data.dataObj.token = token;
|
||||
_self._data.dataObj.key = key;
|
||||
this.list.push({ key, url: response.data.qiniu_url });
|
||||
resolve(true);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
reject(false)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.upload-container {
|
||||
.editor-slide-upload {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
82
src/components/Tinymce/components/editorSlide.vue
Normal file
82
src/components/Tinymce/components/editorSlide.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-button :style="{background:color,borderColor:color}" @click=" dialogVisible=true" type="primary">上传轮播图
|
||||
</el-button>
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<el-upload
|
||||
class="editor-slide-upload"
|
||||
action="https://upload.qbox.me"
|
||||
:data="dataObj"
|
||||
:multiple="true"
|
||||
:show-file-list="true"
|
||||
list-type="picture-card"
|
||||
:on-remove="handleRemove"
|
||||
:before-upload="beforeUpload">
|
||||
<el-button size="small" type="primary">点击上传</el-button>
|
||||
</el-upload>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getToken } from 'api/qiniu';
|
||||
|
||||
export default {
|
||||
name: 'editorSlideUpload',
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: '#20a0ff'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
dataObj: { token: '', key: '' },
|
||||
list: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleSubmit() {
|
||||
const arr = this.list.map(v => v.url);
|
||||
this.$emit('successCBK', arr);
|
||||
this.list = [];
|
||||
this.dialogVisible = false;
|
||||
},
|
||||
handleRemove(file) {
|
||||
const key = file.response.key;
|
||||
for (let i = 0, len = this.list.length; i < len; i++) {
|
||||
if (this.list[i].key === key) {
|
||||
this.list.splice(i, 1);
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUpload() {
|
||||
const _self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
getToken().then(response => {
|
||||
const key = response.data.qiniu_key;
|
||||
const token = response.data.qiniu_token;
|
||||
_self._data.dataObj.token = token;
|
||||
_self._data.dataObj.key = key;
|
||||
this.list.push({ key, url: response.data.qiniu_url });
|
||||
resolve(true);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
reject(false)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.upload-container {
|
||||
.editor-slide-upload {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
167
src/components/Tinymce/components/editorVideo.vue
Normal file
167
src/components/Tinymce/components/editorVideo.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-button :style="{background:color,borderColor:color}" @click=" dialogVisible=true" type="primary">上传视频</el-button>
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="140px" label-position="left">
|
||||
<el-upload
|
||||
class="editor-video-upload"
|
||||
action="https://upload.qbox.me"
|
||||
:data="dataObj"
|
||||
:show-file-list="true"
|
||||
:file-list="videoList"
|
||||
:on-success="handleVideoScucess"
|
||||
:on-change="handleVideoChange"
|
||||
:before-upload="videoBeforeUpload">
|
||||
<el-button size="small" type="primary">上传视频</el-button>
|
||||
</el-upload>
|
||||
<el-form-item prop="url" label="视频URL">
|
||||
<el-input v-model="form.url"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="title" label="视频标题">
|
||||
<el-input v-model="form.title"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="上传视频封面图">
|
||||
</el-form-item>
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
action="https://upload.qbox.me"
|
||||
:show-file-list="false"
|
||||
:data="dataObj"
|
||||
:on-success="handleImageScucess"
|
||||
:before-upload="beforeImageUpload">
|
||||
<img v-if="form.image" :src="form.image" class="image-uploader-image">
|
||||
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
|
||||
</el-upload>
|
||||
</el-form>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getToken } from 'api/qiniu';
|
||||
|
||||
export default {
|
||||
name: 'editorVideoUpload',
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: '#20a0ff'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
dataObj: { token: '', key: '' },
|
||||
videoList: [],
|
||||
tempVideoUrl: '',
|
||||
tempImageUrl: '',
|
||||
form: {
|
||||
title: '',
|
||||
url: '',
|
||||
image: ''
|
||||
},
|
||||
rules: {
|
||||
url: [
|
||||
{ required: true, trigger: 'blur' }
|
||||
],
|
||||
title: [
|
||||
{ required: true, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleSubmit() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.image.length > 0) {
|
||||
this.$emit('successCBK', this.form);
|
||||
this.dialogVisible = false;
|
||||
this.form = {
|
||||
title: '',
|
||||
url: '',
|
||||
image: ''
|
||||
}
|
||||
} else {
|
||||
this.$message('请上传图片');
|
||||
}
|
||||
} else {
|
||||
this.$message('填写有误');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
handleVideoChange(file, fileList) {
|
||||
this.videoList = fileList.slice(-1);
|
||||
},
|
||||
handleVideoScucess() {
|
||||
this.form.url = this.tempVideoUrl
|
||||
},
|
||||
videoBeforeUpload() {
|
||||
const _self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
getToken().then(response => {
|
||||
const key = response.data.qiniu_key;
|
||||
const token = response.data.qiniu_token;
|
||||
_self._data.dataObj.token = token;
|
||||
_self._data.dataObj.key = key;
|
||||
this.tempVideoUrl = response.data.qiniu_url;
|
||||
resolve(true);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
reject(false)
|
||||
});
|
||||
});
|
||||
},
|
||||
handleImageScucess() {
|
||||
this.form.image = this.tempImageUrl
|
||||
},
|
||||
beforeImageUpload() {
|
||||
const _self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
getToken().then(response => {
|
||||
const key = response.data.qiniu_key;
|
||||
const token = response.data.qiniu_token;
|
||||
_self._data.dataObj.token = token;
|
||||
_self._data.dataObj.key = key;
|
||||
this.tempImageUrl = response.data.qiniu_url;
|
||||
resolve(true);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
reject(false)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.upload-container {
|
||||
.editor-video-upload {
|
||||
button {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
.image-uploader {
|
||||
margin: 5px auto;
|
||||
width: 400px;
|
||||
height: 200px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
line-height: 200px;
|
||||
i {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
}
|
||||
.image-uploader-image {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
251
src/components/Tinymce/index.vue
Normal file
251
src/components/Tinymce/index.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div class='tinymce-container editor-container'>
|
||||
<textarea class='tinymce-textarea' :id="id"></textarea>
|
||||
<div class="editor-custom-btn-container">
|
||||
<editorSlide v-if="customButton.indexOf('editorSlide')>=0" color="#3A71A8" class="editor-upload-btn" @successCBK="slideSuccessCBK"></editorSlide>
|
||||
<editorAudio v-if="customButton.indexOf('editorAudio')>=0" color="#30B08F" class="editor-upload-btn" @successCBK="aduioSuccessCBK"></editorAudio>
|
||||
<editorVideo v-if="customButton.indexOf('editorVideo')>=0" color="#E65D6E" class="editor-upload-btn" @successCBK="videoSuccessCBK"></editorVideo>
|
||||
<editorImage v-if="customButton.indexOf('editorImage')>=0" color="#20a0ff" class="editor-upload-btn" @successCBK="imageSuccessCBK"></editorImage>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import editorAudio from './components/editorAudio';
|
||||
import editorVideo from './components/editorVideo';
|
||||
import editorSlide from './components/editorSlide';
|
||||
import editorImage from './components/editorImage';
|
||||
import { getToken, upload } from 'api/qiniu';
|
||||
export default {
|
||||
name: 'tinymce',
|
||||
components: { editorImage, editorAudio, editorSlide, editorVideo },
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: 'tinymceEditor'
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
customButton: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return ['editorAudio', 'editorImage']
|
||||
}
|
||||
},
|
||||
toolbar: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return ['removeformat undo redo | bullist numlist | outdent indent | forecolor | fullscreen code', 'bold italic blockquote | h2 p media link | alignleft aligncenter alignright']
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasChange: false,
|
||||
hasInit: false
|
||||
}
|
||||
},
|
||||
menubar: {
|
||||
default: ''
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 360
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
if (!this.hasChange && this.hasInit) {
|
||||
this.$nextTick(() => tinymce.get(this.id).setContent(val))
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const _this = this;
|
||||
tinymce.init({
|
||||
selector: `#${this.id}`,
|
||||
height: this.height,
|
||||
body_class: 'panel-body ',
|
||||
object_resizing: false,
|
||||
// language: 'zh_CN',
|
||||
// language_url: '/static/tinymce/langs/zh_CN.js',
|
||||
toolbar: this.toolbar,
|
||||
menubar: this.menubar,
|
||||
plugins: 'advlist,autolink,code,powerpaste,textcolor, colorpicker,fullscreen,link,lists,media,wordcount, imagetools,watermark',
|
||||
end_container_on_empty_block: true,
|
||||
powerpaste_word_import: 'clean',
|
||||
code_dialog_height: 450,
|
||||
code_dialog_width: 1000,
|
||||
advlist_bullet_styles: 'square',
|
||||
advlist_number_styles: 'default',
|
||||
block_formats: '普通标签=p;小标题=h2;',
|
||||
imagetools_cors_hosts: ['wpimg.wallstcn.com', 'wallstreetcn.com'],
|
||||
imagetools_toolbar: 'watermark',
|
||||
default_link_target: '_blank',
|
||||
link_title: false,
|
||||
textcolor_map: [
|
||||
'1482f0', '1482f0',
|
||||
'4595e6', '4595e6'],
|
||||
init_instance_callback: editor => {
|
||||
if (_this.value) {
|
||||
editor.setContent(_this.value)
|
||||
}
|
||||
_this.hasInit = true;
|
||||
editor.on('Change', () => {
|
||||
this.hasChange = true;
|
||||
this.$emit('input', editor.getContent({ format: 'raw' }));
|
||||
});
|
||||
},
|
||||
images_dataimg_filter(img) {
|
||||
setTimeout(() => {
|
||||
const $image = $(img);
|
||||
$image.removeAttr('width');
|
||||
$image.removeAttr('height');
|
||||
if ($image[0].height && $image[0].width) {
|
||||
$image.attr('data-wscntype', 'image');
|
||||
$image.attr('data-wscnh', $image[0].height);
|
||||
$image.attr('data-wscnw', $image[0].width);
|
||||
$image.addClass('wscnph');
|
||||
}
|
||||
}, 0);
|
||||
return img
|
||||
},
|
||||
images_upload_handler(blobInfo, success, failure, progress) {
|
||||
progress(0);
|
||||
const token = _this.$store.getters.token;
|
||||
getToken(token).then(response => {
|
||||
const url = response.data.qiniu_url;
|
||||
const formData = new FormData();
|
||||
formData.append('token', response.data.qiniu_token);
|
||||
formData.append('key', response.data.qiniu_key);
|
||||
formData.append('file', blobInfo.blob(), url);
|
||||
upload(formData).then(() => {
|
||||
success(url);
|
||||
progress(100);
|
||||
// setTimeout(() => {
|
||||
// const doc = tinymce.activeEditor.getDoc();
|
||||
// const $$ = tinymce.dom.DomQuery;
|
||||
// const $image = $$(doc).find('img[src="' + url + '"]')
|
||||
// $image.addClass('wscnph');
|
||||
// $image.attr('data-wscntype', 'image');
|
||||
// $image.attr('data-wscnh', $image[0].height || 640);
|
||||
// $image.attr('data-wscnw', $image[0].width || 640);
|
||||
// }, 0);
|
||||
})
|
||||
}).catch(err => {
|
||||
failure('出现未知问题,刷新页面,或者联系程序员')
|
||||
console.log(err);
|
||||
});
|
||||
},
|
||||
setup(editor) {
|
||||
editor.addButton('h2', {
|
||||
title: '小标题', // tooltip text seen on mouseover
|
||||
text: '小标题',
|
||||
onclick() {
|
||||
editor.execCommand('mceToggleFormat', false, 'h2');
|
||||
},
|
||||
onPostRender() {
|
||||
const btn = this;
|
||||
editor.on('init', () => {
|
||||
editor.formatter.formatChanged('h2', state => {
|
||||
btn.active(state);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
editor.addButton('p', {
|
||||
title: '正文', // tooltip text seen on mouseover
|
||||
text: '正文',
|
||||
onclick() {
|
||||
editor.execCommand('mceToggleFormat', false, 'p');
|
||||
},
|
||||
onPostRender() {
|
||||
const btn = this;
|
||||
editor.on('init', () => {
|
||||
editor.formatter.formatChanged('p', state => {
|
||||
btn.active(state);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
imageSuccessCBK(arr) {
|
||||
console.log(arr)
|
||||
const _this = this;
|
||||
arr.forEach(v => {
|
||||
const node = document.createElement('img');
|
||||
node.setAttribute('src', v);
|
||||
node.onload = function() {
|
||||
$(this).addClass('wscnph');
|
||||
$(this).attr('data-wscntype', 'image');
|
||||
$(this).attr('data-wscnh', this.height);
|
||||
$(this).attr('data-wscnw', this.width);
|
||||
tinymce.get(_this.id).insertContent(node.outerHTML)
|
||||
}
|
||||
})
|
||||
},
|
||||
slideSuccessCBK(arr) {
|
||||
const node = document.createElement('img');
|
||||
node.setAttribute('data-wscntype', 'slide');
|
||||
node.setAttribute('data-uri', arr.toString());
|
||||
node.setAttribute('data-wscnh', '190');
|
||||
node.setAttribute('data-wscnw', '200');
|
||||
node.setAttribute('src', ' https://wdl.wallstreetcn.com/6410b47d-a54c-4826-9bc1-c3e5df31280c');
|
||||
node.className = 'wscnph editor-placeholder';
|
||||
tinymce.get(this.id).insertContent(node.outerHTML)
|
||||
},
|
||||
videoSuccessCBK(form) {
|
||||
const node = document.createElement('img');
|
||||
node.setAttribute('data-wscntype', 'video');
|
||||
node.setAttribute('data-uri', form.url);
|
||||
node.setAttribute('data-cover-img-uri', form.image);
|
||||
node.setAttribute('data-title', form.title);
|
||||
node.setAttribute('src', 'https://wdl.wallstreetcn.com/07aeb3e7-f4ca-4207-befb-c987b3dc7011');
|
||||
node.className = 'wscnph editor-placeholder';
|
||||
tinymce.get(this.id).insertContent(node.outerHTML)
|
||||
},
|
||||
aduioSuccessCBK(form) {
|
||||
const node = document.createElement('img');
|
||||
node.setAttribute('data-wscntype', 'audio');
|
||||
node.setAttribute('data-uri', form.url);
|
||||
node.setAttribute('data-title', form.title);
|
||||
node.setAttribute('data-text', form.text);
|
||||
node.setAttribute('src', 'https://wdl.wallstreetcn.com/2ed0c8c8-fb82-499d-b81c-3fd1de114eae');
|
||||
node.className = 'wscnph editor-placeholder';
|
||||
tinymce.get(this.id).insertContent(node.outerHTML)
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
tinymce.get(this.id).destroy();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tinymce-container {
|
||||
position: relative
|
||||
}
|
||||
|
||||
.tinymce-textarea {
|
||||
visibility: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.editor-custom-btn-container {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
/*z-index: 2005;*/
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
.editor-upload-btn {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
128
src/components/Upload/singleImage.vue
Normal file
128
src/components/Upload/singleImage.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
:data="dataObj"
|
||||
drag
|
||||
:multiple="false"
|
||||
:show-file-list="false"
|
||||
action="https://upload.qbox.me"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="handleImageScucess">
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</el-upload>
|
||||
<div class="image-preview">
|
||||
<div class="image-preview-wrapper" v-show="imageUrl.length>1">
|
||||
<img :src="imageUrl+'?imageView2/1/w/200/h/200'">
|
||||
<div class="image-preview-action">
|
||||
<i @click="rmImage" class="el-icon-delete"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
// 预览效果见付费文章
|
||||
import { getToken } from 'api/qiniu';
|
||||
export default {
|
||||
name: 'singleImageUpload',
|
||||
props: {
|
||||
value: String
|
||||
},
|
||||
computed: {
|
||||
imageUrl() {
|
||||
return this.value
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tempUrl: '',
|
||||
dataObj: { token: '', key: '' }
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
rmImage() {
|
||||
this.emitInput('');
|
||||
},
|
||||
emitInput(val) {
|
||||
this.$emit('input', val);
|
||||
},
|
||||
handleImageScucess() {
|
||||
this.emitInput(this.tempUrl)
|
||||
},
|
||||
beforeUpload() {
|
||||
const _self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
getToken().then(response => {
|
||||
const key = response.data.qiniu_key;
|
||||
const token = response.data.qiniu_token;
|
||||
_self._data.dataObj.token = token;
|
||||
_self._data.dataObj.key = key;
|
||||
this.tempUrl = response.data.qiniu_url;
|
||||
resolve(true);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
reject(false)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
@import "src/styles/mixin.scss";
|
||||
.upload-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
@include clearfix;
|
||||
.image-uploader {
|
||||
width: 60%;
|
||||
float: left;
|
||||
}
|
||||
.image-preview {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
position: relative;
|
||||
border: 1px dashed #d9d9d9;
|
||||
float: left;
|
||||
margin-left: 50px;
|
||||
.image-preview-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.image-preview-action {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
font-size: 20px;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
transition: opacity .3s;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
line-height: 200px;
|
||||
.el-icon-delete {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.image-preview-action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
126
src/components/Upload/singleImage2.vue
Normal file
126
src/components/Upload/singleImage2.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="singleImageUpload2 upload-container">
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
:data="dataObj"
|
||||
drag
|
||||
:multiple="false"
|
||||
:show-file-list="false"
|
||||
action="https://upload.qbox.me"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="handleImageScucess">
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">Drag或<em>点击上传</em></div>
|
||||
</el-upload>
|
||||
<div v-show="imageUrl.length>0" class="image-preview">
|
||||
<div class="image-preview-wrapper" v-show="imageUrl.length>1">
|
||||
<img :src="imageUrl">
|
||||
<div class="image-preview-action">
|
||||
<i @click="rmImage" class="el-icon-delete"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
// 预览效果见专题
|
||||
import { getToken } from 'api/qiniu';
|
||||
export default {
|
||||
name: 'singleImageUpload2',
|
||||
props: {
|
||||
value: String
|
||||
},
|
||||
computed: {
|
||||
imageUrl() {
|
||||
return this.value
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tempUrl: '',
|
||||
dataObj: { token: '', key: '' }
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
rmImage() {
|
||||
this.emitInput('');
|
||||
},
|
||||
emitInput(val) {
|
||||
this.$emit('input', val);
|
||||
},
|
||||
handleImageScucess() {
|
||||
this.emitInput(this.tempUrl)
|
||||
},
|
||||
beforeUpload() {
|
||||
const _self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
getToken().then(response => {
|
||||
const key = response.data.qiniu_key;
|
||||
const token = response.data.qiniu_token;
|
||||
_self._data.dataObj.token = token;
|
||||
_self._data.dataObj.key = key;
|
||||
this.tempUrl = response.data.qiniu_url;
|
||||
resolve(true);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
reject(false)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.upload-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
.image-uploader{
|
||||
height: 100%;
|
||||
}
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
.image-preview-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.image-preview-action {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
font-size: 20px;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
transition: opacity .3s;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
line-height: 200px;
|
||||
.el-icon-delete {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.image-preview-action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
154
src/components/Upload/singleImage3.vue
Normal file
154
src/components/Upload/singleImage3.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
:data="dataObj"
|
||||
drag
|
||||
:multiple="false"
|
||||
:show-file-list="false"
|
||||
action="https://upload.qbox.me"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="handleImageScucess">
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</el-upload>
|
||||
<div class="image-preview image-app-preview">
|
||||
<div class="image-preview-wrapper" v-show="imageUrl.length>1">
|
||||
<div class='app-fake-conver'>  全球 付费节目单 最热 经济</div>
|
||||
<img :src="imageUrl+'?imageView2/1/h/180/w/320/q/100'">
|
||||
<div class="image-preview-action">
|
||||
<i @click="rmImage" class="el-icon-delete"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-preview">
|
||||
<div class="image-preview-wrapper" v-show="imageUrl.length>1">
|
||||
<img :src="imageUrl+'?imageView2/1/w/200/h/200'">
|
||||
<div class="image-preview-action">
|
||||
<i @click="rmImage" class="el-icon-delete"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
// 预览效果见文章
|
||||
import { getToken } from 'api/qiniu';
|
||||
export default {
|
||||
name: 'singleImageUpload',
|
||||
props: {
|
||||
value: String
|
||||
},
|
||||
computed: {
|
||||
imageUrl() {
|
||||
return this.value
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tempUrl: '',
|
||||
dataObj: { token: '', key: '' }
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
rmImage() {
|
||||
this.emitInput('');
|
||||
},
|
||||
emitInput(val) {
|
||||
this.$emit('input', val);
|
||||
},
|
||||
handleImageScucess() {
|
||||
this.emitInput(this.tempUrl)
|
||||
},
|
||||
beforeUpload() {
|
||||
const _self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
getToken().then(response => {
|
||||
const key = response.data.qiniu_key;
|
||||
const token = response.data.qiniu_token;
|
||||
_self._data.dataObj.token = token;
|
||||
_self._data.dataObj.key = key;
|
||||
this.tempUrl = response.data.qiniu_url;
|
||||
resolve(true);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
reject(false)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
@import "src/styles/mixin.scss";
|
||||
.upload-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
@include clearfix;
|
||||
.image-uploader {
|
||||
width: 35%;
|
||||
float: left;
|
||||
}
|
||||
.image-preview {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
position: relative;
|
||||
border: 1px dashed #d9d9d9;
|
||||
float: left;
|
||||
margin-left: 50px;
|
||||
.image-preview-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.image-preview-action {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
font-size: 20px;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
transition: opacity .3s;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
line-height: 200px;
|
||||
.el-icon-delete {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.image-preview-action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.image-app-preview{
|
||||
width: 320px;
|
||||
height: 180px;
|
||||
position: relative;
|
||||
border: 1px dashed #d9d9d9;
|
||||
float: left;
|
||||
margin-left: 50px;
|
||||
.app-fake-conver{
|
||||
height: 44px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
// background: rgba(0, 0, 0, .1);
|
||||
text-align: center;
|
||||
line-height: 64px;
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
64
src/components/jsonEditor/index.vue
Normal file
64
src/components/jsonEditor/index.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class='json-editor'>
|
||||
<textarea ref='textarea'></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import CodeMirror from 'codemirror';
|
||||
import 'codemirror/addon/lint/lint.css';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/theme/rubyblue.css';
|
||||
require('script-loader!jsonlint');
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
import 'codemirror/addon/lint/lint'
|
||||
import 'codemirror/addon/lint/json-lint';
|
||||
|
||||
export default {
|
||||
name: 'jsonEditor',
|
||||
data() {
|
||||
return {
|
||||
jsonEditor: false
|
||||
}
|
||||
},
|
||||
props: ['value'],
|
||||
watch: {
|
||||
value(value) {
|
||||
const editor_value = this.jsonEditor.getValue();
|
||||
if (value !== editor_value) {
|
||||
this.jsonEditor.setValue(JSON.stringify(this.value, null, 2));
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.jsonEditor = CodeMirror.fromTextArea(this.$refs.textarea, {
|
||||
lineNumbers: true,
|
||||
mode: 'application/json',
|
||||
gutters: ['CodeMirror-lint-markers'],
|
||||
theme: 'rubyblue',
|
||||
lint: true
|
||||
});
|
||||
|
||||
this.jsonEditor.setValue(JSON.stringify(this.value, null, 2));
|
||||
this.jsonEditor.on('change', cm => {
|
||||
this.$emit('changed', cm.getValue())
|
||||
this.$emit('input', cm.getValue())
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
getValue() {
|
||||
return this.jsonEditor.getValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
}
|
||||
.json-editor .cm-s-rubyblue span.cm-string{
|
||||
color: #F08047;
|
||||
}
|
||||
</style>
|
157
src/components/twoDndList/index.vue
Normal file
157
src/components/twoDndList/index.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div class="twoDndList">
|
||||
<div class="twoDndList-list" :style="{width:width1}">
|
||||
<h3>{{list1Title}}</h3>
|
||||
<draggable :list="list1" class="dragArea" :options="{group:'article'}">
|
||||
<div class="list-complete-item" v-for="element in list1">
|
||||
<div class="list-complete-item-handle">[{{element.id}}] {{element.title}}</div>
|
||||
<div style="position:absolute;right:0px;">
|
||||
<a style="float: left ;margin-top: -20px;margin-right:5px;" :href="'/article/edit/'+element.id" target="_blank"><i style="color:#20a0ff" class="el-icon-information"></i></a>
|
||||
<span style="float: right ;margin-top: -20px;margin-right:5px;" @click="deleteEle(element)">
|
||||
<i style="color:#ff4949" class="el-icon-delete"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
|
||||
<div class="twoDndList-list" :style="{width:width2}">
|
||||
<h3>{{list2Title}}</h3>
|
||||
<draggable :list="filterList2" class="dragArea" :options="{group:'article'}">
|
||||
<div class="list-complete-item" v-for="element in filterList2">
|
||||
<div class='list-complete-item-handle2' @click="pushEle(element)"> [{{element.id}}] {{element.title}}</div>
|
||||
<a style="float: right ;margin-top: -20px;" :href="'/article/edit/'+element.id" target="_blank"><i style="color:#20a0ff" class="el-icon-information"></i></a>
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from 'vuedraggable'
|
||||
export default {
|
||||
name: 'twoDndList',
|
||||
components: { draggable },
|
||||
computed: {
|
||||
filterList2() {
|
||||
return this.list2.filter(v => {
|
||||
if (this.isNotInList1(v)) {
|
||||
return v
|
||||
}
|
||||
return false;
|
||||
})
|
||||
}
|
||||
},
|
||||
props: {
|
||||
list1: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
list2: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
list1Title: {
|
||||
type: String,
|
||||
default: 'list1'
|
||||
},
|
||||
list2Title: {
|
||||
type: String,
|
||||
default: 'list2'
|
||||
},
|
||||
width1: {
|
||||
type: String,
|
||||
default: '48%'
|
||||
},
|
||||
width2: {
|
||||
type: String,
|
||||
default: '48%'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isNotInList1(v) {
|
||||
return this.list1.every(k => v.id !== k.id)
|
||||
},
|
||||
isNotInList2(v) {
|
||||
return this.list2.every(k => v.id !== k.id)
|
||||
},
|
||||
deleteEle(ele) {
|
||||
for (const item of this.list1) {
|
||||
if (item.id === ele.id) {
|
||||
const index = this.list1.indexOf(item);
|
||||
this.list1.splice(index, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (this.isNotInList2(ele)) {
|
||||
this.list2.unshift(ele)
|
||||
}
|
||||
},
|
||||
pushEle(ele) {
|
||||
this.list1.push(ele)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.twoDndList {
|
||||
background: #fff;
|
||||
padding-bottom: 40px;
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
.twoDndList-list {
|
||||
float: left;
|
||||
padding-bottom: 30px;
|
||||
&:first-of-type {
|
||||
margin-right: 2%;
|
||||
}
|
||||
.dragArea {
|
||||
margin-top: 15px;
|
||||
min-height: 50px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-complete-item {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding: 5px 12px;
|
||||
margin-top: 4px;
|
||||
border: 1px solid #bfcbd9;
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
.list-complete-item-handle {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 50px;
|
||||
}
|
||||
.list-complete-item-handle2{
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.list-complete-item.sortable-chosen {
|
||||
background: #4AB7BD;
|
||||
}
|
||||
|
||||
.list-complete-item.sortable-ghost {
|
||||
background: #30B08F;
|
||||
}
|
||||
|
||||
.list-complete-enter, .list-complete-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
101
src/directive/sticky.js
Normal file
101
src/directive/sticky.js
Normal file
@@ -0,0 +1,101 @@
|
||||
(function() {
|
||||
const vueSticky = {};
|
||||
let listenAction;
|
||||
vueSticky.install = Vue => {
|
||||
Vue.directive('sticky', {
|
||||
inserted(el, binding) {
|
||||
const params = binding.value || {},
|
||||
stickyTop = params.stickyTop || 0,
|
||||
zIndex = params.zIndex || 1000,
|
||||
elStyle = el.style;
|
||||
|
||||
elStyle.position = '-webkit-sticky';
|
||||
elStyle.position = 'sticky';
|
||||
|
||||
// if the browser support css sticky(Currently Safari, Firefox and Chrome Canary)
|
||||
// if (~elStyle.position.indexOf('sticky')) {
|
||||
// elStyle.top = `${stickyTop}px`;
|
||||
// elStyle.zIndex = zIndex;
|
||||
// return
|
||||
// }
|
||||
|
||||
const elHeight = el.getBoundingClientRect().height;
|
||||
const elWidth = el.getBoundingClientRect().width;
|
||||
elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}`;
|
||||
|
||||
const parentElm = el.parentNode || document.documentElement;
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.style.display = 'none';
|
||||
placeholder.style.width = `${elWidth}px`;
|
||||
placeholder.style.height = `${elHeight}px`;
|
||||
parentElm.insertBefore(placeholder, el)
|
||||
|
||||
let active = false;
|
||||
|
||||
const getScroll = (target, top) => {
|
||||
const prop = top ? 'pageYOffset' : 'pageXOffset';
|
||||
const method = top ? 'scrollTop' : 'scrollLeft';
|
||||
let ret = target[prop];
|
||||
if (typeof ret !== 'number') {
|
||||
ret = window.document.documentElement[method];
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
const sticky = () => {
|
||||
if (active) {
|
||||
return
|
||||
}
|
||||
if (!elStyle.height) {
|
||||
elStyle.height = `${el.offsetHeight}px`
|
||||
}
|
||||
|
||||
elStyle.position = 'fixed';
|
||||
elStyle.width = `${elWidth}px`;
|
||||
placeholder.style.display = 'inline-block';
|
||||
active = true
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
if (!active) {
|
||||
return
|
||||
}
|
||||
|
||||
elStyle.position = '';
|
||||
placeholder.style.display = 'none';
|
||||
active = false;
|
||||
};
|
||||
|
||||
const check = () => {
|
||||
const scrollTop = getScroll(window, true);
|
||||
const offsetTop = el.getBoundingClientRect().top;
|
||||
if (offsetTop < stickyTop) {
|
||||
sticky();
|
||||
} else {
|
||||
if (scrollTop < elHeight + stickyTop) {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
};
|
||||
listenAction = () => {
|
||||
check()
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', listenAction)
|
||||
},
|
||||
|
||||
unbind() {
|
||||
window.removeEventListener('scroll', listenAction)
|
||||
}
|
||||
})
|
||||
};
|
||||
if (typeof exports == 'object') {
|
||||
module.exports = vueSticky
|
||||
} else if (typeof define == 'function' && define.amd) {
|
||||
define([], () => vueSticky)
|
||||
} else if (window.Vue) {
|
||||
window.vueSticky = vueSticky;
|
||||
Vue.use(vueSticky)
|
||||
}
|
||||
}());
|
||||
|
26
src/directive/waves.css
Normal file
26
src/directive/waves.css
Normal file
@@ -0,0 +1,26 @@
|
||||
.waves-ripple {
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
background-clip: padding-box;
|
||||
pointer-events: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-transform: scale(0);
|
||||
-ms-transform: scale(0);
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.waves-ripple.z-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(2);
|
||||
-ms-transform: scale(2);
|
||||
transform: scale(2);
|
||||
-webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
|
||||
transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
|
||||
transition: opacity 1.2s ease-out, transform 0.6s ease-out;
|
||||
transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
|
||||
}
|
54
src/directive/waves.js
Normal file
54
src/directive/waves.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import './waves.css';
|
||||
(function() {
|
||||
const vueWaves = {};
|
||||
vueWaves.install = (Vue, options = {}) => {
|
||||
Vue.directive('waves', {
|
||||
bind(el, binding) {
|
||||
el.addEventListener('click', e => {
|
||||
const customOpts = Object.assign(options, binding.value);
|
||||
const opts = Object.assign({
|
||||
ele: el, // 波纹作用元素
|
||||
type: 'hit', // hit点击位置扩散center中心点扩展
|
||||
color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
|
||||
}, customOpts),
|
||||
target = opts.ele;
|
||||
if (target) {
|
||||
target.style.position = 'relative';
|
||||
target.style.overflow = 'hidden';
|
||||
const rect = target.getBoundingClientRect();
|
||||
let ripple = target.querySelector('.waves-ripple');
|
||||
if (!ripple) {
|
||||
ripple = document.createElement('span');
|
||||
ripple.className = 'waves-ripple';
|
||||
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px';
|
||||
target.appendChild(ripple);
|
||||
} else {
|
||||
ripple.className = 'waves-ripple';
|
||||
}
|
||||
switch (opts.type) {
|
||||
case 'center':
|
||||
ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px';
|
||||
ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px';
|
||||
break;
|
||||
default:
|
||||
ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px';
|
||||
ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px';
|
||||
}
|
||||
ripple.style.backgroundColor = opts.color;
|
||||
ripple.className = 'waves-ripple z-active';
|
||||
return false;
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
})
|
||||
};
|
||||
if (typeof exports == 'object') {
|
||||
module.exports = vueWaves
|
||||
} else if (typeof define == 'function' && define.amd) {
|
||||
define([], () => vueWaves)
|
||||
} else if (window.Vue) {
|
||||
window.vueWaves = vueWaves;
|
||||
Vue.use(vueWaves)
|
||||
}
|
||||
}());
|
||||
|
108
src/filters/index.js
Normal file
108
src/filters/index.js
Normal file
@@ -0,0 +1,108 @@
|
||||
function pluralize(time, label) {
|
||||
if (time === 1) {
|
||||
return time + label
|
||||
}
|
||||
return time + label + 's'
|
||||
}
|
||||
export function timeAgo(time) {
|
||||
const between = Date.now() / 1000 - Number(time);
|
||||
if (between < 3600) {
|
||||
return pluralize(~~(between / 60), ' minute')
|
||||
} else if (between < 86400) {
|
||||
return pluralize(~~(between / 3600), ' hour')
|
||||
} else {
|
||||
return pluralize(~~(between / 86400), ' day')
|
||||
}
|
||||
}
|
||||
|
||||
export function parseTime(time, cFormat) {
|
||||
if (arguments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((time + '').length === 10) {
|
||||
time = +time * 1000
|
||||
}
|
||||
|
||||
|
||||
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}';
|
||||
let date;
|
||||
if (typeof time == 'object') {
|
||||
date = time;
|
||||
} else {
|
||||
date = new Date(parseInt(time));
|
||||
}
|
||||
const formatObj = {
|
||||
y: date.getFullYear(),
|
||||
m: date.getMonth() + 1,
|
||||
d: date.getDate(),
|
||||
h: date.getHours(),
|
||||
i: date.getMinutes(),
|
||||
s: date.getSeconds(),
|
||||
a: date.getDay()
|
||||
};
|
||||
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
|
||||
let value = formatObj[key];
|
||||
if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1];
|
||||
if (result.length > 0 && value < 10) {
|
||||
value = '0' + value;
|
||||
}
|
||||
return value || 0;
|
||||
});
|
||||
return time_str;
|
||||
}
|
||||
|
||||
export function formatTime(time, option) {
|
||||
time = +time * 1000;
|
||||
const d = new Date(time);
|
||||
const now = Date.now();
|
||||
|
||||
const diff = (now - d) / 1000;
|
||||
|
||||
if (diff < 30) {
|
||||
return '刚刚'
|
||||
} else if (diff < 3600) { // less 1 hour
|
||||
return Math.ceil(diff / 60) + '分钟前'
|
||||
} else if (diff < 3600 * 24) {
|
||||
return Math.ceil(diff / 3600) + '小时前'
|
||||
} else if (diff < 3600 * 24 * 2) {
|
||||
return '1天前'
|
||||
}
|
||||
if (option) {
|
||||
return parseTime(time, option)
|
||||
} else {
|
||||
return d.getMonth() + 1 + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 数字 格式化*/
|
||||
export function nFormatter(num, digits) {
|
||||
const si = [
|
||||
{ value: 1E18, symbol: 'E' },
|
||||
{ value: 1E15, symbol: 'P' },
|
||||
{ value: 1E12, symbol: 'T' },
|
||||
{ value: 1E9, symbol: 'G' },
|
||||
{ value: 1E6, symbol: 'M' },
|
||||
{ value: 1E3, symbol: 'k' }
|
||||
];
|
||||
for (let i = 0; i < si.length; i++) {
|
||||
if (num >= si[i].value) {
|
||||
return (num / si[i].value + 0.1).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol;
|
||||
}
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
|
||||
export function html2Text(val) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = val;
|
||||
return div.textContent || div.innerText;
|
||||
}
|
||||
|
||||
|
||||
export function toThousandslsFilter(num) {
|
||||
return (+num || 0).toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,');
|
||||
}
|
116
src/main.js
Normal file
116
src/main.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// The Vue build version to load with the `import` command
|
||||
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
|
||||
import Vue from 'vue';
|
||||
import App from './App';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import ElementUI from 'element-ui';
|
||||
import 'element-ui/lib/theme-default/index.css';
|
||||
import 'assets/custom-theme/index.css'; // https://github.com/PanJiaChen/custom-element-theme
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import 'normalize.css/normalize.css';
|
||||
import 'styles/index.scss';
|
||||
import 'components/Icon-svg/index';
|
||||
import 'assets/iconfont/iconfont';
|
||||
import * as filters from './filters';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import Sticky from 'components/Sticky';
|
||||
import 'vue-multiselect/dist/vue-multiselect.min.css';
|
||||
import vueWaves from './directive/waves';
|
||||
import vueSticky from './directive/sticky';
|
||||
import errLog from 'store/errLog';
|
||||
// import './styles/mixin.scss';
|
||||
|
||||
// register globally
|
||||
Vue.component('multiselect', Multiselect);
|
||||
Vue.component('Sticky', Sticky);
|
||||
Vue.use(ElementUI);
|
||||
Vue.use(vueWaves);
|
||||
Vue.use(vueSticky);
|
||||
|
||||
|
||||
// register global utility filters.
|
||||
Object.keys(filters).forEach(key => {
|
||||
Vue.filter(key, filters[key])
|
||||
});
|
||||
|
||||
|
||||
function hasPermission(roles, permissionRoles) {
|
||||
if (roles.indexOf('admin') >= 0) return true;
|
||||
return roles.some(role => permissionRoles.indexOf(role) >= 0)
|
||||
}
|
||||
// register global progress.
|
||||
const whiteList = ['/login', '/authredirect', '/reset', '/sendpwd'];// 不重定向白名单
|
||||
router.beforeEach((to, from, next) => {
|
||||
NProgress.start();
|
||||
if (store.getters.token) {
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' });
|
||||
} else {
|
||||
console.log('a')
|
||||
if (to.meta && to.meta.role) {
|
||||
if (hasPermission(store.getters.roles, to.meta.role)) {
|
||||
next();
|
||||
} else {
|
||||
next('/401');
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
next()
|
||||
} else {
|
||||
next('/login')
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done();
|
||||
});
|
||||
|
||||
|
||||
// 异步组件
|
||||
// Vue.component('async-Editor', function (resolve) {
|
||||
// require(['components/Editor'], resolve)
|
||||
// });
|
||||
|
||||
// window.onunhandledrejection = e => {
|
||||
// console.log('unhandled', e.reason, e.promise);
|
||||
// e.preventDefault()
|
||||
// };
|
||||
|
||||
// 生产环境错误日志
|
||||
if (process.env === 'production') {
|
||||
Vue.config.errorHandler = function(err, vm) {
|
||||
console.log(err, window.location.href);
|
||||
errLog.pushLog({
|
||||
err,
|
||||
url: window.location.href,
|
||||
vm
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
// console.log('window')
|
||||
// };
|
||||
//
|
||||
// console.error = (function (origin) {
|
||||
// return function (errorlog) {
|
||||
// // handler();//基于业务的日志记录及数据报错
|
||||
// console.log('console'+errorlog)
|
||||
// origin.call(console, errorlog);
|
||||
// }
|
||||
// })(console.error);
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app');
|
||||
|
||||
|
25
src/mock/login.js
Normal file
25
src/mock/login.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const userMap = {
|
||||
admin: {
|
||||
role: ['admin'],
|
||||
token: 'admin',
|
||||
introduction: '我是超级管理员',
|
||||
avatar: 'https://wdl.wallstreetcn.com/48a3e1e0-ea2c-4a4e-9928-247645e3428b',
|
||||
name: '超级管理员小潘'
|
||||
},
|
||||
editor: {
|
||||
role: ['editor'],
|
||||
token: 'editor',
|
||||
introduction: '我是编辑',
|
||||
avatar: 'https://wdl.wallstreetcn.com/48a3e1e0-ea2c-4a4e-9928-247645e3428b',
|
||||
name: '普通编辑小张'
|
||||
|
||||
},
|
||||
developer: {
|
||||
role: ['develop'],
|
||||
token: 'develop',
|
||||
introduction: '我是开发',
|
||||
avatar: 'https://wdl.wallstreetcn.com/48a3e1e0-ea2c-4a4e-9928-247645e3428b',
|
||||
name: '工程师小王'
|
||||
}
|
||||
}
|
||||
export default userMap
|
81
src/router/index.js
Normal file
81
src/router/index.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
|
||||
/* layout*/
|
||||
import Layout from '../views/layout/Layout';
|
||||
|
||||
// dashboard
|
||||
// import dashboard from '../views/dashboard/index';
|
||||
const dashboard = resolve => require(['../views/dashboard/index'], resolve);
|
||||
/* error*/
|
||||
const Err404 = resolve => require(['../views/error/404'], resolve);
|
||||
const Err401 = resolve => require(['../views/error/401'], resolve);
|
||||
/* login*/
|
||||
import Login from '../views/login/';
|
||||
import authRedirect from '../views/login/authredirect';
|
||||
import sendPWD from '../views/login/sendpwd';
|
||||
import reset from '../views/login/reset';
|
||||
|
||||
/* components*/
|
||||
const Tinymce = resolve => require(['../views/components/tinymce'], resolve);
|
||||
const Markdown = resolve => require(['../views/components/markdown'], resolve);
|
||||
|
||||
/* admin*/
|
||||
// const AdminCreateUser = resolve => require(['../views/admin/createUser'], resolve);
|
||||
// const QuicklyCreateUser = resolve => require(['../views/admin/quicklycreate'], resolve);
|
||||
// const UserProfile = resolve => require(['../views/admin/profile'], resolve);
|
||||
// const UsersList = resolve => require(['../views/admin/usersList'], resolve);
|
||||
|
||||
|
||||
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
export default new Router({
|
||||
mode: 'history',
|
||||
scrollBehavior: () => ({ y: 0 }),
|
||||
routes: [
|
||||
{ path: '/login', component: Login, hidden: true },
|
||||
{ path: '/authredirect', component: authRedirect, hidden: true },
|
||||
{ path: '/sendpwd', component: sendPWD, hidden: true },
|
||||
{ path: '/reset', component: reset, hidden: true },
|
||||
{ path: '/404', component: Err404, hidden: true },
|
||||
{ path: '/401', component: Err401, hidden: true },
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
name: '首页',
|
||||
hidden: true,
|
||||
children: [
|
||||
{ path: 'dashboard', component: dashboard }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
component: Layout,
|
||||
redirect: 'noredirect',
|
||||
name: '组件',
|
||||
icon: 'zujian',
|
||||
children: [
|
||||
{ path: 'tinymce', component: Tinymce, name: '富文本编辑器' },
|
||||
{ path: 'markdown', component: Markdown, name: 'Markdown' }
|
||||
|
||||
]
|
||||
},
|
||||
// {
|
||||
// path: '/admin',
|
||||
// component: Layout,
|
||||
// redirect: 'noredirect',
|
||||
// name: '后台管理',
|
||||
// icon: 'geren1',
|
||||
// children: [
|
||||
// { path: 'createuser', component: AdminCreateUser, name: '管理员', meta: { role: ['admin'] } },
|
||||
// { path: 'list', component: UsersList, name: '后台用户列表', meta: { role: ['super_editor', 'product', 'author_assistant'] } },
|
||||
// { path: 'qicklyCreate', component: QuicklyCreateUser, name: '一键创建账户', meta: { role: ['super_editor', 'gold_editor', 'weex_editor', 'wscn_editor', 'author_assistant', 'product'] } },
|
||||
// { path: 'profile', component: UserProfile, name: '个人' }
|
||||
// ]
|
||||
// },
|
||||
{ path: '*', redirect: '/404', hidden: true }
|
||||
]
|
||||
});
|
13
src/store/errLog.js
Normal file
13
src/store/errLog.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const errLog = {
|
||||
state: {
|
||||
errLog: []
|
||||
},
|
||||
pushLog(log) {
|
||||
this.state.errLog.unshift(log)
|
||||
},
|
||||
clearLog() {
|
||||
this.state.errLog = [];
|
||||
}
|
||||
};
|
||||
|
||||
export default errLog;
|
15
src/store/getters.js
Normal file
15
src/store/getters.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const getters = {
|
||||
sidebar: state => state.app.sidebar,
|
||||
livenewsChannels: state => state.app.livenewsChannels,
|
||||
token: state => state.user.token,
|
||||
avatar: state => state.user.avatar,
|
||||
name: state => state.user.name,
|
||||
uid: state => state.user.uid,
|
||||
email: state => state.user.email,
|
||||
introduction: state => state.user.introduction,
|
||||
auth_type: state => state.user.auth_type,
|
||||
status: state => state.user.status,
|
||||
roles: state => state.user.roles,
|
||||
setting: state => state.user.setting
|
||||
};
|
||||
export default getters
|
17
src/store/index.js
Normal file
17
src/store/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import app from './modules/app';
|
||||
import user from './modules/user';
|
||||
import getters from './getters';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
app,
|
||||
user
|
||||
},
|
||||
getters
|
||||
});
|
||||
|
||||
export default store
|
38
src/store/modules/app.js
Normal file
38
src/store/modules/app.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const app = {
|
||||
state: {
|
||||
sidebar: {
|
||||
opened: !+Cookies.get('sidebarStatus')
|
||||
},
|
||||
theme: 'default',
|
||||
livenewsChannels: Cookies.get('livenewsChannels') || '[]'
|
||||
},
|
||||
mutations: {
|
||||
TOGGLE_SIDEBAR: state => {
|
||||
if (state.sidebar.opened) {
|
||||
Cookies.set('sidebarStatus', 1);
|
||||
} else {
|
||||
Cookies.set('sidebarStatus', 0);
|
||||
}
|
||||
state.sidebar.opened = !state.sidebar.opened;
|
||||
},
|
||||
SET_LIVENEWS_CHANNELS: (status, channels) => {
|
||||
status.livenewsChannels = JSON.stringify(channels);
|
||||
Cookies.set('livenewsChannels', JSON.stringify(channels));
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
ToggleSideBar: ({ commit }) => {
|
||||
commit('TOGGLE_SIDEBAR')
|
||||
},
|
||||
setTheme: ({ commit }, theme) => {
|
||||
commit('SET_THEME', theme)
|
||||
},
|
||||
setlivenewsChannels: ({ commit }, channels) => {
|
||||
commit('SET_LIVENEWS_CHANNELS', channels)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default app;
|
129
src/store/modules/user.js
Normal file
129
src/store/modules/user.js
Normal file
@@ -0,0 +1,129 @@
|
||||
// import { loginByEmail, loginByThirdparty } from 'api/login';
|
||||
// import { userInfo, userLogout } from 'api/adminUser';
|
||||
import Cookies from 'js-cookie';
|
||||
import userMap from 'mock/login';
|
||||
|
||||
const user = {
|
||||
state: {
|
||||
user: '',
|
||||
status: '',
|
||||
email: '',
|
||||
code: '',
|
||||
uid: undefined,
|
||||
auth_type: '',
|
||||
token: Cookies.get('X-Ivanka-Token'),
|
||||
name: '',
|
||||
avatar: '',
|
||||
introduction: '',
|
||||
roles: [],
|
||||
setting: {
|
||||
articlePlatform: []
|
||||
}
|
||||
},
|
||||
|
||||
mutations: {
|
||||
SET_AUTH_TYPE: (state, type) => {
|
||||
state.auth_type = type;
|
||||
},
|
||||
SET_CODE: (state, code) => {
|
||||
state.code = code;
|
||||
},
|
||||
SET_TOKEN: (state, token) => {
|
||||
state.token = token;
|
||||
},
|
||||
SET_UID: (state, uid) => {
|
||||
state.uid = uid;
|
||||
},
|
||||
SET_EMAIL: (state, email) => {
|
||||
state.email = email;
|
||||
},
|
||||
SET_INTRODUCTION: (state, introduction) => {
|
||||
state.introduction = introduction;
|
||||
},
|
||||
SET_SETTING: (state, setting) => {
|
||||
state.setting = setting;
|
||||
},
|
||||
SET_STATUS: (state, status) => {
|
||||
state.status = status;
|
||||
},
|
||||
SET_NAME: (state, name) => {
|
||||
state.name = name;
|
||||
},
|
||||
SET_AVATAR: (state, avatar) => {
|
||||
state.avatar = avatar;
|
||||
},
|
||||
SET_ROLES: (state, roles) => {
|
||||
state.roles = roles;
|
||||
},
|
||||
LOGIN_SUCCESS: () => {
|
||||
console.log('login success')
|
||||
},
|
||||
LOGOUT_USER: state => {
|
||||
state.user = '';
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 邮箱登录
|
||||
LoginByEmail({ commit }, userInfo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const email = userInfo.email.split('@')[0];
|
||||
if (userMap[email]) {
|
||||
commit('SET_ROLES', userMap[email].role);
|
||||
commit('SET_TOKEN', userMap[email].token);
|
||||
Cookies.set('X-Ivanka-Token', userMap[email].token);
|
||||
resolve();
|
||||
} else {
|
||||
reject('账号不正确');
|
||||
}
|
||||
});
|
||||
},
|
||||
// 第三方验证登录
|
||||
LoginByThirdparty({ commit, state }, code) {
|
||||
return new Promise((resolve, reject) => {
|
||||
commit('SET_CODE', code);
|
||||
loginByThirdparty(state.status, state.email, state.code, state.auth_type).then(response => {
|
||||
commit('SET_TOKEN', response.data.token);
|
||||
Cookies.set('X-Ivanka-Token', response.data.token);
|
||||
resolve();
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
// 获取用户信息
|
||||
GetInfo({ commit, state }) {
|
||||
return new Promise(resolve => {
|
||||
const token = state.token;
|
||||
commit('SET_ROLES', userMap[token].role);
|
||||
commit('SET_NAME', userMap[token].name);
|
||||
commit('SET_AVATAR', userMap[token].avatar);
|
||||
commit('SET_INTRODUCTION', userMap[token].introduction);
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
// 登出
|
||||
LogOut({ commit, state }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
userLogout(state.token).then(() => {
|
||||
commit('SET_TOKEN', '');
|
||||
Cookies.remove('X-Ivanka-Token');
|
||||
resolve();
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 前端 登出
|
||||
FedLogOut({ commit }) {
|
||||
return new Promise(resolve => {
|
||||
commit('SET_TOKEN', '');
|
||||
Cookies.remove('X-Ivanka-Token');
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default user;
|
39
src/store/permission.js
Normal file
39
src/store/permission.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const permission = {
|
||||
state: {
|
||||
permissionRoutes: []
|
||||
},
|
||||
init(data) {
|
||||
const roles = data.roles;
|
||||
const router = data.router;
|
||||
const permissionRoutes = router.filter(v => {
|
||||
if (roles.indexOf('admin') >= 0) return true;
|
||||
if (this.hasPermission(roles, v)) {
|
||||
if (v.children && v.children.length > 0) {
|
||||
v.children = v.children.filter(child => {
|
||||
if (this.hasPermission(roles, child)) {
|
||||
return child
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return v
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
this.permissionRoutes = permissionRoutes;
|
||||
},
|
||||
get() {
|
||||
return this.permissionRoutes
|
||||
},
|
||||
hasPermission(roles, route) {
|
||||
if (route.meta && route.meta.role) {
|
||||
return roles.some(role => route.meta.role.indexOf(role) >= 0)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default permission;
|
103
src/styles/btn.scss
Normal file
103
src/styles/btn.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
$blue:#324157;
|
||||
$light-blue:#3A71A8;
|
||||
$red:#C03639;
|
||||
$pink: #E65D6E;
|
||||
$green: #30B08F;
|
||||
$tiffany: #4AB7BD;
|
||||
$yellow:#FEC171;
|
||||
|
||||
$panGreen: #30B08F;
|
||||
|
||||
@mixin colorBtn($color) {
|
||||
background: $color;
|
||||
&:hover {
|
||||
color: $color;
|
||||
&:before, &:after {
|
||||
background: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.blue-btn {
|
||||
@include colorBtn($blue)
|
||||
}
|
||||
|
||||
.light-blue-btn{
|
||||
@include colorBtn($light-blue)
|
||||
}
|
||||
|
||||
|
||||
.red-btn {
|
||||
@include colorBtn($red)
|
||||
}
|
||||
|
||||
.pink-btn {
|
||||
@include colorBtn($pink)
|
||||
}
|
||||
|
||||
.green-btn {
|
||||
@include colorBtn($green)
|
||||
}
|
||||
|
||||
|
||||
.tiffany-btn {
|
||||
@include colorBtn($tiffany)
|
||||
}
|
||||
|
||||
|
||||
.yellow-btn {
|
||||
@include colorBtn($yellow)
|
||||
}
|
||||
|
||||
.pan-btn {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
padding: 14px 36px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
outline: none;
|
||||
margin-right: 25px;
|
||||
transition: 600ms ease all;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
&:hover {
|
||||
background: #fff;
|
||||
&:before, &:after {
|
||||
width: 100%;
|
||||
transition: 600ms ease all;
|
||||
}
|
||||
}
|
||||
&:before, &:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
transition: 400ms ease all;
|
||||
}
|
||||
&::after {
|
||||
right: inherit;
|
||||
top: inherit;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-button{
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
color: #fff;
|
||||
-webkit-appearance: none;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
outline: 0;
|
||||
margin: 0;
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
}
|
348
src/styles/editor.scss
Normal file
348
src/styles/editor.scss
Normal file
@@ -0,0 +1,348 @@
|
||||
//富文本
|
||||
//移除 至static/tinymce/skins/lightgray.content.min.css
|
||||
.small-size {
|
||||
width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
img{
|
||||
max-height: 300px;
|
||||
}
|
||||
.note-color .dropdown-menu li .btn-group{
|
||||
&:first-child{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
//禁止图片缩放
|
||||
.note-control-sizing {
|
||||
display: none;
|
||||
}
|
||||
.panel-body {
|
||||
$blue: #1478F0;
|
||||
font-size: 16px;
|
||||
line-height: 1.4em;
|
||||
& > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
table {
|
||||
width: 100% !important;
|
||||
}
|
||||
embed {
|
||||
max-width: 100%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
p {
|
||||
// margin-bottom: 1em;
|
||||
text-align: justify;
|
||||
word-break: break-all;
|
||||
}
|
||||
ul {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
li {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
a {
|
||||
color: $blue;
|
||||
}
|
||||
hr {
|
||||
margin: 1em auto;
|
||||
border: none;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #DCDCDC;
|
||||
}
|
||||
//add type.css start
|
||||
blockquote p {
|
||||
font-size: 16px;
|
||||
letter-spacing: 1px;
|
||||
line-height: 28px;
|
||||
color: #333;
|
||||
}
|
||||
blockquote p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/* HTML5 媒体文件跟 img 保持一致 */
|
||||
audio,
|
||||
canvas,
|
||||
video {
|
||||
display: inline-block;
|
||||
*display: inline;
|
||||
*zoom: 1;
|
||||
}
|
||||
/* 要注意表单元素并不继承父级 font 的问题 */
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: 500 14px/1.8 'Hiragino Sans GB', Microsoft YaHei, sans-serif;
|
||||
}
|
||||
/* 去掉各Table cell 的边距并让其边重合 */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
/* IE bug fixed: th 不继承 text-align*/
|
||||
th {
|
||||
text-align: inherit;
|
||||
}
|
||||
/* 去除默认边框 */
|
||||
fieldset,
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
/* 解决 IE6-7 图片缩放锯齿问题 */
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
/* ie6 7 8(q) bug 显示为行内表现 */
|
||||
iframe {
|
||||
display: block;
|
||||
}
|
||||
/* 块/段落引用 */
|
||||
blockquote {
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
letter-spacing: 1px;
|
||||
line-height: 28px;
|
||||
margin-bottom: 40px;
|
||||
padding: 20px;
|
||||
background: #f0f2f5;
|
||||
&:before{
|
||||
position: absolute;
|
||||
content: " \300D";
|
||||
top: 10px;
|
||||
left: 2px;
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
color: #333;
|
||||
}
|
||||
&:after{
|
||||
position: absolute;
|
||||
content: " \300D";
|
||||
right: 6px;
|
||||
bottom: 12px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
blockquote blockquote {
|
||||
padding: 0 0 0 1em;
|
||||
margin-left: 2em;
|
||||
border-left: 3px solid $blue;
|
||||
}
|
||||
/* Firefox 以外,元素没有下划线,需添加 */
|
||||
acronym,
|
||||
abbr {
|
||||
border-bottom: 1px dotted;
|
||||
font-variant: normal;
|
||||
}
|
||||
/* 添加鼠标问号,进一步确保应用的语义是正确的(要知道,交互他们也有洁癖,如果你不去掉,那得多花点口舌) */
|
||||
abbr {
|
||||
cursor: help;
|
||||
}
|
||||
/* 一致的 del 样式 */
|
||||
del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
address,
|
||||
caption,
|
||||
cite,
|
||||
code,
|
||||
del,
|
||||
em,
|
||||
th,
|
||||
var {
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
}
|
||||
em {
|
||||
font-style: normal;
|
||||
font-family: sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
/* 对齐是排版最重要的因素, 别让什么都居中 */
|
||||
caption,
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
q:before,
|
||||
q:after {
|
||||
content: '';
|
||||
}
|
||||
/* 统一上标和下标 */
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
:root sub,
|
||||
:root sup {
|
||||
vertical-align: baseline;
|
||||
/* for ie9 and other mordern browsers */
|
||||
}
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
/* 让链接在 hover 状态下显示下划线 */
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* 默认不显示下划线,保持页面简洁 */
|
||||
ins,
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
u,
|
||||
.typo-u {
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* 标记,类似于手写的荧光笔的作用 */
|
||||
mark {
|
||||
background: #fffdd1;
|
||||
}
|
||||
/* 代码片断 */
|
||||
pre,
|
||||
code {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
pre {
|
||||
border: 1px solid #ddd;
|
||||
border-left-width: 0.4em;
|
||||
background: #fbfbfb;
|
||||
padding: 10px;
|
||||
}
|
||||
/* 底部印刷体、版本等标记 */
|
||||
small {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1478f0;
|
||||
border-left: 5px solid #1478f0;
|
||||
padding-left: 10px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
/* 保证块/段落之间的空白隔行 */
|
||||
.typo p,
|
||||
.typo pre,
|
||||
.typo ul,
|
||||
.typo ol,
|
||||
.typo dl,
|
||||
.typo form,
|
||||
.typo hr,
|
||||
.typo table,
|
||||
.typo-p,
|
||||
.typo-pre,
|
||||
.typo-ul,
|
||||
.typo-ol,
|
||||
.typo-dl,
|
||||
.typo-form,
|
||||
.typo-hr,
|
||||
.typo-table {
|
||||
margin-bottom: 15px;
|
||||
line-height: 25px;
|
||||
}
|
||||
/* 标题应该更贴紧内容,并与其他块区分,margin 值要相应做优化 */
|
||||
.typo h1,
|
||||
.typo h2,
|
||||
.typo h3,
|
||||
.typo h4,
|
||||
.typo h5,
|
||||
.typo h6,
|
||||
.typo-h1,
|
||||
.typo-h2,
|
||||
.typo-h3,
|
||||
.typo-h4,
|
||||
.typo-h5,
|
||||
.typo-h6 {
|
||||
margin-bottom: 0.4em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.typo h1,
|
||||
.typo-h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
.typo h2,
|
||||
.typo-h2 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
.typo h3,
|
||||
.typo-h3 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
.typo h4,
|
||||
.typo-h0 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.typo h5,
|
||||
.typo h6,
|
||||
.typo-h5,
|
||||
.typo-h6 {
|
||||
font-size: 1em;
|
||||
}
|
||||
/* 在文章中,应该还原 ul 和 ol 的样式 */
|
||||
.typo ul,
|
||||
.typo-ul {
|
||||
margin-left: 1.3em;
|
||||
list-style: disc;
|
||||
}
|
||||
.typo ol,
|
||||
.typo-ol {
|
||||
list-style: decimal;
|
||||
margin-left: 1.9em;
|
||||
}
|
||||
.typo li ul,
|
||||
.typo li ol,
|
||||
.typo-ul ul,
|
||||
.typo-ul ol,
|
||||
.typo-ol ul,
|
||||
.typo-ol ol {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 2em;
|
||||
}
|
||||
.typo li ul,
|
||||
.typo-ul ul,
|
||||
.typo-ol ul {
|
||||
list-style: circle;
|
||||
}
|
||||
/* 同 ul/ol,在文章中应用 table 基本格式 */
|
||||
.typo table th,
|
||||
.typo table td,
|
||||
.typo-table th,
|
||||
.typo-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.typo table th,
|
||||
.typo-table th {
|
||||
background: #fbfbfb;
|
||||
}
|
||||
.typo table thead th,
|
||||
.typo-table thead th {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
}
|
||||
|
||||
|
392
src/styles/index.scss
Normal file
392
src/styles/index.scss
Normal file
@@ -0,0 +1,392 @@
|
||||
@import './btn.scss';
|
||||
// @import './editor.scss';
|
||||
@import "./mixin.scss";
|
||||
|
||||
body {
|
||||
//height: 100%;
|
||||
//overflow-y: scroll;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Arial,sans-serif;
|
||||
//@include scrollBar;
|
||||
}
|
||||
label{
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.padding-content {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
a:focus,
|
||||
a:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.fr {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.fl {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.pr-5 {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.pl-5 {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inlineBlock {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.components-container{
|
||||
margin: 30px 50px;
|
||||
}
|
||||
|
||||
code{
|
||||
background: #eef1f6;
|
||||
padding: 20px 10px;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: all .2s ease
|
||||
}
|
||||
|
||||
.fade-enter, .fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
//editor
|
||||
//.editor-placeholder {
|
||||
// margin: 0 auto;
|
||||
// display: block;
|
||||
// .editor-placeholder-title {
|
||||
// text-align: center;
|
||||
// font-size: 20px;
|
||||
// padding-bottom: 5px;
|
||||
// }
|
||||
// .editor-placeholder-image {
|
||||
// display: block;
|
||||
// max-height: 100px;
|
||||
// margin: 0 auto;
|
||||
// }
|
||||
//}
|
||||
|
||||
//main-container全局样式
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
//min-height: 100%;
|
||||
}
|
||||
|
||||
//element ui upload
|
||||
.upload-container {
|
||||
.el-upload {
|
||||
width: 100%;
|
||||
.el-upload-dragger {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.singleImageUpload2.upload-container {
|
||||
.el-upload {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.el-upload-dragger {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.el-icon-upload {
|
||||
margin: 30% 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor-video-upload {
|
||||
@include clearfix;
|
||||
margin-bottom: 10px;
|
||||
.el-upload {
|
||||
float: left;
|
||||
width: 100px;
|
||||
|
||||
}
|
||||
.el-upload-list {
|
||||
float: left;
|
||||
.el-upload-list__item:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload-list--picture-card {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wscn-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sub-navbar {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
transition: 600ms ease position;
|
||||
background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
|
||||
.subtitle {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
&.draft {
|
||||
background: #d0d0d0;
|
||||
}
|
||||
&.deleted {
|
||||
background: #d0d0d0;
|
||||
}
|
||||
}
|
||||
|
||||
.link-type,.link-type:focus {
|
||||
color: #337ab7;
|
||||
cursor: pointer;
|
||||
&:hover{
|
||||
color: rgb(32, 160, 255);
|
||||
}
|
||||
}
|
||||
|
||||
.publishedTag, .draftTag, .deletedTag {
|
||||
color: #fff;
|
||||
background-color: $panGreen;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.draftTag {
|
||||
background-color: $yellow;
|
||||
}
|
||||
|
||||
.deletedTag {
|
||||
background-color: $red;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 14px;
|
||||
color: #48576a;
|
||||
line-height: 1;
|
||||
padding: 11px 5px 11px 0;
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
&:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
content: " ";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.no-marginLeft {
|
||||
.el-checkbox {
|
||||
margin: 0 20px 15px 0;
|
||||
}
|
||||
.el-checkbox + .el-checkbox {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
padding-bottom: 10px;
|
||||
.filter-item {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
//文章页textarea修改样式
|
||||
.article-textarea {
|
||||
textarea {
|
||||
padding-right: 40px;
|
||||
resize: none;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
border-bottom: 1px solid #bfcbd9;
|
||||
}
|
||||
}
|
||||
|
||||
//实时新闻创建页特殊处理
|
||||
.recentNews-container {
|
||||
p {
|
||||
display: inline-block;
|
||||
}
|
||||
.el-collapse-item__content{
|
||||
padding-right:0px;
|
||||
}
|
||||
}
|
||||
|
||||
//refine vue-multiselect plugin
|
||||
.multiselect {
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.multiselect--active {
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
//reset element ui
|
||||
.block-checkbox {
|
||||
display: block;
|
||||
}
|
||||
|
||||
//上传页面不显示删除icon
|
||||
.mediaUpload-container {
|
||||
.el-upload__btn-delete {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.operation-container {
|
||||
.cell {
|
||||
padding: 10px !important;
|
||||
}
|
||||
.el-button {
|
||||
&:nth-child(3) {
|
||||
margin-top: 10px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload {
|
||||
input[type="file"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload__input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cell {
|
||||
.el-tag {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
.small-padding{
|
||||
.cell{
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
.status-col {
|
||||
.cell {
|
||||
padding: 0 10px;
|
||||
text-align: center;
|
||||
.el-tag {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//.el-form-item__content{
|
||||
// margin-left: 0px!important;
|
||||
//}
|
||||
.no-border {
|
||||
.el-input-group__prepend, .el-input__inner, .el-date-editor__editor, .multiselect__tags {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select__tags {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.small-space .el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
.el-dropdown-menu__item {
|
||||
padding: 0px;
|
||||
}
|
||||
.el-dropdown-menu {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-hover {
|
||||
.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tooltip-fullwidth {
|
||||
width: 100%;
|
||||
.el-tooltip__rel {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
//暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461
|
||||
.el-dialog{
|
||||
transform: none;
|
||||
left: 0;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
}
|
57
src/styles/mixin.scss
Normal file
57
src/styles/mixin.scss
Normal file
@@ -0,0 +1,57 @@
|
||||
@mixin clearfix {
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin scrollBar {
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background: #d3dce6;
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #99a9bf;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin relative {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@mixin pct($pct) {
|
||||
width: #{$pct};
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@mixin triangle($width, $height, $color, $direction) {
|
||||
$width: $width/2;
|
||||
$color-border-style: $height solid $color;
|
||||
$transparent-border-style: $width solid transparent;
|
||||
height: 0;
|
||||
width: 0;
|
||||
@if $direction == up {
|
||||
border-bottom: $color-border-style;
|
||||
border-left: $transparent-border-style;
|
||||
border-right: $transparent-border-style;
|
||||
} @else if $direction == right {
|
||||
border-left: $color-border-style;
|
||||
border-top: $transparent-border-style;
|
||||
border-bottom: $transparent-border-style;
|
||||
} @else if $direction == down {
|
||||
border-top: $color-border-style;
|
||||
border-left: $transparent-border-style;
|
||||
border-right: $transparent-border-style;
|
||||
} @else if $direction == left {
|
||||
border-right: $color-border-style;
|
||||
border-top: $transparent-border-style;
|
||||
border-bottom: $transparent-border-style;
|
||||
}
|
||||
}
|
8
src/utils/createUniqueString.js
Normal file
8
src/utils/createUniqueString.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Created by jiachenpan on 17/3/8.
|
||||
*/
|
||||
export default function createUniqueString() {
|
||||
const timestamp = +new Date() + '';
|
||||
const randomNum = parseInt((1 + Math.random()) * 65536) + '';
|
||||
return (+(randomNum + timestamp)).toString(32);
|
||||
}
|
72
src/utils/fetch.js
Normal file
72
src/utils/fetch.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import axios from 'axios';
|
||||
import { Message } from 'element-ui';
|
||||
import store from '../store';
|
||||
import router from '../router';
|
||||
|
||||
export default function fetch(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const instance = axios.create({
|
||||
baseURL: process.env.BASE_API,
|
||||
// timeout: 2000,
|
||||
headers: { 'X-Ivanka-Token': store.getters.token }
|
||||
});
|
||||
instance(options)
|
||||
.then(response => {
|
||||
const res = response.data;
|
||||
if (res.code !== 20000) {
|
||||
console.log(options); // for debug
|
||||
Message({
|
||||
message: res.message,
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
});
|
||||
// 50014:Token 过期了 50012:其他客户端登录了 50008:非法的token
|
||||
if (res.code === 50008 || res.code === 50014 || res.code === 50012) {
|
||||
Message({
|
||||
message: res.message,
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
});
|
||||
// router.push({path: '/'})
|
||||
// TODO
|
||||
store.dispatch('FedLogOut').then(() => {
|
||||
router.push({ path: '/login' })
|
||||
});
|
||||
}
|
||||
reject(res);
|
||||
}
|
||||
resolve(res);
|
||||
})
|
||||
.catch(error => {
|
||||
Message({
|
||||
message: '发生异常错误,请刷新页面重试,或联系程序员',
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
});
|
||||
console.log(error); // for debug
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function tpFetch(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const instance = axios.create({
|
||||
// timeout: 2000,
|
||||
});
|
||||
instance(options)
|
||||
.then(response => {
|
||||
const res = response.data;
|
||||
resolve(res);
|
||||
})
|
||||
.catch(error => {
|
||||
Message({
|
||||
message: '发生异常错误,请刷新页面重试,或联系程序员',
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
});
|
||||
console.log(error); // for debug
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
221
src/utils/index.js
Normal file
221
src/utils/index.js
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Created by jiachenpan on 16/11/18.
|
||||
*/
|
||||
|
||||
import showdown from 'showdown' // markdown转化
|
||||
const converter = new showdown.Converter();
|
||||
|
||||
export function parseTime(time, cFormat) {
|
||||
if (arguments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}';
|
||||
let date;
|
||||
if (typeof time == 'object') {
|
||||
date = time;
|
||||
} else {
|
||||
if (('' + time).length === 10) time = parseInt(time) * 1000;
|
||||
date = new Date(time);
|
||||
}
|
||||
const formatObj = {
|
||||
y: date.getFullYear(),
|
||||
m: date.getMonth() + 1,
|
||||
d: date.getDate(),
|
||||
h: date.getHours(),
|
||||
i: date.getMinutes(),
|
||||
s: date.getSeconds(),
|
||||
a: date.getDay()
|
||||
};
|
||||
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
|
||||
let value = formatObj[key];
|
||||
if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1];
|
||||
if (result.length > 0 && value < 10) {
|
||||
value = '0' + value;
|
||||
}
|
||||
return value || 0;
|
||||
});
|
||||
return time_str;
|
||||
}
|
||||
|
||||
export function formatTime(time, option) {
|
||||
time = +time * 1000;
|
||||
const d = new Date(time);
|
||||
const now = Date.now();
|
||||
|
||||
const diff = (now - d) / 1000;
|
||||
|
||||
if (diff < 30) {
|
||||
return '刚刚'
|
||||
} else if (diff < 3600) { // less 1 hour
|
||||
return Math.ceil(diff / 60) + '分钟前'
|
||||
} else if (diff < 3600 * 24) {
|
||||
return Math.ceil(diff / 3600) + '小时前'
|
||||
} else if (diff < 3600 * 24 * 2) {
|
||||
return '1天前'
|
||||
}
|
||||
if (option) {
|
||||
return parseTime(time, option)
|
||||
} else {
|
||||
return d.getMonth() + 1 + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分'
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
|
||||
|
||||
export function getQueryObject(url) {
|
||||
url = url == null ? window.location.href : url;
|
||||
const search = url.substring(url.lastIndexOf('?') + 1);
|
||||
const obj = {};
|
||||
const reg = /([^?&=]+)=([^?&=]*)/g;
|
||||
search.replace(reg, (rs, $1, $2) => {
|
||||
const name = decodeURIComponent($1);
|
||||
let val = decodeURIComponent($2);
|
||||
val = String(val);
|
||||
obj[name] = val;
|
||||
return rs;
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*get getByteLen
|
||||
* @param {Sting} val input value
|
||||
* @returns {number} output value
|
||||
*/
|
||||
export function getByteLen(val) {
|
||||
let len = 0;
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
if (val[i].match(/[^\x00-\xff]/ig) != null) {
|
||||
len += 1;
|
||||
} else { len += 0.5; }
|
||||
}
|
||||
return Math.floor(len);
|
||||
}
|
||||
|
||||
export function cleanArray(actual) {
|
||||
const newArray = [];
|
||||
for (let i = 0; i < actual.length; i++) {
|
||||
if (actual[i]) {
|
||||
newArray.push(actual[i]);
|
||||
}
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
|
||||
export function param(json) {
|
||||
if (!json) return '';
|
||||
return cleanArray(Object.keys(json).map(key => {
|
||||
if (json[key] === undefined) return '';
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(json[key]);
|
||||
})).join('&');
|
||||
}
|
||||
|
||||
export function html2Text(val) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = val;
|
||||
return div.textContent || div.innerText;
|
||||
}
|
||||
|
||||
export function objectMerge(target, source) {
|
||||
/* Merges two objects,
|
||||
giving the last one precedence */
|
||||
|
||||
if (typeof target !== 'object') {
|
||||
target = {};
|
||||
}
|
||||
if (Array.isArray(source)) {
|
||||
return source.slice();
|
||||
}
|
||||
for (const property in source) {
|
||||
if (source.hasOwnProperty(property)) {
|
||||
const sourceProperty = source[property];
|
||||
if (typeof sourceProperty === 'object') {
|
||||
target[property] = objectMerge(target[property], sourceProperty);
|
||||
continue;
|
||||
}
|
||||
target[property] = sourceProperty;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
|
||||
export function scrollTo(element, to, duration) {
|
||||
if (duration <= 0) return;
|
||||
const difference = to - element.scrollTop;
|
||||
const perTick = difference / duration * 10;
|
||||
|
||||
setTimeout(() => {
|
||||
console.log(new Date())
|
||||
element.scrollTop = element.scrollTop + perTick;
|
||||
if (element.scrollTop === to) return;
|
||||
scrollTo(element, to, duration - 10);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
export function toggleClass(element, className) {
|
||||
if (!element || !className) {
|
||||
return;
|
||||
}
|
||||
|
||||
let classString = element.className;
|
||||
const nameIndex = classString.indexOf(className);
|
||||
if (nameIndex === -1) {
|
||||
classString += '' + className;
|
||||
} else {
|
||||
classString = classString.substr(0, nameIndex) + classString.substr(nameIndex + className.length);
|
||||
}
|
||||
element.className = classString;
|
||||
}
|
||||
|
||||
export const pickerOptions = [
|
||||
{
|
||||
text: '今天',
|
||||
onClick(picker) {
|
||||
const end = new Date();
|
||||
const start = new Date(new Date().toDateString());
|
||||
end.setTime(start.getTime());
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}, {
|
||||
text: '最近一周',
|
||||
onClick(picker) {
|
||||
const end = new Date(new Date().toDateString());
|
||||
const start = new Date();
|
||||
start.setTime(end.getTime() - 3600 * 1000 * 24 * 7);
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}, {
|
||||
text: '最近一个月',
|
||||
onClick(picker) {
|
||||
const end = new Date(new Date().toDateString());
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}, {
|
||||
text: '最近三个月',
|
||||
onClick(picker) {
|
||||
const end = new Date(new Date().toDateString());
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
|
||||
picker.$emit('pick', [start, end]);
|
||||
}
|
||||
}]
|
||||
|
||||
export function getTime(type) {
|
||||
if (type === 'start') {
|
||||
return new Date().getTime() - 3600 * 1000 * 24 * 90
|
||||
} else {
|
||||
return new Date(new Date().toDateString())
|
||||
}
|
||||
}
|
||||
|
||||
export function showdownMD(md) {
|
||||
return converter.makeHtml(md)
|
||||
}
|
27
src/utils/openWindow.js
Normal file
27
src/utils/openWindow.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
*Created by jiachenpan on 16/11/29.
|
||||
* @param {Sting} url
|
||||
* @param {Sting} title
|
||||
* @param {Number} w
|
||||
* @param {Number} h
|
||||
*/
|
||||
|
||||
export default function openWindow(url, title, w, h) {
|
||||
// Fixes dual-screen position Most browsers Firefox
|
||||
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left;
|
||||
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top;
|
||||
|
||||
const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
|
||||
const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
|
||||
|
||||
const left = ((width / 2) - (w / 2)) + dualScreenLeft;
|
||||
const top = ((height / 2) - (h / 2)) + dualScreenTop;
|
||||
const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);
|
||||
|
||||
// Puts focus on the newWindow
|
||||
if (window.focus) {
|
||||
newWindow.focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
41
src/utils/validate.js
Normal file
41
src/utils/validate.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Created by jiachenpan on 16/11/18.
|
||||
*/
|
||||
|
||||
/* 是否是公司邮箱*/
|
||||
export function isWscnEmail(str) {
|
||||
const reg = /^[a-z0-9](?:[-_.+]?[a-z0-9]+)*@wallstreetcn\.com$/i;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
/* 合法uri*/
|
||||
export function validateURL(textval) {
|
||||
const urlregex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
|
||||
return urlregex.test(textval);
|
||||
}
|
||||
|
||||
/* 小写字母*/
|
||||
export function validateLowerCase(str) {
|
||||
const reg = /^[a-z]+$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
/* 验证key*/
|
||||
// export function validateKey(str) {
|
||||
// var reg = /^[a-z_\-:]+$/;
|
||||
// return reg.test(str);
|
||||
// }
|
||||
|
||||
/* 大写字母*/
|
||||
export function validateUpperCase(str) {
|
||||
const reg = /^[A-Z]+$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
/* 大小写字母*/
|
||||
export function validatAlphabets(str) {
|
||||
const reg = /^[A-Za-z]+$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
|
179
src/vendor/Blob.js
vendored
Normal file
179
src/vendor/Blob.js
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
/* eslint-disable */
|
||||
/* Blob.js
|
||||
* A Blob implementation.
|
||||
* 2014-05-27
|
||||
*
|
||||
* By Eli Grey, http://eligrey.com
|
||||
* By Devin Samarin, https://github.com/eboyjr
|
||||
* License: X11/MIT
|
||||
* See LICENSE.md
|
||||
*/
|
||||
|
||||
/*global self, unescape */
|
||||
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
|
||||
plusplus: true */
|
||||
|
||||
/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
|
||||
|
||||
(function (view) {
|
||||
"use strict";
|
||||
|
||||
view.URL = view.URL || view.webkitURL;
|
||||
|
||||
if (view.Blob && view.URL) {
|
||||
try {
|
||||
new Blob;
|
||||
return;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Internally we use a BlobBuilder implementation to base Blob off of
|
||||
// in order to support older browsers that only have BlobBuilder
|
||||
var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
|
||||
var
|
||||
get_class = function(object) {
|
||||
return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
|
||||
}
|
||||
, FakeBlobBuilder = function BlobBuilder() {
|
||||
this.data = [];
|
||||
}
|
||||
, FakeBlob = function Blob(data, type, encoding) {
|
||||
this.data = data;
|
||||
this.size = data.length;
|
||||
this.type = type;
|
||||
this.encoding = encoding;
|
||||
}
|
||||
, FBB_proto = FakeBlobBuilder.prototype
|
||||
, FB_proto = FakeBlob.prototype
|
||||
, FileReaderSync = view.FileReaderSync
|
||||
, FileException = function(type) {
|
||||
this.code = this[this.name = type];
|
||||
}
|
||||
, file_ex_codes = (
|
||||
"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
|
||||
+ "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
|
||||
).split(" ")
|
||||
, file_ex_code = file_ex_codes.length
|
||||
, real_URL = view.URL || view.webkitURL || view
|
||||
, real_create_object_URL = real_URL.createObjectURL
|
||||
, real_revoke_object_URL = real_URL.revokeObjectURL
|
||||
, URL = real_URL
|
||||
, btoa = view.btoa
|
||||
, atob = view.atob
|
||||
|
||||
, ArrayBuffer = view.ArrayBuffer
|
||||
, Uint8Array = view.Uint8Array
|
||||
;
|
||||
FakeBlob.fake = FB_proto.fake = true;
|
||||
while (file_ex_code--) {
|
||||
FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
|
||||
}
|
||||
if (!real_URL.createObjectURL) {
|
||||
URL = view.URL = {};
|
||||
}
|
||||
URL.createObjectURL = function(blob) {
|
||||
var
|
||||
type = blob.type
|
||||
, data_URI_header
|
||||
;
|
||||
if (type === null) {
|
||||
type = "application/octet-stream";
|
||||
}
|
||||
if (blob instanceof FakeBlob) {
|
||||
data_URI_header = "data:" + type;
|
||||
if (blob.encoding === "base64") {
|
||||
return data_URI_header + ";base64," + blob.data;
|
||||
} else if (blob.encoding === "URI") {
|
||||
return data_URI_header + "," + decodeURIComponent(blob.data);
|
||||
} if (btoa) {
|
||||
return data_URI_header + ";base64," + btoa(blob.data);
|
||||
} else {
|
||||
return data_URI_header + "," + encodeURIComponent(blob.data);
|
||||
}
|
||||
} else if (real_create_object_URL) {
|
||||
return real_create_object_URL.call(real_URL, blob);
|
||||
}
|
||||
};
|
||||
URL.revokeObjectURL = function(object_URL) {
|
||||
if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
|
||||
real_revoke_object_URL.call(real_URL, object_URL);
|
||||
}
|
||||
};
|
||||
FBB_proto.append = function(data/*, endings*/) {
|
||||
var bb = this.data;
|
||||
// decode data to a binary string
|
||||
if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
|
||||
var
|
||||
str = ""
|
||||
, buf = new Uint8Array(data)
|
||||
, i = 0
|
||||
, buf_len = buf.length
|
||||
;
|
||||
for (; i < buf_len; i++) {
|
||||
str += String.fromCharCode(buf[i]);
|
||||
}
|
||||
bb.push(str);
|
||||
} else if (get_class(data) === "Blob" || get_class(data) === "File") {
|
||||
if (FileReaderSync) {
|
||||
var fr = new FileReaderSync;
|
||||
bb.push(fr.readAsBinaryString(data));
|
||||
} else {
|
||||
// async FileReader won't work as BlobBuilder is sync
|
||||
throw new FileException("NOT_READABLE_ERR");
|
||||
}
|
||||
} else if (data instanceof FakeBlob) {
|
||||
if (data.encoding === "base64" && atob) {
|
||||
bb.push(atob(data.data));
|
||||
} else if (data.encoding === "URI") {
|
||||
bb.push(decodeURIComponent(data.data));
|
||||
} else if (data.encoding === "raw") {
|
||||
bb.push(data.data);
|
||||
}
|
||||
} else {
|
||||
if (typeof data !== "string") {
|
||||
data += ""; // convert unsupported types to strings
|
||||
}
|
||||
// decode UTF-16 to binary string
|
||||
bb.push(unescape(encodeURIComponent(data)));
|
||||
}
|
||||
};
|
||||
FBB_proto.getBlob = function(type) {
|
||||
if (!arguments.length) {
|
||||
type = null;
|
||||
}
|
||||
return new FakeBlob(this.data.join(""), type, "raw");
|
||||
};
|
||||
FBB_proto.toString = function() {
|
||||
return "[object BlobBuilder]";
|
||||
};
|
||||
FB_proto.slice = function(start, end, type) {
|
||||
var args = arguments.length;
|
||||
if (args < 3) {
|
||||
type = null;
|
||||
}
|
||||
return new FakeBlob(
|
||||
this.data.slice(start, args > 1 ? end : this.data.length)
|
||||
, type
|
||||
, this.encoding
|
||||
);
|
||||
};
|
||||
FB_proto.toString = function() {
|
||||
return "[object Blob]";
|
||||
};
|
||||
FB_proto.close = function() {
|
||||
this.size = this.data.length = 0;
|
||||
};
|
||||
return FakeBlobBuilder;
|
||||
}(view));
|
||||
|
||||
view.Blob = function Blob(blobParts, options) {
|
||||
var type = options ? (options.type || "") : "";
|
||||
var builder = new BlobBuilder();
|
||||
if (blobParts) {
|
||||
for (var i = 0, len = blobParts.length; i < len; i++) {
|
||||
builder.append(blobParts[i]);
|
||||
}
|
||||
}
|
||||
return builder.getBlob(type);
|
||||
};
|
||||
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));
|
141
src/vendor/Export2Excel.js
vendored
Normal file
141
src/vendor/Export2Excel.js
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
/* eslint-disable */
|
||||
require('script-loader!file-saver');
|
||||
require('script-loader!vendor/Blob');
|
||||
require('script-loader!xlsx/dist/xlsx.core.min');
|
||||
function generateArray(table) {
|
||||
var out = [];
|
||||
var rows = table.querySelectorAll('tr');
|
||||
var ranges = [];
|
||||
for (var R = 0; R < rows.length; ++R) {
|
||||
var outRow = [];
|
||||
var row = rows[R];
|
||||
var columns = row.querySelectorAll('td');
|
||||
for (var C = 0; C < columns.length; ++C) {
|
||||
var cell = columns[C];
|
||||
var colspan = cell.getAttribute('colspan');
|
||||
var rowspan = cell.getAttribute('rowspan');
|
||||
var cellValue = cell.innerText;
|
||||
if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;
|
||||
|
||||
//Skip ranges
|
||||
ranges.forEach(function (range) {
|
||||
if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
|
||||
for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
|
||||
}
|
||||
});
|
||||
|
||||
//Handle Row Span
|
||||
if (rowspan || colspan) {
|
||||
rowspan = rowspan || 1;
|
||||
colspan = colspan || 1;
|
||||
ranges.push({s: {r: R, c: outRow.length}, e: {r: R + rowspan - 1, c: outRow.length + colspan - 1}});
|
||||
}
|
||||
;
|
||||
|
||||
//Handle Value
|
||||
outRow.push(cellValue !== "" ? cellValue : null);
|
||||
|
||||
//Handle Colspan
|
||||
if (colspan) for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
|
||||
}
|
||||
out.push(outRow);
|
||||
}
|
||||
return [out, ranges];
|
||||
};
|
||||
|
||||
function datenum(v, date1904) {
|
||||
if (date1904) v += 1462;
|
||||
var epoch = Date.parse(v);
|
||||
return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
function sheet_from_array_of_arrays(data, opts) {
|
||||
var ws = {};
|
||||
var range = {s: {c: 10000000, r: 10000000}, e: {c: 0, r: 0}};
|
||||
for (var R = 0; R != data.length; ++R) {
|
||||
for (var C = 0; C != data[R].length; ++C) {
|
||||
if (range.s.r > R) range.s.r = R;
|
||||
if (range.s.c > C) range.s.c = C;
|
||||
if (range.e.r < R) range.e.r = R;
|
||||
if (range.e.c < C) range.e.c = C;
|
||||
var cell = {v: data[R][C]};
|
||||
if (cell.v == null) continue;
|
||||
var cell_ref = XLSX.utils.encode_cell({c: C, r: R});
|
||||
|
||||
if (typeof cell.v === 'number') cell.t = 'n';
|
||||
else if (typeof cell.v === 'boolean') cell.t = 'b';
|
||||
else if (cell.v instanceof Date) {
|
||||
cell.t = 'n';
|
||||
cell.z = XLSX.SSF._table[14];
|
||||
cell.v = datenum(cell.v);
|
||||
}
|
||||
else cell.t = 's';
|
||||
|
||||
ws[cell_ref] = cell;
|
||||
}
|
||||
}
|
||||
if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
|
||||
return ws;
|
||||
}
|
||||
|
||||
function Workbook() {
|
||||
if (!(this instanceof Workbook)) return new Workbook();
|
||||
this.SheetNames = [];
|
||||
this.Sheets = {};
|
||||
}
|
||||
|
||||
function s2ab(s) {
|
||||
var buf = new ArrayBuffer(s.length);
|
||||
var view = new Uint8Array(buf);
|
||||
for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
|
||||
return buf;
|
||||
}
|
||||
|
||||
export function export_table_to_excel(id) {
|
||||
var theTable = document.getElementById(id);
|
||||
console.log('a')
|
||||
var oo = generateArray(theTable);
|
||||
var ranges = oo[1];
|
||||
|
||||
/* original data */
|
||||
var data = oo[0];
|
||||
var ws_name = "SheetJS";
|
||||
console.log(data);
|
||||
|
||||
var wb = new Workbook(), ws = sheet_from_array_of_arrays(data);
|
||||
|
||||
/* add ranges to worksheet */
|
||||
// ws['!cols'] = ['apple', 'banan'];
|
||||
ws['!merges'] = ranges;
|
||||
|
||||
/* add worksheet to workbook */
|
||||
wb.SheetNames.push(ws_name);
|
||||
wb.Sheets[ws_name] = ws;
|
||||
|
||||
var wbout = XLSX.write(wb, {bookType: 'xlsx', bookSST: false, type: 'binary'});
|
||||
|
||||
saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), "test.xlsx")
|
||||
}
|
||||
|
||||
function formatJson(jsonData) {
|
||||
console.log(jsonData)
|
||||
}
|
||||
export function export_json_to_excel(th, jsonData, defaultTitle) {
|
||||
|
||||
/* original data */
|
||||
|
||||
var data = jsonData;
|
||||
data.unshift(th);
|
||||
var ws_name = "SheetJS";
|
||||
|
||||
var wb = new Workbook(), ws = sheet_from_array_of_arrays(data);
|
||||
|
||||
|
||||
/* add worksheet to workbook */
|
||||
wb.SheetNames.push(ws_name);
|
||||
wb.Sheets[ws_name] = ws;
|
||||
|
||||
var wbout = XLSX.write(wb, {bookType: 'xlsx', bookSST: false, type: 'binary'});
|
||||
var title = defaultTitle || '列表'
|
||||
saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), title + ".xlsx")
|
||||
}
|
87
src/views/admin/createUser.vue
Normal file
87
src/views/admin/createUser.vue
Normal 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
404
src/views/admin/profile.vue
Normal 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>     ( 注:重设密码将会登出,请注意!!! )</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>
|
92
src/views/admin/quicklycreate.vue
Normal file
92
src/views/admin/quicklycreate.vue
Normal 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>
|
||||
|
241
src/views/admin/usersList.vue
Normal file
241
src/views/admin/usersList.vue
Normal 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>
|
61
src/views/components/404.vue
Normal file
61
src/views/components/404.vue
Normal 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>
|
22
src/views/components/markdown.vue
Normal file
22
src/views/components/markdown.vue
Normal 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>
|
||||
|
||||
|
28
src/views/components/tinymce.vue
Normal file
28
src/views/components/tinymce.vue
Normal 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>
|
||||
|
||||
|
75
src/views/dashboard/default/index.vue
Normal file
75
src/views/dashboard/default/index.vue
Normal 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>
|
34
src/views/dashboard/editor/articlesChart.vue
Normal file
34
src/views/dashboard/editor/articlesChart.vue
Normal 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>
|
284
src/views/dashboard/editor/index.vue
Normal file
284
src/views/dashboard/editor/index.vue
Normal 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>
|
61
src/views/dashboard/editor/monthKpi.vue
Normal file
61
src/views/dashboard/editor/monthKpi.vue
Normal 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>
|
38
src/views/dashboard/index.vue
Normal file
38
src/views/dashboard/index.vue
Normal 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
82
src/views/error/401.vue
Normal 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
61
src/views/error/404.vue
Normal 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>
|
20
src/views/layout/AppMain.vue
Normal file
20
src/views/layout/AppMain.vue
Normal 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>
|
98
src/views/layout/Layout.vue
Normal file
98
src/views/layout/Layout.vue
Normal 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>
|
48
src/views/layout/Levelbar.vue
Normal file
48
src/views/layout/Levelbar.vue
Normal 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
107
src/views/layout/Navbar.vue
Normal 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>
|
||||
|
||||
|
||||
|
48
src/views/layout/Sidebar.vue
Normal file
48
src/views/layout/Sidebar.vue
Normal 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>
|
7
src/views/layout/index.js
Normal file
7
src/views/layout/index.js
Normal 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';
|
10
src/views/login/authredirect.vue
Normal file
10
src/views/login/authredirect.vue
Normal 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
188
src/views/login/index.vue
Normal 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
178
src/views/login/reset.vue
Normal 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
117
src/views/login/sendpwd.vue
Normal 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>
|
68
src/views/login/socialsignin.vue
Normal file
68
src/views/login/socialsignin.vue
Normal 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>
|
102
src/views/others/mediaUpload.vue
Normal file
102
src/views/others/mediaUpload.vue
Normal 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>
|
20
src/views/previewLayout/Layout.vue
Normal file
20
src/views/previewLayout/Layout.vue
Normal 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>
|
118
src/views/user/components/info.vue
Normal file
118
src/views/user/components/info.vue
Normal 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
125
src/views/user/detail.vue
Normal 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
183
src/views/user/list.vue
Normal 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>
|
Reference in New Issue
Block a user