init
This commit is contained in:
49
src/components/asyncModal/index.vue
Normal file
49
src/components/asyncModal/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<Modal v-model="showModal" :width="width||80" :title="title" v-bind="$attrs">
|
||||
<div class="asyncModal-box">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<Button @click="hide">取消</Button>
|
||||
<Button class="ml30" type="primary" @click="saveInfo">确定</Button>
|
||||
</div>
|
||||
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['title', 'width'],
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
callback: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show(callback) {
|
||||
this.showModal = true
|
||||
this.callback = callback
|
||||
},
|
||||
hide() {
|
||||
this.showModal = false
|
||||
},
|
||||
|
||||
async saveInfo() {
|
||||
if (this.callback) {
|
||||
await this.callback()
|
||||
}
|
||||
this.$emit('on-ok')
|
||||
this.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.asyncModal-box {
|
||||
max-height: 90vh;
|
||||
min-height: 20vh;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
42
src/components/common-icon/common-icon.vue
Normal file
42
src/components/common-icon/common-icon.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<component :is="iconType" :type="iconName" :color="iconColor" :size="iconSize" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icons from '@component/md-icons'
|
||||
export default {
|
||||
name: 'CommonIcon',
|
||||
components: { Icons },
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
},
|
||||
color: String,
|
||||
size: Number,
|
||||
},
|
||||
computed: {
|
||||
iconType() {
|
||||
if (this.type) {
|
||||
return this.type.indexOf('_') === 0 ? 'Icons' : 'Icon'
|
||||
}
|
||||
},
|
||||
iconName() {
|
||||
return this.iconType === 'Icons' ? this.getCustomIconName(this.type) : this.type
|
||||
},
|
||||
iconSize() {
|
||||
return this.size || (this.iconType === 'Icons' ? 12 : undefined)
|
||||
},
|
||||
iconColor() {
|
||||
return this.color || ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getCustomIconName(iconName) {
|
||||
return iconName.slice(1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
2
src/components/common-icon/index.js
Normal file
2
src/components/common-icon/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import CommonIcon from './common-icon.vue'
|
||||
export default CommonIcon
|
||||
105
src/components/cron-input/index.vue
Normal file
105
src/components/cron-input/index.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
|
||||
<Dropdown trigger="click" style="width:100%" @on-visible-change="showDrop">
|
||||
|
||||
<Input :value="value" readonly placeholder="请选择" />
|
||||
|
||||
<Dropdown-menu slot="list">
|
||||
<Form :model="cronForm" ref="cronForm" label-position="left" :label-width="160" class="cron-form">
|
||||
|
||||
<!-- Minute Input -->
|
||||
<FormItem label="Minute">
|
||||
<Select v-model="cronForm.minute" placeholder="Minute" class="scrollable-select">
|
||||
<Option v-for="minute in minutes" :value="minute" :key="minute">{{ minute }}</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<!-- Hour Input -->
|
||||
<FormItem label="Hour">
|
||||
<Select v-model="cronForm.hour" placeholder="Hour" class="scrollable-select">
|
||||
<Option v-for="hour in hours" :value="hour" :key="hour">{{ hour }}</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<!-- Day of Month Input -->
|
||||
<FormItem label="Day">
|
||||
<Select v-model="cronForm.day" placeholder="Day" class="scrollable-select">
|
||||
<Option v-for="day in days" :value="day" :key="day">{{ day }}</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<!-- Month Input -->
|
||||
<FormItem label="Month">
|
||||
<Select v-model="cronForm.month" placeholder="Month" class="scrollable-select">
|
||||
<Option v-for="month in months" :value="month" :key="month">{{ month }}</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<!-- Day of Week Input -->
|
||||
<FormItem label="Week">
|
||||
<Select v-model="cronForm.dayOfWeek" placeholder="Day of Week" class="scrollable-select">
|
||||
<Option v-for="dayOfWeek in daysOfWeek" :value="dayOfWeek" :key="dayOfWeek">{{ dayOfWeek }}</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<Button type="primary" @click="applyCron">确定</Button>
|
||||
|
||||
</Form>
|
||||
</Dropdown-menu>
|
||||
</Dropdown>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
cronForm: {
|
||||
minute: '*',
|
||||
hour: '*',
|
||||
day: '*',
|
||||
month: '*',
|
||||
dayOfWeek: '*'
|
||||
},
|
||||
|
||||
minutes: ['*', ...Array.from({ length: 60 }, (_, i) => i.toString())],
|
||||
hours: ['*', ...Array.from({ length: 24 }, (_, i) => i.toString())],
|
||||
days: ['*', ...Array.from({ length: 31 }, (_, i) => (i + 1).toString())],
|
||||
months: ['*', ...Array.from({ length: 12 }, (_, i) => (i + 1).toString())],
|
||||
daysOfWeek: ['*', '0', '1', '2', '3', '4', '5', '6']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showDrop(visible) {
|
||||
if (visible && this.value) {
|
||||
let timeArray = this.value.split(' ')
|
||||
|
||||
this.cronForm = {
|
||||
minute: timeArray[0],
|
||||
hour: timeArray[1],
|
||||
day: timeArray[2],
|
||||
month: timeArray[3],
|
||||
dayOfWeek: timeArray[4]
|
||||
}
|
||||
}
|
||||
},
|
||||
applyCron() {
|
||||
const { minute, hour, day, month, dayOfWeek } = this.cronForm
|
||||
this.$emit('input', `${minute} ${hour} ${day} ${month} ${dayOfWeek}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ivu-dropdown-menu {
|
||||
padding: 10px;
|
||||
}
|
||||
.scrollable-select {
|
||||
.ivu-select-dropdown {
|
||||
max-height: 200px; /* Adjust this value to your needs */
|
||||
overflow-y: auto; /* Enable vertical scrollbar */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
2
src/components/cropper/index.js
Normal file
2
src/components/cropper/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Cropper from './index.vue'
|
||||
export default Cropper
|
||||
36
src/components/cropper/index.less
Normal file
36
src/components/cropper/index.less
Normal file
@@ -0,0 +1,36 @@
|
||||
.bg{
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC")
|
||||
}
|
||||
.cropper-wrapper{
|
||||
width:100%;
|
||||
height: 100%;
|
||||
.img-box{
|
||||
height: 600px;
|
||||
width: 600px;
|
||||
border: 1px solid #ebebeb;
|
||||
display: inline-block;
|
||||
.bg;
|
||||
img{
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.right-con{
|
||||
display: inline-block;
|
||||
width: 170px;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px;
|
||||
margin-left: 30px;
|
||||
.preview-box{
|
||||
height: 150px !important;
|
||||
width: 100% !important;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ebebeb;
|
||||
.bg;
|
||||
}
|
||||
.button-box{
|
||||
padding: 10px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
155
src/components/cropper/index.vue
Normal file
155
src/components/cropper/index.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="cropper-wrapper">
|
||||
<div class="img-box">
|
||||
<img class="cropper-image" :id="imgId" alt="">
|
||||
</div>
|
||||
<div class="right-con">
|
||||
<div v-if="preview" class="preview-box" :id="previewId"></div>
|
||||
<div class="button-box">
|
||||
<slot>
|
||||
<CustomUpload @chage="beforeUpload"> </CustomUpload>
|
||||
</slot>
|
||||
<div v-show="insideSrc">
|
||||
<Button type="primary" @click="rotate">
|
||||
<Icon type="md-refresh" :size="18" />
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="text-align: right;margin-top: 20px;">
|
||||
<Button style="width: 150px;margin-top: 10px;" type="primary" @click="crop">{{ cropButtonText }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Cropper from 'cropperjs'
|
||||
import './index.less'
|
||||
import 'cropperjs/dist/cropper.min.css'
|
||||
export default {
|
||||
name: 'Cropper',
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
preview: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
moveStep: {
|
||||
type: Number,
|
||||
default: 4,
|
||||
},
|
||||
cropButtonText: {
|
||||
type: String,
|
||||
default: '裁剪',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
actionUrl: window.rootVue.$config.apiUrl + 'sys_file/upload_oos_img',
|
||||
cropper: null,
|
||||
insideSrc: '',
|
||||
file: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
imgId() {
|
||||
return `cropper${this._uid}`
|
||||
},
|
||||
previewId() {
|
||||
return `cropper_preview${this._uid}`
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
src(src) {
|
||||
this.replace(src)
|
||||
},
|
||||
insideSrc(src) {
|
||||
this.replace(src)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
beforeUpload(file) {
|
||||
this.file = file
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = async (event) => {
|
||||
this.getSize(event.srcElement.result).then(({ width, height }) => {
|
||||
if (width < 500 || height < 500) {
|
||||
rootVue.$Message.error('图片尺寸小于 500*500 ,请更换图片')
|
||||
return false
|
||||
}
|
||||
this.insideSrc = event.srcElement.result
|
||||
})
|
||||
}
|
||||
return false
|
||||
},
|
||||
getSize(src) {
|
||||
let pro = new Promise((resolve, reject) => {
|
||||
let img = new Image()
|
||||
img.onload = function () {
|
||||
let { width, height } = img
|
||||
resolve({ width, height })
|
||||
}
|
||||
img.src = src
|
||||
})
|
||||
return pro
|
||||
},
|
||||
replace(src) {
|
||||
this.cropper.replace(src)
|
||||
this.insideSrc = src
|
||||
},
|
||||
rotate() {
|
||||
this.cropper.rotate(90)
|
||||
},
|
||||
shrink() {
|
||||
this.cropper.zoom(-0.1)
|
||||
},
|
||||
magnify() {
|
||||
this.cropper.zoom(0.1)
|
||||
},
|
||||
scale(d) {
|
||||
this.cropper[`scale${d}`](-this.cropper.getData()[`scale${d}`])
|
||||
},
|
||||
move(...argu) {
|
||||
this.cropper.move(...argu)
|
||||
},
|
||||
crop() {
|
||||
this.cropper.getCroppedCanvas({ fillColor: '#fff' }).toBlob(
|
||||
(blob) => {
|
||||
let file = new File([blob], this.file.name, {
|
||||
type: this.file.type,
|
||||
lastModified: Date.now(),
|
||||
})
|
||||
this.$emit('on-crop', file)
|
||||
this.insideSrc = ''
|
||||
this.filefile = null
|
||||
},
|
||||
'image/jpeg',
|
||||
0.8
|
||||
)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
let dom = document.getElementById(this.imgId)
|
||||
this.cropper = new Cropper(dom, {
|
||||
preview: `#${this.previewId}`,
|
||||
dragMode: 'move',
|
||||
checkCrossOrigin: true,
|
||||
aspectRatio: 5 / 5,
|
||||
cropBoxResizable: false,
|
||||
cropBoxMovable: false,
|
||||
outputSize: 0.8,
|
||||
minContainerWidth: 600, // 容器的最小宽度
|
||||
minContainerHeight: 600, // 容器的最小高度
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
</script>
|
||||
95
src/components/editor/index.vue
Normal file
95
src/components/editor/index.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div :id="id" class="edit-box"> </div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WangEditor from 'wangeditor'
|
||||
export default {
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
id: '',
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
let date = new Date()
|
||||
this.id = 'wangeditor_' + date.getTime()
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
let domId = `#${this.id}`
|
||||
this.editor = new WangEditor(domId)
|
||||
this.editor.config.uploadImgShowBase64 = true
|
||||
|
||||
this.editor.config.uploadImgServer = window.rootVue.$config.apiUrl + 'sys_file/upload_oos_img'
|
||||
this.editor.config.uploadImgHooks = {
|
||||
customInsert: (insertImg, result, editor) => {
|
||||
var url = result.data.path
|
||||
insertImg(url)
|
||||
},
|
||||
}
|
||||
|
||||
this.editor.config.menus = [
|
||||
'head', // 标题
|
||||
'bold', // 粗体
|
||||
'fontSize', // 字号
|
||||
'fontName', // 字体
|
||||
'italic', // 斜体
|
||||
'underline', // 下划线
|
||||
'strikeThrough', // 删除线
|
||||
'foreColor', // 文字颜色
|
||||
'backColor', // 背景颜色
|
||||
'link', // 插入链接
|
||||
'list', // 列表
|
||||
'justify', // 对齐方式
|
||||
'image', // 插入图片
|
||||
'fullscreen', // 全屏
|
||||
]
|
||||
|
||||
this.editor.config.pasteTextHandle = function (content) {
|
||||
if (content === '' && !content) return ''
|
||||
var str = content
|
||||
str = str.replace(/<xml>[\s\S]*?<\/xml>/gi, '')
|
||||
str = str.replace(/<style>[\s\S]*?<\/style>/gi, '')
|
||||
str = str.replace(/<\/?[^>]*>/g, '')
|
||||
str = str.replace(/[ | ]*\n/g, '\n')
|
||||
str = str.replace(/ /gi, '')
|
||||
str = str.replace(/spanyes'/gi, 'div')
|
||||
str = str.replace(/spanyes'/gi, '')
|
||||
str = str.replace(/';/gi, '')
|
||||
str = str.replace(/spanyes'/gi, 'span')
|
||||
str = str.replace(/<\/font>/gi, '')
|
||||
return str
|
||||
}
|
||||
|
||||
this.editor.config.onchange = (newHtml) => {
|
||||
this.$emit('input', newHtml)
|
||||
}
|
||||
|
||||
this.editor.create()
|
||||
|
||||
this.editor.$textElem.css('text-align', 'left')
|
||||
// 初始化设置值
|
||||
if (this.value) {
|
||||
this.setHtml(this.value)
|
||||
}
|
||||
},
|
||||
getHtml() {
|
||||
return this.editor.txt.html()
|
||||
},
|
||||
setHtml(text) {
|
||||
this.editor.txt.html(text)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.edit-box {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
2
src/components/info-card/index.js
Normal file
2
src/components/info-card/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import InforCard from './infor-card.vue'
|
||||
export default InforCard
|
||||
94
src/components/info-card/infor-card.vue
Normal file
94
src/components/info-card/infor-card.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<Card :shadow="shadow" class="info-card-wrapper" :padding="0">
|
||||
<div class="content-con">
|
||||
<div class="left-area" :style="{background: color, width: leftWidth}">
|
||||
<common-icon class="icon" :type="icon" :size="iconSize" color="#fff" />
|
||||
</div>
|
||||
<div class="right-area" :style="{width: rightWidth}">
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CommonIcon from '@component/common-icon'
|
||||
export default {
|
||||
name: 'InforCard',
|
||||
components: {
|
||||
CommonIcon,
|
||||
},
|
||||
props: {
|
||||
left: {
|
||||
type: Number,
|
||||
default: 36,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#2d8cf0',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
iconSize: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
shadow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
leftWidth() {
|
||||
return `${this.left}%`
|
||||
},
|
||||
rightWidth() {
|
||||
return `${100 - this.left}%`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.common {
|
||||
float: left;
|
||||
height: 100%;
|
||||
display: table;
|
||||
text-align: center;
|
||||
}
|
||||
.size {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.middle-center {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.info-card-wrapper {
|
||||
.size;
|
||||
overflow: hidden;
|
||||
.ivu-card-body {
|
||||
.size;
|
||||
}
|
||||
.content-con {
|
||||
.size;
|
||||
position: relative;
|
||||
.left-area {
|
||||
.common;
|
||||
& > .icon {
|
||||
.middle-center;
|
||||
}
|
||||
}
|
||||
.right-area {
|
||||
.common;
|
||||
& > div {
|
||||
.middle-center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
src/components/load-flower/index.vue
Normal file
32
src/components/load-flower/index.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div id="load-warp">
|
||||
<div class="m-load"> </div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
#load-warp {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
line-height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
z-index: 99999;
|
||||
display: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.m-load {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
margin-top: -18px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: url('../../assets/images/load.gif') center center no-repeat;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
2
src/components/login-form/index.js
Normal file
2
src/components/login-form/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import LoginForm from './login-form.vue'
|
||||
export default LoginForm
|
||||
68
src/components/login-form/login-form.vue
Normal file
68
src/components/login-form/login-form.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<Form ref="loginForm" :model="form" :rules="rules" @keydown.enter.native="handleSubmit">
|
||||
<FormItem prop="userName">
|
||||
<Input v-model="form.userName" placeholder="请输入用户名">
|
||||
<span slot="prepend">
|
||||
<Icon :size="16" type="ios-person"></Icon>
|
||||
</span>
|
||||
</Input>
|
||||
</FormItem>
|
||||
<FormItem prop="password">
|
||||
<Input type="password" v-model="form.password" placeholder="请输入密码">
|
||||
<span slot="prepend">
|
||||
<Icon :size="14" type="md-lock"></Icon>
|
||||
</span>
|
||||
</Input>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button style="margin-top:20px;" size="large" @click="handleSubmit" type="primary" long>登录</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoginForm',
|
||||
props: {
|
||||
userNameRules: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [{ required: true, message: '账号不能为空', trigger: 'blur' }]
|
||||
}
|
||||
},
|
||||
passwordRules: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [{ required: true, message: '密码不能为空', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
userName: '',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rules() {
|
||||
return {
|
||||
userName: this.userNameRules,
|
||||
password: this.passwordRules
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSubmit() {
|
||||
this.$refs.loginForm.validate(valid => {
|
||||
if (valid) {
|
||||
this.$emit('on-success-valid', {
|
||||
userName: this.form.userName,
|
||||
password: this.form.password
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
2
src/components/main/components/a-back-top/index.js
Normal file
2
src/components/main/components/a-back-top/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import ABackTop from './index.vue'
|
||||
export default ABackTop
|
||||
94
src/components/main/components/a-back-top/index.vue
Normal file
94
src/components/main/components/a-back-top/index.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div :class="classes" :style="styles" @click="back">
|
||||
<slot>
|
||||
<div :class="innerClasses">
|
||||
<i class="ivu-icon ivu-icon-ios-arrow-up"></i>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { scrollTop, on, off } from '@/utils/tools'
|
||||
const prefixCls = 'ivu-back-top'
|
||||
|
||||
export default {
|
||||
name: 'ABackTop',
|
||||
props: {
|
||||
height: {
|
||||
type: Number,
|
||||
default: 400
|
||||
},
|
||||
bottom: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
right: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
},
|
||||
container: {
|
||||
type: null,
|
||||
default: window
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
backTop: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// window.addEventListener('scroll', this.handleScroll, false)
|
||||
// window.addEventListener('resize', this.handleScroll, false)
|
||||
on(this.containerEle, 'scroll', this.handleScroll)
|
||||
on(this.containerEle, 'resize', this.handleScroll)
|
||||
},
|
||||
beforeDestroy() {
|
||||
// window.removeEventListener('scroll', this.handleScroll, false)
|
||||
// window.removeEventListener('resize', this.handleScroll, false)
|
||||
off(this.containerEle, 'scroll', this.handleScroll)
|
||||
off(this.containerEle, 'resize', this.handleScroll)
|
||||
},
|
||||
computed: {
|
||||
classes() {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-show`]: this.backTop
|
||||
}
|
||||
]
|
||||
},
|
||||
styles() {
|
||||
return {
|
||||
bottom: `${this.bottom}px`,
|
||||
right: `${this.right}px`
|
||||
}
|
||||
},
|
||||
innerClasses() {
|
||||
return `${prefixCls}-inner`
|
||||
},
|
||||
containerEle() {
|
||||
return this.container === window
|
||||
? window
|
||||
: document.querySelector(this.container)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleScroll() {
|
||||
this.backTop = this.containerEle.scrollTop >= this.height
|
||||
},
|
||||
back() {
|
||||
let target =
|
||||
typeof this.container === 'string'
|
||||
? this.containerEle
|
||||
: document.documentElement || document.body
|
||||
const sTop = target.scrollTop
|
||||
scrollTop(this.containerEle, sTop, 0, this.duration)
|
||||
this.$emit('on-click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
90
src/components/main/components/fullscreen/fullscreen.vue
Normal file
90
src/components/main/components/fullscreen/fullscreen.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div v-if="showFullScreenBtn" class="full-screen-btn-con">
|
||||
<Tooltip :content="value ? '退出全屏' : '全屏'" placement="bottom">
|
||||
<Icon @click.native="handleChange" :type="value ? 'md-contract' : 'md-expand'" :size="23"></Icon>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Fullscreen',
|
||||
computed: {
|
||||
showFullScreenBtn() {
|
||||
return window.navigator.userAgent.indexOf('MSIE') < 0
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFullscreen() {
|
||||
let main = document.body
|
||||
if (this.value) {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen()
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen()
|
||||
} else if (document.webkitCancelFullScreen) {
|
||||
document.webkitCancelFullScreen()
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen()
|
||||
}
|
||||
} else {
|
||||
if (main.requestFullscreen) {
|
||||
main.requestFullscreen()
|
||||
} else if (main.mozRequestFullScreen) {
|
||||
main.mozRequestFullScreen()
|
||||
} else if (main.webkitRequestFullScreen) {
|
||||
main.webkitRequestFullScreen()
|
||||
} else if (main.msRequestFullscreen) {
|
||||
main.msRequestFullscreen()
|
||||
}
|
||||
}
|
||||
},
|
||||
handleChange() {
|
||||
this.handleFullscreen()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let isFullscreen =
|
||||
document.fullscreenElement ||
|
||||
document.mozFullScreenElement ||
|
||||
document.webkitFullscreenElement ||
|
||||
document.fullScreen ||
|
||||
document.mozFullScreen ||
|
||||
document.webkitIsFullScreen
|
||||
isFullscreen = !!isFullscreen
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
this.$emit('input', !this.value)
|
||||
this.$emit('on-change', !this.value)
|
||||
})
|
||||
document.addEventListener('mozfullscreenchange', () => {
|
||||
this.$emit('input', !this.value)
|
||||
this.$emit('on-change', !this.value)
|
||||
})
|
||||
document.addEventListener('webkitfullscreenchange', () => {
|
||||
this.$emit('input', !this.value)
|
||||
this.$emit('on-change', !this.value)
|
||||
})
|
||||
document.addEventListener('msfullscreenchange', () => {
|
||||
this.$emit('input', !this.value)
|
||||
this.$emit('on-change', !this.value)
|
||||
})
|
||||
this.$emit('input', isFullscreen)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.full-screen-btn-con .ivu-tooltip-rel {
|
||||
height: 64px;
|
||||
line-height: 56px;
|
||||
i {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
2
src/components/main/components/fullscreen/index.js
Normal file
2
src/components/main/components/fullscreen/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Fullscreen from './fullscreen.vue'
|
||||
export default Fullscreen
|
||||
@@ -0,0 +1,4 @@
|
||||
.custom-bread-crumb{
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="custom-bread-crumb">
|
||||
<Breadcrumb :style="{fontSize: `${fontSize}px`}">
|
||||
<BreadcrumbItem v-for="item in list" :to="item.to" :key="`bread-crumb-${item.name}`">
|
||||
<common-icon style="margin-right: 4px;" :type="item.icon || ''" />
|
||||
{{ showTitle(item) }}
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { showTitle } from '@/utils/tools'
|
||||
import CommonIcon from '@component/common-icon/common-icon'
|
||||
import './custom-bread-crumb.less'
|
||||
export default {
|
||||
name: 'customBreadCrumb',
|
||||
components: {
|
||||
CommonIcon,
|
||||
},
|
||||
props: {
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
fontSize: {
|
||||
type: Number,
|
||||
default: 14,
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showTitle(item) {
|
||||
return showTitle(item, this)
|
||||
},
|
||||
isCustomIcon(iconName) {
|
||||
return iconName.indexOf('_') === 0
|
||||
},
|
||||
getCustomIconName(iconName) {
|
||||
return iconName.slice(1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,2 @@
|
||||
import customBreadCrumb from './custom-bread-crumb.vue'
|
||||
export default customBreadCrumb
|
||||
18
src/components/main/components/header-bar/header-bar.less
Normal file
18
src/components/main/components/header-bar/header-bar.less
Normal file
@@ -0,0 +1,18 @@
|
||||
.header-bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.custom-content-con {
|
||||
height: 64px;
|
||||
padding-right: 120px;
|
||||
line-height: 64px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
}
|
||||
39
src/components/main/components/header-bar/header-bar.vue
Normal file
39
src/components/main/components/header-bar/header-bar.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="header-bar">
|
||||
<sider-trigger :collapsed="collapsed" icon="md-menu" @on-change="handleCollpasedChange"></sider-trigger>
|
||||
<custom-bread-crumb show-icon style="margin-left: 30px;" :list="breadCrumbList"></custom-bread-crumb>
|
||||
<div class="custom-content-con">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import siderTrigger from './sider-trigger'
|
||||
import customBreadCrumb from './custom-bread-crumb'
|
||||
import './header-bar.less'
|
||||
export default {
|
||||
name: 'HeaderBar',
|
||||
components: {
|
||||
siderTrigger,
|
||||
customBreadCrumb,
|
||||
},
|
||||
props: {
|
||||
collapsed: Boolean,
|
||||
},
|
||||
computed: {
|
||||
breadCrumbList() {
|
||||
let route = this.$route.matched.map((p) => {
|
||||
let { name, path, meta } = p
|
||||
return { name, path, meta }
|
||||
})
|
||||
route.shift()
|
||||
return route
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleCollpasedChange(state) {
|
||||
this.$emit('on-coll-change', state)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
2
src/components/main/components/header-bar/index.js
Normal file
2
src/components/main/components/header-bar/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import HeaderBar from './header-bar'
|
||||
export default HeaderBar
|
||||
@@ -0,0 +1,2 @@
|
||||
import siderTrigger from './sider-trigger.vue'
|
||||
export default siderTrigger
|
||||
@@ -0,0 +1,21 @@
|
||||
.trans{
|
||||
transition: transform .2s ease;
|
||||
}
|
||||
@size: 40px;
|
||||
.sider-trigger-a{
|
||||
padding: 6px;
|
||||
width: @size;
|
||||
height: @size;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
color: #5c6b77;
|
||||
margin-top: 12px;
|
||||
i{
|
||||
.trans;
|
||||
vertical-align: top;
|
||||
}
|
||||
&.collapsed i{
|
||||
transform: rotateZ(90deg);
|
||||
.trans;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<a @click="handleChange" type="text" :class="['sider-trigger-a', collapsed ? 'collapsed' : '']"><Icon :type="icon" :size="size" /></a>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'siderTrigger',
|
||||
props: {
|
||||
collapsed: Boolean,
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'navicon-round'
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 26
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange() {
|
||||
this.$emit('on-change', !this.collapsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import './sider-trigger.less';
|
||||
</style>
|
||||
2
src/components/main/components/language/index.js
Normal file
2
src/components/main/components/language/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Language from './language.vue'
|
||||
export default Language
|
||||
51
src/components/main/components/language/language.vue
Normal file
51
src/components/main/components/language/language.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<Dropdown trigger="click" @on-click="selectLang">
|
||||
<a href="javascript:void(0)">
|
||||
{{ title }}
|
||||
<Icon :size="18" type="md-arrow-dropdown" />
|
||||
</a>
|
||||
<DropdownMenu slot="list">
|
||||
<DropdownItem v-for="(value, key) in localList" :name="key" :key="`lang-${key}`">{{ value }}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Language',
|
||||
props: {
|
||||
lang: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
langList: {
|
||||
'zh-CN': '语言',
|
||||
'zh-TW': '語言',
|
||||
'en-US': 'Lang'
|
||||
},
|
||||
localList: {
|
||||
'zh-CN': '中文简体',
|
||||
'zh-TW': '中文繁体',
|
||||
'en-US': 'English'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
lang(lang) {
|
||||
this.$i18n.locale = lang
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.langList[this.lang]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectLang(name) {
|
||||
this.$emit('on-lang-change', name)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
58
src/components/main/components/side-menu/collapsed-menu.vue
Normal file
58
src/components/main/components/side-menu/collapsed-menu.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<Dropdown ref="dropdown" @on-click="handleClick" :class="hideTitle ? '' : 'collased-menu-dropdown'" :transfer="hideTitle" :placement="placement">
|
||||
<a class="drop-menu-a" type="text" @mouseover="handleMousemove($event, children)" :style="{textAlign: !hideTitle ? 'left' : ''}">
|
||||
<common-icon :size="rootIconSize" :color="textColor" :type="parentItem.meta.icon" /><span class="menu-title" v-if="!hideTitle">{{
|
||||
showTitle(parentItem) }}</span>
|
||||
<Icon style="float: right;" v-if="!hideTitle" type="ios-arrow-forward" :size="16" />
|
||||
</a>
|
||||
<DropdownMenu ref="dropdown" slot="list">
|
||||
<template v-for="child in children">
|
||||
<collapsed-menu v-if="showChildren(child)" :icon-size="iconSize" :parent-item="child" :key="`drop-${child.name}`"></collapsed-menu>
|
||||
<DropdownItem v-else :key="`drop-${child.name}`" :name="child.name">
|
||||
<common-icon :size="iconSize" :type="child.meta.icon" /><span class="menu-title">{{ showTitle(child) }}</span>
|
||||
</DropdownItem>
|
||||
</template>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</template>
|
||||
<script>
|
||||
import mixin from './mixin'
|
||||
import itemMixin from './item-mixin'
|
||||
import { findNodeUpperByClasses } from '@/utils/tools'
|
||||
|
||||
export default {
|
||||
name: 'CollapsedMenu',
|
||||
mixins: [mixin, itemMixin],
|
||||
props: {
|
||||
hideTitle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rootIconSize: {
|
||||
type: Number,
|
||||
default: 16,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
placement: 'right-end',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick(name) {
|
||||
this.$emit('on-click', name)
|
||||
},
|
||||
handleMousemove(event, children) {
|
||||
const { pageY } = event
|
||||
const height = children.length * 38
|
||||
const isOverflow = pageY + height < window.innerHeight
|
||||
this.placement = isOverflow ? 'right-start' : 'right-end'
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
let dropdown = findNodeUpperByClasses(this.$refs.dropdown.$el, ['ivu-select-dropdown', 'ivu-dropdown-transfer'])
|
||||
if (dropdown) dropdown.style.overflow = 'visible'
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
2
src/components/main/components/side-menu/index.js
Normal file
2
src/components/main/components/side-menu/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import SideMenu from './side-menu.vue'
|
||||
export default SideMenu
|
||||
21
src/components/main/components/side-menu/item-mixin.js
Normal file
21
src/components/main/components/side-menu/item-mixin.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
props: {
|
||||
parentItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
theme: String,
|
||||
iconSize: Number
|
||||
},
|
||||
computed: {
|
||||
parentName() {
|
||||
return this.parentItem.name
|
||||
},
|
||||
children() {
|
||||
return this.parentItem.children
|
||||
},
|
||||
textColor() {
|
||||
return this.theme === 'dark' ? '#fff' : '#495060'
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/components/main/components/side-menu/mixin.js
Normal file
18
src/components/main/components/side-menu/mixin.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import CommonIcon from "@component/common-icon";
|
||||
import { showTitle } from "@/utils/tools";
|
||||
export default {
|
||||
components: {
|
||||
CommonIcon
|
||||
},
|
||||
methods: {
|
||||
showTitle(item) {
|
||||
return showTitle(item, this);
|
||||
},
|
||||
showChildren(item) {
|
||||
return item.type === "菜单";
|
||||
},
|
||||
getNameOrHref(item, children0) {
|
||||
return item.href ? `isTurnByHref_${item.href}` : item.name;
|
||||
}
|
||||
}
|
||||
};
|
||||
25
src/components/main/components/side-menu/side-menu-item.vue
Normal file
25
src/components/main/components/side-menu/side-menu-item.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<Submenu :name="`${parentName}`">
|
||||
<template slot="title">
|
||||
<common-icon :type="parentItem.meta.icon || ''" />
|
||||
<span>{{ showTitle(parentItem) }}</span>
|
||||
</template>
|
||||
<template v-for="item in children">
|
||||
<template v-if="item.is_show_menu">
|
||||
<side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item>
|
||||
<menu-item v-else :name="getNameOrHref(item)" :key="`menu-${item.name}`">
|
||||
<common-icon :type="item.meta.icon || ''" /><span>{{ showTitle(item) }}</span>
|
||||
</menu-item>
|
||||
</template>
|
||||
</template>
|
||||
</Submenu>
|
||||
</template>
|
||||
<script>
|
||||
import mixin from './mixin'
|
||||
import itemMixin from './item-mixin'
|
||||
export default {
|
||||
name: 'SideMenuItem',
|
||||
mixins: [mixin, itemMixin],
|
||||
mounted() {},
|
||||
}
|
||||
</script>
|
||||
39
src/components/main/components/side-menu/side-menu.less
Normal file
39
src/components/main/components/side-menu/side-menu.less
Normal file
@@ -0,0 +1,39 @@
|
||||
.side-menu-wrapper {
|
||||
user-select: none;
|
||||
.menu-collapsed {
|
||||
padding-top: 10px;
|
||||
|
||||
.ivu-dropdown {
|
||||
width: 100%;
|
||||
.ivu-dropdown-rel a {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.ivu-tooltip {
|
||||
width: 100%;
|
||||
.ivu-tooltip-rel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ivu-tooltip-popper .ivu-tooltip-content {
|
||||
.ivu-tooltip-arrow {
|
||||
border-right-color: #fff;
|
||||
}
|
||||
.ivu-tooltip-inner {
|
||||
background: #fff;
|
||||
color: #495060;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
a.drop-menu-a {
|
||||
display: inline-block;
|
||||
padding: 6px 15px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #495060;
|
||||
}
|
||||
}
|
||||
.menu-title {
|
||||
padding-left: 6px;
|
||||
}
|
||||
112
src/components/main/components/side-menu/side-menu.vue
Normal file
112
src/components/main/components/side-menu/side-menu.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="side-menu-wrapper">
|
||||
<slot></slot>
|
||||
<Menu ref="menu" v-show="!collapsed" :active-name="activeName" :open-names="openedNames" :accordion="true" :theme="theme" width="auto" @on-select="handleSelect">
|
||||
<template v-for="item in menuList">
|
||||
<template>
|
||||
<side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item>
|
||||
<menu-item v-else :name="getNameOrHref(item)" :key="`menu-${item.name}`">
|
||||
<common-icon :type="item.meta.icon || ''" /><span>{{ showTitle(item) }}</span>
|
||||
</menu-item>
|
||||
</template>
|
||||
</template>
|
||||
</Menu>
|
||||
<div class="menu-collapsed" v-show="collapsed" :list="menuList">
|
||||
<template v-for="item in menuList">
|
||||
<collapsed-menu v-if="item.type==='菜单'" @on-click="handleSelect" hide-title :root-icon-size="rootIconSize" :icon-size="iconSize" :theme="theme" :parent-item="item" :key="`drop-menu-${item.name}`">
|
||||
</collapsed-menu>
|
||||
<Tooltip transfer v-else :content="showTitle(item.children && item.children[0] ? item.children[0] : item)" placement="right" :key="`drop-menu-${item.name}`">
|
||||
<a @click="handleSelect(getNameOrHref(item, true))" class="drop-menu-a" :style="{textAlign: 'center'}">
|
||||
<common-icon :size="rootIconSize" :color="textColor" :type="item.meta.icon" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import SideMenuItem from './side-menu-item.vue'
|
||||
import CollapsedMenu from './collapsed-menu.vue'
|
||||
import { getUnion } from '@/utils/tools'
|
||||
import mixin from './mixin'
|
||||
|
||||
export default {
|
||||
name: 'SideMenu',
|
||||
mixins: [mixin],
|
||||
components: {
|
||||
SideMenuItem,
|
||||
CollapsedMenu,
|
||||
},
|
||||
props: {
|
||||
menuList: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'light',
|
||||
},
|
||||
rootIconSize: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
iconSize: {
|
||||
type: Number,
|
||||
default: 16,
|
||||
},
|
||||
|
||||
activeName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
openNames: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openedNames: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSelect(name) {
|
||||
this.$emit('on-select', name)
|
||||
},
|
||||
getOpenedNamesByActiveName(name) {
|
||||
let names = this.$route.matched.map((item) => item.name).filter((item) => item !== name)
|
||||
return names
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
textColor() {
|
||||
return this.theme === 'dark' ? '#fff' : '#495060'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeName(name) {
|
||||
this.openedNames = this.getOpenedNamesByActiveName(name)
|
||||
},
|
||||
openNames(newNames) {
|
||||
this.openedNames = newNames
|
||||
},
|
||||
openedNames() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.menu.updateOpened()
|
||||
})
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
let openedNames = getUnion(this.openedNames, this.getOpenedNamesByActiveName(this.activeName))
|
||||
this.openedNames = openedNames
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import './side-menu.less';
|
||||
</style>
|
||||
63
src/components/main/components/terminal/index.vue
Normal file
63
src/components/main/components/terminal/index.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="terminal-box">
|
||||
<Icon :class="iconclass" type="ios-radio" @click="show" />
|
||||
<asyncModal ref="asyncModal" title="终端" top="30" :footer-hide="true">
|
||||
<Terminal></Terminal>
|
||||
</asyncModal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import Terminal from './terminal.vue'
|
||||
export default {
|
||||
components: {
|
||||
Terminal
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isServerRun: 'isServerRun'
|
||||
}),
|
||||
iconclass() {
|
||||
let curClass = 'terminal-icon ml10 '
|
||||
if (this.isServerRun) {
|
||||
curClass += ' run'
|
||||
}
|
||||
return curClass
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
show() {
|
||||
this.$refs['asyncModal'].show()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.terminal-box {
|
||||
margin-top: 3px;
|
||||
}
|
||||
.terminal-icon {
|
||||
color: #c5c8ce;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
|
||||
&.run {
|
||||
animation: colorChange 0.4s infinite; /* 调用颜色变化动画 */
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes colorChange {
|
||||
0% {
|
||||
color: #19be6b; /* 初始颜色 */
|
||||
}
|
||||
|
||||
100% {
|
||||
color: red; /* 返回到初始颜色 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
62
src/components/main/components/terminal/terminal.vue
Normal file
62
src/components/main/components/terminal/terminal.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<div class="log-box">
|
||||
<div class="pa5 flex" style="align-items: flex-end; justify-content: flex-end;">
|
||||
<Button class="ml10" size="small" @click="reloadLog">重新加载日志</Button>
|
||||
<Button type="error" size="small" @click="clearLog">清空日志</Button>
|
||||
</div>
|
||||
<div class="view-log-info" id="view-log-info" style="height:70vh;width: 100%;">{{ infoMsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import uiTool from '@/utils/uiTool'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
mounted() {},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
infoMsg: 'infoMsg'
|
||||
})
|
||||
},
|
||||
|
||||
watch: {
|
||||
infoMsg: {
|
||||
handler() {
|
||||
this.scrollEnd()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clearLog() {
|
||||
this.$store.commit('clearInfoMsg')
|
||||
},
|
||||
reloadLog() {
|
||||
this.$store.dispatch('setInteverLog')
|
||||
},
|
||||
scrollEnd() {
|
||||
setTimeout(() => {
|
||||
let outerDiv = document.querySelector('#view-log-info')
|
||||
if (outerDiv) {
|
||||
outerDiv.scrollTop = outerDiv.scrollHeight + 5
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.log-box {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
2
src/components/main/components/user/index.js
Normal file
2
src/components/main/components/user/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import User from './user.vue'
|
||||
export default User
|
||||
12
src/components/main/components/user/user.less
Normal file
12
src/components/main/components/user/user.less
Normal file
@@ -0,0 +1,12 @@
|
||||
.user{
|
||||
&-avator-dropdown{
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
// height: 64px;
|
||||
vertical-align: middle;
|
||||
// line-height: 64px;
|
||||
.ivu-badge-dot{
|
||||
top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/components/main/components/user/user.vue
Normal file
68
src/components/main/components/user/user.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="user-avator-dropdown">
|
||||
<label class="loginName-box">{{userName}}</label>
|
||||
<Dropdown>
|
||||
|
||||
<Avatar :src="typeof userAvator === 'string' ? userAvator : userIcon" />
|
||||
|
||||
<Icon :size="18" type="md-arrow-dropdown"></Icon>
|
||||
<DropdownMenu slot="list">
|
||||
<a @click="logout">
|
||||
<DropdownItem name="logout">
|
||||
<Icon type="md-exit" />
|
||||
退出登录
|
||||
</DropdownItem>
|
||||
</a>
|
||||
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import './user.less'
|
||||
import { mapMutations, mapActions } from 'vuex'
|
||||
|
||||
const userIcon = require("@/assets/images/administrato.png").default
|
||||
|
||||
export default {
|
||||
name: 'User',
|
||||
props: {
|
||||
userName: String,
|
||||
userAvator: {
|
||||
type: [String, Object],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
userIcon: userIcon
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['handleLogOut']),
|
||||
logout() {
|
||||
this.handleLogOut(this)
|
||||
},
|
||||
|
||||
message() {
|
||||
this.$router.push({
|
||||
name: 'message_page',
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.user-avator-dropdown {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.loginName-box {
|
||||
font-weight: bold;
|
||||
color: #4b81d3;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
2
src/components/main/index.js
Normal file
2
src/components/main/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Main from './main.vue'
|
||||
export default Main
|
||||
109
src/components/main/main.less
Normal file
109
src/components/main/main.less
Normal file
@@ -0,0 +1,109 @@
|
||||
.main {
|
||||
.logo-con {
|
||||
height: 64px;
|
||||
padding: 10px;
|
||||
padding-left: 0px;
|
||||
color: white;
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.header-con {
|
||||
padding: 0px 10px;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.main-content-con {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
color: darkgray;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.main-layout-con {
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tag-nav-wrapper {
|
||||
padding: 0;
|
||||
height: 40px;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 0px 0.05rem;
|
||||
height: ~"calc(100% - 40px)";
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.left-sider {
|
||||
.ivu-layout-sider-children {
|
||||
overflow-y: scroll;
|
||||
margin-right: -10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ivu-menu-item > i {
|
||||
margin-right: 12px !important;
|
||||
}
|
||||
|
||||
.ivu-menu-submenu > .ivu-menu > .ivu-menu-item > i {
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
|
||||
.collased-menu-dropdown {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
line-height: normal;
|
||||
padding: 7px 0 6px 16px;
|
||||
clear: both;
|
||||
font-size: 12px !important;
|
||||
white-space: nowrap;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: rgba(100, 100, 100, 0.1);
|
||||
}
|
||||
|
||||
& * {
|
||||
color: #515a6e;
|
||||
}
|
||||
|
||||
.ivu-menu-item > i {
|
||||
margin-right: 12px !important;
|
||||
}
|
||||
|
||||
.ivu-menu-submenu > .ivu-menu > .ivu-menu-item > i {
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ivu-select-dropdown.ivu-dropdown-transfer {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.police-box {
|
||||
margin-left: 10px;
|
||||
color: darkgoldenrod;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
span {
|
||||
margin: 0px 5px;
|
||||
}
|
||||
}
|
||||
145
src/components/main/main.vue
Normal file
145
src/components/main/main.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
|
||||
<Layout style="height: 100%" class="main">
|
||||
|
||||
<Sider hide-trigger collapsible :width="256" :collapsed-width="64" v-model="collapsed" class="left-sider" :style="{overflow: 'hidden'}">
|
||||
<div class="sidebar-brand" @click="goHome" v-if="!collapsed">
|
||||
<img style="width:50px" :src="sysFormModel.logoUrl" />
|
||||
<span class="ml10">{{sysFormModel.title}}</span>
|
||||
</div>
|
||||
|
||||
<side-menu class="mt30" theme="dark" ref="sideMenu" :active-name="$route.name" :collapsed="collapsed" @on-select="turnToPage" :menu-list="menuList"> </side-menu>
|
||||
</Sider>
|
||||
|
||||
<Content class="main-content-con">
|
||||
<Layout class="main-layout-con">
|
||||
|
||||
<Header>
|
||||
<headerBar class="header-con" @on-coll-change="collpasedChange" :collapsed="collapsed">
|
||||
<Terminal></Terminal>
|
||||
<user :userName="userName" :user-avator="userAvator || ''" />
|
||||
</headerBar>
|
||||
</Header>
|
||||
<Layout>
|
||||
<Content class="content-wrapper">
|
||||
|
||||
<router-view />
|
||||
<loadFlower> </loadFlower>
|
||||
<ABackTop :height="100" :bottom="80" :right="50" container=".content-wrapper"></ABackTop>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Content>
|
||||
</Layout>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
const { title } = require('../../config')
|
||||
import SideMenu from './components/side-menu'
|
||||
import HeaderBar from './components/header-bar'
|
||||
|
||||
import User from './components/user'
|
||||
import ABackTop from './components/a-back-top'
|
||||
import Fullscreen from './components/fullscreen'
|
||||
import Language from './components/language'
|
||||
import { mapGetters } from 'vuex'
|
||||
import headerBar from './components/header-bar'
|
||||
import loadFlower from '../load-flower/index'
|
||||
import Terminal from './components/terminal'
|
||||
|
||||
import './main.less'
|
||||
|
||||
export default {
|
||||
name: 'Main',
|
||||
components: {
|
||||
SideMenu,
|
||||
HeaderBar,
|
||||
Language,
|
||||
|
||||
Fullscreen,
|
||||
User,
|
||||
Terminal,
|
||||
ABackTop,
|
||||
loadFlower,
|
||||
headerBar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title,
|
||||
collapsed: false,
|
||||
isFullscreen: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
sysFormModel: 'sysFormModel',
|
||||
menuList: 'menuList',
|
||||
tagRouter: 'tagRouter',
|
||||
userName: 'userName',
|
||||
userAvator: 'avatorImgPath'
|
||||
}),
|
||||
cacheList() {
|
||||
return ['ParentView']
|
||||
}
|
||||
},
|
||||
watch: {},
|
||||
|
||||
created() {
|
||||
this.init()
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
await this.$store.dispatch('getSysTitle')
|
||||
},
|
||||
collpasedChange(collapsed) {
|
||||
this.collapsed = collapsed
|
||||
},
|
||||
goHome() {
|
||||
this.$router.push({ path: '/' })
|
||||
},
|
||||
turnToPage(route) {
|
||||
let { name, params, query } = {}
|
||||
if (typeof route === 'string') name = route
|
||||
else {
|
||||
name = route.name
|
||||
params = route.params
|
||||
query = route.query
|
||||
}
|
||||
if (name.indexOf('isTurnByHref_') > -1) {
|
||||
window.open(name.split('_')[1])
|
||||
return
|
||||
}
|
||||
this.$router.push({
|
||||
name,
|
||||
params,
|
||||
query
|
||||
})
|
||||
},
|
||||
handleCollapsedChange(state) {
|
||||
this.collapsed = state
|
||||
},
|
||||
|
||||
handleClick(item) {
|
||||
this.turnToPage(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.shop-select {
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
height: 60px;
|
||||
line-height: 25px;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
23
src/components/main/pageHead.vue
Normal file
23
src/components/main/pageHead.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="table-head-tool ss">
|
||||
<Button shape="circle" v-if="$route.meta.type==='功能'" icon="ios-undo" @click="goBack">返回</Button>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
created() {},
|
||||
|
||||
methods: {
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
2
src/components/markdown/index.js
Normal file
2
src/components/markdown/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import MarkdownEditor from './markdown.vue'
|
||||
export default MarkdownEditor
|
||||
78
src/components/markdown/markdown.vue
Normal file
78
src/components/markdown/markdown.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="markdown-wrapper">
|
||||
<textarea ref="editor"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Simplemde from 'simplemde'
|
||||
import 'simplemde/dist/simplemde.min.css'
|
||||
export default {
|
||||
name: 'MarkdownEditor',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
localCache: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editor: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addEvents() {
|
||||
this.editor.codemirror.on('change', () => {
|
||||
let value = this.editor.value()
|
||||
if (this.localCache) localStorage.markdownContent = value
|
||||
this.$emit('input', value)
|
||||
this.$emit('on-change', value)
|
||||
})
|
||||
this.editor.codemirror.on('focus', () => {
|
||||
this.$emit('on-focus', this.editor.value())
|
||||
})
|
||||
this.editor.codemirror.on('blur', () => {
|
||||
this.$emit('on-blur', this.editor.value())
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.editor = new Simplemde(
|
||||
Object.assign(this.options, {
|
||||
element: this.$refs.editor
|
||||
})
|
||||
)
|
||||
/**
|
||||
* 事件列表为Codemirror编辑器的事件,更多事件类型,请参考:
|
||||
* https://codemirror.net/doc/manual.html#events
|
||||
*/
|
||||
this.addEvents()
|
||||
let content = localStorage.markdownContent
|
||||
if (content) this.editor.value(content)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.markdown-wrapper {
|
||||
.editor-toolbar.fullscreen {
|
||||
z-index: 9999;
|
||||
}
|
||||
.CodeMirror-fullscreen {
|
||||
z-index: 9999;
|
||||
}
|
||||
.CodeMirror-fullscreen ~ .editor-preview-side {
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/components/md-icons/icons.vue
Normal file
34
src/components/md-icons/icons.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<i :class="`iconfont icon-${type}`" :style="styles"></i>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Icons',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#5c6b77'
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 16
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
styles() {
|
||||
return {
|
||||
fontSize: `${this.size}px`,
|
||||
color: this.color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
2
src/components/md-icons/index.js
Normal file
2
src/components/md-icons/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Icons from './icons.vue'
|
||||
export default Icons
|
||||
2
src/components/parent-view/index.js
Normal file
2
src/components/parent-view/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import ParentView from './parent-view.vue'
|
||||
export default ParentView
|
||||
18
src/components/parent-view/parent-view.vue
Normal file
18
src/components/parent-view/parent-view.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<!-- <keep-alive :include="cacheList" :exclude="notCacheName"> -->
|
||||
<router-view ref="child" />
|
||||
<!-- </keep-alive> -->
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ParentView',
|
||||
computed: {
|
||||
notCacheName() {
|
||||
return [this.$route.meta && this.$route.meta.notCache ? this.$route.name : '']
|
||||
},
|
||||
cacheList() {
|
||||
return ['ParentView']
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
2
src/components/paste-editor/index.js
Normal file
2
src/components/paste-editor/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import PasteEditor from './paste-editor.vue'
|
||||
export default PasteEditor
|
||||
26
src/components/paste-editor/paste-editor.less
Normal file
26
src/components/paste-editor/paste-editor.less
Normal file
@@ -0,0 +1,26 @@
|
||||
.paste-editor-wrapper{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px dashed gainsboro;
|
||||
textarea.textarea-el{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.CodeMirror{
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
.CodeMirror-code div .CodeMirror-line > span > span.cm-tab{
|
||||
&::after{
|
||||
content: '→';
|
||||
color: #BFBFBF;
|
||||
}
|
||||
}
|
||||
}
|
||||
.first-row{
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
.incorrect-row{
|
||||
background: #F5CBD1;
|
||||
}
|
||||
}
|
||||
120
src/components/paste-editor/paste-editor.vue
Normal file
120
src/components/paste-editor/paste-editor.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="paste-editor-wrapper">
|
||||
<textarea ref="codemirror" class="textarea-el"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import { forEach } from '@/utils/tools'
|
||||
import createPlaceholder from './plugins/placeholder'
|
||||
export default {
|
||||
name: 'PasteEditor',
|
||||
props: {
|
||||
value: Array,
|
||||
pasteData: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default:
|
||||
'从网页或其他应用软件复制表格数据,粘贴到这里 。默认第一行是表头,使用回车键添加新行,使用Tab键区分列。'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pasteDataArr: [],
|
||||
rowArrLength: 0,
|
||||
editor: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
pasteData(val) {
|
||||
if (val === '') {
|
||||
this.editor.setValue('')
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rowNum() {
|
||||
return this.pasteDataArr.length
|
||||
},
|
||||
colNum() {
|
||||
return this.pasteDataArr[0] ? this.pasteDataArr[0].length : 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleKeyup(e) {
|
||||
this.handleAreaData()
|
||||
},
|
||||
/**
|
||||
* @description 处理粘贴操作
|
||||
*/
|
||||
handleContentChanged(content) {
|
||||
let pasteData = content.trim()
|
||||
this.$emit('on-content-change', pasteData)
|
||||
let rows = pasteData.split(/[\n\u0085\u2028\u2029]|\r\n?/g).map(row => {
|
||||
return row.split('\t')
|
||||
})
|
||||
if (content === '') rows = []
|
||||
this.pasteDataArr = rows
|
||||
this.clearLineClass()
|
||||
this.checkColNumInEveryRow()
|
||||
this.$emit('input', this.pasteDataArr)
|
||||
},
|
||||
/**
|
||||
* @description 检查除第一行的每一行列数是否与第一行相同
|
||||
*/
|
||||
checkColNumInEveryRow() {
|
||||
let i = 0
|
||||
const len = this.rowNum
|
||||
if (len === 0) return
|
||||
while (++i < len) {
|
||||
let item = this.pasteDataArr[i]
|
||||
if (
|
||||
item.length !== this.colNum &&
|
||||
(!(i === len - 1 && item.length === 1 && item[0] === '') ||
|
||||
i !== len - 1)
|
||||
) {
|
||||
this.markIncorrectRow(i)
|
||||
this.$emit('on-error', i)
|
||||
return false
|
||||
}
|
||||
}
|
||||
this.$emit('on-success', this.pasteDataArr)
|
||||
return true
|
||||
},
|
||||
/**
|
||||
* @description 标记不符合格式的一行
|
||||
*/
|
||||
markIncorrectRow(index) {
|
||||
this.editor.addLineClass(index, 'text', 'incorrect-row')
|
||||
},
|
||||
/**
|
||||
* @description 标记不符合格式的一行
|
||||
*/
|
||||
clearLineClass() {
|
||||
forEach(this.pasteDataArr, (item, index) => {
|
||||
this.editor.removeLineClass(index, 'text', 'incorrect-row')
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
createPlaceholder(CodeMirror)
|
||||
this.editor = CodeMirror.fromTextArea(this.$refs.codemirror, {
|
||||
lineNumbers: true,
|
||||
tabSize: 1,
|
||||
lineWrapping: true,
|
||||
placeholder: this.placeholder
|
||||
})
|
||||
this.editor.on('change', editor => {
|
||||
this.handleContentChanged(editor.getValue())
|
||||
})
|
||||
this.editor.addLineClass(0, 'text', 'first-row')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import './paste-editor.less';
|
||||
</style>
|
||||
61
src/components/paste-editor/plugins/placeholder.js
Normal file
61
src/components/paste-editor/plugins/placeholder.js
Normal file
@@ -0,0 +1,61 @@
|
||||
export default codemirror => {
|
||||
;(function(mod) {
|
||||
mod(codemirror)
|
||||
})(function(CodeMirror) {
|
||||
CodeMirror.defineOption('placeholder', '', function(cm, val, old) {
|
||||
var prev = old && old !== CodeMirror.Init
|
||||
if (val && !prev) {
|
||||
cm.on('blur', onBlur)
|
||||
cm.on('change', onChange)
|
||||
cm.on('swapDoc', onChange)
|
||||
onChange(cm)
|
||||
} else if (!val && prev) {
|
||||
cm.off('blur', onBlur)
|
||||
cm.off('change', onChange)
|
||||
cm.off('swapDoc', onChange)
|
||||
clearPlaceholder(cm)
|
||||
var wrapper = cm.getWrapperElement()
|
||||
wrapper.className = wrapper.className.replace(' CodeMirror-empty', '')
|
||||
}
|
||||
|
||||
if (val && !cm.hasFocus()) onBlur(cm)
|
||||
})
|
||||
|
||||
function clearPlaceholder(cm) {
|
||||
if (cm.state.placeholder) {
|
||||
cm.state.placeholder.parentNode.removeChild(cm.state.placeholder)
|
||||
cm.state.placeholder = null
|
||||
}
|
||||
}
|
||||
function setPlaceholder(cm) {
|
||||
clearPlaceholder(cm)
|
||||
var elt = (cm.state.placeholder = document.createElement('pre'))
|
||||
elt.style.cssText = 'height: 0; overflow: visible; color: #80848f;'
|
||||
elt.style.direction = cm.getOption('direction')
|
||||
elt.className = 'CodeMirror-placeholder'
|
||||
var placeHolder = cm.getOption('placeholder')
|
||||
if (typeof placeHolder === 'string')
|
||||
placeHolder = document.createTextNode(placeHolder)
|
||||
elt.appendChild(placeHolder)
|
||||
cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild)
|
||||
}
|
||||
|
||||
function onBlur(cm) {
|
||||
if (isEmpty(cm)) setPlaceholder(cm)
|
||||
}
|
||||
function onChange(cm) {
|
||||
let wrapper = cm.getWrapperElement()
|
||||
let empty = isEmpty(cm)
|
||||
wrapper.className =
|
||||
wrapper.className.replace(' CodeMirror-empty', '') +
|
||||
(empty ? ' CodeMirror-empty' : '')
|
||||
|
||||
if (empty) setPlaceholder(cm)
|
||||
else clearPlaceholder(cm)
|
||||
}
|
||||
|
||||
function isEmpty(cm) {
|
||||
return cm.lineCount() === 1 && cm.getLine(0) === ''
|
||||
}
|
||||
})
|
||||
}
|
||||
2
src/components/split-pane/index.js
Normal file
2
src/components/split-pane/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Split from './split.vue'
|
||||
export default Split
|
||||
114
src/components/split-pane/index.less
Normal file
114
src/components/split-pane/index.less
Normal file
@@ -0,0 +1,114 @@
|
||||
@split-prefix-cls: ~"ivu-split";
|
||||
@box-shadow: 0 0 4px 0 rgba(28, 36, 56, 0.4);
|
||||
@trigger-bar-background: rgba(23, 35, 61, 0.25);
|
||||
@trigger-background: #F8F8F9;
|
||||
@trigger-width: 6px;
|
||||
@trigger-bar-width: 4px;
|
||||
@trigger-bar-offset: (@trigger-width - @trigger-bar-width) / 2;
|
||||
@trigger-bar-interval: 3px;
|
||||
@trigger-bar-weight: 1px;
|
||||
@trigger-bar-con-height: (@trigger-bar-weight + @trigger-bar-interval) * 8;
|
||||
|
||||
.@{split-prefix-cls}{
|
||||
&-wrapper{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
&-pane{
|
||||
position: absolute;
|
||||
&.left-pane, &.right-pane{
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
&.left-pane{
|
||||
left: 0px;
|
||||
}
|
||||
&.right-pane{
|
||||
right: 0px;
|
||||
}
|
||||
&.top-pane, &.bottom-pane{
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
&.top-pane{
|
||||
top: 0px;
|
||||
}
|
||||
&.bottom-pane{
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
&-trigger{
|
||||
&-con{
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
&-bar-con{
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
&.vertical{
|
||||
left: @trigger-bar-offset;
|
||||
top: 50%;
|
||||
height: @trigger-bar-con-height;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
&.horizontal{
|
||||
left: 50%;
|
||||
top: @trigger-bar-offset;
|
||||
width: @trigger-bar-con-height;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
&-vertical{
|
||||
width: @trigger-width;
|
||||
height: 100%;
|
||||
background: @trigger-background;
|
||||
box-shadow: @box-shadow;
|
||||
cursor: col-resize;
|
||||
.@{split-prefix-cls}-trigger-bar{
|
||||
width: @trigger-bar-width;
|
||||
height: 1px;
|
||||
background: @trigger-bar-background;
|
||||
float: left;
|
||||
margin-top: @trigger-bar-interval;
|
||||
}
|
||||
}
|
||||
&-horizontal{
|
||||
height: @trigger-width;
|
||||
width: 100%;
|
||||
background: @trigger-background;
|
||||
box-shadow: @box-shadow;
|
||||
cursor: row-resize;
|
||||
.@{split-prefix-cls}-trigger-bar{
|
||||
height: @trigger-bar-width;
|
||||
width: 1px;
|
||||
background: @trigger-bar-background;
|
||||
float: left;
|
||||
margin-right: @trigger-bar-interval;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-horizontal{
|
||||
.@{split-prefix-cls}-trigger-con{
|
||||
top: 50%;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
&-vertical{
|
||||
.@{split-prefix-cls}-trigger-con{
|
||||
left: 50%;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.no-select{
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
188
src/components/split-pane/split.vue
Normal file
188
src/components/split-pane/split.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div ref="outerWrapper" :class="wrapperClasses">
|
||||
<div v-if="isHorizontal" :class="`${prefix}-horizontal`">
|
||||
<div :style="{right: `${anotherOffset}%`}" :class="[`${prefix}-pane`, 'left-pane']"><slot name="left"/></div>
|
||||
<div :class="`${prefix}-trigger-con`" :style="{left: `${offset}%`}" @mousedown="handleMousedown">
|
||||
<slot name="trigger">
|
||||
<trigger mode="vertical"/>
|
||||
</slot>
|
||||
</div>
|
||||
<div :style="{left: `${offset}%`}" :class="[`${prefix}-pane`, 'right-pane']"><slot name="right"/></div>
|
||||
</div>
|
||||
<div v-else :class="`${prefix}-vertical`">
|
||||
<div :style="{bottom: `${anotherOffset}%`}" :class="[`${prefix}-pane`, 'top-pane']"><slot name="top"/></div>
|
||||
<div :class="`${prefix}-trigger-con`" :style="{top: `${offset}%`}" @mousedown="handleMousedown">
|
||||
<slot name="trigger">
|
||||
<trigger mode="horizontal"/>
|
||||
</slot>
|
||||
</div>
|
||||
<div :style="{top: `${offset}%`}" :class="[`${prefix}-pane`, 'bottom-pane']"><slot name="bottom"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { oneOf, on, off } from '@/utils/tools'
|
||||
import Trigger from './trigger.vue'
|
||||
export default {
|
||||
name: 'SplitPane',
|
||||
components: {
|
||||
Trigger
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: [Number, String],
|
||||
default: 0.5
|
||||
},
|
||||
mode: {
|
||||
validator(value) {
|
||||
return oneOf(value, ['horizontal', 'vertical'])
|
||||
},
|
||||
default: 'horizontal'
|
||||
},
|
||||
min: {
|
||||
type: [Number, String],
|
||||
default: '40px'
|
||||
},
|
||||
max: {
|
||||
type: [Number, String],
|
||||
default: '40px'
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Events
|
||||
* @on-move-start
|
||||
* @on-moving 返回值:事件对象,但是在事件对象中加入了两个参数:atMin(当前是否在最小值处), atMax(当前是否在最大值处)
|
||||
* @on-move-end
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
prefix: 'ivu-split',
|
||||
offset: 0,
|
||||
oldOffset: 0,
|
||||
isMoving: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
wrapperClasses() {
|
||||
return [`${this.prefix}-wrapper`, this.isMoving ? 'no-select' : '']
|
||||
},
|
||||
isHorizontal() {
|
||||
return this.mode === 'horizontal'
|
||||
},
|
||||
anotherOffset() {
|
||||
return 100 - this.offset
|
||||
},
|
||||
valueIsPx() {
|
||||
return typeof this.value === 'string'
|
||||
},
|
||||
offsetSize() {
|
||||
return this.isHorizontal ? 'offsetWidth' : 'offsetHeight'
|
||||
},
|
||||
computedMin() {
|
||||
return this.getComputedThresholdValue('min')
|
||||
},
|
||||
computedMax() {
|
||||
return this.getComputedThresholdValue('max')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
px2percent(numerator, denominator) {
|
||||
return parseFloat(numerator) / parseFloat(denominator)
|
||||
},
|
||||
getComputedThresholdValue(type) {
|
||||
let size = this.$refs.outerWrapper[this.offsetSize]
|
||||
if (this.valueIsPx)
|
||||
return typeof this[type] === 'string' ? this[type] : size * this[type]
|
||||
else
|
||||
return typeof this[type] === 'string'
|
||||
? this.px2percent(this[type], size)
|
||||
: this[type]
|
||||
},
|
||||
getMin(value1, value2) {
|
||||
if (this.valueIsPx)
|
||||
return `${Math.min(parseFloat(value1), parseFloat(value2))}px`
|
||||
else return Math.min(value1, value2)
|
||||
},
|
||||
getMax(value1, value2) {
|
||||
if (this.valueIsPx)
|
||||
return `${Math.max(parseFloat(value1), parseFloat(value2))}px`
|
||||
else return Math.max(value1, value2)
|
||||
},
|
||||
getAnotherOffset(value) {
|
||||
let res = 0
|
||||
if (this.valueIsPx)
|
||||
res = `${this.$refs.outerWrapper[this.offsetSize] -
|
||||
parseFloat(value)}px`
|
||||
else res = 1 - value
|
||||
return res
|
||||
},
|
||||
handleMove(e) {
|
||||
let pageOffset = this.isHorizontal ? e.pageX : e.pageY
|
||||
let offset = pageOffset - this.initOffset
|
||||
let outerWidth = this.$refs.outerWrapper[this.offsetSize]
|
||||
let value = this.valueIsPx
|
||||
? `${parseFloat(this.oldOffset) + offset}px`
|
||||
: this.px2percent(outerWidth * this.oldOffset + offset, outerWidth)
|
||||
let anotherValue = this.getAnotherOffset(value)
|
||||
if (parseFloat(value) <= parseFloat(this.computedMin))
|
||||
value = this.getMax(value, this.computedMin)
|
||||
if (parseFloat(anotherValue) <= parseFloat(this.computedMax))
|
||||
value = this.getAnotherOffset(
|
||||
this.getMax(anotherValue, this.computedMax)
|
||||
)
|
||||
e.atMin = this.value === this.computedMin
|
||||
e.atMax = this.valueIsPx
|
||||
? this.getAnotherOffset(this.value) === this.computedMax
|
||||
: this.getAnotherOffset(this.value).toFixed(5) ===
|
||||
this.computedMax.toFixed(5)
|
||||
this.$emit('input', value)
|
||||
this.$emit('on-moving', e)
|
||||
},
|
||||
handleUp() {
|
||||
this.isMoving = false
|
||||
off(document, 'mousemove', this.handleMove)
|
||||
off(document, 'mouseup', this.handleUp)
|
||||
this.$emit('on-move-end')
|
||||
},
|
||||
handleMousedown(e) {
|
||||
this.initOffset = this.isHorizontal ? e.pageX : e.pageY
|
||||
this.oldOffset = this.value
|
||||
this.isMoving = true
|
||||
on(document, 'mousemove', this.handleMove)
|
||||
on(document, 'mouseup', this.handleUp)
|
||||
this.$emit('on-move-start')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.offset =
|
||||
((this.valueIsPx
|
||||
? this.px2percent(
|
||||
this.value,
|
||||
this.$refs.outerWrapper[this.offsetSize]
|
||||
)
|
||||
: this.value) *
|
||||
10000) /
|
||||
100
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.offset =
|
||||
((this.valueIsPx
|
||||
? this.px2percent(
|
||||
this.value,
|
||||
this.$refs.outerWrapper[this.offsetSize]
|
||||
)
|
||||
: this.value) *
|
||||
10000) /
|
||||
100
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import './index.less';
|
||||
</style>
|
||||
45
src/components/split-pane/trigger.vue
Normal file
45
src/components/split-pane/trigger.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<div :class="barConClasses">
|
||||
<i :class="`${prefix}-bar`" v-once v-for="i in 8" :key="`trigger-${i}`"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Trigger',
|
||||
props: {
|
||||
mode: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
prefix: 'ivu-split-trigger',
|
||||
initOffset: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isVertical() {
|
||||
return this.mode === 'vertical'
|
||||
},
|
||||
classes() {
|
||||
return [
|
||||
this.prefix,
|
||||
this.isVertical
|
||||
? `${this.prefix}-vertical`
|
||||
: `${this.prefix}-horizontal`
|
||||
]
|
||||
},
|
||||
barConClasses() {
|
||||
return [
|
||||
`${this.prefix}-bar-con`,
|
||||
this.isVertical ? 'vertical' : 'horizontal'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import './index.less';
|
||||
</style>
|
||||
279
src/components/tables/editModal.vue
Normal file
279
src/components/tables/editModal.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<Modal v-model="isOPen" :title="title||'编辑'" :width="curWidth" @on-cancel="cancel" :scrollable='true' @on-visible-change='visibleChange' :mask-closable='false'>
|
||||
|
||||
<Form v-if="isRefresh" class="modal-form" ref="From" :model="row" :label-width="120" inline>
|
||||
<div class="line-row">
|
||||
<FormItem>
|
||||
<slot name="top">
|
||||
</slot>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<template v-for="col in curColumns">
|
||||
<div :class="col.inLine?'inline-row':'line-row'" :key="col.name" v-if="col.key&&!col.display">
|
||||
<FormItem :label="col.title" :prop="col.key" :rules="curRules[col.key]" :style=" col.rowStyle">
|
||||
<Row>
|
||||
|
||||
<Select class="text-left" filterable v-if="col.com==='Select'" v-model='row[col.key]' v-bind="col" :disabled="getDisabled(col)">
|
||||
<Option :value="item.key" :key="item.key" v-for="item in col.source">{{item.value }}</Option>
|
||||
</Select>
|
||||
|
||||
<RadioGroup v-else-if="col.com==='Radio'" v-model="row[col.key]" v-bind="col" :disabled="getDisabled(col)">
|
||||
<Radio :label="item.key" :key="item.key" v-for="item in col.source">
|
||||
{{item.value}}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
|
||||
<AutoComplete v-else-if="col.com==='SelectIcon'" icon="ios-search" v-model="row[col.key]" v-bind="col" :disabled="getDisabled(col)">
|
||||
<Option v-for="(icon,index) in icons" :value="icon" :key="index">
|
||||
<Icon size="20" :type="icon" />
|
||||
{{ icon }}
|
||||
</Option>
|
||||
</AutoComplete>
|
||||
|
||||
<UploadSingle v-else-if="col.com==='upload_Img'" v-model="row[col.key]" v-bind="col" :disabled="getDisabled(col)">
|
||||
|
||||
</UploadSingle>
|
||||
|
||||
<TextArea v-else-if="col.com==='TextArea'" v-model="row[col.key]" v-bind="col" :disabled="getDisabled(col)">
|
||||
|
||||
</TextArea>
|
||||
|
||||
<templateRender v-else-if="col.editRender" :value="row[col.key]" :render='col.editRender'></templateRender>
|
||||
|
||||
<Input v-else-if="!col.com" v-model="row[col.key]" v-bind="col" style="width:100%;" :disabled="getDisabled(col)" />
|
||||
|
||||
<component v-else v-bind:is="col.com" v-model="row[col.key]" v-bind="col" :disabled="getDisabled(col)"></component>
|
||||
|
||||
</Row>
|
||||
</FormItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<slot name='bottom'></slot>
|
||||
|
||||
</Form>
|
||||
|
||||
<div slot="footer">
|
||||
<div v-if="isFooter">
|
||||
<Button type="primary" @click="save">确定</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import templateRender from './templateRender'
|
||||
const icons = require('@/config/icons')
|
||||
export default {
|
||||
props: ['columns', 'width'],
|
||||
components: {
|
||||
templateRender
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: '新增',
|
||||
isFooter: true,
|
||||
isRefresh: true,
|
||||
isEdit: false,
|
||||
isOPen: false,
|
||||
icons: [],
|
||||
row: {},
|
||||
callback: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
curWidth() {
|
||||
return this.width || '80'
|
||||
},
|
||||
curColumns() {
|
||||
if (this.columns) {
|
||||
return this.columns.filter((p) => p.type !== 'template' && p.key !== 'id')
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
||||
curRules() {
|
||||
let ruleObj = {}
|
||||
let ruleCols = this.columns.filter((p) => p.required)
|
||||
if (ruleCols) {
|
||||
ruleCols.forEach((col) => {
|
||||
let com = col.com || 'Input'
|
||||
let trigger = com === 'Input' ? 'blur' : 'change'
|
||||
|
||||
if (com === 'Input') {
|
||||
ruleObj[col.key] = [{ required: true, message: `${col.title}不能为空`, trigger }]
|
||||
} else if (com === 'Select') {
|
||||
ruleObj[col.key] = [{ required: true, type: 'number', message: `${col.title}不能为空`, trigger }]
|
||||
}
|
||||
})
|
||||
}
|
||||
return this.$attrs.rules || ruleObj
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 判断字段是否禁用
|
||||
getDisabled(col) {
|
||||
// 如果是编辑模式且字段设置了disabled,则禁用
|
||||
if (this.isEdit && col.disabled === true) {
|
||||
return true
|
||||
}
|
||||
// 如果是新增模式且字段设置了disabledOnAdd,则禁用
|
||||
if (!this.isEdit && col.disabledOnAdd === true) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.isRefresh = false
|
||||
this.$nextTick(() => {
|
||||
this.isRefresh = true
|
||||
})
|
||||
},
|
||||
setFooter(bol) {
|
||||
this.isFooter = bol
|
||||
},
|
||||
visibleChange(res) {
|
||||
this.$emit('on-visible-change', res)
|
||||
},
|
||||
|
||||
// 兼容方法:统一的 showModal 方法
|
||||
async showModal(row) {
|
||||
if (row && row.id) {
|
||||
// 编辑模式
|
||||
await this.editShow(row, async (formData) => {
|
||||
await this.handleSave(formData, true)
|
||||
})
|
||||
} else {
|
||||
// 新增模式
|
||||
await this.addShow(row || {}, async (formData) => {
|
||||
await this.handleSave(formData, false)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 处理保存逻辑
|
||||
async handleSave(formData, isEdit) {
|
||||
this.$emit('on-save', { data: formData, isEdit })
|
||||
},
|
||||
|
||||
editShow(row, callback) {
|
||||
this.title = '编辑'
|
||||
this.isEdit = true
|
||||
this.init(row)
|
||||
this.show()
|
||||
this.callback = callback
|
||||
},
|
||||
addShow(row, callback) {
|
||||
this.title = '新增'
|
||||
this.isEdit = false
|
||||
this.init(row)
|
||||
|
||||
this.show()
|
||||
this.callback = callback
|
||||
},
|
||||
show() {
|
||||
this.refresh()
|
||||
this.isOPen = true
|
||||
},
|
||||
hide() {
|
||||
this.isOPen = false
|
||||
},
|
||||
async init(row, isgl) {
|
||||
this.icons = icons.map((item) => item.trim())
|
||||
row = row || {}
|
||||
let rules = this.curRules
|
||||
if (isgl) {
|
||||
this.row = JSON.parse(JSON.stringify(row))
|
||||
} else {
|
||||
this.row = row
|
||||
}
|
||||
|
||||
let keys = Object.keys(rules)
|
||||
if (keys && keys.length > 0) {
|
||||
this.$refs['From'].resetFields()
|
||||
}
|
||||
|
||||
this.curColumns
|
||||
.filter((p) => p.key !== undefined)
|
||||
.forEach((col) => {
|
||||
let defaultVal = row[col.key]
|
||||
|
||||
if (col.data_type === 'number') {
|
||||
defaultVal = parseFloat(defaultVal) || 0
|
||||
Vue.set(this.row, col.key, defaultVal)
|
||||
} else if (col.data_type === 'date') {
|
||||
Vue.set(this.row, col.key, dayjs(defaultVal).toDate())
|
||||
} else if (col.data_type === 'boolean') {
|
||||
Vue.set(this.row, col.key, defaultVal === 1 || defaultVal === true)
|
||||
} else {
|
||||
defaultVal = defaultVal !== undefined ? defaultVal : ''
|
||||
Vue.set(this.row, col.key, defaultVal)
|
||||
}
|
||||
})
|
||||
},
|
||||
async save() {
|
||||
let rules = this.curRules
|
||||
let keys = Object.keys(rules)
|
||||
|
||||
if (keys && keys.length > 0) {
|
||||
let validate = await this.$refs['From'].validate()
|
||||
if (!validate) {
|
||||
Promise.reject(new Error('请检查表单'))
|
||||
this.$Message.error('请检查表单')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let row = JSON.parse(JSON.stringify(this.row))
|
||||
|
||||
let dateCols = this.curColumns.filter((p) => p.type === 'date')
|
||||
if (dateCols && dateCols.length > 0) {
|
||||
dateCols.forEach((col) => {
|
||||
let format = col.type === 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'
|
||||
row[col.key] = dayjs(row[col.key]).format(format)
|
||||
})
|
||||
}
|
||||
|
||||
if (this.callback) {
|
||||
await this.callback(row)
|
||||
this.hide()
|
||||
}
|
||||
},
|
||||
cancel() {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.inline-row {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
.ivu-form-item {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.line-row {
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-top: 0.1rem;
|
||||
.ivu-form-item {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
.ivu-radio-wrapper {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.ivu-date-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
17
src/components/tables/fieldItem.vue
Normal file
17
src/components/tables/fieldItem.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<FormItem :label="name" :required="required">
|
||||
<slot></slot>
|
||||
</FormItem>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['name', 'required'],
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ivu-form-item {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
270
src/components/tables/index.vue
Normal file
270
src/components/tables/index.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<div class="table-warp">
|
||||
|
||||
<div class="page-box " v-if="pageOption.page">
|
||||
<Page :total="pageOption.total" :current="pageOption.page" :page-size="pageOption.pageSize" show-total @on-change="onChangePage"></Page>
|
||||
</div>
|
||||
<div class="table-content" ref="table_content" :style="{ maxHeight: tableMaxHeight }">
|
||||
<div class="table-title" v-if="title||isDown">
|
||||
<label>{{title}}
|
||||
<span class="gray font12 ml10" v-if="tip"> {{tip}}</span>
|
||||
</label>
|
||||
<slot name="header"></slot>
|
||||
<a v-if="isDown&&value&&value.length>0" class="link right-link" @click="downExecl">下载</a>
|
||||
</div>
|
||||
|
||||
<Table :class="tbClass" ref="tablesMain" border :data="insideTableData" :width='defaultWidth' :height='defaultHeight' :columns="insideColumns" v-bind="$attrs" @on-selection-change='onSelect'>
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
<slot name="loading" slot="loading"></slot>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
export default {
|
||||
name: 'Tables',
|
||||
props: {
|
||||
isDown: Boolean,
|
||||
title: String,
|
||||
tip: '',
|
||||
value: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
pageOption: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
|
||||
width: {
|
||||
type: [Number, String],
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
},
|
||||
// 表格容器最大高度偏移量(从视窗高度减去的值)
|
||||
// 设置为 null 或 'auto' 时自动计算,也可手动指定数字
|
||||
maxHeightOffset: {
|
||||
type: [Number, String],
|
||||
default: 'auto'
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Events
|
||||
* @on-start-edit 返回值 {Object} :同iview中render函数中的params对象 { row, index, column }
|
||||
* @on-cancel-edit 返回值 {Object} 同上
|
||||
* @on-save-edit 返回值 {Object} :除上面三个参数外,还有一个value: 修改后的数据
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
defaultHeight: undefined,
|
||||
defaultWidth: undefined,
|
||||
insideTableData: [],
|
||||
editOption: {
|
||||
index: 0,
|
||||
key: '',
|
||||
},
|
||||
// 动态计算的偏移量
|
||||
calculatedOffset: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
tbClass() {
|
||||
return 'tb_' + uuidv4()
|
||||
},
|
||||
|
||||
insideColumns() {
|
||||
let columns = this.columns.map((item, index) => {
|
||||
// 设置默认对齐方式
|
||||
if (!item.align) {
|
||||
item.align = 'center'
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
item.children = item.children.map((sonItem) => {
|
||||
if (!sonItem.align) {
|
||||
sonItem.align = 'center'
|
||||
}
|
||||
return sonItem
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 确保列宽设置正确,优先使用 width,其次使用 minWidth
|
||||
// 如果都没有设置,则设置默认 minWidth
|
||||
if (!item.width && !item.minWidth) {
|
||||
item.minWidth = 120
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
return columns
|
||||
},
|
||||
|
||||
editColumns() {
|
||||
let columns = this.columns.filter((p) => p.editOption && p.editOption.inEdit)
|
||||
return columns
|
||||
},
|
||||
|
||||
// 动态计算表格容器最大高度
|
||||
tableMaxHeight() {
|
||||
// 如果设置为 auto,则使用动态计算的值
|
||||
if (this.maxHeightOffset === 'auto' || this.maxHeightOffset === null) {
|
||||
return this.calculatedOffset !== null ? `calc(100vh - ${this.calculatedOffset}px)` : '100vh'
|
||||
}
|
||||
// 使用手动指定的偏移量
|
||||
return `calc(100vh - ${this.maxHeightOffset}px)`
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
downExecl() {
|
||||
this.$emit('downExecl', { value: this.value, columns: this.columns })
|
||||
},
|
||||
|
||||
handleTableData() {
|
||||
let values = this.value || []
|
||||
let data = values.filter((p) => p)
|
||||
this.insideTableData = data.map((item, index) => {
|
||||
let res = item
|
||||
return res
|
||||
})
|
||||
},
|
||||
onChangePage(page) {
|
||||
this.pageOption.page = page
|
||||
this.$emit('changePage', this.pageOption.page)
|
||||
},
|
||||
onSelect(selection, row) {
|
||||
this.$emit('on-select', selection, row)
|
||||
},
|
||||
|
||||
// 动态计算表格容器上方所有元素的高度总和
|
||||
calculateOffset() {
|
||||
this.$nextTick(() => {
|
||||
const tableContent = this.$refs.table_content
|
||||
if (!tableContent) return
|
||||
|
||||
// 获取表格容器距离视窗顶部的距离
|
||||
const rect = tableContent.getBoundingClientRect()
|
||||
const topOffset = rect.top
|
||||
|
||||
// 获取分页器高度(如果存在)
|
||||
const pageBox = this.$el.querySelector('.page-box')
|
||||
const pageBoxHeight = pageBox ? pageBox.offsetHeight : 0
|
||||
|
||||
// 底部预留空间(避免滚动条贴边)
|
||||
const bottomPadding = 20
|
||||
|
||||
// 计算总偏移量
|
||||
this.calculatedOffset = topOffset + pageBoxHeight + bottomPadding
|
||||
})
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.handleTableData()
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.handleTableData()
|
||||
|
||||
// 自适应高度
|
||||
this.$nextTick(() => {
|
||||
if (this.height) {
|
||||
this.defaultHeight = this.height
|
||||
}
|
||||
|
||||
if (this.width) {
|
||||
this.defaultWidth = this.width
|
||||
}
|
||||
|
||||
// 动态计算偏移量
|
||||
this.calculateOffset()
|
||||
})
|
||||
|
||||
// 监听窗口大小变化,重新计算偏移量
|
||||
window.addEventListener('resize', this.calculateOffset)
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
// 移除窗口大小监听
|
||||
window.removeEventListener('resize', this.calculateOffset)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.table-warp {
|
||||
width: 100%;
|
||||
|
||||
.table-content {
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
|
||||
.table-title {
|
||||
font-weight: bold;
|
||||
color: #515a6e;
|
||||
font-size: 14px;
|
||||
background-color: #e8eaec;
|
||||
padding: 0rem 0.15rem;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
.right-link {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-box {
|
||||
padding: 2px;
|
||||
text-align: right;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
|
||||
.search-con {
|
||||
padding: 10px 0;
|
||||
.search {
|
||||
&-col {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
&-input {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
&-btn {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保表格列对齐 */
|
||||
::v-deep .ivu-table {
|
||||
th, td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 确保表格单元格内容正确显示 */
|
||||
.ivu-table-cell {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
9
src/components/tables/templateRender.js
Normal file
9
src/components/tables/templateRender.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
name: 'templateRender',
|
||||
props: ['render', 'value'],
|
||||
functional: true,
|
||||
render: (h, ctx) => {
|
||||
const params = ctx.props.value
|
||||
return ctx.props.render(h, params)
|
||||
},
|
||||
}
|
||||
21
src/components/text-area/index.vue
Normal file
21
src/components/text-area/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<Input :value="value" type="textarea" :rows="4" placeholder="请输入内容" @on-blur="changeInput" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
changeInput(e) {
|
||||
let value = e.currentTarget._value
|
||||
this.$emit('input', value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
24
src/components/treeGrid/component/renderCol.vue
Normal file
24
src/components/treeGrid/component/renderCol.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'RenderCol',
|
||||
props: {
|
||||
col: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
param: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
render(h) {
|
||||
try {
|
||||
const content = this.col(h, this.param)
|
||||
return <div class="text-center">{content}</div>
|
||||
} catch (e) {
|
||||
// 渲染失败时提供一个容错显示
|
||||
return <div class="text-center">-</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
23
src/components/treeGrid/component/subColmns.vue
Normal file
23
src/components/treeGrid/component/subColmns.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<colgroup>
|
||||
<col v-for="(col, index) in columns" :key="col.key || index" :style="colStyle(col)" />
|
||||
</colgroup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SubColmns',
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
colStyle(col) {
|
||||
const width = col && col.width ? col.width : '150px'
|
||||
return { width }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
31
src/components/treeGrid/component/subThead.vue
Normal file
31
src/components/treeGrid/component/subThead.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<thead class="ivu-table-header">
|
||||
<tr>
|
||||
<th v-for="(col, index) in columns" :key="col.key || index" :style="headerStyle(col)">
|
||||
<div class="ivu-table-cell">
|
||||
<span>{{ col.title }}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SubThead',
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
headerStyle(col) {
|
||||
const align = col && col.align ? col.align : 'center'
|
||||
return { textAlign: align }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
341
src/components/treeGrid/component/subTreeGrid.vue
Normal file
341
src/components/treeGrid/component/subTreeGrid.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<table cellspacing="0" width="100%" cellpadding="0" border="0">
|
||||
<SubColmns :columns="columns"></SubColmns>
|
||||
<tbody class="endTbody" v-for="(row, index) in subData" :key="row.id">
|
||||
|
||||
<tr class="tr-row" v-if="row">
|
||||
<td v-for="(col, sonColIndex) in columns"
|
||||
:key="sonColIndex"
|
||||
:class="[
|
||||
{'td-expand': sonColIndex === 0},
|
||||
{'td-operations': col.type === 'operation'},
|
||||
col.className
|
||||
]"
|
||||
:style="{
|
||||
width: col.width || 'auto',
|
||||
minWidth: col.minWidth || (sonColIndex === 0 ? '200px' : '100px')
|
||||
}"
|
||||
>
|
||||
<RenderCol v-if="col.render" :col="col.render" :param="{ row, index, col }" />
|
||||
<template v-else>
|
||||
<div class="first-box" v-if="sonColIndex == 1">
|
||||
<div class="col-row expand-row" v-if="row.children && row.children.length > 0" @click="expander(row)">
|
||||
<span class="indent-span" :style="{width:`${row.indentWidth}px`}"></span>
|
||||
<i :class="['ivu-icon', 'link', 'ivu-icon-' + row.indexIcon]" aria-hidden="true"></i>
|
||||
<label>{{ row[col.key] }}</label>
|
||||
</div>
|
||||
<div class="col-row" v-else>
|
||||
<span class="indent-span" :style="{width:`${row.indentWidth}px`}"></span>
|
||||
<label>{{ row[col.key] }}</label>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<label v-else>{{ row[col.key] }}</label>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-show="row._expand" class="treegrid-tr-tree ivu-table-row">
|
||||
<th :colspan="columns.length" class="non-right ">
|
||||
<SubTreeGrid :columns="columns" :data="row['children']" :grade="grade + 1" />
|
||||
</th>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SubTreeGrid from './subTreeGrid.vue'
|
||||
import SubColmns from './subColmns.vue'
|
||||
import RenderCol from './renderCol.vue'
|
||||
export default {
|
||||
name: 'SubTreeGrid',
|
||||
props: ['columns', 'data', 'grade'],
|
||||
components: {
|
||||
RenderCol,
|
||||
SubTreeGrid,
|
||||
SubColmns
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
subData: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.setData()
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
handler() {
|
||||
this.setData()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 本地存储 key,用于持久化展开状态
|
||||
_expandStoreKey() {
|
||||
return 'treegrid_expand_map_v1'
|
||||
},
|
||||
|
||||
loadExpandMap() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this._expandStoreKey())
|
||||
return raw ? JSON.parse(raw) : {}
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
|
||||
saveExpandMap(map) {
|
||||
try {
|
||||
localStorage.setItem(this._expandStoreKey(), JSON.stringify(map))
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
// 递归收集当前 subData 的展开状态(用于在 data 更新时保留)
|
||||
collectExpandMapFromList(list, map) {
|
||||
if (!list || !list.length) return
|
||||
list.forEach((r) => {
|
||||
const id = r.id || r.timeKey
|
||||
if (id !== undefined) map[id] = !!r._expand
|
||||
if (r.children && r.children.length) this.collectExpandMapFromList(r.children, map)
|
||||
})
|
||||
},
|
||||
|
||||
setData() {
|
||||
if (this.data && this.data.length > 0) {
|
||||
// 优先从本地存储读取上次的展开状态
|
||||
const storedMap = this.loadExpandMap()
|
||||
|
||||
// 也从当前内存 subData 中读取(以便在快速编辑时保留)
|
||||
const currentMap = {}
|
||||
this.collectExpandMapFromList(this.subData, currentMap)
|
||||
|
||||
let { expand } = this.$attrs
|
||||
|
||||
const subData = this.data.map((p) => {
|
||||
// 不直接修改传入对象,复制一份以避免副作用
|
||||
const row = Object.assign({}, p)
|
||||
|
||||
const id = row.id || row.timeKey
|
||||
|
||||
// 优先使用 currentMap(内存),其次使用本地存储,最后使用传入的默认 expand
|
||||
if (id !== undefined && currentMap.hasOwnProperty(id)) {
|
||||
row._expand = !!currentMap[id]
|
||||
} else if (id !== undefined && storedMap.hasOwnProperty(id)) {
|
||||
row._expand = !!storedMap[id]
|
||||
} else {
|
||||
row._expand = expand
|
||||
}
|
||||
|
||||
// 保留已有 timeKey,若不存在则创建
|
||||
row.timeKey = row.timeKey || new Date().getTime()
|
||||
row.indexIcon = row._expand ? 'md-remove' : 'md-add'
|
||||
row.indentWidth = (this.grade - 1) * 30
|
||||
// 递归处理 children(保留结构,不展开子项的数据填充在子组件中)
|
||||
if (row.children && row.children.length) {
|
||||
// children 内容将在子组件中处理,不在此处深拷贝以节约性能
|
||||
}
|
||||
return row
|
||||
})
|
||||
|
||||
// 保存当前展开状态
|
||||
const newMap = {}
|
||||
this.collectExpandMapFromList(subData, newMap)
|
||||
// 合并之前的存储(优先保留 newMap 中的值)
|
||||
const merged = Object.assign({}, storedMap, newMap)
|
||||
this.saveExpandMap(merged)
|
||||
|
||||
this.subData = subData
|
||||
} else {
|
||||
this.subData = []
|
||||
}
|
||||
},
|
||||
|
||||
expander(row) {
|
||||
console.log('expander called:', {
|
||||
id: row.id,
|
||||
timeKey: row.timeKey,
|
||||
children: row.children,
|
||||
currentExpand: row._expand,
|
||||
icon: row.indexIcon
|
||||
});
|
||||
|
||||
// use Vue.set for reactivity
|
||||
const newExpand = !row._expand;
|
||||
this.$set(row, '_expand', newExpand);
|
||||
this.$set(row, 'indexIcon', newExpand ? 'md-remove' : 'md-add');
|
||||
|
||||
// 更新本地存储状态
|
||||
const map = this.loadExpandMap();
|
||||
const id = row.id || row.timeKey;
|
||||
if (id !== undefined) {
|
||||
map[id] = newExpand;
|
||||
this.saveExpandMap(map);
|
||||
}
|
||||
|
||||
// 打印展开后的状态
|
||||
console.log('expander updated:', {
|
||||
id: id,
|
||||
newExpand: newExpand,
|
||||
newIcon: row.indexIcon,
|
||||
storedState: map[id]
|
||||
});
|
||||
|
||||
// 确保视图更新
|
||||
this.$nextTick(() => {
|
||||
console.log('After nextTick:', {
|
||||
element: document.querySelector(`.tr-row[data-id="${id}"] .expand-row`),
|
||||
expandState: row._expand,
|
||||
icon: row.indexIcon
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/* 表格容器样式 */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* 确保表格可以在容器内滚动 */
|
||||
.endTbody {
|
||||
max-height: calc(100vh - 200px); /* 预留头部和其他元素的空间 */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #2d8cf0;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.expand-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(45,140,240,0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.fallback-icon {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
background: rgba(45,140,240,0.08);
|
||||
color: #2d8cf0;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tr-row {
|
||||
padding: 0px 5px;
|
||||
td {
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.first-box {
|
||||
padding: 0px 10px;
|
||||
|
||||
.col-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
.indent-span {
|
||||
display: inline-block;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.non-right {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* 表格基础样式 */
|
||||
.tr-row {
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
td {
|
||||
position: relative;
|
||||
padding: 12px 8px;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
|
||||
/* 基础文本溢出处理 */
|
||||
&:not(.td-operations) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
/* 展开列样式 */
|
||||
.td-expand {
|
||||
width: auto;
|
||||
min-width: 200px; /* 给展开图标和文本预留足够空间 */
|
||||
}
|
||||
|
||||
/* 操作列样式 */
|
||||
.td-operations {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
min-width: 180px; /* 增加按钮空间 */
|
||||
padding-right: 16px;
|
||||
overflow: visible !important; /* 强制确保内容可见 */
|
||||
position: relative;
|
||||
z-index: 1; /* 确保按钮在最上层 */
|
||||
}
|
||||
|
||||
/* 确保操作列中的按钮组样式 */
|
||||
.td-operations .ivu-btn-group {
|
||||
display: inline-flex;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 确保单个按钮样式 */
|
||||
.td-operations .ivu-btn {
|
||||
display: inline-block;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
54
src/components/treeGrid/index.vue
Normal file
54
src/components/treeGrid/index.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="ivu-table-wrapper">
|
||||
<div class="ivu-table ivu-table-border">
|
||||
<table cellspacing="0" width="100%" cellpadding="0" border="0">
|
||||
<SubColmns :columns="columns"></SubColmns>
|
||||
<subTheads :columns="columns"></subTheads>
|
||||
|
||||
<tr class="ivu-table-tbody">
|
||||
<td style="width: 100%;" :colspan="columns.length" class="non-right">
|
||||
<SubTreeGrid :columns="columns" :data="data" :grade="1" v-bind="$attrs" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SubTreeGrid from './component/subTreeGrid.vue'
|
||||
import SubColmns from './component/subColmns.vue'
|
||||
import subTheads from './component/subThead.vue'
|
||||
|
||||
export default {
|
||||
props: ['columns', 'data'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
components: {
|
||||
SubTreeGrid,
|
||||
SubColmns,
|
||||
subTheads
|
||||
},
|
||||
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.ivu-table {
|
||||
/* 限制最大宽度,超出显示横向滚动 */
|
||||
overflow: auto !important;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.ivu-table-tbody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.non-right {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
108
src/components/upload/Custom.vue
Normal file
108
src/components/upload/Custom.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="custom-file-box">
|
||||
|
||||
<Input :value="value" disabled />
|
||||
<FileBtn class="mt10" ref="fileBtn" accept="*" @change="changeFile" :text="btnText"></FileBtn>
|
||||
<p class="link">{{ ossFileUrl }}</p>
|
||||
<div class="file-box" v-if="file">
|
||||
<div class="name ml5">{{ file.name }}</div>
|
||||
<div class="slider-box">
|
||||
<Progress :percent="uploadProcess"></Progress>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tools from '@/utils/tools'
|
||||
import FileBtn from './mod/fileBtn.vue'
|
||||
import uploadOssMixin from '@/mixin/uploadOssMixin.js'
|
||||
export default {
|
||||
components: { FileBtn },
|
||||
mixins: [uploadOssMixin],
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: '*'
|
||||
},
|
||||
btnText: {
|
||||
type: String,
|
||||
default: '上传文件'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
file: null,
|
||||
uploadProcess: 0,
|
||||
ossFileUrl: ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
clear() {
|
||||
this.clearFile()
|
||||
},
|
||||
clearFile() {
|
||||
if (this.$refs['fileBtn']) {
|
||||
this.$refs['fileBtn'].clear()
|
||||
}
|
||||
},
|
||||
async changeFile(e) {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
let file = e.target.files[0]
|
||||
this.file = file
|
||||
|
||||
let ossFileUrl = await this.singlePartUpload(file, ({ uploadProcess, fileName }) => {
|
||||
this.uploadProcess = uploadProcess
|
||||
console.log('singlePartUpload', { uploadProcess, fileName })
|
||||
})
|
||||
|
||||
this.ossFileUrl = ossFileUrl
|
||||
|
||||
this.$emit('input', ossFileUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.custom-file-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
.file-box {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.slider-box {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-file {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
93
src/components/upload/Multiple.vue
Normal file
93
src/components/upload/Multiple.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div class="upload-list" v-for="(item,index) in uploadList" :key='index'>
|
||||
<template>
|
||||
<img :src="item.url">
|
||||
<div class="upload-list-cover">
|
||||
<Icon type="ios-eye-outline" @click.native="handleView(item)"></Icon>
|
||||
<Icon type="ios-trash-outline" @click.native="handleRemove(item)"></Icon>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<Upload ref="upload" :show-upload-list="false" :on-success="handleSuccess" :headers="headers" :format="['jpg','jpeg','png']" :max-size="2048" :on-exceeded-size="handleMaxSize" :on-format-error="handleFormatError" :before-upload="handleBeforeUpload" multiple type="drag" :action="actionUrl" style="display: inline-block;width:100px;">
|
||||
<div style="width: 100px;height:100px;line-height: 100px;">
|
||||
<Icon type="ios-camera" size="20"></Icon>
|
||||
</div>
|
||||
</Upload>
|
||||
|
||||
<Modal title="查看图片" v-model="visible" footer-hide>
|
||||
<img :src="imgSrc" v-if="visible" style="width: 100%">
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import './upload.less'
|
||||
import store from '@/store'
|
||||
let headers = {
|
||||
'admin-token': store.state.user.token,
|
||||
}
|
||||
export default {
|
||||
name: 'UoloadImg',
|
||||
props: { isOss: { default: true } },
|
||||
data() {
|
||||
return {
|
||||
headers,
|
||||
uploadList: [],
|
||||
actionUrl: window.rootVue.$config.apiUrl + 'sys_file/upload_oos_img',
|
||||
imgSrc: '',
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
computed: {},
|
||||
methods: {
|
||||
setList(list) {
|
||||
this.uploadList = list
|
||||
},
|
||||
getList() {
|
||||
return this.uploadList
|
||||
},
|
||||
handleView(item) {
|
||||
this.imgSrc = item.url
|
||||
this.visible = true
|
||||
},
|
||||
handleSuccess(res, file) {
|
||||
let row = res.data
|
||||
file.url = row.path
|
||||
file.name = row.name
|
||||
|
||||
this.uploadList.push({ name: row.name, url: row.path })
|
||||
this.$emit('handleSuccess', { file, uploadList: this.uploadList })
|
||||
},
|
||||
handleRemove(file) {
|
||||
const fileList = this.uploadList
|
||||
this.uploadList.splice(fileList.indexOf(file), 1)
|
||||
},
|
||||
handleFormatError(file) {
|
||||
this.$Notice.warning({
|
||||
title: '提示',
|
||||
desc: '文件格式 ' + file.name + ' 错误, 请上传png,jpg格式',
|
||||
})
|
||||
},
|
||||
handleMaxSize(file) {
|
||||
this.$Notice.warning({
|
||||
title: '提示',
|
||||
desc: '文件 ' + file.name + ' 太大, 请上传文件不超过 2M.',
|
||||
})
|
||||
},
|
||||
handleBeforeUpload() {
|
||||
const check = this.uploadList.length < 4
|
||||
if (!check) {
|
||||
this.$Notice.warning({
|
||||
title: '文件数量不能超过4个',
|
||||
})
|
||||
}
|
||||
return check
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
75
src/components/upload/Single.vue
Normal file
75
src/components/upload/Single.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="single-box">
|
||||
<div class="upload-list" v-if="value">
|
||||
<template>
|
||||
<img :src="value" />
|
||||
<div class="upload-list-cover">
|
||||
<Icon type="ios-eye-outline" @click="handleView(value)"></Icon>
|
||||
<Icon type="ios-trash-outline" @click="handleRemove(value)"></Icon>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<Upload ref="upload" v-else :show-upload-list="false" :before-upload="handleBeforeUpload" :on-success="handleSuccess" :max-size="2048" :on-exceeded-size="handleMaxSize" :on-format-error="handleFormatError" :headers="headers" :format="['jpg','jpeg','png']" type="drag" :action="actionUrl" style="display: inline-block;width:100px;">
|
||||
<div style="width: 100px;height:100px;line-height: 100px;">
|
||||
<Icon type="ios-camera" size="20"></Icon>
|
||||
</div>
|
||||
</Upload>
|
||||
<Modal title="查看大图" v-model="visible" footer-hide>
|
||||
<img :src="imgUrl" style="width: 100%" />
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import './upload.less'
|
||||
import store from '@/store'
|
||||
|
||||
let headers = {
|
||||
'admin-token': store.state.user.token,
|
||||
}
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
computed: {},
|
||||
data() {
|
||||
return {
|
||||
headers,
|
||||
actionUrl: window.rootVue.$config.apiUrl + 'sys_file/upload_oos_img',
|
||||
imgUrl: '',
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {},
|
||||
methods: {
|
||||
handleFormatError(file) {
|
||||
this.$Message.warning('文件格式 ' + file.name + ' 错误, 请上传png,jpg格式')
|
||||
},
|
||||
handleMaxSize(file) {
|
||||
this.$Message.warning('文件 ' + file.name + ' 太大, 请上传文件不超过 2M.')
|
||||
},
|
||||
handleBeforeUpload() {},
|
||||
handleView(url) {
|
||||
this.imgUrl = url
|
||||
this.visible = true
|
||||
},
|
||||
handleRemove(url) {
|
||||
this.$emit('input', '')
|
||||
},
|
||||
handleSuccess(res, file) {
|
||||
let row = res.data
|
||||
file.url = row.path
|
||||
file.name = row.name
|
||||
setTimeout(() => {
|
||||
this.$emit('input', row.path)
|
||||
this.$emit('change', row.path)
|
||||
}, 200)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.single-box {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
51
src/components/upload/mod/fileBtn.vue
Normal file
51
src/components/upload/mod/fileBtn.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="file-upload-box">
|
||||
<input ref="file" class="file" type="file" :multiple="multiple" :accept="accept" @change="changeFiles" />
|
||||
<slot>
|
||||
<Button type="primary" icon="md-arrow-round-up" round>{{text}} </Button>
|
||||
</slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
default: '选择文件'
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: `.mp4,.flv,.avi,.mkv,.mov,.webm`
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
this.$refs['file'].value = ''
|
||||
},
|
||||
changeFiles(e) {
|
||||
this.$emit('change', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.file-upload-box {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
.file {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
177
src/components/upload/mod/fileListModal.vue
Normal file
177
src/components/upload/mod/fileListModal.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
|
||||
<asyncModal ref="fileListModal" v-bind="$attrs">
|
||||
<div class="upload-local-multiple-file-box w100 ">
|
||||
|
||||
<div class="top-box">
|
||||
|
||||
<div class="tip">
|
||||
<p>上传过程中请勿关闭窗口</p>
|
||||
</div>
|
||||
|
||||
<div class="upload-tool-box">
|
||||
<FileBtn ref="fileBtn" :multiple="true" @change="changeFiles" text="批量选择文件" :accept="accept"></FileBtn>
|
||||
<Button round class="ml10" size="mini" @click="clear">清空列表</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fileListBox mt20">
|
||||
<Tables ref="table" :data="curRows" :columns="columns"></Tables>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</asyncModal>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import uiTool from '@/utils/uiTool'
|
||||
import uploadOssMixin from '@/mixin/uploadOssMixin.js'
|
||||
import FileBtn from './fileBtn.vue'
|
||||
import uploadServer from '@/api/system/uploadServer'
|
||||
|
||||
export default {
|
||||
mixins: [uploadOssMixin],
|
||||
components: { FileBtn },
|
||||
props: {
|
||||
isUpload: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
formatRow: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
default: [
|
||||
{ key: 'name', title: '名称' },
|
||||
{ key: 'infoSize', title: '文件大小' },
|
||||
{ key: 'resolutionRatio', title: '分辨率' },
|
||||
{ key: 'preViewUrl', title: '预览图' }
|
||||
]
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
callback: null,
|
||||
gridOption: {
|
||||
data: []
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
curRows() {
|
||||
let rows = this.gridOption.data
|
||||
return rows
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
this.gridOption.data = []
|
||||
this.clearFile()
|
||||
},
|
||||
clearFile() {
|
||||
if (this.$refs['fileBtn']) {
|
||||
this.$refs['fileBtn'].clear()
|
||||
}
|
||||
},
|
||||
|
||||
hide() {
|
||||
this.$refs['fileListModal'].hide()
|
||||
},
|
||||
|
||||
async show(callback) {
|
||||
this.clear()
|
||||
this.$refs['fileListModal'].setBtnState(true)
|
||||
this.callback = callback
|
||||
this.$refs['fileListModal'].show(async () => {
|
||||
let { data } = this.gridOption
|
||||
|
||||
// 立马开始上传
|
||||
if (this.isUpload) {
|
||||
await this.startHandUpload()
|
||||
}
|
||||
|
||||
if (this.callback) {
|
||||
await this.callback(data)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async changeFiles(e) {
|
||||
let files = e.currentTarget.files
|
||||
|
||||
this.gridOption.data = []
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i]
|
||||
let { name, size, type } = file
|
||||
|
||||
const url = window.URL.createObjectURL(file)
|
||||
let infoSize = uiTool.fileSizeTransfer(size / 1024 / 1024)
|
||||
|
||||
let row = { name, size, type, infoSize, url, file, uploadProcess: 0 }
|
||||
|
||||
// 需要外层处理 数据
|
||||
if (this.formatRow) {
|
||||
let tempRow = await this.formatRow(row)
|
||||
row = Object.assign({}, row, tempRow)
|
||||
}
|
||||
|
||||
this.gridOption.data.push(row)
|
||||
}
|
||||
|
||||
this.$emit('changeFileComplete', this.gridOption.data)
|
||||
this.$refs['fileListModal'].setBtnState(false)
|
||||
this.clearFile()
|
||||
},
|
||||
|
||||
async startHandUpload() {
|
||||
let data = this.gridOption.data
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let { file } = data[i]
|
||||
|
||||
// 大于100m 走 分片上传
|
||||
if (file.size / 1024 / 1024 > 50) {
|
||||
let ossUrl = await this.singlePartUpload(file, ({ uploadProcess }) => {
|
||||
this.$set(data[i], 'uploadProcess', uploadProcess)
|
||||
})
|
||||
|
||||
this.$set(data[i], 'url', ossUrl)
|
||||
} else {
|
||||
let ossUrl = await this.uploadFile(file)
|
||||
this.$set(data[i], 'uploadProcess', 100)
|
||||
this.$set(data[i], 'url', ossUrl)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async uploadFile(file) {
|
||||
let res = await uploadServer.uploadFile({ file })
|
||||
let url = res.data
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.top-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.upload-tool-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.fileListBox {
|
||||
.img {
|
||||
height: 60px;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
40
src/components/upload/upload.less
Normal file
40
src/components/upload/upload.less
Normal file
@@ -0,0 +1,40 @@
|
||||
.upload-list {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
line-height: 100px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
margin-right: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-list-cover {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.upload-list:hover .upload-list-cover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.upload-list-cover i {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
margin: 0 2px;
|
||||
}
|
||||
Reference in New Issue
Block a user