This commit is contained in:
molong
2022-05-12 18:12:20 +08:00
parent c31729ec7e
commit fe38e19924
284 changed files with 33456 additions and 11 deletions
+32
View File
@@ -0,0 +1,32 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
+101
View File
@@ -0,0 +1,101 @@
<!--
* @Descripttion: scContextmenu组件
* @version: 1.0
* @Author: sakuya
* @Date: 2021年7月23日09:25:57
* @LastEditors:
* @LastEditTime:
* @other: 代码完全开源欢迎参考也欢迎PR
-->
<template>
<transition name="el-zoom-in-top">
<div v-if="visible" ref="contextmenu" class="sc-contextmenu" :style="{left:left+'px',top:top+'px'}" @contextmenu.prevent="fun">
<ul class="sc-contextmenu__menu">
<slot></slot>
</ul>
</div>
</transition>
</template>
<script>
export default {
provide() {
return {
menuClick: this.menuClick
}
},
data() {
return {
visible: false,
top: 0,
left: 0
}
},
watch: {
visible(value) {
var _this = this;
var cm = function(e){
let sp = _this.$refs.contextmenu
if(sp&&!sp.contains(e.target)){
_this.closeMenu()
}
}
if (value) {
document.body.addEventListener('click', e=>cm(e))
}else{
document.body.removeEventListener('click', e=>cm(e))
}
}
},
mounted() {
},
methods: {
menuClick(command){
this.closeMenu()
this.$emit('command', command)
},
openMenu(e) {
e.preventDefault()
this.visible = true
this.left = e.clientX + 1
this.top = e.clientY + 1
this.$nextTick(() => {
var ex = e.clientX + 1
var ey = e.clientY + 1
var innerWidth = window.innerWidth
var innerHeight = window.innerHeight
var menuHeight = this.$refs.contextmenu.offsetHeight
var menuWidth = this.$refs.contextmenu.offsetWidth
//位置修正公示
//left = (当前点击X + 菜单宽度 > 可视区域宽度 ? 可视区域宽度 - 菜单宽度 : 当前点击X)
//top = (当前点击Y + 菜单高度 > 可视区域高度 ? 当前点击Y - 菜单高度 : 当前点击Y)
this.left = ex + menuWidth > innerWidth ? innerWidth - menuWidth : ex
this.top = ey + menuHeight > innerHeight ? ey - menuHeight : ey
})
this.$emit('visibleChange', true)
},
closeMenu() {
this.visible = false;
this.$emit('visibleChange', false)
},
fun(){
return false;
}
}
}
</script>
<style>
.sc-contextmenu {position: fixed;z-index: 3000;}
.sc-contextmenu__menu {display: inline-block;min-width: 120px;border: 1px solid #e4e7ed;background: #fff;box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);z-index: 3000;list-style-type: none;padding: 10px 0;}
.sc-contextmenu__menu > hr {margin:5px 0;border: none;height: 1px;font-size: 0px;background-color: #ebeef5;}
.sc-contextmenu__menu > li {margin:0;cursor: pointer;line-height: 30px;padding: 0 17px 0 10px;color: #606266;display: flex;justify-content: space-between;white-space: nowrap;text-decoration: none;position: relative;}
.sc-contextmenu__menu > li:hover {background-color: #ecf5ff;color: #66b1ff;}
.sc-contextmenu__menu > li.disabled {cursor: not-allowed;color: #bbb;background: transparent;}
.sc-contextmenu__icon {display: inline-block;width: 14px;font-size: 14px;margin-right: 10px;}
.sc-contextmenu__suffix {margin-left: 40px;color: #999;}
.sc-contextmenu__menu li ul {position: absolute;top:0px;left:100%;display: none;margin: -11px 0;}
</style>
+84
View File
@@ -0,0 +1,84 @@
<!--
* @Descripttion: scContextmenuItem组件
* @version: 1.2
* @Author: sakuya
* @Date: 2021年7月23日16:29:36
* @LastEditors: sakuya
* @LastEditTime: 2022年2月8日15:51:07
-->
<template>
<hr v-if="divided">
<li :class="disabled?'disabled':''" @click.stop="liClick" @mouseenter="openSubmenu($event)" @mouseleave="closeSubmenu($event)">
<span class="title">
<el-icon class="sc-contextmenu__icon"><component v-if="icon" :is="icon" /></el-icon>
{{title}}
</span>
<span class="sc-contextmenu__suffix">
<el-icon v-if="$slots.default"><el-icon-arrow-right /></el-icon>
<template v-else>{{suffix}}</template>
</span>
<ul v-if="$slots.default" class="sc-contextmenu__menu">
<slot></slot>
</ul>
</li>
</template>
<script>
export default {
props: {
command: { type: String, default: "" },
title: { type: String, default: "" },
suffix: { type: String, default: "" },
icon: { type: String, default: "" },
divided: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
},
inject: ['menuClick'],
methods: {
liClick(){
if(this.$slots.default){
return false
}
if(this.disabled){
return false
}
this.menuClick(this.command)
},
openSubmenu(e){
var menu = e.target.querySelector('ul')
if(!menu){
return false
}
menu.style.display = 'inline-block'
var rect = menu.getBoundingClientRect()
var menuX = rect.left
var menuY = rect.top
var innerWidth = window.innerWidth
var innerHeight = window.innerHeight
var menuHeight = menu.offsetHeight
var menuWidth = menu.offsetWidth
if(menuX + menuWidth > innerWidth){
menu.style.left = 'auto'
menu.style.right = '100%'
}
if(menuY + menuHeight > innerHeight){
menu.style.top = 'auto'
menu.style.bottom = '0'
}
},
closeSubmenu(e){
var menu = e.target.querySelector('ul')
if(!menu){
return false
}
menu.removeAttribute("style")
menu.style.display = 'none'
}
}
}
</script>
<style>
</style>
+730
View File
@@ -0,0 +1,730 @@
<!--
* @Descripttion: cron规则生成器
* @version: 1.0
* @Author: sakuya
* @Date: 2021年12月29日15:23:54
* @LastEditors:
* @LastEditTime:
-->
<template>
<el-input v-model="defaultValue" v-bind="$attrs">
<template #append>
<el-dropdown size="medium" @command="handleShortcuts">
<el-button icon="el-icon-arrow-down"></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="0 * * * * ?">每分钟</el-dropdown-item>
<el-dropdown-item command="0 0 * * * ?">每小时</el-dropdown-item>
<el-dropdown-item command="0 0 0 * * ?">每天零点</el-dropdown-item>
<el-dropdown-item command="0 0 0 1 * ?">每月一号零点</el-dropdown-item>
<el-dropdown-item command="0 0 0 L * ?">每月最后一天零点</el-dropdown-item>
<el-dropdown-item command="0 0 0 ? * 1">每周星期日零点</el-dropdown-item>
<el-dropdown-item v-for="(item, index) in shortcuts" :key="item.value" :divided="index==0" :command="item.value">{{item.text}}</el-dropdown-item>
<el-dropdown-item icon="el-icon-plus" divided command="custom">自定义</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-input>
<el-dialog title="cron规则生成器" v-model="dialogVisible" :width="580" destroy-on-close append-to-body>
<div class="sc-cron">
<el-tabs>
<el-tab-pane>
<template #label>
<div class="sc-cron-num">
<h2></h2>
<h4>{{value_second}}</h4>
</div>
</template>
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="value.second.type">
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="范围" v-if="value.second.type==1">
<el-input-number v-model="value.second.range.start" :min="0" :max="59" controls-position="right"></el-input-number>
<span style="padding:0 15px;">-</span>
<el-input-number v-model="value.second.range.end" :min="0" :max="59" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="间隔" v-if="value.second.type==2">
<el-input-number v-model="value.second.loop.start" :min="0" :max="59" controls-position="right"></el-input-number>
秒开始
<el-input-number v-model="value.second.loop.end" :min="0" :max="59" controls-position="right"></el-input-number>
秒执行一次
</el-form-item>
<el-form-item label="指定" v-if="value.second.type==3">
<el-select v-model="value.second.appoint" multiple style="width: 100%;">
<el-option v-for="(item, index) in data.second" :key="index" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane>
<template #label>
<div class="sc-cron-num">
<h2>分钟</h2>
<h4>{{value_minute}}</h4>
</div>
</template>
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="value.minute.type">
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="范围" v-if="value.minute.type==1">
<el-input-number v-model="value.minute.range.start" :min="0" :max="59" controls-position="right"></el-input-number>
<span style="padding:0 15px;">-</span>
<el-input-number v-model="value.minute.range.end" :min="0" :max="59" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="间隔" v-if="value.minute.type==2">
<el-input-number v-model="value.minute.loop.start" :min="0" :max="59" controls-position="right"></el-input-number>
分钟开始
<el-input-number v-model="value.minute.loop.end" :min="0" :max="59" controls-position="right"></el-input-number>
分钟执行一次
</el-form-item>
<el-form-item label="指定" v-if="value.minute.type==3">
<el-select v-model="value.minute.appoint" multiple style="width: 100%;">
<el-option v-for="(item, index) in data.minute" :key="index" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane>
<template #label>
<div class="sc-cron-num">
<h2>小时</h2>
<h4>{{value_hour}}</h4>
</div>
</template>
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="value.hour.type">
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="范围" v-if="value.hour.type==1">
<el-input-number v-model="value.hour.range.start" :min="0" :max="23" controls-position="right"></el-input-number>
<span style="padding:0 15px;">-</span>
<el-input-number v-model="value.hour.range.end" :min="0" :max="23" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="间隔" v-if="value.hour.type==2">
<el-input-number v-model="value.hour.loop.start" :min="0" :max="23" controls-position="right"></el-input-number>
小时开始
<el-input-number v-model="value.hour.loop.end" :min="0" :max="23" controls-position="right"></el-input-number>
小时执行一次
</el-form-item>
<el-form-item label="指定" v-if="value.hour.type==3">
<el-select v-model="value.hour.appoint" multiple style="width: 100%;">
<el-option v-for="(item, index) in data.hour" :key="index" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane>
<template #label>
<div class="sc-cron-num">
<h2></h2>
<h4>{{value_day}}</h4>
</div>
</template>
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="value.day.type">
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
<el-radio-button label="4">本月最后一天</el-radio-button>
<el-radio-button label="5">不指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="范围" v-if="value.day.type==1">
<el-input-number v-model="value.day.range.start" :min="1" :max="31" controls-position="right"></el-input-number>
<span style="padding:0 15px;">-</span>
<el-input-number v-model="value.day.range.end" :min="1" :max="31" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="间隔" v-if="value.day.type==2">
<el-input-number v-model="value.day.loop.start" :min="1" :max="31" controls-position="right"></el-input-number>
号开始
<el-input-number v-model="value.day.loop.end" :min="1" :max="31" controls-position="right"></el-input-number>
天执行一次
</el-form-item>
<el-form-item label="指定" v-if="value.day.type==3">
<el-select v-model="value.day.appoint" multiple style="width: 100%;">
<el-option v-for="(item, index) in data.day" :key="index" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane>
<template #label>
<div class="sc-cron-num">
<h2></h2>
<h4>{{value_month}}</h4>
</div>
</template>
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="value.month.type">
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="范围" v-if="value.month.type==1">
<el-input-number v-model="value.month.range.start" :min="1" :max="12" controls-position="right"></el-input-number>
<span style="padding:0 15px;">-</span>
<el-input-number v-model="value.month.range.end" :min="1" :max="12" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="间隔" v-if="value.month.type==2">
<el-input-number v-model="value.month.loop.start" :min="1" :max="12" controls-position="right"></el-input-number>
月开始
<el-input-number v-model="value.month.loop.end" :min="1" :max="12" controls-position="right"></el-input-number>
月执行一次
</el-form-item>
<el-form-item label="指定" v-if="value.month.type==3">
<el-select v-model="value.month.appoint" multiple style="width: 100%;">
<el-option v-for="(item, index) in data.month" :key="index" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane>
<template #label>
<div class="sc-cron-num">
<h2></h2>
<h4>{{value_week}}</h4>
</div>
</template>
<el-form>
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="value.week.type">
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
<el-radio-button label="4">本月最后一周</el-radio-button>
<el-radio-button label="5">不指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="范围" v-if="value.week.type==1">
<el-select v-model="value.week.range.start">
<el-option v-for="(item, index) in data.week" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
<span style="padding:0 15px;">-</span>
<el-select v-model="value.week.range.end">
<el-option v-for="(item, index) in data.week" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="间隔" v-if="value.week.type==2">
<el-input-number v-model="value.week.loop.start" :min="1" :max="4" controls-position="right"></el-input-number>
周的星期
<el-select v-model="value.week.loop.end">
<el-option v-for="(item, index) in data.week" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
执行一次
</el-form-item>
<el-form-item label="指定" v-if="value.week.type==3">
<el-select v-model="value.week.appoint" multiple style="width: 100%;">
<el-option v-for="(item, index) in data.week" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="最后一周" v-if="value.week.type==4">
<el-select v-model="value.week.last">
<el-option v-for="(item, index) in data.week" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-form>
</el-tab-pane>
<el-tab-pane>
<template #label>
<div class="sc-cron-num">
<h2></h2>
<h4>{{value_year}}</h4>
</div>
</template>
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="value.year.type">
<el-radio-button label="-1">忽略</el-radio-button>
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="范围" v-if="value.year.type==1">
<el-input-number v-model="value.year.range.start" controls-position="right"></el-input-number>
<span style="padding:0 15px;">-</span>
<el-input-number v-model="value.year.range.end" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="间隔" v-if="value.year.type==2">
<el-input-number v-model="value.year.loop.start" controls-position="right"></el-input-number>
年开始
<el-input-number v-model="value.year.loop.end" :min="1" controls-position="right"></el-input-number>
年执行一次
</el-form-item>
<el-form-item label="指定" v-if="value.year.type==3">
<el-select v-model="value.year.appoint" multiple style="width: 100%;">
<el-option v-for="(item, index) in data.year" :key="index" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<el-button @click="dialogVisible=false" > </el-button>
<el-button type="primary" @click="submit()"> </el-button>
</template>
</el-dialog>
</template>
<script>
export default {
props: {
modelValue: { type: String, default: "* * * * * ?" },
shortcuts: { type: Array, default: () => [] }
},
data() {
return {
type: '0',
defaultValue: '',
dialogVisible: false,
value:{
second: {
type: '0',
range: {
start: 1,
end: 2
},
loop: {
start: 0,
end: 1
},
appoint: []
},
minute: {
type: '0',
range: {
start: 1,
end: 2
},
loop: {
start: 0,
end: 1
},
appoint: []
},
hour: {
type: '0',
range: {
start: 1,
end: 2
},
loop: {
start: 0,
end: 1
},
appoint: []
},
day: {
type: '0',
range: {
start: 1,
end: 2
},
loop: {
start: 1,
end: 1
},
appoint: []
},
month: {
type: '0',
range: {
start: 1,
end: 2
},
loop: {
start: 1,
end: 1
},
appoint: []
},
week: {
type: '5',
range: {
start: '2',
end: '3'
},
loop: {
start: 0,
end: '2'
},
last: '2',
appoint: []
},
year: {
type: '-1',
range: {
start: this.getYear()[0],
end: this.getYear()[1]
},
loop: {
start: this.getYear()[0],
end: 1
},
appoint: []
}
},
data: {
second: ['0','5','15','20','25','30','35','40','45','50','55','59'],
minute: ['0','5','15','20','25','30','35','40','45','50','55','59'],
hour: ['0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22','23'],
day: ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24','25','26','27','28','29','30','31'],
month: ['1','2','3','4','5','6','7','8','9','10','11','12'],
week: [
{
value: '1',
label: '周日'
},
{
value: '2',
label: '周一'
},
{
value: '3',
label: '周二'
},
{
value: '4',
label: '周三'
},
{
value: '5',
label: '周四'
},
{
value: '6',
label: '周五'
},
{
value: '7',
label: '周六'
}
],
year: this.getYear()
}
}
},
watch: {
'value.week.type'(val){
if(val != '5'){
this.value.day.type = '5'
}
},
'value.day.type'(val){
if(val != '5'){
this.value.week.type = '5'
}
}
},
computed: {
value_second(){
let v = this.value.second
if(v.type == 0){
return '*'
}else if(v.type==1){
return v.range.start + '-' +v.range.end
}else if(v.type==2){
return v.loop.start + '/' + v.loop.end
}else if(v.type==3){
return v.appoint.length>0 ? v.appoint.join(',') : '*'
}else{
return '*'
}
},
value_minute(){
let v = this.value.minute
if(v.type == 0){
return '*'
}else if(v.type==1){
return v.range.start + '-' +v.range.end
}else if(v.type==2){
return v.loop.start + '/' + v.loop.end
}else if(v.type==3){
return v.appoint.length>0 ? v.appoint.join(',') : '*'
}else{
return '*'
}
},
value_hour(){
let v = this.value.hour
if(v.type == 0){
return '*'
}else if(v.type==1){
return v.range.start + '-' +v.range.end
}else if(v.type==2){
return v.loop.start + '/' + v.loop.end
}else if(v.type==3){
return v.appoint.length>0 ? v.appoint.join(',') : '*'
}else{
return '*'
}
},
value_day(){
let v = this.value.day
if(v.type == 0){
return '*'
}else if(v.type==1){
return v.range.start + '-' +v.range.end
}else if(v.type==2){
return v.loop.start + '/' + v.loop.end
}else if(v.type==3){
return v.appoint.length>0 ? v.appoint.join(',') : '*'
}else if(v.type==4){
return 'L'
}else if(v.type==5){
return '?'
}else{
return '*'
}
},
value_month(){
let v = this.value.month
if(v.type == 0){
return '*'
}else if(v.type==1){
return v.range.start + '-' +v.range.end
}else if(v.type==2){
return v.loop.start + '/' + v.loop.end
}else if(v.type==3){
return v.appoint.length>0 ? v.appoint.join(',') : '*'
}else{
return '*'
}
},
value_week(){
let v = this.value.week
if(v.type == 0){
return '*'
}else if(v.type==1){
return v.range.start + '-' +v.range.end
}else if(v.type==2){
return v.loop.end + '#' + v.loop.start
}else if(v.type==3){
return v.appoint.length>0 ? v.appoint.join(',') : '*'
}else if(v.type==4){
return v.last + 'L'
}else if(v.type==5){
return '?'
}else{
return '*'
}
},
value_year(){
let v = this.value.year
if(v.type == -1){
return ''
}else if(v.type==0){
return '*'
}else if(v.type==1){
return v.range.start + '-' +v.range.end
}else if(v.type==2){
return v.loop.start + '/' + v.loop.end
}else if(v.type==3){
return v.appoint.length>0 ? v.appoint.join(',') : ''
}else{
return ''
}
},
},
mounted() {
this.defaultValue = this.modelValue
},
methods: {
handleShortcuts(command){
if(command == 'custom'){
this.open()
}else{
this.defaultValue = command
this.$emit('update:modelValue', this.defaultValue)
}
},
open(){
this.set()
this.dialogVisible = true
},
set(){
this.defaultValue = this.modelValue
let arr = (this.modelValue || "* * * * * ?").split(" ")
//简单检查
if(arr.length < 6){
this.$message.warning("cron表达式错误,已转换为默认表达式")
arr = "* * * * * ?".split(" ")
}
//秒
if(arr[0]=='*'){
this.value.second.type = '0'
}else if(arr[0].includes('-')){
this.value.second.type = '1'
this.value.second.range.start = Number(arr[0].split("-")[0])
this.value.second.range.end = Number(arr[0].split("-")[1])
}else if(arr[0].includes('/')){
this.value.second.type = '2'
this.value.second.loop.start = Number(arr[0].split("/")[0])
this.value.second.loop.end = Number(arr[0].split("/")[1])
}else{
this.value.second.type = '3'
this.value.second.appoint = arr[0].split(",")
}
//分
if(arr[1]=='*'){
this.value.minute.type = '0'
}else if(arr[1].includes('-')){
this.value.minute.type = '1'
this.value.minute.range.start = Number(arr[1].split("-")[0])
this.value.minute.range.end = Number(arr[1].split("-")[1])
}else if(arr[1].includes('/')){
this.value.minute.type = '2'
this.value.minute.loop.start = Number(arr[1].split("/")[0])
this.value.minute.loop.end = Number(arr[1].split("/")[1])
}else{
this.value.minute.type = '3'
this.value.minute.appoint = arr[1].split(",")
}
//小时
if(arr[2]=='*'){
this.value.hour.type = '0'
}else if(arr[2].includes('-')){
this.value.hour.type = '1'
this.value.hour.range.start = Number(arr[2].split("-")[0])
this.value.hour.range.end = Number(arr[2].split("-")[1])
}else if(arr[2].includes('/')){
this.value.hour.type = '2'
this.value.hour.loop.start = Number(arr[2].split("/")[0])
this.value.hour.loop.end = Number(arr[2].split("/")[1])
}else{
this.value.hour.type = '3'
this.value.hour.appoint = arr[2].split(",")
}
//日
if(arr[3]=='*'){
this.value.day.type = '0'
}else if(arr[3]=='L'){
this.value.day.type = '4'
}else if(arr[3]=='?'){
this.value.day.type = '5'
}else if(arr[3].includes('-')){
this.value.day.type = '1'
this.value.day.range.start = Number(arr[3].split("-")[0])
this.value.day.range.end = Number(arr[3].split("-")[1])
}else if(arr[3].includes('/')){
this.value.day.type = '2'
this.value.day.loop.start = Number(arr[3].split("/")[0])
this.value.day.loop.end = Number(arr[3].split("/")[1])
}else{
this.value.day.type = '3'
this.value.day.appoint = arr[3].split(",")
}
//月
if(arr[4]=='*'){
this.value.month.type = '0'
}else if(arr[4].includes('-')){
this.value.month.type = '1'
this.value.month.range.start = Number(arr[4].split("-")[0])
this.value.month.range.end = Number(arr[4].split("-")[1])
}else if(arr[4].includes('/')){
this.value.month.type = '2'
this.value.month.loop.start = Number(arr[4].split("/")[0])
this.value.month.loop.end = Number(arr[4].split("/")[1])
}else{
this.value.month.type = '3'
this.value.month.appoint = arr[4].split(",")
}
//周
if(arr[5]=='*'){
this.value.week.type = '0'
}else if(arr[5]=='?'){
this.value.week.type = '5'
}else if(arr[5].includes('-')){
this.value.week.type = '1'
this.value.week.range.start = arr[5].split("-")[0]
this.value.week.range.end = arr[5].split("-")[1]
}else if(arr[5].includes('#')){
this.value.week.type = '2'
this.value.week.loop.start = Number(arr[5].split("#")[1])
this.value.week.loop.end = arr[5].split("#")[0]
}else if(arr[5].includes('L')){
this.value.week.type = '4'
this.value.week.last = arr[5].split("L")[0]
}else{
this.value.week.type = '3'
this.value.week.appoint = arr[5].split(",")
}
//年
if(!arr[6]){
this.value.year.type = '-1'
}else if(arr[6]=='*'){
this.value.year.type = '0'
}else if(arr[6].includes('-')){
this.value.year.type = '1'
this.value.year.range.start = Number(arr[6].split("-")[0])
this.value.year.range.end = Number(arr[6].split("-")[1])
}else if(arr[6].includes('/')){
this.value.year.type = '2'
this.value.year.loop.start = Number(arr[6].split("/")[1])
this.value.year.loop.end = Number(arr[6].split("/")[0])
}else{
this.value.year.type = '3'
this.value.year.appoint = arr[6].split(",")
}
},
getYear(){
let v = []
let y = new Date().getFullYear()
for (let i = 0; i < 11; i++) {
v.push(y+i)
}
return v
},
submit(){
let year = this.value_year ? ' '+this.value_year : ''
this.defaultValue = this.value_second + ' ' + this.value_minute + ' ' + this.value_hour + ' ' + this.value_day + ' ' + this.value_month + ' ' + this.value_week + year
this.$emit('update:modelValue', this.defaultValue)
this.dialogVisible = false
}
}
}
</script>
<style scoped>
.sc-cron:deep(.el-tabs__item) {height: auto;line-height: 1;padding:0 7px;vertical-align: bottom;}
.sc-cron-num {text-align: center;margin-bottom: 15px;width: 100%;}
.sc-cron-num h2 {font-size: 12px;margin-bottom: 15px;font-weight: normal;}
.sc-cron-num h4 {display: block;height: 32px;line-height: 30px;width: 100%;font-size: 12px;padding:0 15px;background: var(--el-color-primary-light-9);border-radius:4px;}
.sc-cron:deep(.el-tabs__item.is-active) .sc-cron-num h4 {background: var(--el-color-primary);color: #fff;}
[data-theme='dark'] .sc-cron-num h4 {background: var(--el-color-white);}
</style>
+84
View File
@@ -0,0 +1,84 @@
<!--
* @Descripttion: 图像裁剪组件
* @version: 1.0
* @Author: sakuya
* @Date: 2021年7月24日17:05:43
* @LastEditors:
* @LastEditTime:
* @other: 代码完全开源欢迎参考也欢迎PR
-->
<template>
<div class="sc-cropper">
<div class="sc-cropper__img">
<img :src="src" ref="img">
</div>
<div class="sc-cropper__preview">
<h4>图像预览</h4>
<div class="sc-cropper__preview__img" ref="preview"></div>
</div>
</div>
</template>
<script>
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
export default {
props: {
src: { type: String, default: "" },
compress: {type: Number, default: 1},
aspectRatio: {type: Number, default: NaN},
},
data() {
return {
crop: null
}
},
watch:{
aspectRatio(val){
this.crop.setAspectRatio(val)
}
},
mounted() {
this.init()
},
methods: {
init(){
this.crop = new Cropper(this.$refs.img, {
viewMode: 2,
dragMode: 'move',
responsive: false,
aspectRatio: this.aspectRatio,
preview: this.$refs.preview
})
},
setAspectRatio(aspectRatio){
this.crop.setAspectRatio(aspectRatio)
},
getCropData(cb, type='image/jpeg'){
cb(this.crop.getCroppedCanvas().toDataURL(type, this.compress))
},
getCropBlob(cb, type='image/jpeg'){
this.crop.getCroppedCanvas().toBlob((blob) => {
cb(blob)
}, type, this.compress)
},
getCropFile(cb, fileName='fileName.jpg', type='image/jpeg'){
this.crop.getCroppedCanvas().toBlob((blob) => {
let file = new File([blob], fileName, {type: type})
cb(file)
}, type, this.compress)
}
}
}
</script>
<style scoped>
.sc-cropper {height:300px;}
.sc-cropper__img {height:100%;width:400px;float: left;background: #EBEEF5;}
.sc-cropper__img img {display: none;}
.sc-cropper__preview {width: 120px;margin-left: 20px;float: left;}
.sc-cropper__preview h4 {font-weight: normal;font-size: 12px;color: #999;margin-bottom: 20px;}
.sc-cropper__preview__img {overflow: hidden;width: 120px;height: 120px;border: 1px solid #ebeef5;}
</style>
+141
View File
@@ -0,0 +1,141 @@
<!--
* @Descripttion: 弹窗扩展组件
* @version: 1.0
* @Author: sakuya
* @Date: 2021年8月27日08:51:52
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-dialog" ref="scDialog">
<el-dialog ref="dialog" v-model="dialogVisible" :fullscreen="isFullscreen" v-bind="$attrs" :show-close="false">
<template #title>
<slot name="title">
<span class="el-dialog__title">{{ title }}</span>
</slot>
<div class="sc-dialog__headerbtn">
<button v-if="showFullscreen" aria-label="fullscreen" type="button" @click="setFullscreen">
<el-icon v-if="isFullscreen" class="el-dialog__close"><el-icon-bottom-left /></el-icon>
<el-icon v-else class="el-dialog__close"><el-icon-full-screen /></el-icon>
</button>
<button v-if="showClose" aria-label="close" type="button" @click="closeDialog">
<el-icon class="el-dialog__close"><el-icon-close /></el-icon>
</button>
</div>
</template>
<div v-loading="loading">
<slot></slot>
</div>
<template #footer>
<slot name="footer"></slot>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
props: {
modelValue: { type: Boolean, default: false },
title: { type: String, default: "" },
showClose: { type: Boolean, default: true },
showFullscreen: { type: Boolean, default: true },
drag: { type: Boolean, default: true },
loading: { type: Boolean, default: false }
},
data() {
return {
dialogVisible: false,
isFullscreen: false
}
},
watch:{
modelValue(){
this.dialogVisible = this.modelValue
if(this.dialogVisible){
this.$refs.scDialog.querySelector('.el-dialog').style.top = '0px'
this.$refs.scDialog.querySelector('.el-dialog').style.left = '0px'
this.isFullscreen = false
}
}
},
mounted() {
this.dialogVisible = this.modelValue
this.drag && this.dialogdrag()
},
methods: {
//关闭
closeDialog(){
this.dialogVisible = false
},
//最大化
setFullscreen(){
this.isFullscreen = !this.isFullscreen
},
//绑定拖拽
dialogdrag(){
const dialogHeaderEl = this.$refs.scDialog.querySelector('.el-dialog__header')
const dragDom = this.$refs.scDialog.querySelector('.el-dialog')
//dialogHeaderEl.style.cursor = 'move'
dialogHeaderEl.onmousedown = (e) => {
const disX = e.clientX - dialogHeaderEl.offsetLeft
const disY = e.clientY - dialogHeaderEl.offsetTop
const screenWidth = document.body.clientWidth
const screenHeight = document.documentElement.clientHeight
const dragDomWidth = dragDom.offsetWidth
const dragDomheight = dragDom.offsetHeight
let minDragDomLeft = -dragDom.offsetLeft
let maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
let minDragDomTop = -dragDom.offsetTop
let maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight
if(screenHeight < dragDomheight){
return false
}
dragDom.style.marginBottom = '0px'
let styL = getComputedStyle(dragDom).left
let styT = getComputedStyle(dragDom).top
if(styL.includes('%')) {
styL = +document.body.clientWidth * (+styL.replace('%', '') / 100)
styT = +document.body.clientHeight * (+styT.replace('%', '') / 100)
}else {
styL = +styL.replace('px', '')
styT = +styT.replace('px', '')
}
document.onmousemove = function (e) {
let left = e.clientX - disX
let top = e.clientY - disY
if (left < minDragDomLeft) {
left = minDragDomLeft
} else if (left > maxDragDomLeft) {
left = maxDragDomLeft
}
if (top < minDragDomTop) {
top = minDragDomTop
} else if (top > maxDragDomTop) {
top = maxDragDomTop
}
dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
}
document.onmouseup = function () {
document.onmousemove = null
document.onmouseup = null
}
}
}
}
}
</script>
<style scoped>
.sc-dialog__headerbtn {position: absolute;top: var(--el-dialog-padding-primary);right: var(--el-dialog-padding-primary);}
.sc-dialog__headerbtn button {padding: 0;background: transparent;border: none;outline: none;cursor: pointer;font-size: var(--el-message-close-size,16px);margin-left: 15px;color: var(--el-color-info);}
.sc-dialog__headerbtn button:hover .el-dialog__close {color: var(--el-color-primary);}
.sc-dialog:deep(.el-dialog).is-fullscreen {display: flex;flex-direction: column;top:0px !important;left:0px !important;}
.sc-dialog:deep(.el-dialog).is-fullscreen .el-dialog__header {}
.sc-dialog:deep(.el-dialog).is-fullscreen .el-dialog__body {flex:1;overflow: auto;}
.sc-dialog:deep(.el-dialog).is-fullscreen .el-dialog__footer {padding-bottom: 10px;border-top: 1px solid var(--el-border-color-base);}
</style>
@@ -0,0 +1,74 @@
const T = {
"color": [
"#409EFF",
"#36CE9E",
"#f56e6a",
"#626c91",
"#edb00d",
"#909399"
],
'grid': {
'left': '3%',
'right': '3%',
'bottom': '10',
'top': '40',
'containLabel': true
},
"legend": {
"textStyle": {
"color": "#999"
},
"inactiveColor": "rgba(128,128,128,0.4)"
},
"categoryAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "rgba(128,128,128,0.2)",
"width": 1
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"color": "#999"
},
"splitLine": {
"show": false,
"lineStyle": {
"color": [
"#eee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(255,255,255,0.01)",
"rgba(0,0,0,0.01)"
]
}
}
},
"valueAxis": {
"axisLine": {
"show": false,
"lineStyle": {
"color": "#999"
}
},
"splitLine": {
"show": true,
"lineStyle": {
"color": "rgba(128,128,128,0.2)"
}
}
}
}
export default T
+64
View File
@@ -0,0 +1,64 @@
<template>
<div ref="scEcharts" :style="{height:height, width:width}"></div>
</template>
<script>
import * as echarts from 'echarts';
import T from './echarts-theme-T.js';
echarts.registerTheme('T', T);
const unwarp = (obj) => obj && (obj.__v_raw || obj.valueOf() || obj);
export default {
...echarts,
name: "scEcharts",
props: {
height: { type: String, default: "100%" },
width: { type: String, default: "100%" },
nodata: {type: Boolean, default: false },
option: { type: Object, default: () => {} }
},
data() {
return {
isActivat: false,
myChart: null
}
},
watch: {
option: {
deep:true,
handler (v) {
unwarp(this.myChart).setOption(v);
}
}
},
computed: {
myOptions: function() {
return this.option || {};
}
},
activated(){
if(!this.isActivat){
this.$nextTick(() => {
this.myChart.resize()
})
}
},
deactivated(){
this.isActivat = false;
},
mounted(){
this.isActivat = true;
this.$nextTick(() => {
this.draw();
})
},
methods: {
draw(){
var myChart = echarts.init(this.$refs.scEcharts, 'T');
myChart.setOption(this.myOptions);
this.myChart = myChart;
window.addEventListener('resize', () => myChart.resize());
}
}
}
</script>
+111
View File
@@ -0,0 +1,111 @@
<template>
<div class="sceditor">
<Editor v-model="contentValue" :init="init" :disabled="disabled" :placeholder="placeholder" @onClick="onClick" />
</div>
</template>
<script>
import API from "@/api";
import Editor from '@tinymce/tinymce-vue'
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/silver'
import 'tinymce/icons/default'
// 引入编辑器插件
import 'tinymce/plugins/code' //编辑源码
import 'tinymce/plugins/image' //插入编辑图片
import 'tinymce/plugins/link' //超链接
import 'tinymce/plugins/preview'//预览
import 'tinymce/plugins/table' //表格
export default {
components: {
Editor
},
props: {
modelValue: {
type: String,
default: ""
},
placeholder: {
type: String,
default: ""
},
height: {
type: Number,
default: 300,
},
disabled: {
type: Boolean,
default: false
},
plugins: {
type: [String, Array],
default: 'code image link preview table'
},
toolbar: {
type: [String, Array],
default: 'undo redo | forecolor backcolor bold italic underline strikethrough link | formatselect fontselect fontsizeselect | \
alignleft aligncenter alignright alignjustify outdent indent lineheight | bullist numlist | \
image table preview | code selectall'
}
},
data() {
return {
init: {
language_url: 'tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: 'tinymce/skins/ui/oxide',
content_css: "tinymce/skins/content/default/content.css",
menubar: false,
statusbar: true,
plugins: this.plugins,
toolbar: this.toolbar,
fontsize_formats: '12px 14px 16px 18px 20px 22px 24px 28px 32px 36px 48px 56px 72px',
height: this.height,
placeholder: this.placeholder,
branding: false,
resize: true,
elementpath: true,
content_style: "",
images_upload_handler: async (blobInfo, success, failure) => {
const data = new FormData();
data.append("file", blobInfo.blob() ,blobInfo.filename());
try {
const res = await API.common.upload.post(data)
success(res.data.src)
}catch (error) {
failure("Image upload failed")
}
},
setup: function(editor) {
editor.on('init', function() {
this.getBody().style.fontSize = '14px';
})
}
},
contentValue: this.modelValue
}
},
watch: {
modelValue(val) {
this.contentValue = val
},
contentValue(val){
this.$emit('update:modelValue', val);
}
},
mounted() {
tinymce.init({})
},
methods: {
onClick(e){
this.$emit('onClick', e, tinymce)
}
}
}
</script>
<style>
</style>
+283
View File
@@ -0,0 +1,283 @@
<!--
* @Descripttion: 资源文件选择器
* @version: 1.0
* @Author: sakuya
* @Date: 2021年10月11日16:01:40
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-file-select">
<div class="sc-file-select__side" v-loading="menuLoading">
<div class="sc-file-select__side-menu">
<el-tree ref="group" class="menu" :data="menu" :node-key="treeProps.key" :props="treeProps" :current-node-key="menu.length>0?menu[0][treeProps.key]:''" highlight-current @node-click="groupClick">
<template #default="{ node }">
<span class="el-tree-node__label">
<el-icon class="icon"><el-icon-folder /></el-icon>{{node.label}}
</span>
</template>
</el-tree>
</div>
<div class="sc-file-select__side-msg" v-if="multiple">
已选择 <b>{{value.length}}</b> / <b>{{max}}</b>
</div>
</div>
<div class="sc-file-select__files" v-loading="listLoading">
<div class="sc-file-select__top">
<div class="upload" v-if="!hideUpload">
<el-upload class="sc-file-select__upload" action="" multiple :show-file-list="false" :accept="accept" :on-change="uploadChange" :before-upload="uploadBefore" :on-progress="uploadProcess" :on-success="uploadSuccess" :on-error="uploadError" :http-request="uploadRequest">
<el-button type="primary" icon="el-icon-upload">本地上传</el-button>
</el-upload>
<span class="tips"><el-icon><el-icon-warning /></el-icon>大小不超过{{maxSize}}MB</span>
</div>
<div class="keyword">
<el-input v-model="keyword" prefix-icon="el-icon-search" placeholder="文件名搜索" clearable @keyup.enter="search" @clear="search"></el-input>
</div>
</div>
<div class="sc-file-select__list">
<el-scrollbar ref="scrollbar">
<el-empty v-if="fileList.length==0 && data.length==0" description="无数据" :image-size="80"></el-empty>
<div v-for="(file, index) in fileList" :key="index" class="sc-file-select__item">
<div class="sc-file-select__item__file">
<div class="sc-file-select__item__upload">
<el-progress type="circle" :percentage="file.progress" :width="70"></el-progress>
</div>
<el-image :src="file.tempImg" fit="contain"></el-image>
</div>
<p>{{file.name}}</p>
</div>
<div v-for="item in data" :key="item[fileProps.key]" class="sc-file-select__item" :class="{active: value.includes(item[fileProps.url]) }" @click="select(item)">
<div class="sc-file-select__item__file">
<div class="sc-file-select__item__checkbox" v-if="multiple">
<el-icon><el-icon-check /></el-icon>
</div>
<div class="sc-file-select__item__select" v-else>
<el-icon><el-icon-check /></el-icon>
</div>
<div class="sc-file-select__item__box"></div>
<el-image v-if="_isImg(item[fileProps.url])" :src="item[fileProps.url]" fit="contain" lazy></el-image>
<div v-else class="item-file item-file-doc">
<i v-if="files[_getExt(item[fileProps.url])]" :class="files[_getExt(item[fileProps.url])].icon" :style="{color:files[_getExt(item[fileProps.url])].color}"></i>
<i v-else class="sc-icon-file-list-fill" style="color: #999;"></i>
</div>
</div>
<p :title="item[fileProps.fileName]">{{item[fileProps.fileName]}}</p>
</div>
</el-scrollbar>
</div>
<div class="sc-file-select__pagination">
<el-pagination small background layout="prev, pager, next" :total="total" :page-size="pageSize" v-model:currentPage="currentPage" @current-change="reload"></el-pagination>
</div>
<div class="sc-file-select__do">
<slot name="do"></slot>
<el-button type="primary" :disabled="value.length<=0" @click="submit"> </el-button>
</div>
</div>
</div>
</template>
<script>
import config from "@/config/fileSelect"
export default {
props: {
modelValue: null,
hideUpload: { type: Boolean, default: false },
multiple: { type: Boolean, default: false },
max: {type: Number, default: config.max},
onlyImage: { type: Boolean, default: false },
maxSize: {type: Number, default: config.maxSize},
},
data() {
return {
keyword: null,
pageSize: 20,
total: 0,
currentPage: 1,
data: [],
menu: [],
menuId: '',
value: this.multiple ? [] : '',
fileList: [],
accept: this.onlyImage ? "image/gif, image/jpeg, image/png" : "",
listLoading: false,
menuLoading: false,
treeProps: config.menuProps,
fileProps: config.fileProps,
files: config.files
}
},
watch: {
multiple(){
this.value = this.multiple ? [] : ''
this.$emit('update:modelValue', JSON.parse(JSON.stringify(this.value)));
}
},
mounted() {
this.getMenu()
this.getData()
},
methods: {
//获取分类数据
async getMenu(){
this.menuLoading = true
var res = await config.menuApiObj.get()
this.menu = res.data
this.menuLoading = false
},
//获取列表数据
async getData(){
this.listLoading = true
var reqData = {
[config.request.menuKey]: this.menuId,
[config.request.page]: this.currentPage,
[config.request.pageSize]: this.pageSize,
[config.request.keyword]: this.keyword
}
if(this.onlyImage){
reqData.type = 'image'
}
var res = await config.listApiObj.get(reqData)
var parseData = config.listParseData(res)
this.data = parseData.rows
this.total = parseData.total
this.listLoading = false
this.$refs.scrollbar.setScrollTop(0)
},
//树点击事件
groupClick(data){
this.menuId = data.id
this.currentPage = 1
this.keyword = null
this.getData()
},
//分页刷新表格
reload(){
this.getData()
},
search(){
this.currentPage = 1
this.getData()
},
select(item){
const itemUrl = item[this.fileProps.url]
if(this.multiple){
if(this.value.includes(itemUrl)){
this.value.splice(this.value.findIndex(f => f == itemUrl), 1)
}else{
this.value.push(itemUrl)
}
}else{
if(this.value.includes(itemUrl)){
this.value = ''
}else{
this.value = itemUrl
}
}
},
submit(){
const value = JSON.parse(JSON.stringify(this.value))
this.$emit('update:modelValue', value);
this.$emit('submit', value);
},
//上传处理
uploadChange(file, fileList){
file.tempImg = URL.createObjectURL(file.raw);
this.fileList = fileList
},
uploadBefore(file){
const maxSize = file.size / 1024 / 1024 < this.maxSize;
if (!maxSize) {
this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
return false;
}
},
uploadRequest(param){
var apiObj = config.apiObj;
const data = new FormData();
data.append("file", param.file);
data.append([config.request.menuKey], this.menuId);
apiObj.post(data, {
onUploadProgress: e => {
param.onProgress(e)
}
}).then(res => {
param.onSuccess(res)
}).catch(err => {
param.onError(err)
})
},
uploadProcess(event, file){
file.progress = Number((event.loaded / event.total * 100).toFixed(2))
},
uploadSuccess(res, file){
this.fileList.splice(this.fileList.findIndex(f => f.uid == file.uid), 1)
var response = config.uploadParseData(res);
this.data.unshift({
[this.fileProps.key]: response.id,
[this.fileProps.fileName]: response.fileName,
[this.fileProps.url]: response.url
})
if(!this.multiple){
this.value = response.url
}
},
uploadError(err){
this.$notify.error({
title: '上传文件错误',
message: err
})
},
//内置函数
_isImg(fileUrl){
const imgExt = ['.jpg', '.jpeg', '.png', '.gif', '.bmp']
const fileExt = fileUrl.substring(fileUrl.lastIndexOf("."))
return imgExt.indexOf(fileExt) != -1
},
_getExt(fileUrl){
return fileUrl.substring(fileUrl.lastIndexOf(".") + 1)
}
}
}
</script>
<style scoped>
.sc-file-select {display: flex;}
.sc-file-select__files {flex: 1;}
.sc-file-select__list {height:400px;}
.sc-file-select__item {display: inline-block;float: left;margin:0 15px 25px 0;width:110px;cursor: pointer;}
.sc-file-select__item__file {width:110px;height:110px;position: relative;}
.sc-file-select__item__file .el-image {width:110px;height:110px;}
.sc-file-select__item__box {position: absolute;top:0;right:0;bottom:0;left:0;border: 2px solid var(--el-color-success);z-index: 1;display: none;}
.sc-file-select__item__box::before {content: '';position: absolute;top:0;right:0;bottom:0;left:0;background: var(--el-color-success);opacity: 0.2;display: none;}
.sc-file-select__item:hover .sc-file-select__item__box {display: block;}
.sc-file-select__item.active .sc-file-select__item__box {display: block;}
.sc-file-select__item.active .sc-file-select__item__box::before {display: block;}
.sc-file-select__item p {margin-top: 10px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;-webkit-text-overflow:ellipsis;text-align: center;}
.sc-file-select__item__checkbox {position: absolute;width: 20px;height: 20px;top:7px;right:7px;z-index: 2;background: rgba(0,0,0,0.2);border: 1px solid #fff;display: flex;flex-direction: column;align-items: center;justify-content: center;}
.sc-file-select__item__checkbox i {font-size: 14px;color: #fff;font-weight: bold;display: none;}
.sc-file-select__item__select {position: absolute;width: 20px;height: 20px;top:0px;right:0px;z-index: 2;background: var(--el-color-success);display: none;flex-direction: column;align-items: center;justify-content: center;}
.sc-file-select__item__select i {font-size: 14px;color: #fff;font-weight: bold;}
.sc-file-select__item.active .sc-file-select__item__checkbox {background: var(--el-color-success);}
.sc-file-select__item.active .sc-file-select__item__checkbox i {display: block;}
.sc-file-select__item.active .sc-file-select__item__select {display: flex;}
.sc-file-select__item__file .item-file {width:110px;height:110px;display: flex;flex-direction: column;align-items: center;justify-content: center;}
.sc-file-select__item__file .item-file i {font-size: 40px;}
.sc-file-select__item__file .item-file.item-file-doc {color: #409eff;}
.sc-file-select__item__upload {position: absolute;top:0;right:0;bottom:0;left:0;z-index: 1;background: rgba(255,255,255,0.7);display: flex;flex-direction: column;align-items: center;justify-content: center;}
.sc-file-select__side {width: 200px;margin-right: 15px;border-right: 1px solid rgba(128,128,128,0.2);display: flex;flex-flow: column;}
.sc-file-select__side-menu {flex: 1;}
.sc-file-select__side-msg {height:32px;line-height: 32px;}
.sc-file-select__top {margin-bottom: 15px;display: flex;justify-content: space-between;}
.sc-file-select__upload {display: inline-block;}
.sc-file-select__top .tips {font-size: 12px;margin-left: 10px;color: #999;}
.sc-file-select__top .tips i {font-size: 14px;margin-right: 5px;position: relative;bottom: -0.125em;}
.sc-file-select__pagination {margin:15px 0;}
.sc-file-select__do {text-align: right;}
</style>
+310
View File
@@ -0,0 +1,310 @@
<!--
* @Descripttion: 过滤器V2
* @version: 2.4
* @Author: sakuya
* @Date: 2021年7月30日14:48:41
* @LastEditors: sakuya
* @LastEditTime: 2022年2月8日15:28:24
-->
<template>
<div class="sc-filterBar">
<slot :filterLength="filterObjLength" :openFilter="openFilter">
<el-badge :value="filterObjLength" type="danger" :hidden="filterObjLength<=0">
<el-button icon="el-icon-filter" @click="openFilter"></el-button>
</el-badge>
</slot>
<el-drawer title="过滤器" v-model="drawer" :size="650" append-to-body>
<el-container v-loading="saveLoading">
<el-main style="padding:0">
<el-tabs class="root">
<el-tab-pane lazy>
<template #label>
<div class="tabs-label">过滤项</div>
</template>
<el-scrollbar>
<div class="sc-filter-main">
<h2>设置过滤条件</h2>
<div v-if="filter.length<=0" class="nodata">
没有默认过滤条件请点击增加过滤项
</div>
<table v-else>
<colgroup>
<col width="50">
<col width="140">
<col v-if="showOperator" width="120">
<col>
<col width="40">
</colgroup>
<tr v-for="(item,index) in filter" :key="index">
<td>
<el-tag>{{index+1}}</el-tag>
</td>
<td>
<py-select v-model="item.field" :options="fields" placeholder="过滤字段" filterable @change="fieldChange(item)">
</py-select>
</td>
<td v-if="showOperator">
<el-select v-model="item.operator" placeholder="运算符">
<el-option v-for="ope in item.field.operators || operator" :key="ope.value" :label="ope.label" :value="ope.value"></el-option>
</el-select>
</td>
<td>
<el-input v-if="!item.field.type" v-model="item.value" placeholder="请选择过滤字段" disabled></el-input>
<!-- 输入框 -->
<el-input v-if="item.field.type=='text'" v-model="item.value" :placeholder="item.field.placeholder||'请输入'"></el-input>
<!-- 下拉框 -->
<el-select v-if="item.field.type=='select'" v-model="item.value" :placeholder="item.field.placeholder||'请选择'" filterable :multiple="item.field.extend.multiple" :loading="item.selectLoading" @visible-change="visibleChange($event, item)" :remote="item.field.extend.remote" :remote-method="(query)=>{remoteMethod(query, item)}">
<el-option v-for="field in item.field.extend.data" :key="field.value" :label="field.label" :value="field.value"></el-option>
</el-select>
<!-- 日期 -->
<el-date-picker v-if="item.field.type=='date'" v-model="item.value" type="date" value-format="YYYY-MM-DD" :placeholder="item.field.placeholder||'请选择日期'" style="width: 100%;"></el-date-picker>
<!-- 日期范围 -->
<el-date-picker v-if="item.field.type=='daterange'" v-model="item.value" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 100%;"></el-date-picker>
<!-- 日期时间 -->
<el-date-picker v-if="item.field.type=='datetime'" v-model="item.value" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" :placeholder="item.field.placeholder||'请选择日期'" style="width: 100%;"></el-date-picker>
<!-- 日期时间范围 -->
<el-date-picker v-if="item.field.type=='datetimerange'" v-model="item.value" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 100%;"></el-date-picker>
<!-- 开关 -->
<el-switch v-if="item.field.type=='switch'" v-model="item.value" active-value="1" inactive-value="0"></el-switch>
<!-- 标签 -->
<el-select v-if="item.field.type=='tags'" v-model="item.value" multiple filterable allow-create default-first-option no-data-text="输入关键词后按回车确认" :placeholder="item.field.placeholder||'请输入'"></el-select>
</td>
<td>
<el-icon class="del" @click="delFilter(index)"><el-icon-delete /></el-icon>
</td>
</tr>
</table>
<el-button type="text" icon="el-icon-plus" @click="addFilter">增加过滤项</el-button>
</div>
</el-scrollbar>
</el-tab-pane>
<el-tab-pane lazy>
<template #label>
<div class="tabs-label">常用</div>
</template>
<el-scrollbar>
<my ref="my" :data="myFilter" :filterName="filterName" @selectMyfilter="selectMyfilter"></my>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
</el-main>
<el-footer>
<el-button type="primary" @click="ok" :disabled="filter.length<=0">立即过滤</el-button>
<el-button type="primary" plain @click="saveMy" :disabled="filter.length<=0">另存为常用</el-button>
<el-button @click="clear">清空过滤</el-button>
</el-footer>
</el-container>
</el-drawer>
</div>
</template>
<script>
import config from "@/config/filterBar"
import pySelect from './pySelect'
import my from './my'
export default {
name: 'filterBar',
components: {
pySelect,
my
},
props: {
filterName: { type: String, default: "" },
showOperator: { type: Boolean, default: true },
options: { type: Object, default: () => {} }
},
data() {
return {
drawer: false,
operator: config.operator,
fields: this.options,
filter: [],
myFilter: [],
filterObjLength: 0,
saveLoading: false
}
},
computed: {
filterObj(){
const obj = {}
this.filter.forEach((item) => {
obj[item.field.value] = this.showOperator ? `${item.value}${config.separator}${item.operator}` : `${item.value}`
})
return obj
}
},
mounted(){
//默认显示的过滤项
this.fields.forEach((item) => {
if(item.selected){
this.filter.push({
field: item,
operator: item.operator || 'include',
value: ''
})
}
})
},
methods: {
//打开过滤器
openFilter(){
this.drawer = true
},
//增加过滤项
addFilter(){
if(this.fields.length<=0){
this.$message.warning('无过滤项');
return false
}
const filterNum = this.fields[this.filter.length] || this.fields[0]
this.filter.push({
field: filterNum,
operator: filterNum.operator || 'include',
value: ''
})
},
//删除过滤项
delFilter(index){
this.filter.splice(index, 1)
},
//过滤项字段变更事件
fieldChange(tr){
let oldType = tr.field.type
tr.field.type = ''
this.$nextTick(() => {
tr.field.type = oldType
})
tr.operator = tr.field.operator || 'include'
tr.value = ''
},
//下拉框显示事件处理异步
async visibleChange(isopen, item){
if(isopen && item.field.extend.request && !item.field.extend.remote){
item.selectLoading = true;
try {
var data = await item.field.extend.request()
}catch (error) {
console.log(error);
}
item.field.extend.data = data;
item.selectLoading = false;
}
},
//下拉框显示事件处理异步搜索
async remoteMethod(query, item){
if(query !== ''){
item.selectLoading = true;
try {
var data = await item.field.extend.request(query);
}catch (error) {
console.log(error);
}
item.field.extend.data = data;
item.selectLoading = false;
}else{
item.field.extend.data = [];
}
},
//选择常用过滤
selectMyfilter(item){
//常用过滤回显当前过滤项
this.filter = []
this.fields.forEach((field) => {
var filterValue = item.filterObj[field.value]
if(filterValue){
var operator = filterValue.split("|")[1]
var value = filterValue.split("|")[0]
if(field.type=='select' && field.extend.multiple){
value = value.split(",")
}else if(field.type=='daterange'){
value = value.split(",")
}
this.filter.push({
field: field,
operator: operator,
value: value
})
}
})
this.filterObjLength = Object.keys(item.filterObj).length
this.$emit('filterChange',item.filterObj)
this.drawer = false
},
//立即过滤
ok(){
this.filterObjLength = this.filter.length
this.$emit('filterChange',this.filterObj)
this.drawer = false
},
//保存常用
saveMy(){
this.$prompt('常用过滤名称', '另存为常用', {
inputPlaceholder: '请输入识别度较高的常用过滤名称',
inputPattern: /\S/,
inputErrorMessage: '名称不能为空'
})
.then(async ({ value }) => {
this.saveLoading = true
const saveObj = {
title: value,
filterObj: this.filterObj
}
try {
var save = await config.saveMy(this.filterName, saveObj)
}catch (error) {
this.saveLoading = false
console.log(error);
return false
}
if(!save){
return false
}
this.myFilter.push(saveObj)
this.$message.success(`${this.filterName} 保存常用成功`)
this.saveLoading = false
})
.catch(() => {
//
})
},
//清空过滤
clear(){
this.filter = []
this.filterObjLength = 0
this.$emit('filterChange',this.filterObj)
}
}
}
</script>
<style scoped>
.tabs-label {padding:0 20px;}
.nodata {height:46px;line-height: 46px;margin:15px 0;border: 1px dashed #e6e6e6;color: #999;text-align: center;border-radius: 3px;}
.sc-filter-main {padding:20px;border-bottom: 1px solid #e6e6e6;background: #fff;}
.sc-filter-main h2 {font-size: 12px;color: #999;font-weight: normal;}
.sc-filter-main table {width: 100%;margin: 15px 0;}
.sc-filter-main table tr {}
.sc-filter-main table td {padding:5px 10px 5px 0;}
.sc-filter-main table td:deep(.el-input .el-input__inner) {vertical-align: top;}
.sc-filter-main table td .el-select {display: block;}
.sc-filter-main table td .el-date-editor.el-input {display: block;width: 100%;}
.sc-filter-main table td .del {background: #fff;color: #999;width: 32px;height: 32px;line-height: 32px;text-align: center;border-radius:50%;font-size: 12px;cursor: pointer;}
.sc-filter-main table td .del:hover {background: #F56C6C;color: #fff;}
.root {display: flex;height: 100%;flex-direction: column}
.root:deep(.el-tabs__header) {margin: 0;}
.root:deep(.el-tabs__content) {flex: 1;background: #f6f8f9;}
.root:deep(.el-tabs__content) .el-tab-pane{overflow: auto;height:100%;}
[data-theme='dark'] .root:deep(.el-tabs__content) {background: none;}
[data-theme='dark'] .sc-filter-main {background: none;border-color:var(--el-border-color-base);}
[data-theme='dark'] .sc-filter-main table td .del {background: none;}
[data-theme='dark'] .sc-filter-main table td .del:hover {background: #F56C6C;}
[data-theme='dark'] .nodata {border-color:var(--el-border-color-base);}
</style>
+112
View File
@@ -0,0 +1,112 @@
<!--
* @Descripttion: 过滤器V2 常用组件
* @version: 2.0
* @Author: sakuya
* @Date: 2021年7月31日16:49:56
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-filter-my">
<div v-if="loading" class="sc-filter-my-loading">
<el-skeleton :rows="2" animated />
</div>
<template v-else>
<el-empty v-if="myFilter.length<=0" :image-size="100">
<template #description>
<h2>没有常用的过滤</h2>
<p style="margin-top: 10px;max-width: 300px;">常用过滤可以将多个过滤条件保存为一个集合方便下次进行相同条件的过滤</p>
</template>
</el-empty>
<ul v-else class="sc-filter-my-list">
<h2>我的常用过滤</h2>
<li v-for="(item, index) in myFilter" :key="index" @click="selectMyfilter(item)">
<label>{{item.title}}</label>
<el-popconfirm title="确认删除此常用过滤吗?" @confirm="closeMyfilter(item, index)">
<template #reference>
<el-icon class="del" @click.stop="()=>{}"><el-icon-delete /></el-icon>
</template>
</el-popconfirm>
</li>
</ul>
</template>
</div>
</template>
<script>
import config from "@/config/filterBar";
export default {
props: {
filterName: { type: String, default: "" },
data: { type: Object, default: () => {} }
},
data() {
return {
loading: false,
myFilter: []
}
},
watch:{
data: {
handler(){
this.myFilter = this.data
},
deep: true
}
},
mounted() {
this.myFilter = this.data
this.getMyfilter()
},
methods: {
//选择常用过滤
selectMyfilter(item){
this.$emit('selectMyfilter', item)
},
//删除常用过滤
async closeMyfilter(item, index){
try {
var del = await config.delMy(this.filterName)
}catch (error) {
return false
}
if(!del){
return false
}
this.myFilter.splice(index, 1)
this.$message.success('删除常用成功')
},
//远程获取我的常用
async getMyfilter(){
this.loading = true
try {
this.myFilter = await config.getMy(this.filterName)
}catch (error) {
return false
}
this.loading = false
}
}
}
</script>
<style scoped>
.sc-filter-my {}
.sc-filter-my-loading {padding:15px;}
.sc-filter-my-list {list-style-type: none;background: #fff;border-bottom: 1px solid #e6e6e6;}
.sc-filter-my-list h2 {font-size: 12px;color: #999;font-weight: normal;padding:20px;}
.sc-filter-my-list li {padding:12px 20px;cursor: pointer;position: relative;color: #3c4a54;padding-right:80px;}
.sc-filter-my-list li:hover {background: #ecf5ff;color: #409EFF;}
.sc-filter-my-list li label {cursor: pointer;font-size: 14px;line-height: 1.8;}
.sc-filter-my-list li label span {color: #999;margin-right: 10px;}
.sc-filter-my-list li .del {position: absolute;right:20px;top:8px;border-radius:50%;width: 32px;height: 32px;display: flex;align-items: center;justify-content: center;color: #999;}
.sc-filter-my-list li .del:hover {background: #F56C6C;color: #fff;}
[data-theme='dark'] .sc-filter-my .el-empty h2 {color: #fff;}
[data-theme='dark'] .sc-filter-my-list {background: none;border-color:var(--el-border-color-base);}
[data-theme='dark'] .sc-filter-my-list li {color: #d0d0d0;}
[data-theme='dark'] .sc-filter-my-list li:hover {background: var(--el-color-white);}
</style>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
<!--
* @Descripttion: 二次封装el-select 支持拼音
* @version: 1.0
* @Author: sakuya
* @Date: 2021年7月31日22:26:56
* @LastEditors:
* @LastEditTime:
-->
<template>
<el-select v-bind="$attrs" :filter-method="filterMethod" @visible-change="visibleChange">
<el-option v-for="field in optionsList" :key="field.value" :label="field.label" :value="field"></el-option>
</el-select>
</template>
<script>
import pinyin from './pinyin'
export default {
props: {
options: { type: Array, default: () => [] }
},
data() {
return {
optionsList: [],
optionsList_: []
}
},
mounted() {
this.optionsList = this.options
this.optionsList_ = [...this.options]
},
methods: {
filterMethod(keyword){
if(keyword){
this.optionsList = this.optionsList_
this.optionsList = this.optionsList.filter((item) =>
pinyin.match(item.label, keyword)
);
}else{
this.optionsList = this.optionsList_
}
},
visibleChange(isopen){
if(isopen){
this.optionsList = this.optionsList_
}
}
}
}
</script>
+272
View File
@@ -0,0 +1,272 @@
<!--
* @Descripttion: 动态表单渲染器
* @version: 1.0
* @Author: sakuya
* @Date: 2021年9月22日09:26:25
* @LastEditors:
* @LastEditTime:
-->
<template>
<el-skeleton v-if="renderLoading || Object.keys(form).length==0" animated />
<el-form v-else ref="form" :model="form" :label-width="config.labelWidth" :label-position="config.labelPosition" v-loading="loading" element-loading-text="Loading...">
<el-row :gutter="15">
<template v-for="(item, index) in config.formItems" :key="index">
<el-col :span="item.span || 24" v-if="!hideHandle(item)">
<sc-title v-if="item.component=='title'" :title="item.label"></sc-title>
<el-form-item v-else :prop="item.name" :rules="rulesHandle(item)">
<template #label>
{{item.label}}
<el-tooltip v-if="item.tips" :content="item.tips">
<el-icon><el-icon-question-filled /></el-icon>
</el-tooltip>
</template>
<!-- input -->
<template v-if="item.component=='input'" >
<el-input v-model="form[item.name]" :placeholder="item.options.placeholder" clearable :maxlength="item.options.maxlength" show-word-limit></el-input>
</template>
<!-- checkbox -->
<template v-else-if="item.component=='checkbox'" >
<template v-if="item.name" >
<el-checkbox v-model="form[item.name][_item.name]" :label="_item.label" v-for="(_item, _index) in item.options.items" :key="_index"></el-checkbox>
</template>
<template v-else >
<el-checkbox v-model="form[_item.name]" :label="_item.label" v-for="(_item, _index) in item.options.items" :key="_index"></el-checkbox>
</template>
</template>
<!-- checkboxGroup -->
<template v-else-if="item.component=='checkboxGroup'" >
<el-checkbox-group v-model="form[item.name]">
<el-checkbox v-for="_item in item.options.items" :key="_item.value" :label="_item.value">{{_item.label}}</el-checkbox>
</el-checkbox-group>
</template>
<!-- upload -->
<template v-else-if="item.component=='upload'" >
<el-col v-for="(_item, _index) in item.options.items" :key="_index">
<el-form-item :prop="_item.name">
<sc-upload v-model="form[_item.name]" :title="_item.label"></sc-upload>
</el-form-item>
</el-col>
</template>
<!-- switch -->
<template v-else-if="item.component=='switch'" >
<el-switch v-model="form[item.name]" />
</template>
<!-- select -->
<template v-else-if="item.component=='select'" >
<el-select v-model="form[item.name]" :multiple="item.options.multiple" :placeholder="item.options.placeholder" clearable filterable style="width: 100%;">
<el-option v-for="option in item.options.items" :key="option.value" :label="option.label" :value="option.value"></el-option>
</el-select>
</template>
<!-- cascader -->
<template v-else-if="item.component=='cascader'" >
<el-cascader v-model="form[item.name]" :options="item.options.items" clearable></el-cascader>
</template>
<!-- date -->
<template v-else-if="item.component=='date'" >
<el-date-picker v-model="form[item.name]" :type="item.options.type" :shortcuts="item.options.shortcuts" :default-time="item.options.defaultTime" :value-format="item.options.valueFormat" :placeholder="item.options.placeholder || '请选择'"></el-date-picker>
</template>
<!-- number -->
<template v-else-if="item.component=='number'" >
<el-input-number v-model="form[item.name]" controls-position="right"></el-input-number>
</template>
<!-- radio -->
<template v-else-if="item.component=='radio'" >
<el-radio-group v-model="form[item.name]">
<el-radio v-for="_item in item.options.items" :key="_item.value" :label="_item.value">{{_item.label}}</el-radio>
</el-radio-group>
</template>
<!-- color -->
<template v-else-if="item.component=='color'" >
<el-color-picker v-model="form[item.name]" />
</template>
<!-- rate -->
<template v-else-if="item.component=='rate'" >
<el-rate style="margin-top: 6px;" v-model="form[item.name]"></el-rate>
</template>
<!-- slider -->
<template v-else-if="item.component=='slider'" >
<el-slider v-model="form[item.name]" :marks="item.options.marks"></el-slider>
</template>
<!-- tableselect -->
<template v-else-if="item.component=='tableselect'" >
<tableselect-render v-model="form[item.name]" :item="item"></tableselect-render>
</template>
<!-- editor -->
<template v-else-if="item.component=='editor'" >
<sc-editor v-model="form[item.name]" placeholder="请输入" :height="400"></sc-editor>
</template>
<!-- noComponent -->
<template v-else>
<el-tag type="danger">[{{item.component}}] Component not found</el-tag>
</template>
<div v-if="item.message" class="el-form-item-msg">{{item.message}}</div>
</el-form-item>
</el-col>
</template>
<el-col :span="24">
<el-form-item>
<slot>
<el-button type="primary" @click="submit">提交</el-button>
</slot>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
import http from "@/utils/request"
import { defineAsyncComponent } from 'vue'
const tableselectRender = defineAsyncComponent(() => import('./items/tableselect'))
const scEditor = defineAsyncComponent(() => import('@/components/scEditor'))
export default {
props: {
modelValue: { type: Object, default: () => {} },
config: { type: Object, default: () => {} },
loading: { type: Boolean, default: false },
},
components: {
tableselectRender,
scEditor
},
data() {
return {
form: {},
renderLoading: false
}
},
watch:{
modelValue(){
if(this.hasConfig){
this.deepMerge(this.form, this.modelValue)
}
},
config(){
this.render()
},
form:{
handler(val){
this.$emit("update:modelValue", val)
},
deep: true
}
},
computed: {
hasConfig(){
return Object.keys(this.config).length>0
},
hasValue(){
return Object.keys(this.modelValue).length>0
}
},
created() {
},
mounted() {
if(this.hasConfig){
this.render()
}
},
methods: {
//构建form对象
render() {
this.config.formItems.forEach((item) => {
if(item.component == 'checkbox'){
if(item.name){
const value = {}
item.options.items.forEach((option) => {
value[option.name] = option.value
})
this.form[item.name] = value
}else{
item.options.items.forEach((option) => {
this.form[option.name] = option.value
})
}
}else if(item.component == 'upload'){
if(item.name){
const value = {}
item.options.items.forEach((option) => {
value[option.name] = option.value
})
this.form[item.name] = value
}else{
item.options.items.forEach((option) => {
this.form[option.name] = option.value
})
}
}else{
this.form[item.name] = item.value
}
})
if(this.hasValue){
this.form = this.deepMerge(this.form, this.modelValue)
}
this.getData()
},
//处理远程选项数据
getData() {
this.renderLoading = true
var remoteData = []
this.config.formItems.forEach((item) => {
if(item.options && item.options.remote){
var req = http.get(item.options.remote.api, item.options.remote.data).then(res=>{
item.options.items = res.data
})
remoteData.push(req)
}
})
Promise.all(remoteData).then(()=>{
this.renderLoading = false
})
},
//合并深结构对象
deepMerge(obj1, obj2) {
let key;
for (key in obj2) {
obj1[key] = obj1[key] && obj1[key].toString() === "[object Object]" && (obj2[key] && obj2[key].toString() === "[object Object]") ? this.deepMerge(obj1[key], obj2[key]) : (obj1[key] = obj2[key])
}
return obj1
//return JSON.parse(JSON.stringify(obj1))
},
//处理动态隐藏
hideHandle(item){
if(item.hideHandle){
const exp = eval(item.hideHandle.replace(/\$/g,"this.form"))
return exp
}
return false
},
//处理动态必填
rulesHandle(item){
if(item.requiredHandle){
const exp = eval(item.requiredHandle.replace(/\$/g,"this.form"))
var requiredRule = item.rules.find(t => 'required' in t)
requiredRule.required = exp
}
return item.rules
},
//数据验证
validate(valid, obj){
return this.$refs.form.validate(valid, obj)
},
scrollToField(prop){
return this.$refs.form.scrollToField(prop)
},
resetFields(){
return this.$refs.form.resetFields()
},
//提交
submit(){
this.$emit("submit", this.form)
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,37 @@
<template>
<sc-table-select v-model="value" :apiObj="apiObj" :table-width="600" :multiple="item.options.multiple" :props="item.options.props" style="width: 100%;">
<el-table-column v-for="(_item, _index) in item.options.column" :key="_index" :prop="_item.prop" :label="_item.label" :width="_item.width"></el-table-column>
</sc-table-select>
</template>
<script>
export default {
name: 'uploadRender',
props: {
modelValue: [String, Number, Boolean, Date, Object, Array],
item: { type: Object, default: () => {} }
},
data() {
return {
value: this.modelValue,
apiObj: this.getApiObj()
}
},
watch:{
value(val){
this.$emit("update:modelValue", val)
}
},
mounted() {
},
methods: {
getApiObj(){
return eval(`this.`+this.item.options.apiObj)
}
}
}
</script>
<style>
</style>
+98
View File
@@ -0,0 +1,98 @@
<template>
<div class="sc-form-table">
<el-table :data="data" ref="table" :key="toggleIndex" border stripe>
<el-table-column type="index" width="50" fixed="left">
<template #header>
<el-button type="primary" icon="el-icon-plus" size="small" circle @click="rowAdd"></el-button>
</template>
<template #default="scope">
<div class="sc-form-table-handle">
<span>{{scope.$index + 1}}</span>
<el-button type="danger" icon="el-icon-delete" size="small" plain circle @click="rowDel(scope.row, scope.$index)"></el-button>
</div>
</template>
</el-table-column>
<el-table-column label="" width="58" v-if="dragSort">
<template #default>
<el-tag class="move" style="cursor: move;"><el-icon-d-caret style="width: 1em; height: 1em;"/></el-tag>
</template>
</el-table-column>
<slot></slot>
<el-table-column min-width="1"></el-table-column>
<template #empty>
{{placeholder}}
</template>
</el-table>
</div>
</template>
<script>
import Sortable from 'sortablejs'
export default {
props: {
modelValue: { type: Array, default: () => [] },
addTemplate: { type: Object, default: () => {} },
placeholder: { type: String, default: "暂无数据" },
dragSort: { type: Boolean, default: false }
},
data(){
return {
data: [],
toggleIndex: 0
}
},
mounted(){
this.data = this.modelValue
if(this.dragSort){
this.rowDrop()
}
},
watch:{
modelValue(){
this.data = this.modelValue
},
data: {
handler(){
this.$emit('update:modelValue', this.data);
},
deep: true
}
},
methods: {
rowDrop(){
const _this = this
const tbody = this.$refs.table.$el.querySelector('.el-table__body-wrapper tbody')
Sortable.create(tbody, {
handle: ".move",
animation: 300,
ghostClass: "ghost",
onEnd({ newIndex, oldIndex }) {
const tableData = _this.data
const currRow = tableData.splice(oldIndex, 1)[0]
tableData.splice(newIndex, 0, currRow)
_this.toggleIndex += 1
_this.$nextTick(() => {
_this.rowDrop()
})
}
})
},
rowAdd(){
const temp = JSON.parse(JSON.stringify(this.addTemplate))
this.data.push(temp)
},
rowDel(row, index){
this.data.splice(index, 1)
}
}
}
</script>
<style scoped>
.sc-form-table .sc-form-table-handle {text-align: center;}
.sc-form-table .sc-form-table-handle span {display: inline-block;}
.sc-form-table .sc-form-table-handle button {display: none;}
.sc-form-table .hover-row .sc-form-table-handle span {display: none;}
.sc-form-table .hover-row .sc-form-table-handle button {display: inline-block;}
</style>
+94
View File
@@ -0,0 +1,94 @@
<!--
* @Descripttion: 图标选择器组件
* @version: 1.3
* @Author: sakuya
* @Date: 2021年7月27日10:02:46
* @LastEditors: sakuya
* @LastEditTime: 2022年2月8日15:47:13
-->
<template>
<div class="sc-icon-select">
<el-input v-model="defaultValue" :prefix-icon="defaultValue||'none'" :placeholder="placeholder" :clearable="clearable" :disabled="disabled">
<template #append><el-button icon="el-icon-more-filled" @click="open"></el-button></template>
</el-input>
<el-dialog title="图标选择器" v-model="dialogVisible" :width="780" destroy-on-close>
<el-tabs style="margin-top: -30px;">
<el-tab-pane v-for="item in data" :key="item.name" lazy>
<template #label>
{{item.name}} <el-tag size="small" type="info">{{item.icons.length}}</el-tag>
</template>
<div class="sc-icon-select__list">
<el-scrollbar>
<ul @click="selectIcon">
<li v-for="icon in item.icons" :key="icon">
<span :data-icon="icon"></span>
<el-icon><component :is="icon" /></el-icon>
</li>
</ul>
</el-scrollbar>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</div>
</template>
<script>
import config from "@/config/iconSelect"
export default {
props: {
modelValue: { type: String, default: "" },
placeholder: { type: String, default: "请输入或者选择图标" },
clearable: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
},
data() {
return {
defaultValue: '',
dialogVisible: false,
data: []
}
},
watch:{
modelValue(val){
this.defaultValue = val
},
defaultValue(val){
this.$emit('update:modelValue', val)
}
},
mounted() {
this.defaultValue = this.modelValue
this.data.push(...config.icons)
},
methods: {
open(){
if(this.disabled){
return false
}
this.dialogVisible = true
},
selectIcon(e){
if(e.target.tagName != 'SPAN'){
return false
}
this.defaultValue = e.target.dataset.icon
this.dialogVisible = false
this.$emit('update:modelValue', this.defaultValue);
}
}
}
</script>
<style scoped>
.sc-icon-select {display: inline-block;}
.sc-icon-select__list {height:360px;overflow: auto;}
.sc-icon-select__list ul {}
.sc-icon-select__list li {display: inline-block;width:80px;height:80px;margin:5px;vertical-align: top;box-shadow: 0 0 0 1px #eee;transition: all 0.1s;border-radius: 4px;position: relative;}
.sc-icon-select__list li span {position: absolute;top:0;left:0;right:0;bottom:0;z-index: 1;cursor: pointer;}
.sc-icon-select__list li i {display: inline-block;width: 100%;height:100%;font-size: 26px;color: #6d7882;background: #fff;display: flex;justify-content: center;align-items: center;border-radius: 4px;}
.sc-icon-select__list li:hover {box-shadow: 0 0 1px 4px rgba(64,158,255,1);}
.sc-icon-select__list li:hover i {color: #409EFF;}
</style>
@@ -0,0 +1,47 @@
<!--
* @Descripttion: 状态指示器
* @version: 1.0
* @Author: sakuya
* @Date: 2021年11月11日09:30:12
* @LastEditors:
* @LastEditTime:
-->
<template>
<span class="sc-state" :class="[{'sc-status-processing':pulse}, 'sc-state-bg--'+type]"></span>
</template>
<script>
export default {
props: {
type: { type: String, default: "primary" },
pulse: { type: Boolean, default: false }
}
}
</script>
<style scoped>
.sc-state {display: inline-block;background: #000;width: 8px;height: 8px;border-radius: 50%;vertical-align: middle;}
.sc-status-processing {position: relative;}
.sc-status-processing:after {position: absolute;top:0px;left:0px;width: 100%;height: 100%;border-radius: 50%;background: inherit;content: '';animation: warn 1.2s ease-in-out infinite;}
.sc-state-bg--primary {background: var(--el-color-primary);}
.sc-state-bg--success {background: var(--el-color-success);}
.sc-state-bg--warning {background: var(--el-color-warning);}
.sc-state-bg--danger {background: var(--el-color-danger);}
.sc-state-bg--info {background: var(--el-color-info);}
@keyframes warn {
0% {
transform: scale(0.5);
opacity: 1;
}
30% {
opacity: 0.7;
}
100% {
transform: scale(2.5);
opacity: 0;
}
}
</style>
+66
View File
@@ -0,0 +1,66 @@
<!--
* @Descripttion: 趋势标记
* @version: 1.0
* @Author: sakuya
* @Date: 2021年11月11日11:07:10
* @LastEditors:
* @LastEditTime:
-->
<template>
<span class="sc-trend" :class="'sc-trend--'+type">
<el-icon v-if="iconType=='P'" class="sc-trend-icon"><el-icon-top /></el-icon>
<el-icon v-if="iconType=='N'" class="sc-trend-icon"><el-icon-bottom /></el-icon>
<el-icon v-if="iconType=='Z'" class="sc-trend-icon"><el-icon-right /></el-icon>
<em class="sc-trend-prefix">{{prefix}}</em>
<em class="sc-trend-value">{{modelValue}}</em>
<em class="sc-trend-suffix">{{suffix}}</em>
</span>
</template>
<script>
export default {
props: {
modelValue: { type: Number, default: 0 },
prefix: { type: String, default: "" },
suffix: { type: String, default: "" },
reverse: { type: Boolean, default: false }
},
computed: {
absValue(){
return Math.abs(this.modelValue);
},
iconType(v){
if(this.modelValue == 0){
v = 'Z'
}else if(this.modelValue < 0){
v = 'N'
}else if(this.modelValue > 0){
v = 'P'
}
return v
},
type(v){
if(this.modelValue == 0){
v = 'Z'
}else if(this.modelValue < 0){
v = this.reverse?'P':'N'
}else if(this.modelValue > 0){
v = this.reverse?'N':'P'
}
return v
}
}
}
</script>
<style scoped>
.sc-trend {display: flex;align-items: center;}
.sc-trend-icon {margin-right: 2px;}
.sc-trend em {font-style: normal;}
.sc-trend-prefix {margin-right: 2px;}
.sc-trend-suffix {margin-left: 2px;}
.sc-trend--P {color: #f56c6c;}
.sc-trend--N {color: #67c23a;}
.sc-trend--Z {color: #555;}
</style>
+52
View File
@@ -0,0 +1,52 @@
<!--
* @Descripttion: 页面头部样式组件
* @version: 1.0
* @Author: sakuya
* @Date: 2021年7月20日08:49:07
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-page-header">
<div v-if="icon" class="sc-page-header__icon">
<span>
<el-icon><component :is="icon" /></el-icon>
</span>
</div>
<div class="sc-page-header__title">
<h2>{{ title }}</h2>
<p v-if="description || $slots.default">
<slot>
{{ description }}
</slot>
</p>
</div>
<div v-if="$slots.main" class="sc-page-header__main">
<slot name="main"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
title: { type: String, required: true, default: "" },
description: { type: String, default: "" },
icon: { type: String, default: "" },
}
}
</script>
<style scoped>
.sc-page-header {background: #fff;border-bottom: 1px solid #e6e6e6;padding:20px 25px;display: flex;}
.sc-page-header__icon {width: 50px;}
.sc-page-header__icon span {display: inline-block;width: 30px;height: 30px;background: #409EFF;border-radius: 40%;display: flex;align-items: center;justify-content: center;}
.sc-page-header__icon span i {color: #fff;font-size: 14px;}
.sc-page-header__title {flex: 1;}
.sc-page-header__title h2 {font-size: 17px;color: #3c4a54;font-weight: bold;margin-top: 3px;}
.sc-page-header__title p {font-size: 13px;color: #999;margin-top: 15px;}
[data-theme='dark'] .sc-page-header {background:#2b2b2b ;border-color:var(--el-border-color-base);}
[data-theme='dark'] .sc-page-header__title h2 {color: #d0d0d0;}
</style>
+88
View File
@@ -0,0 +1,88 @@
<!--
* @Descripttion: 生成二维码组件
* @version: 1.0
* @Author: sakuya
* @Date: 2021年12月20日14:22:20
* @LastEditors:
* @LastEditTime:
-->
<template>
<img ref="img"/>
</template>
<script>
import QRcode from "qrcodejs2"
export default {
props: {
text: { type: String, required: true, default: "" },
size: { type: Number, default: 100 },
logo: { type: String, default: "" },
logoSize: { type: Number, default: 30 },
logoPadding: { type: Number, default: 5 },
colorDark: { type: String, default: "#000000" },
colorLight: { type: String, default: "#ffffff" },
correctLevel: { type: Number, default: 2 },
},
data() {
return {
qrcode: null
}
},
watch:{
text(){
this.draw()
}
},
mounted() {
this.draw()
},
methods: {
//创建原始二维码DOM
async create(){
return new Promise((resolve) => {
var element = document.createElement("div");
new QRcode(element, {
text: this.text,
width: this.size,
height: this.size,
colorDark: this.colorDark,
colorLight: this.colorLight,
correctLevel: this.correctLevel
})
if (element.getElementsByTagName("canvas")[0]) {
this.qrcode = element
resolve()
}
})
},
//绘制LOGO
async drawLogo(){
return new Promise((resolve) => {
var logo = new Image()
logo.src = this.logo
const logoPos = (this.size - this.logoSize) / 2
const rectSize = this.logoSize + this.logoPadding
const rectPos = (this.size - rectSize) / 2
var ctx = this.qrcode.getElementsByTagName("canvas")[0].getContext("2d")
logo.onload = ()=>{
ctx.fillRect(rectPos, rectPos, rectSize, rectSize)
ctx.drawImage(logo, logoPos, logoPos, this.logoSize, this.logoSize)
resolve()
}
})
},
async draw(){
await this.create()
if(this.logo){
await this.drawLogo()
}
this.$refs.img.src = this.qrcode.getElementsByTagName("canvas")[0].toDataURL("image/png")
},
}
}
</script>
<style>
</style>
+80
View File
@@ -0,0 +1,80 @@
<!--
* @Descripttion: 异步选择器
* @version: 1.0
* @Author: sakuya
* @Date: 2021年8月3日15:53:37
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-select">
<div v-if="initloading" class="sc-select-loading">
<el-icon class="is-loading"><el-icon-loading /></el-icon>
</div>
<el-select v-bind="$attrs" :loading="loading" @visible-change="visibleChange">
<el-option v-for="item in options" :key="item[props.value]" :label="item[props.label]" :value="item[props.value]">
<slot name="option" :data="item"></slot>
</el-option>
</el-select>
</div>
</template>
<script>
import config from "@/config/select";
export default {
props: {
apiObj: { type: Object, default: () => {} },
dic: { type: String, default: "" },
params: { type: Object, default: () => ({}) }
},
data() {
return {
dicParams: this.params,
loading: false,
options: [],
props: config.props,
initloading: false
}
},
created() {
//如果有默认值就去请求接口获取options
if(this.$attrs.modelValue && this.$attrs.modelValue.length > 0){
this.initloading = true
this.getRemoteData()
}
},
methods: {
//选项显示隐藏事件
visibleChange(ispoen){
if(ispoen && this.options.length==0 && (this.dic || this.apiObj)){
this.getRemoteData()
}
},
//获取数据
async getRemoteData(){
this.loading = true
this.dicParams[config.request.name] = this.dic
var res = {}
if(this.apiObj){
res = await this.apiObj.get(this.params)
}else if(this.dic){
res = await config.dicApiObj.get(this.params)
}
var response = config.parseData(res)
this.options = response.data
this.loading = false
this.initloading = false
}
}
}
</script>
<style scoped>
.sc-select {display: inline-block;position: relative;}
.sc-select-loading {position: absolute;top:0;left:0;right:0;bottom:0;background: #fff;z-index: 100;border-radius: 5px;border: 1px solid #EBEEF5;display: flex;align-items: center;padding-left:10px;}
.sc-select-loading i {font-size: 14px;}
[data-theme='dark'] .sc-select-loading {background: var(--el-color-white);border-color: var(--el-border-color-base) ;}
</style>
+70
View File
@@ -0,0 +1,70 @@
<!--
* @Descripttion: 统计数值组件
* @version: 1.0
* @Author: sakuya
* @Date: 2021年6月23日13:11:32
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-statistic">
<div class="sc-statistic-title">
{{ title }}
<el-tooltip v-if="tips" effect="light">
<template #content>
<div style="width: 200px;line-height: 2;">
{{ tips }}
</div>
</template>
<el-icon class="sc-statistic-tips"><el-icon-question-filled/></el-icon>
</el-tooltip>
</div>
<div class="sc-statistic-content">
<span v-if="prefix" class="sc-statistic-content-prefix">{{ prefix }}</span>
<span class="sc-statistic-content-value">{{ cmtValue }}</span>
<span v-if="suffix" class="sc-statistic-content-suffix">{{ suffix }}</span>
</div>
<div v-if="description || $slots.default" class="sc-statistic-description">
<slot>
{{ description }}
</slot>
</div>
</div>
</template>
<script>
export default {
props: {
title: { type: String, required: true, default: "" },
value: { type: String, required: true, default: "" },
prefix: { type: String, default: "" },
suffix: { type: String, default: "" },
description: { type: String, default: "" },
tips: { type: String, default: "" },
groupSeparator: { type: Boolean, default: false }
},
data() {
return {
}
},
computed: {
cmtValue(){
return this.groupSeparator ? this.$TOOL.groupSeparator(this.value) : this.value
}
}
}
</script>
<style scoped>
.sc-statistic-title {font-size: 12px;color: #999;margin-bottom: 10px;display: flex;align-items: center;}
.sc-statistic-tips {margin-left: 5px;}
.sc-statistic-content {font-size: 20px;color: #333;}
.sc-statistic-content-value {font-weight: bold;}
.sc-statistic-content-prefix {margin-right: 5px;}
.sc-statistic-content-suffix {margin-left: 5px;font-size: 12px;}
.sc-statistic-description {margin-top: 10px;color: #999;}
[data-theme='dark'] .sc-statistic-content {color: #d0d0d0;}
</style>
+117
View File
@@ -0,0 +1,117 @@
<template>
<div v-if="usercolumn.length>0" class="setting-column" v-loading="isSave">
<div class="setting-column__title">
<span class="move_b"></span>
<span class="show_b">显示</span>
<span class="name_b">名称</span>
<span class="width_b">宽度</span>
<span class="sortable_b">排序</span>
<span class="fixed_b">固定</span>
</div>
<div class="setting-column__list" ref="list">
<ul>
<li v-for="item in usercolumn" :key="item.prop">
<span class="move_b">
<el-tag class="move" style="cursor: move;"><el-icon-d-caret style="width: 1em; height: 1em;"/></el-tag>
</span>
<span class="show_b">
<el-switch v-model="item.hide" :active-value="false" :inactive-value="true"></el-switch>
</span>
<span class="name_b" :title="item.prop">{{ item.label }}</span>
<span class="width_b">
<el-input v-model="item.width" placeholder="auto" size="small"></el-input>
</span>
<span class="sortable_b">
<el-switch v-model="item.sortable"></el-switch>
</span>
<span class="fixed_b">
<el-switch v-model="item.fixed"></el-switch>
</span>
</li>
</ul>
</div>
<div class="setting-column__bottom">
<el-button @click="backDefaul" :disabled="isSave">重置</el-button>
<el-button @click="save" type="primary">保存</el-button>
</div>
</div>
<el-empty v-else description="暂无可配置的列" :image-size="80"></el-empty>
</template>
<script>
import Sortable from 'sortablejs'
export default {
components: {
Sortable
},
props: {
column: { type: Object, default: () => {} }
},
data() {
return {
isSave: false,
usercolumn: JSON.parse(JSON.stringify(this.column||[]))
}
},
watch:{
usercolumn: {
handler(){
this.$emit('userChange', this.usercolumn)
},
deep: true
}
},
mounted() {
this.usercolumn.length>0 && this.rowDrop()
},
methods: {
rowDrop(){
const _this = this
const tbody = this.$refs.list.querySelector('ul')
Sortable.create(tbody, {
handle: ".move",
animation: 300,
ghostClass: "ghost",
onEnd({ newIndex, oldIndex }) {
const tableData = _this.usercolumn
const currRow = tableData.splice(oldIndex, 1)[0]
tableData.splice(newIndex, 0, currRow)
}
})
},
backDefaul(){
this.$emit('back', this.usercolumn)
},
save(){
this.$emit('save', this.usercolumn)
}
}
}
</script>
<style scoped>
.setting-column {}
.setting-column__title {border-bottom: 1px solid #EBEEF5;padding-bottom:15px;}
.setting-column__title span {display: inline-block;font-weight: bold;color: #909399;font-size: 12px;}
.setting-column__title span.move_b {width: 30px;margin-right:15px;}
.setting-column__title span.show_b {width: 60px;}
.setting-column__title span.name_b {width: 140px;}
.setting-column__title span.width_b {width: 60px;margin-right:15px;}
.setting-column__title span.sortable_b {width: 60px;}
.setting-column__title span.fixed_b {width: 60px;}
.setting-column__list {max-height:314px;overflow: auto;}
.setting-column__list li {list-style: none;margin:10px 0;display: flex;align-items: center;}
.setting-column__list li>span {display: inline-block;font-size: 12px;}
.setting-column__list li span.move_b {width: 30px;margin-right:15px;}
.setting-column__list li span.show_b {width: 60px;}
.setting-column__list li span.name_b {width: 140px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;cursor:default;}
.setting-column__list li span.width_b {width: 60px;margin-right:15px;}
.setting-column__list li span.sortable_b {width: 60px;}
.setting-column__list li span.fixed_b {width: 60px;}
.setting-column__list li.ghost {opacity: 0.3;}
.setting-column__bottom {border-top: 1px solid #EBEEF5;padding-top:15px;text-align: right;}
</style>
+353
View File
@@ -0,0 +1,353 @@
<!--
* @Descripttion: 数据表格组件
* @version: 1.7
* @Author: sakuya
* @Date: 2021年11月29日21:51:15
* @LastEditors: sakuya
* @LastEditTime: 2022年2月9日09:59:37
-->
<template>
<div class="scTable" :style="{'height':_height}" ref="scTableMain" v-loading="loading">
<div class="scTable-table">
<el-table v-bind="$attrs" :data="tableData" :row-key="rowKey" :key="toggleIndex" ref="scTable" :height="height=='auto'?null:'100%'" :size="config.size" :border="config.border" :stripe="config.stripe" :summary-method="remoteSummary?remoteSummaryMethod:summaryMethod" @sort-change="sortChange" @filter-change="filterChange">
<slot></slot>
<template v-for="(item, index) in userColumn" :key="index">
<el-table-column v-if="!item.hide" :column-key="item.prop" :label="item.label" :prop="item.prop" :width="item.width" :sortable="item.sortable" :fixed="item.fixed" :filters="item.filters" :filter-method="remoteFilter||!item.filters?null:filterHandler" :show-overflow-tooltip="item.showOverflowTooltip">
<template #default="scope">
<slot :name="item.prop" v-bind="scope">
{{scope.row[item.prop]}}
</slot>
</template>
</el-table-column>
</template>
<el-table-column min-width="1"></el-table-column>
<template #empty>
<el-empty :description="emptyText" :image-size="100"></el-empty>
</template>
</el-table>
</div>
<div class="scTable-page" v-if="!hidePagination&&!hideDo">
<div class="scTable-pagination">
<el-pagination v-if="!hidePagination" background :small="true" :layout="paginationLayout" :total="total" :page-size="pageSize" v-model:currentPage="currentPage" @current-change="paginationChange"></el-pagination>
</div>
<div class="scTable-do" v-if="!hideDo">
<el-button v-if="!hideRefresh" @click="refresh" icon="el-icon-refresh" circle style="margin-left:15px"></el-button>
<el-popover v-if="column" placement="top" title="列设置" :width="500" trigger="click" :hide-after="0" @show="customColumnShow=true" @after-leave="customColumnShow=false">
<template #reference>
<el-button icon="el-icon-set-up" circle style="margin-left:15px"></el-button>
</template>
<columnSetting v-if="customColumnShow" ref="columnSetting" @userChange="columnSettingChange" @save="columnSettingSave" @back="columnSettingBack" :column="userColumn"></columnSetting>
</el-popover>
<el-popover v-if="!hideSetting" placement="top" title="表格设置" :width="400" trigger="click" :hide-after="0">
<template #reference>
<el-button icon="el-icon-setting" circle style="margin-left:15px"></el-button>
</template>
<el-form label-width="80px" label-position="left">
<el-form-item label="表格尺寸">
<el-radio-group v-model="config.size" size="small" @change="configSizeChange">
<el-radio-button label="large"></el-radio-button>
<el-radio-button label="default">正常</el-radio-button>
<el-radio-button label="small"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="样式">
<el-checkbox v-model="config.border" label="纵向边框"></el-checkbox>
<el-checkbox v-model="config.stripe" label="斑马纹"></el-checkbox>
</el-form-item>
</el-form>
</el-popover>
</div>
</div>
</div>
</template>
<script>
import config from "@/config/table";
import columnSetting from './columnSetting'
export default {
name: 'scTable',
components: {
columnSetting
},
props: {
tableName: { type: String, default: "" },
apiObj: { type: Object, default: () => {} },
params: { type: Object, default: () => ({}) },
data: { type: Object, default: () => {} },
height: { type: [String,Number], default: "100%" },
size: { type: String, default: "default" },
border: { type: Boolean, default: false },
stripe: { type: Boolean, default: false },
pageSize: { type: Number, default: config.pageSize },
rowKey: { type: String, default: "" },
summaryMethod: { type: Function, default: null },
column: { type: Object, default: () => {} },
remoteSort: { type: Boolean, default: false },
remoteFilter: { type: Boolean, default: false },
remoteSummary: { type: Boolean, default: false },
hidePagination: { type: Boolean, default: false },
hideDo: { type: Boolean, default: false },
hideRefresh: { type: Boolean, default: false },
hideSetting: { type: Boolean, default: false },
paginationLayout: { type: String, default: "total, prev, pager, next, jumper" },
},
watch: {
//监听从props里拿到值了
data(){
this.tableData = this.data;
this.total = this.tableData.length;
},
apiObj(){
this.tableParams = this.params;
this.refresh();
}
},
computed: {
_height() {
return Number(this.height)?Number(this.height)+'px':this.height
}
},
data() {
return {
isActivat: true,
emptyText: "暂无数据",
toggleIndex: 0,
tableData: [],
total: 0,
currentPage: 1,
prop: null,
order: null,
loading: false,
tableHeight:'100%',
tableParams: this.params,
userColumn: [],
customColumnShow: false,
summary: {},
config: {
size: this.size,
border: this.border,
stripe: this.stripe
}
}
},
mounted() {
//判断是否开启自定义列
if(this.column){
this.getCustomColumn()
}else{
this.userColumn = this.column
}
//判断是否静态数据
if(this.apiObj){
this.getData();
}else if(this.data){
this.tableData = this.data;
this.total = this.tableData.length
}
},
activated(){
if(!this.isActivat){
this.$refs.scTable.doLayout()
}
},
deactivated(){
this.isActivat = false;
},
methods: {
//获取列
async getCustomColumn(){
const userColumn = await config.columnSettingGet(this.tableName, this.column)
this.userColumn = userColumn
},
//获取数据
async getData(){
this.loading = true;
var reqData = {
[config.request.page]: this.currentPage,
[config.request.pageSize]: this.pageSize,
[config.request.prop]: this.prop,
[config.request.order]: this.order
}
if(this.hidePagination){
delete reqData[config.request.page]
delete reqData[config.request.pageSize]
}
Object.assign(reqData, this.tableParams)
try {
var res = await this.apiObj.get(reqData);
}catch(error){
this.loading = false;
this.emptyText = error.statusText;
return false;
}
try {
var response = config.parseData(res);
}catch(error){
this.loading = false;
this.emptyText = "数据格式错误";
return false;
}
if(response.code != config.successCode){
this.loading = false;
this.emptyText = response.msg;
}else{
this.emptyText = "暂无数据";
if(this.hidePagination){
this.tableData = response.data || [];
}else{
this.tableData = response.rows || [];
}
this.total = response.total || 0;
this.summary = response.summary || {};
this.loading = false;
}
this.$refs.scTable.$el.querySelector('.el-table__body-wrapper').scrollTop = 0
this.$emit('dataChange', res, this.tableData)
},
//分页点击
paginationChange(){
this.getData();
},
//刷新数据
refresh(){
this.$refs.scTable.clearSelection();
this.getData();
},
//更新数据 合并上一次params
upData(params, page=1){
this.currentPage = page;
this.$refs.scTable.clearSelection();
Object.assign(this.tableParams, params || {})
this.getData()
},
//重载数据 替换params
reload(params, page=1){
this.currentPage = page;
this.tableParams = params || {}
this.$refs.scTable.clearSelection();
this.$refs.scTable.clearSort()
this.$refs.scTable.clearFilter()
this.getData()
},
//自定义变化事件
columnSettingChange(userColumn){
this.userColumn = userColumn;
this.toggleIndex += 1;
},
//自定义列保存
async columnSettingSave(userColumn){
this.$refs.columnSetting.isSave = true
try {
await config.columnSettingSave(this.tableName, userColumn)
}catch(error){
this.$message.error('保存失败')
this.$refs.columnSetting.isSave = false
}
this.$message.success('保存成功')
this.$refs.columnSetting.isSave = false
},
//自定义列重置
async columnSettingBack(){
this.$refs.columnSetting.isSave = true
try {
const column = await config.columnSettingReset(this.tableName, this.column)
this.userColumn = column
this.$refs.columnSetting.usercolumn = JSON.parse(JSON.stringify(this.userColumn||[]))
}catch(error){
this.$message.error('重置失败')
this.$refs.columnSetting.isSave = false
}
this.$refs.columnSetting.isSave = false
},
//排序事件
sortChange(obj){
if(!this.remoteSort){
return false
}
if(obj.column && obj.prop){
this.prop = obj.prop
this.order = obj.order
}else{
this.prop = null
this.order = null
}
this.getData()
},
//本地过滤
filterHandler(value, row, column){
const property = column.property;
return row[property] === value;
},
//过滤事件
filterChange(filters){
if(!this.remoteFilter){
return false
}
Object.keys(filters).forEach(key => {
filters[key] = filters[key].join(',')
})
this.upData(filters)
},
//远程合计行处理
remoteSummaryMethod(param){
const {columns} = param
const sums = []
columns.forEach((column, index) => {
if(index === 0) {
sums[index] = '合计'
return
}
const values = this.summary[column.property]
if(values){
sums[index] = values
}else{
sums[index] = ''
}
})
return sums
},
configSizeChange(){
this.$refs.scTable.doLayout()
},
//原生方法转发
clearSelection(){
this.$refs.scTable.clearSelection()
},
toggleRowSelection(row, selected){
this.$refs.scTable.toggleRowSelection(row, selected)
},
toggleAllSelection(){
this.$refs.scTable.toggleAllSelection()
},
toggleRowExpansion(row, expanded){
this.$refs.scTable.toggleRowExpansion(row, expanded)
},
setCurrentRow(row){
this.$refs.scTable.setCurrentRow(row)
},
clearSort(){
this.$refs.scTable.clearSort()
},
clearFilter(columnKey){
this.$refs.scTable.clearFilter(columnKey)
},
doLayout(){
this.$refs.scTable.doLayout()
},
sort(prop, order){
this.$refs.scTable.sort(prop, order)
}
}
}
</script>
<style scoped>
.scTable {}
.scTable-table {height: calc(100% - 50px);}
.scTable-page {height:50px;display: flex;align-items: center;justify-content: space-between;padding:0 15px;}
.scTable-do {white-space: nowrap;}
.scTable:deep(.el-table__footer) .cell {font-weight: bold;}
</style>
+229
View File
@@ -0,0 +1,229 @@
<!--
* @Descripttion: 表格选择器组件
* @version: 1.2
* @Author: sakuya
* @Date: 2021年6月10日10:04:07
* @LastEditors: sakuya
* @LastEditTime: 2022年2月28日09:39:03
-->
<template>
<el-select ref="select" v-model="defaultValue" clearable :multiple="multiple" filterable :placeholder="placeholder" :disabled="disabled" :filter-method="filterMethod" @remove-tag="removeTag" @visible-change="visibleChange" @clear="clear">
<template #empty>
<div class="sc-table-select__table" :style="{width: tableWidth+'px'}" v-loading="loading">
<div class="sc-table-select__header">
<slot name="header" :form="formData" :submit="formSubmit"></slot>
</div>
<el-table ref="table" :data="tableData" :height="245" :highlight-current-row="!multiple" @row-click="click" @select="select" @select-all="selectAll">
<el-table-column v-if="multiple" type="selection" width="45"></el-table-column>
<el-table-column v-else type="index" width="45">
<template #default="scope"><span>{{scope.$index+(currentPage - 1) * pageSize + 1}}</span></template>
</el-table-column>
<slot></slot>
</el-table>
<div class="sc-table-select__page">
<el-pagination small background layout="prev, pager, next" :total="total" :page-size="pageSize" v-model:currentPage="currentPage" @current-change="reload"></el-pagination>
</div>
</div>
</template>
</el-select>
</template>
<script>
import config from "@/config/tableSelect";
export default {
props: {
modelValue: null,
apiObj: { type: Object, default: () => {} },
params: { type: Object, default: () => {} },
placeholder: { type: String, default: "请选择" },
multiple: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
tableWidth: {type: Number, default: 400},
mode: { type: String, default: "popover" },
props: { type: Object, default: () => {} }
},
data() {
return {
loading: false,
keyword: null,
defaultValue: [],
tableData: [],
pageSize: config.pageSize,
total: 0,
currentPage: 1,
defaultProps: {
label: config.props.label,
value: config.props.value,
page: config.request.page,
pageSize: config.request.pageSize,
keyword: config.request.keyword
},
formData: {}
}
},
computed: {
},
watch: {
modelValue:{
handler(){
this.defaultValue = this.modelValue
this.autoCurrentLabel()
},
deep: true
}
},
mounted() {
this.defaultProps = Object.assign(this.defaultProps, this.props);
this.defaultValue = this.modelValue
this.autoCurrentLabel()
},
methods: {
//表格显示隐藏回调
visibleChange(visible){
if(visible){
this.currentPage = 1
this.keyword = null
this.formData = {}
this.getData()
}else{
this.autoCurrentLabel()
}
},
//获取表格数据
async getData(){
this.loading = true;
var reqData = {
[this.defaultProps.page]: this.currentPage,
[this.defaultProps.pageSize]: this.pageSize,
[this.defaultProps.keyword]: this.keyword
}
Object.assign(reqData, this.params, this.formData)
var res = await this.apiObj.get(reqData);
var parseData = config.parseData(res)
this.tableData = parseData.rows;
this.total = parseData.total;
this.loading = false;
//表格默认赋值
this.$nextTick(() => {
if(this.multiple){
this.defaultValue.forEach(row => {
var setrow = this.tableData.filter(item => item[this.defaultProps.value]===row[this.defaultProps.value] )
if(setrow.length > 0){
this.$refs.table.toggleRowSelection(setrow[0], true);
}
})
}else{
var setrow = this.tableData.filter(item => item[this.defaultProps.value]===this.defaultValue[this.defaultProps.value] )
this.$refs.table.setCurrentRow(setrow[0]);
}
this.$refs.table.$el.querySelector('.el-table__body-wrapper').scrollTop = 0
})
},
//插糟表单提交
formSubmit(){
this.currentPage = 1
this.keyword = null
this.getData()
},
//分页刷新表格
reload(){
this.getData()
},
//自动模拟options赋值
autoCurrentLabel(){
this.$nextTick(() => {
if(this.multiple){
this.$refs.select.selected.forEach(item => {
item.currentLabel = item.value[this.defaultProps.label]
})
}else{
this.$refs.select.selectedLabel = this.defaultValue[this.defaultProps.label]
}
})
},
//表格勾选事件
select(rows, row){
var isSelect = rows.length && rows.indexOf(row) !== -1
if(isSelect){
this.defaultValue.push(row)
}else{
this.defaultValue.splice(this.defaultValue.findIndex(item => item[this.defaultProps.value] == row[this.defaultProps.value]), 1)
}
this.autoCurrentLabel()
this.$emit('update:modelValue', this.defaultValue);
this.$emit('change', this.defaultValue);
},
//表格全选事件
selectAll(rows){
var isAllSelect = rows.length > 0
if(isAllSelect){
rows.forEach(row => {
var isHas = this.defaultValue.find(item => item[this.defaultProps.value] == row[this.defaultProps.value])
if(!isHas){
this.defaultValue.push(row)
}
})
}else{
this.tableData.forEach(row => {
var isHas = this.defaultValue.find(item => item[this.defaultProps.value] == row[this.defaultProps.value])
if(isHas){
this.defaultValue.splice(this.defaultValue.findIndex(item => item[this.defaultProps.value] == row[this.defaultProps.value]), 1)
}
})
}
this.autoCurrentLabel()
this.$emit('update:modelValue', this.defaultValue);
this.$emit('change', this.defaultValue);
},
click(row){
if(this.multiple){
//处理多选点击行
}else{
this.defaultValue = row
this.$refs.select.blur()
this.autoCurrentLabel()
this.$emit('update:modelValue', this.defaultValue);
this.$emit('change', this.defaultValue);
}
},
//tags删除后回调
removeTag(tag){
var row = this.findRowByKey(tag[this.defaultProps.value])
this.$refs.table.toggleRowSelection(row, false);
this.$emit('update:modelValue', this.defaultValue);
},
//清空后的回调
clear(){
this.$emit('update:modelValue', this.defaultValue);
},
// 关键值查询表格数据行
findRowByKey (value) {
return this.tableData.find(item => item[this.defaultProps.value] === value)
},
filterMethod(keyword){
if(!keyword){
this.keyword = null;
return false;
}
this.keyword = keyword;
this.getData()
},
// 触发select隐藏
blur(){
this.$refs.select.blur();
},
// 触发select显示
focus(){
this.$refs.select.focus();
}
}
}
</script>
<style scoped>
.sc-table-select__table {padding:12px;}
.sc-table-select__page {padding-top: 12px;}
</style>
+25
View File
@@ -0,0 +1,25 @@
<template>
<div class="sc-title">
{{title}}
</div>
</template>
<script>
export default {
props: {
title: { type: String, required: true, default: "" },
},
data() {
return {
}
},
computed: {
}
}
</script>
<style scoped>
.sc-title {border-bottom: 1px solid #eee;margin-bottom: 20px;font-size: 17px;padding-bottom: 15px;color: #3c4a54;font-weight: bold;}
</style>
+217
View File
@@ -0,0 +1,217 @@
<template>
<div class="sc-upload" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.5)" :style="style">
<div v-if="tempImg || img" class="sc-upload-file">
<div class="mask">
<span class="del" @click.stop="del"><el-icon><el-icon-delete /></el-icon></span>
</div>
<el-image v-if="fileIsImg" class="image" :src="tempImg || img" :preview-src-list="[img]" fit="cover" hide-on-click-modal append-to-body :z-index="9999"></el-image>
<a v-else :href="img" class="file" target="_blank"><el-icon><el-icon-document /></el-icon></a>
</div>
<div v-else class="sc-upload-uploader" @click="fileSelect && showfileSelect()">
<el-upload ref="upload" class="uploader" :disabled="fileSelect" :auto-upload="!cropper" :on-change="change" :accept="accept" :action="action" :show-file-list="false" :before-upload="before" :on-success="success" :on-error="error" :http-request="request">
<slot>
<div class="file-empty">
<el-icon><component :is="icon" /></el-icon>
<h4 v-if="title">{{title}}</h4>
</div>
</slot>
</el-upload>
</div>
<el-dialog title="剪裁" v-model="cropperDialogVisible" :width="580" destroy-on-close>
<sc-cropper :src="cropperImg" :compress="compress" :aspectRatio="aspectRatio" ref="cropper"></sc-cropper>
<template #footer>
<el-button @click="cropperDialogVisible=false" > </el-button>
<el-button type="primary" @click="cropperSave"> </el-button>
</template>
</el-dialog>
<el-dialog title="打开" v-model="fileSelectDialogVisible" :width="880" destroy-on-close>
<sc-file-select @submit="fileSelectSubmit">
<template #do>
<el-button @click="fileSelectDialogVisible=false" > </el-button>
</template>
</sc-file-select>
</el-dialog>
<span style="display:none!important"><el-input v-model="img"></el-input></span>
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
import config from "@/config/upload"
const scCropper = defineAsyncComponent(() => import('@/components/scCropper'))
const scFileSelect = defineAsyncComponent(() => import('@/components/scFileSelect'))
export default {
props: {
height: {type: Number, default: 120},
width: {type: Number, default: 120},
modelValue: { type: String, default: "" },
action: { type: String, default: "" },
apiObj: { type: Object, default: () => {} },
accept: { type: String, default: "image/gif, image/jpeg, image/png" },
maxSize: { type: Number, default: config.maxSize },
title: { type: String, default: "" },
icon: { type: String, default: "el-icon-plus" },
fileSelect: { type: Boolean, default: false },
cropper: { type: Boolean, default: false },
compress: {type: Number, default: 1},
aspectRatio: {type: Number, default: NaN},
onSuccess: { type: Function, default: () => { return true } }
},
components: {
scCropper,
scFileSelect
},
data() {
return {
loading: false,
fileIsImg: true,
img: "",
tempImg: "",
style: {
width: this.width + "px",
height: this.height + "px"
},
cropperDialogVisible: false,
cropperImg: "",
cropperUploadFile: null,
fileSelectDialogVisible: false,
}
},
watch:{
modelValue(){
this.isImg(this.modelValue)
this.img = this.modelValue;
},
img(){
this.$emit('update:modelValue', this.img);
}
},
mounted() {
this.isImg(this.modelValue)
this.img = this.modelValue;
},
methods: {
showfileSelect(){
this.fileSelectDialogVisible = true
},
fileSelectSubmit(val){
this.img = val
this.fileSelectDialogVisible = false
},
cropperSave(){
var uploadFile = this.$refs.upload.uploadFiles[0].raw
this.$refs.cropper.getCropFile(file => {
this.cropperUploadFile = file
this.$refs.upload.submit()
}, uploadFile.name, uploadFile.type)
this.cropperDialogVisible = false
},
isImg(fileUrl){
var strRegex = "(.jpg|.png|.gif|.jpeg)$";
var re = new RegExp(strRegex);
if (re.test(fileUrl.toLowerCase())){
this.fileIsImg=true;
}else{
this.fileIsImg=false;
}
},
change(file){
if(this.cropper && file.status=='ready'){
this.isImg(file.name)
if(!this.fileIsImg){
this.$notify.warning({
title: '上传文件警告',
message: '选择的文件非图像类文件'
})
return false
}
this.cropperDialogVisible = true
this.cropperImg = URL.createObjectURL(file.raw)
}
},
before(file){
file = this.cropper ? this.cropperUploadFile : file
const maxSize = file.size / 1024 / 1024 < this.maxSize;
if (!maxSize) {
this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
return false;
}
this.isImg(file.name)
this.tempImg = URL.createObjectURL(file);
this.loading = true;
},
success(res){
this.loading = false;
this.tempImg = "";
var os = this.onSuccess(res);
if(os!=undefined && os==false){
return false;
}
var response = config.parseData(res);
if(response.code != config.successCode){
this.$message.warning(response.msg || "上传文件未知错误")
}else{
this.img = response.src;
}
},
error(err){
this.$notify.error({
title: '上传文件错误',
message: err
})
this.loading = false;
this.tempImg = "";
this.img = ""
},
del(){
this.img = ""
},
request(param){
var apiObj = config.apiObj;
if(this.apiObj){
apiObj = this.apiObj;
}
const data = new FormData();
var file = this.cropper ? this.cropperUploadFile : param.file
data.append("file", file);
apiObj.post(data).then(res => {
param.onSuccess(res)
}).catch(err => {
param.onError(err)
})
}
}
}
</script>
<style>
.sc-upload+.sc-upload {margin-left: 10px;}
</style>
<style scoped>
.el-form-item.is-error .sc-upload-uploader {border: 1px dashed #F56C6C;}
.sc-upload {width: 120px;height: 120px;display: inline-block;vertical-align: top;box-sizing: border-box;}
.sc-upload-file {position: relative;width: 100%;height: 100%;}
.sc-upload-file .mask {display: none;position: absolute;top:0px;right:0px;line-height: 1;z-index: 1;}
.sc-upload-file .mask span {display: inline-block;width: 25px;height:25px;line-height: 23px;text-align: center;cursor: pointer;color: #fff;}
.sc-upload-file .mask span i {font-size: 12px;}
.sc-upload-file .mask .del {background: #F56C6C;}
.sc-upload-file .image {width: 100%;height: 100%;}
.sc-upload-file .image img {vertical-align: bottom;}
.sc-upload-file .file {width: 100%;height: 100%;display: flex;flex-direction: column;align-items: center;justify-content: center;border: 1px solid #DCDFE6;}
.sc-upload-file .file i {font-size: 30px;color: #409EFF;}
.sc-upload-file:hover .mask {display: inline-block;}
.sc-upload-uploader {border: 1px dashed #d9d9d9;box-sizing: border-box;width: 100%;height: 100%;}
.sc-upload-uploader:hover {border: 1px dashed #409eff;}
.sc-upload-uploader .uploader {width: 100%;height: 100%;}
.sc-upload-uploader:deep(.el-upload) {width: 100%;height: 100%;}
.sc-upload-uploader .file-empty {width: 100%;height: 100%;line-height: 1;display: flex;flex-direction: column;align-items: center;justify-content: center;}
.sc-upload-uploader .file-empty i {font-size: 28px;color: #8c939d;}
.sc-upload-uploader .file-empty h4 {font-size: 12px;font-weight: normal;color: #8c939d;margin-top: 10px;}
</style>
+227
View File
@@ -0,0 +1,227 @@
<template>
<div class="sc-upload-multiple">
<div class="sc-upload-list">
<ul>
<li v-for="(file, index) in fileList" :key="index">
<div v-if="file.status!='success'" class="sc-upload-item" v-loading="true" element-loading-background="rgba(0, 0, 0, 0.5)">
<el-image class="image" :src="file.tempImg" fit="cover" :z-index="9999"></el-image>
</div>
<div v-else class="sc-upload-item">
<div class="mask">
<span class="del" @click.stop="del(index)"><el-icon><el-icon-delete /></el-icon></span>
</div>
<el-image class="image" :src="file.url" fit="cover" :preview-src-list="preview" hide-on-click-modal append-to-body>
<template #placeholder>
<div class="image-slot">
<el-icon><el-icon-more-filled /></el-icon>
</div>
</template>
</el-image>
</div>
</li>
</ul>
</div>
<div class="sc-upload-uploader" @click="fileSelect && showfileSelect()">
<el-upload ref="upload" class="uploader" :disabled="fileSelect" :action="action" :accept="accept" multiple :show-file-list="false" :file-list="defaultFileList" :before-upload="before" :on-progress="progress" :on-success="success" :on-change="change" :on-remove="remove" :on-error="error" :http-request="request">
<div class="file-empty">
<el-icon><component :is="icon" /></el-icon>
<h4 v-if="title">{{title}}</h4>
</div>
</el-upload>
</div>
<el-dialog title="打开" v-model="fileSelectDialogVisible" :width="880" destroy-on-close>
<sc-file-select multiple @submit="fileSelectSubmit">
<template #do>
<el-button @click="fileSelectDialogVisible=false" > </el-button>
</template>
</sc-file-select>
</el-dialog>
<span style="display:none!important"><el-input v-model="value"></el-input></span>
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
import config from "@/config/upload";
const scFileSelect = defineAsyncComponent(() => import('@/components/scFileSelect'))
export default {
props: {
modelValue: { type: String, default: "" },
action: { type: String, default: "" },
apiObj: { type: Object, default: () => {} },
accept: { type: String, default: "image/gif, image/jpeg, image/png" },
maxSize: { type: Number, default: config.maxSize },
title: { type: String, default: "" },
icon: { type: String, default: "el-icon-plus" },
fileSelect: { type: Boolean, default: false }
},
components: {
scFileSelect
},
data(){
return {
value: "",
defaultFileList: [],
fileList: [],
fileSelectDialogVisible: false,
}
},
watch:{
modelValue(){
this.$refs.upload.uploadFiles=this.modelValuetoArr
this.fileList = this.modelValuetoArr
this.value = this.modelValue
},
fileList: {
handler(){
if(this.isAllSuccess){
this.$emit('update:modelValue', this.fileListtoStr);
}
},
deep: true
}
},
computed: {
modelValuetoArr(){
return this.toArr(this.modelValue)
},
fileListtoStr(){
return this.toStr(this.fileList)
},
preview(){
return this.fileList.map(v => v.url)
},
isAllSuccess(){
var all_length = this.fileList.length;
var success_length = 0
this.fileList.forEach(item => {
if(item.status == "success"){
success_length += 1
}
})
return success_length == all_length
}
},
mounted() {
this.defaultFileList = this.toArr(this.modelValue);
this.fileList = this.toArr(this.modelValue)
this.value = this.modelValue
},
methods: {
showfileSelect(){
this.fileSelectDialogVisible = true
},
fileSelectSubmit(val){
const newval = [...this.modelValue.split(","),...val].join(",")
this.$emit('update:modelValue', newval);
this.fileSelectDialogVisible = false
},
//默认值转换为数组
toArr(str){
var _arr = [];
var arr = str.split(",");
arr.forEach(item => {
if(item){
_arr.push({
name: "F",
status: "success",
url: item
})
}
})
return _arr;
},
//数组转换为原始值
toStr(arr){
var _arr = [];
arr.forEach(item => {
_arr.push(item.url)
})
var str = _arr.join(",")
return str;
},
before(file){
const maxSize = file.size / 1024 / 1024 < this.maxSize;
if (!maxSize) {
this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
return false;
}
},
change(file, fileList){
file.tempImg = URL.createObjectURL(file.raw);
this.fileList = fileList
},
success(res, file){
var response = config.parseData(res);
file.url = response.src
},
progress(){
},
remove(){
},
error(err){
this.$notify.error({
title: '上传文件错误',
message: err
})
},
del(index){
this.fileList.splice(index, 1);
},
request(param){
var apiObj = config.apiObj;
if(this.apiObj){
apiObj = this.apiObj;
}
const data = new FormData();
data.append("file", param.file);
apiObj.post(data).then(res => {
param.onSuccess(res)
}).catch(err => {
param.onError(err)
})
}
}
}
</script>
<style scoped>
.el-form-item.is-error .sc-upload-uploader {border: 1px dashed #F56C6C;}
.sc-upload-multiple {display: inline-block;}
.sc-upload-list {display: inline-block;}
.sc-upload-list li {list-style: none;display: inline-block;width: 120px;height: 120px;margin-right: 10px;vertical-align: top;box-sizing: border-box;}
.sc-upload-item {position: relative;width: 100%;height: 100%;}
.sc-upload-item .mask {display: none;position: absolute;top:0px;right:0px;line-height: 1;z-index: 1;}
.sc-upload-item .mask span {display: inline-block;width: 25px;height:25px;line-height: 23px;text-align: center;cursor: pointer;color: #fff;}
.sc-upload-item .mask span i {font-size: 12px;}
.sc-upload-item .mask .del {background: #F56C6C;}
.sc-upload-item:hover .mask {display: inline-block;}
.sc-upload-list .image {width: 100%;height: 100%;}
.sc-upload-list .image-slot {display: flex;justify-content: center;align-items: center;width: 100%;height: 100%;background: #f5f7fa;color: #909399;}
.sc-upload-list .image-slot i {font-size: 20px;}
.sc-upload-uploader {border: 1px dashed #d9d9d9;width: 120px;height: 120px;display: inline-block;vertical-align: top;box-sizing: border-box;}
.sc-upload-uploader:hover {border: 1px dashed #409eff;}
.sc-upload-uploader .uploader {width: 100%;height: 100%;}
.sc-upload-uploader:deep(.el-upload) {width: 100%;height: 100%;}
.sc-upload-uploader .file-empty {width: 100%;height: 100%;line-height: 1;display: flex;flex-direction: column;align-items: center;justify-content: center;}
.sc-upload-uploader .file-empty i {font-size: 28px;color: #8c939d;}
.sc-upload-uploader .file-empty h4 {font-size: 12px;font-weight: normal;color: #8c939d;margin-top: 10px;}
</style>
+123
View File
@@ -0,0 +1,123 @@
<!--
* @Descripttion: xgplayer二次封装
* @version: 1.0
* @Author: sakuya
* @Date: 2021年11月29日12:10:06
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-video" ref="scVideo"></div>
</template>
<script>
import Player from 'xgplayer'
import HlsPlayer from 'xgplayer-hls'
export default {
props: {
//视频路径
src: { type: String, required: true, default: "" },
//封面
poster: { type: String, default: "" },
//音量
volume: { type: Number, default: 0.8 },
//是否显示控制
controls: { type: Boolean, default: true },
//是否直播场景
isLive: { type: Boolean, default: false },
//自动播放
autoplay: { type: Boolean, default: false },
//循环播放
loop: { type: Boolean, default: false },
//初始化显示首帧
videoInit: { type: Boolean, default: false },
//画中画
pip: { type: Boolean, default: false },
//倍速播放
playbackRate: { type: [Array, String], default: () => [0.5, 0.75, 1, 1.5, 2] },
//记忆播放
lastPlayTime: { type: Number, default: 0 },
//弹幕
danmu: { type: [Array, String], default: "" },
//源切换
resource: { type: Array, default: () => [] },
//进度条特殊点标记
progressDot: { type: Array, default: () => [] },
},
data() {
return {
player: null
}
},
mounted() {
if(this.isLive){
this.initHls()
}else{
this.init()
}
},
methods: {
init(){
this.player = new Player({
el: this.$refs.scVideo,
url: this.src,
fluid: true,
poster: this.poster,
lang: 'zh-cn',
volume: this.volume,
autoplay: this.autoplay,
loop: this.loop,
videoInit: this.videoInit,
playbackRate: this.playbackRate,
lastPlayTime: this.lastPlayTime,
pip: this.pip,
controls: this.controls,
danmu: this.formatDanmu(this.danmu),
progressDot: this.progressDot
})
this.player.emit('resourceReady', this.resource)
},
initHls(){
this.player = new HlsPlayer({
el: this.$refs.scVideo,
url: this.src,
fluid: true,
poster: this.poster,
isLive: true,
ignores: ['time','progress'],
lang: 'zh-cn',
volume: this.volume,
pip: this.pip,
controls: this.controls,
})
},
formatDanmu(danmu){
if(!danmu){
return false
}
let newDanmu = []
danmu.forEach(item => {
newDanmu.push({
id: item.id || '',
start: item.start || 0,
txt: item.txt || '',
duration: 10000,
mode: item.mode || 'scroll',
style: item.style || {}
})
})
return {
comments: newDanmu
}
}
}
}
</script>
<style scoped>
.sc-video:deep(.danmu) > * {color: #fff;font-size:20px;font-weight:bold;text-shadow:1px 1px 0 #000,-1px -1px 0 #000,-1px 1px 0 #000,1px -1px 0 #000;}
.sc-video:deep(.xgplayer-controls) {background-image: linear-gradient(180deg, transparent, rgba(0,0,0,0.3));}
.sc-video:deep(.xgplayer-progress-tip) {border:0;color: #fff;background: rgba(0,0,0,.5);line-height: 25px;padding: 0 10px;border-radius: 25px;}
</style>
+66
View File
@@ -0,0 +1,66 @@
<!--
* @Descripttion: 局部水印组件
* @version: 1.1
* @Author: sakuya
* @Date: 2021年12月18日12:16:16
* @LastEditors: sakuya
* @LastEditTime: 2022年1月5日09:52:59
-->
<template>
<div class="sc-water-mark" ref="scWaterMark">
<slot></slot>
</div>
</template>
<script>
export default {
props: {
text: { type: String, required: true, default: "" },
subtext: { type: String, default: "" },
color: { type: String, default: "rgba(128,128,128,0.2)" }
},
data() {
return {
}
},
mounted() {
this.create()
},
methods: {
create(){
this.clear()
//创建画板
var canvas = document.createElement('canvas')
canvas.width = 150
canvas.height = 150
canvas.style.display = 'none'
//绘制文字
var text = canvas.getContext('2d')
text.rotate(-45 * Math.PI / 180)
text.translate(-75, 25)
text.fillStyle = this.color
text.font = "bold 20px SimHei"
text.textAlign = "center"
text.fillText(this.text, canvas.width / 2, canvas.height / 2)
text.font = "14px Microsoft YaHei"
text.fillText(this.subtext, canvas.width / 2, canvas.height / 2 + 20)
//创建水印容器
var watermark = document.createElement('div')
watermark.setAttribute('class', 'watermark')
const styleStr = `position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;pointer-events:none;background-repeat:repeat;background-image:url('${canvas.toDataURL("image/png")}');`
watermark.setAttribute('style', styleStr);
this.$refs.scWaterMark.appendChild(watermark)
},
clear(){
var wmDom = this.$refs.scWaterMark.querySelector('.watermark')
wmDom && wmDom.remove()
}
}
}
</script>
<style scoped>
.sc-water-mark {position: relative;display: inherit;width: 100%;height: 100%;}
</style>
+154
View File
@@ -0,0 +1,154 @@
<!--
* @Descripttion: 仿钉钉流程设计器
* @version: 1.2
* @Author: sakuya
* @Date: 2021年9月14日08:38:35
* @LastEditors: sakuya
* @LastEditTime: 2022年2月9日16:48:49
-->
<template>
<div class="sc-workflow-design">
<div class="box-scale">
<node-wrap v-if="nodeConfig" v-model="nodeConfig"></node-wrap>
<div class="end-node">
<div class="end-node-circle"></div>
<div class="end-node-text">流程结束</div>
</div>
</div>
<use-select v-if="selectVisible" ref="useselect" @closed="selectVisible=false"></use-select>
</div>
</template>
<script>
import nodeWrap from './nodeWrap'
import useSelect from './select'
export default {
provide(){
return {
select: this.selectHandle
}
},
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
nodeWrap,
useSelect
},
data() {
return {
nodeConfig: this.modelValue,
selectVisible: false
}
},
watch:{
modelValue(val){
this.nodeConfig = val
},
nodeConfig(val){
this.$emit("update:modelValue", val)
}
},
mounted() {
},
methods: {
selectHandle(type, data){
this.selectVisible = true
this.$nextTick(() => {
this.$refs.useselect.open(type, data)
})
}
}
}
</script>
<style lang="scss">
.sc-workflow-design {width: 100%;}
.sc-workflow-design .box-scale {display: inline-block;position: relative;width: 100%;padding: 54.5px 0px;align-items: flex-start;justify-content: center;flex-wrap: wrap;min-width: min-content;}
.sc-workflow-design {
.node-wrap {display: inline-flex;width: 100%;flex-flow: column wrap;justify-content: flex-start;align-items: center;padding: 0px 50px;position: relative;z-index: 1;}
.node-wrap-box {display: inline-flex;flex-direction: column;position: relative;width: 220px;min-height: 72px;flex-shrink: 0;background: rgb(255, 255, 255);border-radius: 4px;cursor: pointer;box-shadow: 0 2px 5px 0 rgba(0,0,0,.1);}
.node-wrap-box::before {content: "";position: absolute;top: -12px;left: 50%;transform: translateX(-50%);width: 0px;border-style: solid;border-width: 8px 6px 4px;border-color: rgb(202, 202, 202) transparent transparent;background: #f6f8f9;}
.node-wrap-box.start-node:before {content: none}
.node-wrap-box .title {height:24px;line-height: 24px;color: #fff;padding-left: 16px;padding-right: 30px;border-radius: 4px 4px 0 0;position: relative;display: flex;align-items: center;}
.node-wrap-box .title .icon {margin-right: 5px;}
.node-wrap-box .title .close {font-size: 15px;position: absolute;top:50%;transform: translateY(-50%);right:10px;display: none;}
.node-wrap-box .content {position: relative;padding: 15px;}
.node-wrap-box .content .placeholder {color: #999;}
.node-wrap-box:hover .close {display: block;}
.add-node-btn-box {width: 240px;display: inline-flex;flex-shrink: 0;position: relative;z-index: 1;}
.add-node-btn-box:before {content: "";position: absolute;top: 0px;left: 0px;right: 0px;bottom: 0px;z-index: -1;margin: auto;width: 2px;height: 100%;background-color: rgb(202, 202, 202);}
.add-node-btn {user-select: none;width: 240px;padding: 20px 0px 32px;display: flex;justify-content: center;flex-shrink: 0;flex-grow: 1;}
.add-node-btn span {}
.add-branch {justify-content: center;padding: 0px 10px;position: absolute;top: -16px;left: 50%;transform: translateX(-50%);transform-origin: center center;z-index: 1;display: inline-flex;align-items: center;}
.branch-wrap {display: inline-flex;width: 100%;}
.branch-box-wrap {display: flex;flex-flow: column wrap;align-items: center;min-height: 270px;width: 100%;flex-shrink: 0;}
.col-box {display: inline-flex;flex-direction: column;align-items: center;position: relative;background: #f6f8f9;}
.branch-box {display: flex;overflow: visible;min-height: 180px;height: auto;border-bottom: 2px solid #ccc;border-top: 2px solid #ccc;position: relative;margin-top: 15px;}
.branch-box .col-box::before {content: "";position: absolute;top: 0px;left: 0px;right: 0px;bottom: 0px;z-index: 0;margin: auto;width: 2px;height: 100%;background-color: rgb(202, 202, 202);}
.condition-node {display: inline-flex;flex-direction: column;min-height: 220px;}
.condition-node-box {padding-top: 30px;padding-right: 50px;padding-left: 50px;justify-content: center;align-items: center;flex-grow: 1;position: relative;display: inline-flex;flex-direction: column;}
.condition-node-box::before {content: "";position: absolute;top: 0px;left: 0px;right: 0px;bottom: 0px;margin: auto;width: 2px;height: 100%;background-color: rgb(202, 202, 202);}
.auto-judge {position: relative;width: 220px;min-height: 72px;background: rgb(255, 255, 255);border-radius: 4px;padding: 15px 15px;cursor: pointer;box-shadow: 0 2px 5px 0 rgba(0,0,0,.1);}
.auto-judge::before {content: "";position: absolute;top: -12px;left: 50%;transform: translateX(-50%);width: 0px;border-style: solid;border-width: 8px 6px 4px;border-color: rgb(202, 202, 202) transparent transparent;background: rgb(245, 245, 247);}
.auto-judge .title {line-height: 16px;}
.auto-judge .title .node-title {color: #15BC83;}
.auto-judge .title .close {font-size: 15px;position: absolute;top:15px;right:15px;color: #999;display: none;}
.auto-judge .title .priority-title {position: absolute;top:15px;right:15px;color: #999;}
.auto-judge .content {position: relative;padding-top: 15px;}
.auto-judge .content .placeholder {color: #999;}
.auto-judge:hover {
.close {display: block;}
.priority-title {display: none;}
}
.top-left-cover-line, .top-right-cover-line {position: absolute;height: 3px;width: 50%;background-color: #f6f8f9;top: -2px;}
.bottom-left-cover-line, .bottom-right-cover-line {position: absolute;height: 3px;width: 50%;background-color: #f6f8f9;bottom: -2px;}
.top-left-cover-line {left: -1px;}
.top-right-cover-line {right: -1px;}
.bottom-left-cover-line {left: -1px;}
.bottom-right-cover-line {right: -1px;}
.end-node {border-radius: 50%;font-size: 14px;color: rgba(25,31,37,.4);text-align: left;}
.end-node-circle {width: 10px;height: 10px;margin: auto;border-radius: 50%;background: #dbdcdc;}
.end-node-text {margin-top: 5px;text-align: center;}
.auto-judge:hover {
.sort-left {display: flex;}
.sort-right {display: flex;}
}
.auto-judge .sort-left {position: absolute;top: 0;bottom: 0;z-index: 1;left: 0;display: none;justify-content: center;align-items: center;flex-direction: column;}
.auto-judge .sort-right {position: absolute;top: 0;bottom: 0;z-index: 1;right: 0;display: none;justify-content: center;align-items: center;flex-direction: column;}
.auto-judge .sort-left:hover, .auto-judge .sort-right:hover {background: #eee;}
.auto-judge:after {pointer-events: none;content: "";position: absolute;top:0;bottom:0;left:0;right:0;z-index: 2;border-radius: 4px;transition: all .1s;}
.auto-judge:hover:after {border: 1px solid #3296fa;box-shadow: 0 0 6px 0 rgba(50,150,250,.3);}
.node-wrap-box:after {pointer-events: none;content: "";position: absolute;top:0;bottom:0;left:0;right:0;z-index: 2;border-radius: 4px;transition: all .1s;}
.node-wrap-box:hover:after {border: 1px solid #3296fa;box-shadow: 0 0 6px 0 rgba(50,150,250,.3);}
}
.tags-list {margin-top: 15px;width: 100%;}
.add-node-popover-body {}
.add-node-popover-body li {display: inline-block;width: 80px;text-align: center;padding:10px 0;}
.add-node-popover-body li i {border: 1px solid var(--el-border-color-light);width:40px;height:40px;border-radius: 50%;text-align: center;line-height: 38px;font-size: 18px;cursor: pointer;}
.add-node-popover-body li i:hover {border: 1px solid #3296fa;background: #3296fa;color: #fff!important;}
.add-node-popover-body li p {font-size: 12px;margin-top: 5px;}
.node-wrap-drawer__title {padding-right:40px;}
.node-wrap-drawer__title label {cursor: pointer;}
.node-wrap-drawer__title label:hover {border-bottom: 1px dashed #409eff;}
.node-wrap-drawer__title .node-wrap-drawer__title-edit {color: #409eff;margin-left: 10px;vertical-align: middle;}
[data-theme='dark'] .sc-workflow-design {
.node-wrap-box,.auto-judge {background: #2b2b2b;}
.col-box {background: #222225;}
.top-left-cover-line,
.top-right-cover-line,
.bottom-left-cover-line,
.bottom-right-cover-line {background-color: #222225;}
.node-wrap-box::before,.auto-judge::before {background-color: #222225;}
.branch-box .add-branch {background: #222225;}
.end-node .end-node-text {color: #d0d0d0;}
.auto-judge .sort-left:hover, .auto-judge .sort-right:hover {background: #222225;}
}
</style>
+57
View File
@@ -0,0 +1,57 @@
<template>
<promoter v-if="nodeConfig.type==0" v-model="nodeConfig"></promoter>
<approver v-if="nodeConfig.type==1" v-model="nodeConfig"></approver>
<send v-if="nodeConfig.type==2" v-model="nodeConfig"></send>
<branch v-if="nodeConfig.type==4" v-model="nodeConfig">
<template v-slot="slot">
<node-wrap v-if="slot.node" v-model="slot.node.childNode"></node-wrap>
</template>
</branch>
<node-wrap v-if="nodeConfig.childNode" v-model="nodeConfig.childNode"></node-wrap>
</template>
<script>
import approver from './nodes/approver'
import promoter from './nodes/promoter'
import branch from './nodes/branch'
import send from './nodes/send'
export default {
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
approver,
promoter,
branch,
send
},
data() {
return {
nodeConfig: {},
}
},
watch:{
modelValue(val){
this.nodeConfig = val
},
nodeConfig(val){
this.$emit("update:modelValue", val)
}
},
mounted() {
this.nodeConfig = this.modelValue
},
methods: {
}
}
</script>
<style>
</style>
@@ -0,0 +1,102 @@
<template>
<div class="add-node-btn-box">
<div class="add-node-btn">
<el-popover placement="right-start" :width="270" trigger="click" :hide-after="0" :show-after="0">
<template #reference>
<el-button type="primary" icon="el-icon-plus" circle></el-button>
</template>
<div class="add-node-popover-body">
<ul>
<li>
<el-icon style="color: #ff943e;" @click="addType(1)"><el-icon-user-filled /></el-icon>
<p>审批节点</p>
</li>
<li>
<el-icon style="color: #3296fa;" @click="addType(2)"><el-icon-promotion /></el-icon>
<p>抄送节点</p>
</li>
<li>
<el-icon style="color: #15BC83;" @click="addType(4)"><el-icon-share /></el-icon>
<p>条件分支</p>
</li>
</ul>
</div>
</el-popover>
</div>
</div>
</template>
<script>
export default {
props: {
modelValue: { type: Object, default: () => {} }
},
data() {
return {
}
},
mounted() {
},
methods: {
addType(type){
var node = {}
if (type == 1) {
node = {
nodeName: "审核人",
type: 1, //节点类型
setType: 1, //审核人类型
nodeUserList: [], //审核人成员
nodeRoleList: [], //审核角色
examineLevel: 1, //指定主管层级
directorLevel: 1, //自定义连续主管审批层级
selectMode: 1, //发起人自选类型
termAuto: false, //审批期限超时自动审批
term: 0, //审批期限
termMode: 1, //审批期限超时后执行类型
examineMode: 1, //多人审批时审批方式
directorMode: 0, //连续主管审批方式
childNode: this.modelValue
}
}else if(type == 2){
node = {
nodeName: "抄送人",
type: 2,
userSelectFlag: true,
nodeUserList: [],
childNode: this.modelValue
}
}else if(type == 4){
node = {
nodeName: "条件路由",
type: 4,
conditionNodes: [
{
nodeName: "条件1",
type: 3,
priorityLevel: 1,
conditionMode: 1,
conditionList: []
},
{
nodeName: "条件2",
type: 3,
priorityLevel: 2,
conditionMode: 1,
conditionList: []
}
],
childNode: this.modelValue
}
}
this.$emit("update:modelValue", node)
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,193 @@
<template>
<div class="node-wrap">
<div class="node-wrap-box" @click="show">
<div class="title" style="background: #ff943e;">
<el-icon class="icon"><el-icon-user-filled /></el-icon>
<span>{{ nodeConfig.nodeName }}</span>
<el-icon class="close" @click.stop="delNode()"><el-icon-close /></el-icon>
</div>
<div class="content">
<span v-if="toText(nodeConfig)">{{ toText(nodeConfig) }}</span>
<span v-else class="placeholder">请选择</span>
</div>
</div>
<add-node v-model="nodeConfig.childNode"></add-node>
<el-drawer title="审批人设置" v-model="drawer" destroy-on-close append-to-body :size="500">
<template #title>
<div class="node-wrap-drawer__title">
<label @click="editTitle" v-if="!isEditTitle">{{form.nodeName}}<el-icon class="node-wrap-drawer__title-edit"><el-icon-edit /></el-icon></label>
<el-input v-if="isEditTitle" ref="nodeTitle" v-model="form.nodeName" clearable @blur="saveTitle" @keyup.enter="saveTitle"></el-input>
</div>
</template>
<el-container>
<el-main style="padding:0 20px 20px 20px">
<el-form label-position="top">
<el-form-item label="审批人员类型">
<el-select v-model="form.setType">
<el-option :value="1" label="指定成员"></el-option>
<el-option :value="2" label="主管"></el-option>
<el-option :value="3" label="角色"></el-option>
<el-option :value="4" label="发起人自选"></el-option>
<el-option :value="5" label="发起人自己"></el-option>
<el-option :value="7" label="连续多级主管"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="form.setType==1" label="选择成员">
<el-button type="primary" icon="el-icon-plus" round @click="selectHandle(1, form.nodeUserList)">选择人员</el-button>
<div class="tags-list">
<el-tag v-for="(user, index) in form.nodeUserList" :key="user.id" closable @close="delUser(index)">{{user.name}}</el-tag>
</div>
</el-form-item>
<el-form-item v-if="form.setType==2" label="指定主管">
发起人的第 <el-input-number v-model="form.examineLevel" :min="1"/> 级主管
</el-form-item>
<el-form-item v-if="form.setType==3" label="选择角色">
<el-button type="primary" icon="el-icon-plus" round @click="selectHandle(2, form.nodeRoleList)">选择角色</el-button>
<div class="tags-list">
<el-tag v-for="(role, index) in form.nodeRoleList" :key="role.id" type="info" closable @close="delRole(index)">{{role.name}}</el-tag>
</div>
</el-form-item>
<el-form-item v-if="form.setType==4" label="发起人自选">
<el-radio-group v-model="form.selectMode">
<el-radio :label="1">自选一个人</el-radio>
<el-radio :label="2">自选多个人</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.setType==7" label="连续主管审批终点">
<el-radio-group v-model="form.directorMode">
<el-radio :label="0">直到最上层主管</el-radio>
<el-radio :label="1">自定义审批终点</el-radio>
</el-radio-group>
<p v-if="form.directorMode==1">直到发起人的第 <el-input-number v-model="form.directorLevel" :min="1"/> 级主管</p>
</el-form-item>
<el-divider></el-divider>
<el-form-item label="">
<el-checkbox v-model="form.termAuto" label="超时自动审批"></el-checkbox>
</el-form-item>
<template v-if="form.termAuto">
<el-form-item label="审批期限(为 0 则不生效)">
<el-input-number v-model="form.term" :min="0"/> 小时
</el-form-item>
<el-form-item label="审批期限超时后执行">
<el-radio-group v-model="form.termMode">
<el-radio :label="0">自动通过</el-radio>
<el-radio :label="1">自动拒绝</el-radio>
</el-radio-group>
</el-form-item>
</template>
<el-divider></el-divider>
<el-form-item label="多人审批时审批方式">
<el-radio-group v-model="form.examineMode">
<p style="width: 100%;"><el-radio :label="1">按顺序依次审批</el-radio></p>
<p style="width: 100%;"><el-radio :label="2">会签 (可同时审批每个人必须审批通过)</el-radio></p>
<p style="width: 100%;"><el-radio :label="3">或签 (有一人审批通过即可)</el-radio></p>
</el-radio-group>
</el-form-item>
</el-form>
</el-main>
<el-footer>
<el-button type="primary" @click="save">保存</el-button>
<el-button @click="drawer=false">取消</el-button>
</el-footer>
</el-container>
</el-drawer>
</div>
</template>
<script>
import addNode from './addNode'
export default {
inject: ['select'],
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
addNode
},
data() {
return {
nodeConfig: {},
drawer: false,
isEditTitle: false,
form: {}
}
},
watch:{
modelValue(){
this.nodeConfig = this.modelValue
}
},
mounted() {
this.nodeConfig = this.modelValue
},
methods: {
show(){
this.form = {}
this.form = JSON.parse(JSON.stringify(this.nodeConfig))
this.drawer = true
},
editTitle(){
this.isEditTitle = true
this.$nextTick(()=>{
this.$refs.nodeTitle.focus()
})
},
saveTitle(){
this.isEditTitle = false
},
save(){
this.$emit("update:modelValue", this.form)
this.drawer = false
},
delNode(){
this.$emit("update:modelValue", this.nodeConfig.childNode)
},
delUser(index){
this.form.nodeUserList.splice(index, 1)
},
delRole(index){
this.form.nodeRoleList.splice(index, 1)
},
selectHandle(type, data){
this.select(type, data)
},
toText(nodeConfig){
if(nodeConfig.setType == 1){
if (nodeConfig.nodeUserList && nodeConfig.nodeUserList.length>0) {
const users = nodeConfig.nodeUserList.map(item=>item.name).join("、")
return users
}else{
return false
}
}else if (nodeConfig.setType == 2) {
return nodeConfig.examineLevel == 1 ? '直接主管' : `发起人的第${nodeConfig.examineLevel}级主管`
}else if (nodeConfig.setType == 3) {
if (nodeConfig.nodeRoleList && nodeConfig.nodeRoleList.length>0) {
const roles = nodeConfig.nodeRoleList.map(item=>item.name).join("、")
return '角色-' + roles
}else{
return false
}
}else if (nodeConfig.setType == 4) {
return "发起人自选"
}else if (nodeConfig.setType == 5) {
return "发起人自己"
}else if (nodeConfig.setType == 7) {
return "连续多级主管"
}
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,222 @@
<template>
<div class="branch-wrap">
<div class="branch-box-wrap">
<div class="branch-box">
<el-button class="add-branch" type="success" plain round @click="addTerm">添加条件</el-button>
<div class="col-box" v-for="(item,index) in nodeConfig.conditionNodes" :key="index">
<div class="condition-node">
<div class="condition-node-box">
<div class="auto-judge" @click="show(index)">
<div class="sort-left" v-if="index!=0" @click.stop="arrTransfer(index,-1)">
<el-icon><el-icon-arrow-left /></el-icon>
</div>
<div class="title">
<span class="node-title">{{ item.nodeName }}</span>
<span class="priority-title">优先级{{item.priorityLevel}}</span>
<el-icon class="close" @click.stop="delTerm(index)"><el-icon-close /></el-icon>
</div>
<div class="content">
<span v-if="toText(nodeConfig, index)">{{ toText(nodeConfig, index) }}</span>
<span v-else class="placeholder">请设置条件</span>
</div>
<div class="sort-right" v-if="index!=nodeConfig.conditionNodes.length-1" @click.stop="arrTransfer(index)">
<el-icon><el-icon-arrow-right /></el-icon>
</div>
</div>
<add-node v-model="item.childNode"></add-node>
</div>
</div>
<slot v-if="item.childNode" :node="item"></slot>
<div class="top-left-cover-line" v-if="index==0"></div>
<div class="bottom-left-cover-line" v-if="index==0"></div>
<div class="top-right-cover-line" v-if="index==nodeConfig.conditionNodes.length-1"></div>
<div class="bottom-right-cover-line" v-if="index==nodeConfig.conditionNodes.length-1"></div>
</div>
</div>
<add-node v-model="nodeConfig.childNode"></add-node>
</div>
<el-drawer title="条件设置" v-model="drawer" destroy-on-close append-to-body :size="600">
<template #title>
<div class="node-wrap-drawer__title">
<label @click="editTitle" v-if="!isEditTitle">{{form.nodeName}}<el-icon class="node-wrap-drawer__title-edit"><el-icon-edit /></el-icon></label>
<el-input v-if="isEditTitle" ref="nodeTitle" v-model="form.nodeName" clearable @blur="saveTitle" @keyup.enter="saveTitle"></el-input>
</div>
</template>
<el-container>
<el-main style="padding:0 20px 20px 20px">
<el-form label-position="top">
<el-form-item label="条件关系">
<el-radio-group v-model="form.conditionMode">
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
</el-radio-group>
</el-form-item>
<el-divider></el-divider>
<el-form-item>
<el-table :data="form.conditionList">
<el-table-column prop="label" label="描述">
<template #default="scope">
<el-input v-model="scope.row.label" placeholder="描述"></el-input>
</template>
</el-table-column>
<el-table-column prop="field" label="条件字段" width="130">
<template #default="scope">
<el-input v-model="scope.row.field" placeholder="条件字段"></el-input>
</template>
</el-table-column>
<el-table-column prop="operator" label="运算符" width="130">
<template #default="scope">
<el-select v-model="scope.row.operator" placeholder="Select">
<el-option label="等于" value="="></el-option>
<el-option label="不等于" value="!="></el-option>
<el-option label="大于" value=">"></el-option>
<el-option label="大于等于" value=">="></el-option>
<el-option label="小于" value="<"></el-option>
<el-option label="小于等于" value="<="></el-option>
<el-option label="包含" value="include"></el-option>
<el-option label="不包含" value="notinclude"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="value" label="值" width="100">
<template #default="scope">
<el-input v-model="scope.row.value" placeholder="值"></el-input>
</template>
</el-table-column>
<el-table-column prop="value" label="移除" width="55">
<template #default="scope">
<el-button size="small" type="text" @click="deleteConditionList(scope.$index)">移除</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
<p><el-button type="primary" icon="el-icon-plus" round @click="addConditionList">增加条件</el-button></p>
</el-form>
</el-main>
<el-footer>
<el-button type="primary" @click="save">保存</el-button>
<el-button @click="drawer=false">取消</el-button>
</el-footer>
</el-container>
</el-drawer>
</div>
</template>
<script>
import addNode from './addNode'
export default {
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
addNode
},
data() {
return {
nodeConfig: {},
drawer: false,
isEditTitle: false,
index: 0,
form: {}
}
},
watch:{
modelValue(){
this.nodeConfig = this.modelValue
}
},
mounted() {
this.nodeConfig = this.modelValue
},
methods: {
show(index){
this.index = index
this.form = {}
this.form = JSON.parse(JSON.stringify(this.nodeConfig.conditionNodes[index]))
this.drawer = true
},
editTitle(){
this.isEditTitle = true
this.$nextTick(()=>{
this.$refs.nodeTitle.focus()
})
},
saveTitle(){
this.isEditTitle = false
},
save(){
this.nodeConfig.conditionNodes[this.index] = this.form
this.$emit("update:modelValue", this.nodeConfig)
this.drawer = false
},
addTerm(){
let len = this.nodeConfig.conditionNodes.length + 1
this.nodeConfig.conditionNodes.push({
nodeName: "条件" + len,
type: 3,
priorityLevel: len,
conditionMode: 1,
conditionList: []
})
},
delTerm(index){
this.nodeConfig.conditionNodes.splice(index, 1)
if (this.nodeConfig.conditionNodes.length == 1) {
if (this.nodeConfig.childNode) {
if (this.nodeConfig.conditionNodes[0].childNode) {
this.reData(this.nodeConfig.conditionNodes[0].childNode, this.nodeConfig.childNode)
}else{
this.nodeConfig.conditionNodes[0].childNode = this.nodeConfig.childNode
}
}
this.$emit("update:modelValue", this.nodeConfig.conditionNodes[0].childNode);
}
},
reData(data, addData) {
if (!data.childNode) {
data.childNode = addData
} else {
this.reData(data.childNode, addData)
}
},
arrTransfer(index, type = 1){
this.nodeConfig.conditionNodes[index] = this.nodeConfig.conditionNodes.splice(index + type, 1, this.nodeConfig.conditionNodes[index])[0]
this.nodeConfig.conditionNodes.map((item, index) => {
item.priorityLevel = index + 1
})
this.$emit("update:modelValue", this.nodeConfig)
},
addConditionList(){
this.form.conditionList.push({
label: '',
field: '',
operator: '=',
value: ''
})
},
deleteConditionList(index){
this.form.conditionList.splice(index, 1)
},
toText(nodeConfig, index){
var { conditionList } = nodeConfig.conditionNodes[index]
if (conditionList && conditionList.length == 1) {
const text = conditionList.map(item => `${item.label}${item.operator}${item.value}`).join(" 和 ")
return text
}else if(conditionList && conditionList.length > 1){
const conditionModeText = nodeConfig.conditionNodes[index].conditionMode==1?'且行':'或行'
return conditionList.length + "个条件," + conditionModeText
}else{
if(index == nodeConfig.conditionNodes.length - 1){
return "其他条件进入此流程"
}else{
return false
}
}
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,106 @@
<template>
<div class="node-wrap">
<div class="node-wrap-box start-node" @click="show">
<div class="title" style="background: #576a95;">
<el-icon class="icon"><el-icon-user-filled /></el-icon>
<span>{{ nodeConfig.nodeName }}</span>
</div>
<div class="content">
<span>{{ toText(nodeConfig) }}</span>
</div>
</div>
<add-node v-model="nodeConfig.childNode"></add-node>
<el-drawer title="发起人" v-model="drawer" destroy-on-close append-to-body :size="500">
<template #title>
<div class="node-wrap-drawer__title">
<label @click="editTitle" v-if="!isEditTitle">{{form.nodeName}}<el-icon class="node-wrap-drawer__title-edit"><el-icon-edit /></el-icon></label>
<el-input v-if="isEditTitle" ref="nodeTitle" v-model="form.nodeName" clearable @blur="saveTitle" @keyup.enter="saveTitle"></el-input>
</div>
</template>
<el-container>
<el-main style="padding:0 20px 20px 20px">
<el-form label-position="top">
<el-form-item label="谁可以发起此审批">
<el-button type="primary" icon="el-icon-plus" round @click="selectHandle(2, form.nodeRoleList)">选择角色</el-button>
<div class="tags-list">
<el-tag v-for="(role, index) in form.nodeRoleList" :key="role.id" type="info" closable @close="delRole(index)">{{role.name}}</el-tag>
</div>
</el-form-item>
<el-alert v-if="form.nodeRoleList.length==0" title="不指定则默认所有人都可发起此审批" type="info" :closable="false"/>
</el-form>
</el-main>
<el-footer>
<el-button type="primary" @click="save">保存</el-button>
<el-button @click="drawer=false">取消</el-button>
</el-footer>
</el-container>
</el-drawer>
</div>
</template>
<script>
import addNode from './addNode'
export default {
inject: ['select'],
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
addNode
},
data() {
return {
nodeConfig: {},
drawer: false,
isEditTitle: false,
form: {}
}
},
watch:{
modelValue(){
this.nodeConfig = this.modelValue
}
},
mounted() {
this.nodeConfig = this.modelValue
},
methods: {
show(){
this.form = {}
this.form = JSON.parse(JSON.stringify(this.nodeConfig))
this.isEditTitle = false
this.drawer = true
},
editTitle(){
this.isEditTitle = true
this.$nextTick(()=>{
this.$refs.nodeTitle.focus()
})
},
saveTitle(){
this.isEditTitle = false
},
selectHandle(type, data){
this.select(type, data)
},
delRole(index){
this.form.nodeRoleList.splice(index, 1)
},
save(){
this.$emit("update:modelValue", this.form)
this.drawer = false
},
toText(nodeConfig){
if(nodeConfig.nodeRoleList && nodeConfig.nodeRoleList.length > 0){
return nodeConfig.nodeRoleList.map(item=>item.name).join("、")
}else{
return "所有人"
}
}
}
}
</script>
<style>
</style>
+118
View File
@@ -0,0 +1,118 @@
<template>
<div class="node-wrap">
<div class="node-wrap-box" @click="show">
<div class="title" style="background: #3296fa;">
<el-icon class="icon"><el-icon-promotion /></el-icon>
<span>{{ nodeConfig.nodeName }}</span>
<el-icon class="close" @click.stop="delNode()"><el-icon-close /></el-icon>
</div>
<div class="content">
<span v-if="toText(nodeConfig)">{{ toText(nodeConfig) }}</span>
<span v-else class="placeholder">请选择人员</span>
</div>
</div>
<add-node v-model="nodeConfig.childNode"></add-node>
<el-drawer title="抄送人设置" v-model="drawer" destroy-on-close append-to-body :size="500">
<template #title>
<div class="node-wrap-drawer__title">
<label @click="editTitle" v-if="!isEditTitle">{{form.nodeName}}<el-icon class="node-wrap-drawer__title-edit"><el-icon-edit /></el-icon></label>
<el-input v-if="isEditTitle" ref="nodeTitle" v-model="form.nodeName" clearable @blur="saveTitle" @keyup.enter="saveTitle"></el-input>
</div>
</template>
<el-container>
<el-main style="padding:0 20px 20px 20px">
<el-form label-position="top">
<el-form-item label="选择要抄送的人员">
<el-button type="primary" icon="el-icon-plus" round @click="selectHandle(1, form.nodeUserList)">选择人员</el-button>
<div class="tags-list">
<el-tag v-for="(user, index) in form.nodeUserList" :key="user.id" closable @close="delUser(index)">{{user.name}}</el-tag>
</div>
</el-form-item>
<el-form-item label="">
<el-checkbox v-model="form.userSelectFlag" label="允许发起人自选抄送人"></el-checkbox>
</el-form-item>
</el-form>
</el-main>
<el-footer>
<el-button type="primary" @click="save">保存</el-button>
<el-button @click="drawer=false">取消</el-button>
</el-footer>
</el-container>
</el-drawer>
</div>
</template>
<script>
import addNode from './addNode'
export default {
inject: ['select'],
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
addNode
},
data() {
return {
nodeConfig: {},
drawer: false,
isEditTitle: false,
form: {}
}
},
watch:{
modelValue(){
this.nodeConfig = this.modelValue
}
},
mounted() {
this.nodeConfig = this.modelValue
},
methods: {
show(){
this.form = {}
this.form = JSON.parse(JSON.stringify(this.nodeConfig))
this.drawer = true
},
editTitle(){
this.isEditTitle = true
this.$nextTick(()=>{
this.$refs.nodeTitle.focus()
})
},
saveTitle(){
this.isEditTitle = false
},
save(){
this.$emit("update:modelValue", this.form)
this.drawer = false
},
delNode(){
this.$emit("update:modelValue", this.nodeConfig.childNode)
},
delUser(index){
this.form.nodeUserList.splice(index, 1)
},
selectHandle(type, data){
this.select(type, data)
},
toText(nodeConfig){
if (nodeConfig.nodeUserList && nodeConfig.nodeUserList.length>0) {
const users = nodeConfig.nodeUserList.map(item=>item.name).join("、")
return users
}else{
if(nodeConfig.userSelectFlag){
return "发起人自选"
}else{
return false
}
}
}
}
}
</script>
<style>
</style>
+269
View File
@@ -0,0 +1,269 @@
<template>
<el-dialog v-model="dialogVisible" :title="titleMap[type-1]" :width="type==1?680:460" destroy-on-close append-to-body @closed="$emit('closed')">
<template v-if="type==1">
<div class="sc-user-select">
<div class="sc-user-select__left">
<div class="sc-user-select__search">
<el-input v-model="keyword" prefix-icon="el-icon-search" placeholder="搜索成员">
<template #append>
<el-button icon="el-icon-search" @click="search"></el-button>
</template>
</el-input>
</div>
<div class="sc-user-select__select">
<div class="sc-user-select__tree" v-loading="showGrouploading">
<el-scrollbar>
<el-tree class="menu" ref="groupTree" :data="group" :node-key="groupProps.key" :props="groupProps" highlight-current :expand-on-click-node="false" :current-node-key="groupId" @node-click="groupClick"/>
</el-scrollbar>
</div>
<div class="sc-user-select__user" v-loading="showUserloading">
<div class="sc-user-select__user__list">
<el-scrollbar ref="userScrollbar">
<el-tree class="menu" ref="userTree" :data="user" :node-key="userProps.key" :props="userProps" :default-checked-keys="selectedIds" show-checkbox check-on-click-node @check-change="userClick"></el-tree>
</el-scrollbar>
</div>
<footer>
<el-pagination background layout="prev,next" small :total="total" :page-size="pageSize" v-model:currentPage="currentPage" @current-change="paginationChange"></el-pagination>
</footer>
</div>
</div>
</div>
<div class="sc-user-select__toicon"><el-icon><el-icon-arrow-right /></el-icon></div>
<div class="sc-user-select__selected">
<header>已选 ({{selected.length}})</header>
<ul>
<el-scrollbar>
<li v-for="(item, index) in selected" :key="item.id">
<span class="name">
<el-avatar size="small">{{item.name.substring(0,1)}}</el-avatar>
<label>{{item.name}}</label>
</span>
<span class="delete">
<el-button type="text" icon="el-icon-delete" circle size="small" @click="deleteSelected(index)"></el-button>
</span>
</li>
</el-scrollbar>
</ul>
</div>
</div>
</template>
<template v-if="type==2">
<div class="sc-user-select sc-user-select-role">
<div class="sc-user-select__left">
<div class="sc-user-select__select">
<div class="sc-user-select__tree" v-loading="showGrouploading">
<el-scrollbar>
<el-tree class="menu" ref="groupTree" :data="role" :node-key="roleProps.key" :props="roleProps" show-checkbox check-strictly check-on-click-node :expand-on-click-node="false" :default-checked-keys="selectedIds" @check-change="roleClick"/>
</el-scrollbar>
</div>
</div>
</div>
<div class="sc-user-select__toicon"><el-icon><el-icon-arrow-right /></el-icon></div>
<div class="sc-user-select__selected">
<header>已选 ({{selected.length}})</header>
<ul>
<el-scrollbar>
<li v-for="(item, index) in selected" :key="item.id">
<span class="name">
<label>{{item.name}}</label>
</span>
<span class="delete">
<el-button type="text" icon="el-icon-delete" circle size="mini" @click="deleteSelected(index)"></el-button>
</span>
</li>
</el-scrollbar>
</ul>
</div>
</div>
</template>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="save"> </el-button>
</template>
</el-dialog>
</template>
<script>
import config from "@/config/workflow";
export default {
props: {
modelValue: { type: Boolean, default: false }
},
data() {
return {
groupProps: config.group.props,
userProps: config.user.props,
roleProps: config.role.props,
titleMap: ['人员选择', '角色选择'],
dialogVisible: false,
showGrouploading: false,
showUserloading: false,
keyword: '',
groupId: '',
pageSize: config.user.pageSize,
total: 0,
currentPage: 1,
group: [],
user: [],
role: [],
type: 1,
selected: [],
value: []
}
},
computed: {
selectedIds(){
return this.selected.map(t => t.id)
}
},
mounted() {
},
methods: {
//打开赋值
open(type, data){
this.type = type
this.value = data||[]
this.selected = JSON.parse(JSON.stringify(data||[]))
this.dialogVisible = true
if(this.type==1){
this.getGroup()
this.getUser()
}else if(this.type==2){
this.getRole()
}
},
//获取组织
async getGroup(){
this.showGrouploading = true;
var res = await config.group.apiObj.get();
this.showGrouploading = false;
var allNode = {[config.group.props.key]: '', [config.group.props.label]: '所有'}
res.data.unshift(allNode);
this.group = config.group.parseData(res).rows
},
//获取用户
async getUser(){
this.showUserloading = true;
var params = {
[config.user.request.keyword]: this.keyword || null,
[config.user.request.groupId]: this.groupId || null,
[config.user.request.page]: this.currentPage,
[config.user.request.pageSize]: this.pageSize
}
var res = await config.user.apiObj.get(params);
this.showUserloading = false;
this.user = config.user.parseData(res).rows;
this.total = config.user.parseData(res).total || 0;
this.$refs.userScrollbar.setScrollTop(0)
},
//获取角色
async getRole(){
this.showGrouploading = true;
var res = await config.role.apiObj.get();
this.showGrouploading = false;
this.role = config.role.parseData(res).rows
},
//组织点击
groupClick(data){
this.keyword = ''
this.currentPage = 1
this.groupId = data[config.group.props.key]
this.getUser()
},
//用户点击
userClick(data, checked){
if(checked){
this.selected.push({
id: data[config.user.props.key],
name: data[config.user.props.label]
})
}else{
this.selected = this.selected.filter(item => item.id != data[config.user.props.key])
}
},
//用户分页点击
paginationChange(){
this.getUser()
},
//用户搜索
search(){
this.groupId = ''
this.$refs.groupTree.setCurrentKey(this.groupId)
this.currentPage = 1
this.getUser()
},
//删除已选
deleteSelected(index){
this.selected.splice(index,1);
if(this.type==1){
this.$refs.userTree.setCheckedKeys(this.selectedIds)
}else if(this.type==2){
this.$refs.groupTree.setCheckedKeys(this.selectedIds)
}
},
//角色点击
roleClick(data, checked){
if(checked){
this.selected.push({
id: data[config.role.props.key],
name: data[config.role.props.label]
})
}else{
this.selected = this.selected.filter(item => item.id != data[config.role.props.key])
}
},
//提交保存
save(){
this.value.splice(0,this.value.length);
this.selected.map(item => {
this.value.push(item)
})
this.dialogVisible = false
}
}
}
</script>
<style scoped>
.sc-user-select {display: flex;}
.sc-user-select__left {width: 400px;}
.sc-user-select__right {flex: 1;}
.sc-user-select__search {padding-bottom:10px;}
.sc-user-select__select {display: flex;border: 1px solid var(--el-border-color-light);background: var(--el-color-white);}
.sc-user-select__tree {width: 200px;height:300px;border-right: 1px solid var(--el-border-color-light);}
.sc-user-select__user {width: 200px;height:300px;display: flex;flex-direction: column;}
.sc-user-select__user__list {flex: 1;overflow: auto;}
.sc-user-select__user footer {height:36px;padding-top:5px;border-top: 1px solid var(--el-border-color-light);}
.sc-user-select__toicon {display: flex;justify-content: center;align-items: center;margin:0 10px;}
.sc-user-select__toicon i {display: flex;justify-content: center;align-items: center;background: #ccc;width: 20px;height: 20px;text-align: center;line-height: 20px;border-radius:50%;color: #fff;}
.sc-user-select__selected {height:345px;width: 200px;border: 1px solid var(--el-border-color-light);background: var(--el-color-white);}
.sc-user-select__selected header {height:43px;line-height: 43px;border-bottom: 1px solid var(--el-border-color-light);padding:0 15px;font-size: 12px;}
.sc-user-select__selected ul {height:300px;overflow: auto;}
.sc-user-select__selected li {display: flex;align-items: center;justify-content: space-between;padding:5px 5px 5px 15px;height:38px;}
.sc-user-select__selected li .name {display: flex;align-items: center;}
.sc-user-select__selected li .name .el-avatar {background: #409eff;margin-right: 10px;}
.sc-user-select__selected li .name label {}
.sc-user-select__selected li .delete {display: none;}
.sc-user-select__selected li:hover {background: var(--el-color-primary-light-9);}
.sc-user-select__selected li:hover .delete {display: inline-block;}
.sc-user-select-role .sc-user-select__left {width: 200px;}
.sc-user-select-role .sc-user-select__tree {border: none;height: 343px;}
.sc-user-select-role .sc-user-select__selected {}
[data-theme='dark'] .sc-user-select__selected li:hover {background: rgba(0, 0, 0, 0.2);}
[data-theme='dark'] .sc-user-select__toicon i {background: #383838;}
</style>