mb-list mb-form mb-xxx等 统一请求方法 列表(post) 保存(post) 删除(delete) 详情(get) 等

This commit is contained in:
吕金泽
2022-03-08 00:56:23 +08:00
parent 0f4095a564
commit 3114b2c205
58 changed files with 1017 additions and 351 deletions
@@ -0,0 +1,136 @@
<template>
<el-button
v-bind="el_"
@click="buttonClick"
>
{{ el_.text }}
</el-button>
</template>
<script>
import { getToken } from '@/scripts/auth'
import {ElNotification} from "element-plus";
export default {
name: 'MbButton',
props: {
el: {
type: Object,
default: () => {}
},
btnType: {
type: String,
default: ''
},
requestMethod: {
type: String,
default: 'get'
},
requestUrl: {
type: String,
default: ''
},
requestData: {
type: Object,
default: () => {}
},
beforeConfirm: {
type: String,
default: ''
},
successTips: {
type: String,
default: ''
},
failTips: {
type: String,
default: ''
},
confirmType: {
type: String,
default: 'warning'
},
afterHandler: {
type: Function,
default: () => {}
},
isOpen: {
type: Boolean,
default: false
}
},
data() {
return {
el_: this.el || {},
requestMethod_: this.requestMethod,
beforeConfirm_: this.beforeConfirm,
successTips_: this.successTips,
failTips_: this.failTips
}
},
created() {
if (this.btnType) {
if (this.btnType === 'delete') {
this.requestMethod_ = 'delete'
this.el_.type = 'danger'
this.el_.text = '删除'
this.el_.icon = 'ElDelete'
this.beforeConfirm_ = '此操作将永久删除该数据, 是否继续?'
this.successTips_ = '删除成功!'
this.failTips_ = '删除失败!'
}
}
},
methods: {
async buttonClick() {
if (this.beforeConfirm_) {
this.$confirm(this.beforeConfirm_, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: this.confirmType
}).then((res) => {
this.buttonClickRequest().then(() => {
this.afterHandler()
})
})
}
},
buttonClickRequest() {
// var requestOptions = {}
// requestOptions.url = this.requestUrl
// requestOptions.method = this.requestMethod_
// if (requestOptions.method === 'get') {
// requestOptions.params = this.requestData
// } else {
// requestOptions.data = this.requestData
// }
if (this.isOpen) {
return new Promise(() => {
window.open(this.$common.getUrl(process.env.VUE_APP_BASE_API + this.requestUrl, this.requestData) + '&token=' + getToken())
})
}
return new Promise((resolve, reject) => {
this.$request({
url: this.requestUrl,
method: this.requestMethod_,
params: this.requestData,
data: this.requestData
}).then(res => {
const { data } = res
if (data) {
ElNotification({
title: '成功',
message: this.successTips_,
type: 'success',
duration: 2000
})
} else {
this.$message.error(this.failTips_)
}
resolve()
})
})
}
}
}
</script>
@@ -0,0 +1,45 @@
<template>
<el-date-picker
v-model="modelValue"
:type="type"
:format="format"
:value-format="valueFormat"
:placeholder="placeholder"
:start-placeholder="startPlaceholder"
:end-placeholder="endPlaceholder"
v-bind="props.props"
/>
</template>
<script setup>
import { watch } from 'vue'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: String,
type: String,
placeholder: {
type: String,
default: '请选择时间'
},
format: {
type: String,
default: 'yyyy-MM-dd'
},
valueFormat: {
type: String,
default: 'yyyy-MM-dd'
},
startPlaceholder: {
type: String,
default: '开始时间'
},
endPlaceholder: {
type: String,
default: '结束时间'
},
props: Object
})
watch(() => props.modelValue, (value) => {
emit('update:modelValue', value)
})
</script>
@@ -0,0 +1,95 @@
<template>
<el-dialog :fullscreen="fullscreen" :width="width" :title="title" v-model="dialogVisible" :close-on-click-modal="false" :append-to-body="true" draggable @opened="opened">
<slot name="content" />
<template #footer>
<div slot="footer" class="dialog-footer">
<slot name="btns">
<el-button @click="dialogVisible = false">
关闭
</el-button>
<el-button type="primary" :loading="confirmLoading" @click="confirmClick">
确认
</el-button>
</slot>
</div>
</template>
</el-dialog>
</template>
<script>
export default {
emits: ['confirm-click'],
props: {
title: {
type: String,
default: ''
},
width: {
type: String,
default: '50%'
},
fullscreen: {
type: Boolean,
default: false
},
opened: {
type: Function,
default: () => {}
}
},
data() {
return {
dialogVisible: false,
confirmLoading: false
}
},
created() {
if (this.fullscreen) {
document.body.style.setProperty('--el-dialog__wrapper-bottom', '0vh')
document.body.style.setProperty('--el-dialog__wrapper-top', '0vh')
document.body.style.setProperty('--el-dialog__body-max-height', '100vh')
} else {
document.body.style.setProperty('--el-dialog__wrapper-bottom', '15vh')
document.body.style.setProperty('--el-dialog__wrapper-top', '15vh')
document.body.style.setProperty('--el-dialog__body-max-height', '60vh')
}
},
methods: {
confirmClick() {
this.$emit('confirm-click', this)
},
loading(){
this.confirmLoading = true
},
hideLoading(){
this.confirmLoading = false
},
show() {
this.dialogVisible = true
},
hide() {
this.dialogVisible = false
}
}
}
</script>
<style scoped>
.el-dialog__wrapper{
padding-bottom: var(--el-dialog__wrapper-bottom);
padding-top: var(--el-dialog__wrapper-top);
overflow: hidden;
}
.el-dialog__wrapper >>> .el-dialog{
margin-top: 0vh!important;
}
.el-dialog__wrapper >>> .el-dialog__body{
max-height: var(--el-dialog__body-max-height);
overflow: auto;
padding: 25px!important;
}
</style>
@@ -0,0 +1,77 @@
<template>
<div>
<el-row style="margin-bottom: 6px">
<el-button type="primary" @click="tableOptions.data.push({})">添加一行</el-button>
</el-row>
<mb-table v-bind="tableOptions">
<template v-for="col in cols" #[col.field]="{ index }">
<el-input v-if="col.type === 'input'" v-bind="col.properties" v-model="tableOptions.data[index][col.field]" @change="dataChange" />
<mb-select v-else-if="col.type === 'select'" v-bind="col.properties" v-model="tableOptions.data[index][col.field]" @change="dataChange" />
</template>
</mb-table>
</div>
</template>
<script>
export default {
name: 'MbEditorTable',
model: {
prop: 'value',
event: 'change'
},
props: {
// eslint-disable-next-line vue/require-prop-types
value: {
required: true
},
cols: {
type: Array,
default: () => []
},
showNo: {
type: Boolean,
default: true
}
},
data() {
return {
tableOptions: {
data: [],
cols: [],
showNo: this.showNo
}
}
},
created() {
for (var i in this.cols) {
var col = this.cols[i]
this.tableOptions.cols.push({
type: 'dynamic',
field: col.field,
title: col.title
})
}
this.tableOptions.cols.push({
title: '操作',
type: 'btns',
width: 85,
fixed: 'right',
btns: [{
title: '删除',
type: 'danger',
click: (row, index) => {
this.tableOptions.data.splice(index, 1)
}
}]
})
},
methods: {
dataChange() {
console.log('更新')
this.$emit('update:value', this.tableOptions.data)
this.$emit('change', this.tableOptions.data)
}
}
}
</script>
@@ -0,0 +1,27 @@
<template>
<svg aria-hidden="true" class="mb-icon">
<use :xlink:href="symbolId" :class="className"/>
</svg>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
prefix: {
type: String,
default: 'mb-icon'
},
icon: String,
size: String,
});
const symbolId = computed(() => props.icon&&props.icon.startsWith('#') ? props.icon : `#${props.prefix}-${props.icon}`)
const className = computed(() => props.icon&&props.icon.startsWith('#') ? props.icon.substring(1) : `${props.prefix}-${props.icon}`)
</script>
<style scoped>
svg {
width: 1.3em;
height: 1.3em;
vertical-align: -0.25em;
overflow: hidden;
fill: var(--mb-main-icon-color)
}
</style>
@@ -0,0 +1,19 @@
<template>
<el-input v-model="modelValue" :type="type" :value="value" :placeholder="placeholder || (label && '请输入' + label)" v-bind="props.props" />
</template>
<script setup>
import { watch } from 'vue'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: String,
label: String,
placeholder: String,
value: String,
type: String,
props: Object
})
watch(() => props.modelValue, (value) => {
emit('update:modelValue', value)
})
</script>
@@ -0,0 +1,97 @@
<template>
<div :class="{'hidden':hidden}" class="pagination-container">
<el-pagination
:background="background"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:total="total"
v-bind="$attrs"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { scrollTo } from '@/scripts/scroll-to'
const props = defineProps({
total: {
required: true,
type: Number
},
page: {
type: Number,
default: 1
},
limit: {
type: Number,
default: 10
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50]
}
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true
},
autoScroll: {
type: Boolean,
default: true
},
hidden: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:page', 'update:limit', 'pagination'])
const currentPage = computed({
get: () => props.page,
set: (val) => {
emit('update:page', val)
}
})
const pageSize = computed({
get: () => props.limit,
set: (val) => {
emit('update:limit', val)
}
})
function handleSizeChange(val) {
emit('pagination', { page: currentPage, limit: val })
if (props.autoScroll) {
scrollTo(0, 800)
}
}
function handleCurrentChange(val) {
emit('pagination', { page: val, limit: pageSize })
if (props.autoScroll) {
scrollTo(0, 800)
}
}
</script>
<style scoped>
.pagination-container {
background: #fff;
padding: 32px 16px;
}
.pagination-container.hidden {
display: none;
}
</style>
@@ -0,0 +1,17 @@
<template>
<el-radio-group v-model="modelValue" v-bind="props.props">
<el-radio-button v-for="it in options" :label="it.value" :disabled="it.disabled" :name="it.disabled">{{ it.label }}</el-radio-button>
</el-radio-group>
</template>
<script setup>
import { watch } from 'vue'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: String,
options: Array
})
watch(() => props.modelValue, (value) => {
emit('update:modelValue', value)
})
</script>
@@ -0,0 +1,3 @@
<template>
</template>
@@ -0,0 +1,108 @@
<template>
<div class="filter-container">
<el-form :inline="true" @keyup.enter="search">
<span v-for="(it, i) in where">
<el-form-item v-if="it && it.label" :label="it.label" :key="i">
<el-input v-if="!it.type || it.type == 'input'" @input="input(it.input)" v-model="it.value" :placeholder="it.placeholder || ('请输入' + it.label)" style="width: 200px;" class="filter-item" />
<mb-select v-else-if="it.type == 'select'" v-model="it.value" :placeholder="'请选择' + it.label" v-bind="it.properties" />
<el-date-picker
v-else-if="it.type == 'date' || it.type == 'datetime' || it.type == 'daterange' || it.type == 'datetimerange'"
v-model="it.value"
align="right"
:format="it.type.startsWith('datetime') ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'"
:value-format="it.type.startsWith('datetime') ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'"
:type="it.type"
start-placeholder="开始时间"
end-placeholder="结束时间"
:placeholder="it.type.startsWith('datetime') ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'"
>
</el-date-picker>
<component v-else :is="it.type" v-model="it.value" v-bind="it.properties" />
</el-form-item>
</span>
<el-form-item>
<el-button class="filter-item" type="primary" icon="ElSearch" @click="search">
搜索
</el-button>
<el-button class="filter-item" icon="ElDelete" @click="reset">
清空
</el-button>
</el-form-item>
<el-form-item>
<slot name="btns" />
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { nextTick, watch } from 'vue'
const props = defineProps({
where: {
type: Object,
default: () => {}
},
notReset: {
type: String,
default: ''
}
})
for(var key in props.where){
if(props.where[key] instanceof Object && props.where[key].value == undefined){
props.where[key].value = null
}
}
watch(() => props.where,() => {
console.log(props.where)
})
const emit = defineEmits(['search'])
function input(input){
if(input){
emit('search')
}
}
function search(){
for(var key in props.where){
if(props.where[key] instanceof Object){
if(props.where[key].type && props.where[key].type.startsWith('date') && props.where[key].value instanceof Array){
props.where[key].value = props.where[key].value.join(',')
}
}
}
nextTick(() => {
emit('search')
for(var key in props.where){
if(props.where[key] instanceof Object){
if(props.where[key].type && props.where[key].type.startsWith('date')){
props.where[key].value = props.where[key].value.split(',')
}
}
}
})
}
function reset() {
for(var key in props.where){
if(props.notReset.indexOf(key) == -1){
if(props.where[key] instanceof Object){
props.where[key].value = null
}else{
props.where[key] = null
}
}
}
nextTick(() => emit('search'))
}
</script>
<style scoped>
</style>
@@ -0,0 +1,154 @@
<template>
<el-select v-if="mbType === 'select'" v-model="selectValue" v-bind="el" :style="{ width }" :placeholder="placeholder || '请选择'" filterable clearable>
<el-option
v-for="item in selectList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-radio-group v-if="mbType === 'radio'" v-model="selectValue">
<el-radio v-for="item in selectList" :key="item.value" :label="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</template>
<script setup>
import { ref, watch, onMounted, getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['update:modelValue', 'change'])
const props = defineProps({
data: {
type: Array,
default: () => []
},
type: {
type: String,
default: ''
},
// eslint-disable-next-line vue/require-prop-types
modelValue: {
required: true
},
width: {
type: String,
default: '100%'
},
allOption: {
type: Boolean,
default: false
},
url: {
type: String,
default: ''
},
params: {
type: Object,
default: () => {}
},
labelField: {
type: String,
default: 'label'
},
valueField: {
type: String,
default: 'value'
},
mbType: {
type: String,
default: 'select'
},
el: {
type: Object,
default: () => {}
},
placeholder: {
type: String,
default: ''
}
})
const selectList = ref([])
const selectValue = ref('')
watch(() => props.type, () => {
if (props.modelValue instanceof Array || props.modelValue.toString().indexOf(',') !== -1) {
selectValue.value = []
} else {
selectValue.value = ''
}
loadData()
})
watch(() => props.modelValue, () => {
loadData()
})
watch(selectValue, (value) => {
if (props.el && props.el.multiple && value.length > 0) {
value = value.join(',')
}
emit('update:modelValue', value)
emit('change', value)
})
onMounted(() => {
loadData()
})
async function loadData() {
if (props.modelValue || props.modelValue == '') {
if ((!(props.modelValue instanceof Array) && props.modelValue.toString().indexOf(',') !== -1)) {
selectValue.value = props.modelValue.split(',')
} else {
if (props.el && props.el.multiple && !(props.modelValue instanceof Array)) {
selectValue.value = [props.modelValue]
} else {
selectValue.value = props.modelValue
}
}
} else {
if (props.el && props.el.multiple) {
selectValue.value = []
}
}
if (props.data && props.data.length > 0) {
listConcat(handlerData(props.data))
} else if (props.url) {
proxy.$get(props.url, props.params).then(res => {
listConcat(handlerData(res.data.list || res.data))
})
} else {
listConcat(proxy.$common.getDictType(props.type))
}
}
function listConcat(dictData) {
if (props.allOption) {
selectList.value = [{
value: '',
label: '全部'
}]
selectList.value = selectList.value.concat(dictData)
} else {
selectList.value = dictData
}
}
function handlerData(data) {
var newData = []
data.forEach(it => {
newData.push({
label: it[props.labelField],
value: it[props.valueField].toString()
})
})
return newData
}
</script>
@@ -0,0 +1,22 @@
<template>
<el-switch
v-model="modelValue"
:active-value="activeValue"
:inactive-value="inactiveValue"
v-bind="props.props"
/>
</template>
<script setup>
import { watch } from 'vue'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: String,
activeValue: String,
inactiveValue: String,
props: Object
})
watch(() => props.modelValue, (value) => {
emit('update:modelValue', value)
})
</script>
@@ -0,0 +1,50 @@
<template>
<el-table-column
:key="col.field"
:label="col.title"
:prop="col.field"
:align="col.align || 'center'"
:width="col.width"
:fixed="col.fixed"
:sortable="col.sortable"
>
<template v-if="!col.cols" #default="scope">
<span v-if="col.templet" v-html="col.templet(scope.row)" />
<span v-else-if="col.dictType">
{{ $common.getDictLabel(col.dictType, scope.row[col.field] + '') }}
</span>
<slot v-else-if="col.type == 'dynamic'" :name="col.field" :row="scope.row" :index="scope.$index" />
<el-switch
v-else-if="col.type == 'switch'"
v-model="scope.row[col.field]"
:active-value="col.activeValue || 1"
:inactive-value="col.inactiveValue || 0"
@change="col.change(scope.row)"
/>
<div v-else-if="col.type == 'btns'">
<template v-for="btn in col.btns">
<el-button v-if="btn.if === undefined ? true : btn.if(scope.row)" :icon="btn.icon" :key="btn.title" v-permission="btn.permission" :type="btn.type" :size="btn.size || 'small'" :class="btn.class" @click="btn.click(scope.row, scope.$index)">
{{ btn.title }}
</el-button>
</template>
</div>
<el-image v-else-if="col.type === 'image'" :src="scope.row[col.field]" :preview-src-list="[scope.row[col.field]]" />
<span v-else-if="col.type === 'html'" v-html="scope.row[col.field]"></span>
<span v-else-if="col.click">
<a style="color: blue" @click="col.click(scope.row)">{{ scope.row[col.field] }}</a>
</span>
<span v-else-if="col.field">{{ scope.row[col.field] }}</span>
</template>
<mb-table-column v-for="(col2, j) in col.cols" :key="j" :col="col2">
<template v-for="(value, key) in $slots" #[key]="{ row, index }">
<slot :row="row" :index="index" :name="key" />
</template>
</mb-table-column>
</el-table-column>
</template>
<script setup>
defineProps({
col: Object
})
</script>
@@ -0,0 +1,240 @@
<template>
<div>
<el-table
:key="tableKey"
v-loading="listLoading"
:data="list"
border
fit
highlight-current-row
v-bind="el"
style="width: 100%;"
@sort-change="sortChange"
@selection-change="selectionChange"
>
<el-table-column v-if="selection" align="center" type="selection" width="50" />
<el-table-column v-if="showNo" label="序号" prop="num" align="center" width="65">
<template #default="scope">
<span>{{ scope.$index+1 }}</span>
</template>
</el-table-column>
<mb-table-column v-for="(col, i) in cols" :key="i" :col="col">
<template v-for="(value, key) in $slots" #[key]="{ row, index }">
<slot :row="row" :index="index" :name="key" />
</template>
</mb-table-column>
<template empty>
<el-empty :description="emptyText" />
</template>
</el-table>
<mb-pagination v-show="total > 0 && page" :total="total || 0" v-model:page="listCurrent" v-model:limit="limit" @pagination="handlerPagination" />
</div>
</template>
<script setup>
import { ref, reactive, watch, onMounted, getCurrentInstance,defineExpose } from 'vue'
import request from '@/scripts/request'
const { proxy } = getCurrentInstance()
const props = defineProps({
el: {
type: Object,
default: () => {}
},
url: {
type: String,
default: ''
},
limit: {
type: Number,
default: 10
},
page: {
type: Boolean,
default: true
},
done: {
type: Function,
default: () => {}
},
where: {
type: Object,
default: () => {
return {}
}
},
showNo: {
type: Boolean,
default: true
},
selection: {
type: Boolean,
default: false
},
data: {
type: Array,
default: null
},
method: {
type: String,
default: 'post'
},
cols: {
type: Array,
default: () => {
return []
}
},
emptyText: {
type: String,
default: '暂无数据'
}
})
const emit = defineEmits(['selection-change'])
const listCurrent = ref(1)
const total = ref(0)
const list = ref([])
const listLoading = ref(false)
const tableKey = ref(0)
let newWhere = reactive({})
function renderWhere(){
newWhere = reactive(proxy.$common.renderWhere(props.where))
}
function getList() {
renderWhere()
listLoading.value = true
if (props.page) {
newWhere.current = listCurrent.value
newWhere.size = props.limit
} else {
newWhere.size = 99999999
}
request({
url: props.url,
method: props.method,
params: newWhere,
data: newWhere
}).then(res => {
const { data } = res
total.value = data.total
list.value = data.list
listLoading.value = false
props.done()
})
}
function sortChange(column) {
let order = column.order
if (order) {
order = order === 'descending' ? 'desc' : ''
order = column.prop + ' ' + order
} else {
order = null
}
newWhere.orderBy = order
reloadList()
}
function selectionChange(columns) {
emit('selection-change', columns)
}
function reloadList() {
if (props.url) {
newWhere.current = 1
listCurrent.value = 1
getList()
}
}
function handlerData() {
listLoading.value = true
total.value = props.data.length
var currPageData = []
props.data.forEach((it, i) => {
if (i >= ((listCurrent.value - 1) * props.limit) && i < (listCurrent.value * props.limit) && currPageData.length < props.limit) {
currPageData.push(it)
}
})
list.value = currPageData
props.done()
listLoading.value = false
}
function handlerPagination() {
if (props.url) {
getList()
}
if (props.data) {
handlerData()
}
}
function keyup(){
document.onkeyup = (e) => {
if(e.target.nodeName != 'INPUT'){
if (e && e.keyCode == 37) {
if(listCurrent.value != 1){
listCurrent.value -= 1
handlerPagination()
}
} else if (e && e.keyCode == 39) {
if(listCurrent.value != parseInt((total.value + props.limit - 1) / props.limit)){
listCurrent.value += 1
handlerPagination()
}
}
}
}
}
watch(() => props.data, () => {
listCurrent.value = 1
handlerData()
})
watch(() => props.where,() => {
renderWhere()
},{ deep: true })
renderWhere()
onMounted(() => {
keyup()
if (props.data) {
handlerData()
}
if (props.url) {
getList()
}
})
defineExpose({ reloadList })
</script>
<style scoped>
.el-image >>> .el-image__inner {
max-height: 100px;
width: auto;
height: auto;
}
</style>
<style>
.el-table-column--selection .cell {
padding:0px 15px!important;
}
.el-table th {
background: #F5F7FA;
}
</style>
@@ -0,0 +1,159 @@
<template>
<div>
<div style="margin-bottom: 5px;" v-if="expand || checked">
<el-button v-if="expand" type="primary" icon="ElSort" plain @click="doExpand">展开/折叠</el-button>
<el-button v-if="checked" type="primary" icon="ElCheck" plain @click="() => { treeAllChecked = !treeAllChecked; checkedAll(searchData, treeAllChecked) }">全选/全不选</el-button>
</div>
<div style="margin-bottom: 5px;" v-if="search">
<el-input v-model="searchValue" placeholder="输入关键字进行过滤" @input="searchTree" :style="{ width: searchWidth }" />
</div>
<el-tree
v-if="refreshTree"
ref="tree"
:data="searchData"
v-bind="el"
node-key="id"
:default-expand-all="defaultExpandAll"
:default-checked-keys="checkedIds"
@check-change="checkChange"
@node-click="nodeClick"
:props="defaultProps"
:style="{ 'max-height': maxHeight ? maxHeight : '100%' }"
style="overflow: auto"
/>
</div>
</template>
<script setup>
import { watch, ref, reactive, defineExpose, nextTick, getCurrentInstance, onBeforeMount } from 'vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['update:select-values', 'check-change', 'node-click'])
const props = defineProps({
url: {
type: String,
default: ''
},
params: {
type: Object,
default: () => {}
},
selectValues: {
type: String,
default: ''
},
maxHeight: {
type: String,
default: ''
},
el: {
type: Object,
default: () => {}
},
expand: {
type: Boolean,
default: true
},
checked: {
type: Boolean,
default: true
},
search: {
type: Boolean,
default: false
},
searchWidth: {
type: String,
default: '230px'
},
checkedIds: {
type: Array,
default: () => []
}
})
const tree = ref()
const treeData = ref([])
const searchData = ref([])
const defaultProps = reactive({
children: 'children',
label: 'name'
})
const defaultExpandAll = ref(true)
const refreshTree = ref(true)
const treeAllChecked = ref(false)
const searchValue = ref('')
onBeforeMount(async () => {
await loadTreeData()
})
watch(() => props.selectValues, async () => {
await loadTreeData()
checkedAll(searchData.value, false)
var values = props.selectValues.split(',');
for(var i in values){
tree.value.setChecked(values[i], true, false)
}
})
function searchTree() {
if(searchValue.value){
searchData.value = proxy.$treeTable.recursionSearch(['name'], proxy.$common.copyNew(treeData.value), searchValue.value, false)
}else{
searchData.value = treeData.value
}
}
function doExpand() {
refreshTree.value = false
defaultExpandAll.value = !defaultExpandAll.value
nextTick(() => refreshTree.value = true)
}
async function loadTreeData() {
if(treeData.value.length == 0){
await proxy.$get(props.url, props.params).then((res) => {
treeData.value = res.data.list
searchData.value = treeData.value
})
}
}
function getTree() {
return tree.value
}
function checkChange(node) {
var selectMenus = []
var checkedNodes = tree.value.getCheckedNodes(false, true)
for (var i = 0; i < checkedNodes.length; i++) {
selectMenus.push(checkedNodes[i].id)
}
emit('update:select-values', selectMenus.join(','))
emit('check-change', selectMenus.join(','))
}
function nodeClick(param1, param2, param3){
emit('node-click', param1, param2, param3)
}
function checkedAll(children, checked) {
if (tree.value) {
for (var i in children) {
var id = children[i].id
if(children[i].children && children[i].children.length > 0){
checkedAll(children[i].children, checked)
}
tree.value.setChecked(id, checked, true)
}
}
}
defineExpose({ getTree })
</script>
@@ -0,0 +1,30 @@
<template>
<treeselect v-model="modelValue" :options="options" :key="modelValue" :placeholder="placeholder || (label && '请选择' + label)" :show-count="true" v-bind="props.props" />
</template>
<script setup>
import { ref, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance()
const props = defineProps({
modelValue: {
type: String,
default: null
},
url: {
type: String,
default: '',
required: true
},
label: String,
placeholder: String,
props: Object
})
const options = ref([])
proxy.$get(props.url).then(res => {
options.value = res.data.list
proxy.$treeTable.deleteEmptyChildren(options.value)
})
</script>
@@ -0,0 +1,231 @@
<template>
<el-upload
:id="uploadDomId"
class="upload-demo"
:action="action"
:headers="headers"
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-remove="beforeRemove"
:multiple="multiple"
:limit="limit"
:on-exceed="handleExceed"
:file-list="fileList"
:before-upload="beforeAvatarUpload"
:on-success="handleAvatarSuccess"
>
<el-button size="small" type="primary" :disabled="!multiple && fileList.length == 1">点击上传</el-button>
<div slot="tip" class="el-upload__tip">支持上传{{ getSettingSuffixs().replaceAll(',', '') }}文件且不超过{{ maxFileSize }}MB</div>
</el-upload>
</template>
<script>
import { getToken } from '@/scripts/auth'
export default {
name: 'MbUploadFile',
emits: ['change', 'update:modelValue'],
model: {
prop: 'modelValue',
event: 'change'
},
props: {
modelValue: {
required: false
},
multiple: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: 20
},
maxFileSize: {
type: Number,
default: 200
},
accept: {
type: String,
default: ''
},
externalId: {
type: String,
default: ''
},
externalType: {
type: String,
default: ''
},
formats: {
type: String,
default: ''
}
},
data() {
return {
acceptList: {
image: 'png,jpg,gif,jpeg',
wps: 'pdf,pptx,xls,xlsx,csv,docx,doc',
compress: 'zip,rar,7z',
video: 'avi,flv,mp4,mpeg,mov'
},
imageUrl: '',
action: import.meta.env.VITE_APP_BASE_API + 'file/upload',
headers: {
token: getToken()
},
urls: [],
uploadDomId: Math.random(),
fileList: []
}
},
watch: {
value(newValue) {
this.renderFile()
}
},
created() {
if (this.externalId) {
this.$get('file/files', { externalId: this.externalId, externalType: this.externalType }).then(res => {
const { data } = res
this.fileList = data
})
this.action = this.action + `?externalId=${this.externalId}&externalType=${this.externalType}`
} else {
this.renderFile()
}
},
methods: {
renderFile() {
if (this.value instanceof Array && this.value.length > 0) {
this.fileList = this.value.map(it => {
return {
name: it.substring(it.lastIndexOf('/') + 1),
response: {
data: {
url: it
}
}
}
})
} else {
if (this.value) {
this.fileList.push({
name: this.value.substring(this.value.lastIndexOf('/') + 1),
response: {
data: {
url: this.value
}
}
})
}
}
},
handleRemove(file, fileList) {
var url = file.response.data.url
this.urls.splice(this.urls.indexOf(url), 1)
this.fileList.forEach((it, i) => {
if (it && it.response.data.url.indexOf(url) !== -1) {
this.fileList.splice(i, 1)
}
})
if (this.multiple) {
this.$emit('update:modelValue', this.urls)
this.$emit('change', this.urls)
} else {
document.getElementById(this.uploadDomId).getElementsByClassName('el-upload__input')[0].removeAttribute('disabled')
this.$emit('update:modelValue', '')
this.$emit('change', '')
}
this.$delete('file/delete', { url: encodeURI(url) })
},
handlePreview(file) {
window.open(this.$global.filePrefix + file.response.data.url)
},
handleExceed(files, fileList) {
this.$message.warning(`当前限制选择 ${this.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
},
beforeRemove(file, fileList) {
return this.$confirm(`确定移除 ${file.name}`)
},
handleAvatarSuccess(res, file, fileList) {
if (res.data) {
if (this.multiple) {
this.urls.push(res.data.url)
this.$emit('update:modelValue', this.urls)
this.$emit('change', this.urls)
} else {
document.getElementById(this.uploadDomId).getElementsByClassName('el-upload__input')[0].setAttribute('disabled', '')
this.$emit('update:modelValue', res.data.url)
this.$emit('change', res.data.url)
}
}
},
getSettingSuffixs() {
if (this.formats) {
return this.formats
}
var suffixs = this.acceptList[this.accept]
if (!suffixs) {
suffixs = this.getAllSuffixs()
}
return suffixs
},
beforeAvatarUpload(file, fileList) {
var fileName = file.name
var accepts = this.accept.split(',')
if (accepts) {
for (var i = 0; i < accepts.length; i++) {
if (!this.validAccept(fileName, accepts[i])) {
this.$message.error('上传文件格式只能为:' + this.getSettingSuffixs().replaceAll(',', ''))
return false
}
}
} else {
if (!this.validAccept(fileName, 'null')) {
this.$message.error('上传文件格式只能为:' + this.getAllSuffixs().replaceAll(',', ''))
return false
}
}
const isLt2M = file.size / 1024 / 1024 < this.maxFileSize
if (!isLt2M) {
this.$message.error(`上传文件大小不能超过 ${this.maxFileSize}MB`)
return isLt2M
}
},
getAllSuffixs() {
var suffixs = ''
for (const key in this.acceptList) {
suffixs += this.acceptList[key] + ','
}
suffixs = suffixs.substring(0, suffixs.length - 1)
return suffixs
},
validAccept(fileName, accept) {
if (this.formats) {
return this.validEndsWith(fileName, this.formats)
}
if (accept && this.acceptList[accept]) {
return this.validEndsWith(fileName, this.acceptList[accept])
} else {
return this.validEndsWith(fileName, this.getAllSuffixs())
}
},
validEndsWith(fileName, suffixs) {
suffixs = suffixs.split(',')
for (var i = 0; i < suffixs.length; i++) {
const suffix = suffixs[i]
if (fileName.toLowerCase().endsWith('.' + suffix)) {
return true
}
}
return false
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,367 @@
<template>
<div>
<vuedraggable
v-model="urls"
class="vue-draggable"
tag="div"
draggable=".draggable-item"
@end="onDragEnd"
>
<template #item="{ element }">
<div
class="draggable-item"
:style="{ width: width.replace('px', '') + 'px', height: height.replace('px', '') + 'px' }"
>
<el-image
:src="$global.filePrefix + element"
:preview-src-list="[$global.filePrefix + element]"
/>
<div class="tools">
<div class="shadow" @click="handleRemove(element)">
<el-icon>
<ElDelete />
</el-icon>
</div>
<div class="shadow" @click="beforeCropper(element)">
<el-icon>
<ElScissor />
</el-icon>
</div>
</div>
</div>
</template>
<template #footer>
<el-upload
v-if="(!multiple && urls.length == 0) || (multiple && urls.length < limit)"
ref="uploadRef"
class="uploadBox"
:style="{ width: width.replace('px', '') + 'px', height: height.replace('px', '') + 'px' }"
:action="action"
:file-list="fileList"
:headers="headers"
accept=".jpg,.jpeg,.png,.gif"
:show-file-list="false"
:multiple="multiple"
:limit="limit"
:on-success="handleAvatarSuccess"
:on-exceed="onExceed"
>
<el-icon class="uploadIcon">
<ElPlus />
<span v-show="isUploading" class="uploading">正在上传...</span>
<span
v-if="!isUploading && limit && limit!==99 && multiple"
class="limitTxt"
>最多{{ limit }}</span>
</el-icon>
</el-upload>
</template>
</vuedraggable>
<mb-dialog ref="cropperDialog" @confirm-click="cropper">
<template #content>
<div class="cropper-content">
<div class="cropper" style="text-align:center">
<vueCropper
ref="cropper"
v-bind="cropperOption"
:outputSize="cropperOption.outputSize === undefined ? 0.8 : cropperOption.outputSize"
:outputType="cropperOption.outputType === undefined ? 'jpeg' : cropperOption.outputType"
:canMove="cropperOption.canMove === undefined ? true : cropperOption.canMove"
:canMoveBox="cropperOption.canMoveBox === undefined ? true : cropperOption.canMoveBox"
:autoCrop="cropperOption.autoCrop === undefined ? true : cropperOption.autoCrop"
:centerBox="cropperOption.centerBox === undefined ? true : cropperOption.centerBox"
/>
</div>
</div>
</template>
</mb-dialog>
</div>
</template>
<script>
import 'vue-cropper/dist/index.css'
import { VueCropper } from 'vue-cropper'
import vuedraggable from 'vuedraggable'
import { getToken } from '@/scripts/auth'
export default {
name: 'MbUploadImage',
emits: ['update:modelValue', 'change'],
components: { vuedraggable, VueCropper },
model: {
prop: 'modelValue',
event: 'change'
},
props: {
modelValue: {
required: false
},
externalId: {
type: String,
default: ''
},
externalType: {
type: String,
default: ''
},
multiple: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: 2
},
cropperConfig: {
type: Object,
default: () => {}
},
width: {
type: String,
default: '100'
},
height: {
type: String,
default: '100'
}
},
data() {
return {
action: import.meta.env.VITE_APP_BASE_API + 'file/upload',
headers: {
token: getToken()
},
dialogImageUrl: '',
dialogVisible: false,
disabled: false,
isUploading: false,
cropperOption: {},
urls: [],
fileList: []
}
},
watch: {
modelValue(newValue) {
if (newValue instanceof Array) {
this.urls = newValue
this.fileList = this.urls.map(it => { return { response: { data: { url: it }}} })
} else {
if (newValue && this.urls.length === 0) {
this.urls.push(newValue)
}
}
}
},
created() {
this.cropperOption = this.cropperConfig || {}
this.cropperOption.img = ''
if (this.externalId) {
this.$get('file/files', { externalId: this.externalId, externalType: this.externalType }).then(res => {
this.urls = res.data
})
this.action = this.action + `?externalId=${this.externalId}&externalType=${this.externalType}`
} else {
if (this.modelValue instanceof Array) {
this.urls = this.modelValue
this.fileList = this.urls.map(it => { return { response: { data: { url: it }}} })
} else {
if (this.modelValue) {
this.urls.push(this.modelValue)
}
}
}
},
methods: {
handleRemove(url) {
this.urls.splice(this.urls.indexOf(url), 1)
this.fileList.forEach((it, i) => {
if (it && it.response.data.url.indexOf(url) !== -1) {
this.fileList.splice(i, 1)
}
})
this.$delete('file/delete', { url: encodeURI(url) })
if (this.multiple) {
this.$emit('update:modelValue', this.urls)
this.$emit('change', this.urls)
} else {
this.$emit('update:modelValue', '')
this.$emit('change', '')
}
},
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url
this.dialogVisible = true
},
handleDownload(file) {
console.log(file)
},
handleAvatarSuccess(res, file, fileList) {
this.fileList = fileList
if (res.data) {
this.urls.push(res.data.url)
console.log(this.urls)
if (this.multiple) {
this.$emit('update:modelValue', this.urls)
this.$emit('change', this.urls)
} else {
this.$emit('update:modelValue', res.data.url)
this.$emit('change', res.data.url)
}
this.onDragEnd()
} else {
this.$message({ type: 'error', message: res.msg })
}
this.isUploading = false
},
onDragEnd() {
var newUrls = []
this.urls.forEach(url => {
newUrls.push(encodeURI(url))
})
this.$get('file/resort', { urls: newUrls.join(',') })
},
onExceed() {
this.$message({
type: 'warning',
message: `图片超限,最多可上传${this.limit}张图片`
})
},
beforeCropper(url) {
this.cropperOption.img = this.$global.filePrefix + url
this.cropperOption.relativeImg = url
this.$refs.cropperDialog.show()
},
cropper() {
this.$refs.cropper.getCropBlob((data) => {
var dataFile = new File([data], this.cropperOption.relativeImg.substring(this.cropperOption.relativeImg.lastIndexOf('/') + 1), { type: data.type, lastModified: Date.now() })
var formData = new FormData()
formData.append('file', dataFile)
formData.append('url', encodeURI(this.cropperOption.relativeImg))
this.$request({
url: 'file/cropper',
method: 'post',
data: formData
}).then(res => {
this.urls.forEach((it, i) => {
if (this.cropperOption.img.indexOf(it) !== -1) {
this.urls[i] = res.data.url
this.$refs.cropperDialog.hide()
}
})
})
})
}
}
}
</script>
<style scoped>
.vue-draggable >>> .el-upload {
width: 100%;
height: 100%;
}
</style>
<style lang="scss" scoped>
// 上传按钮
.uploadIcon {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #c0ccda;
background-color: #fbfdff;
border-radius: 6px;
font-size: 20px;
color: #999;
.limitTxt,
.uploading {
position: absolute;
bottom: 10%;
left: 0;
width: 100%;
font-size: 14px;
text-align: center;
}
}
// 拖拽
.vue-draggable {
display: flex;
flex-wrap: wrap;
.draggable-item {
margin-right: 5px;
margin-bottom: 5px;
border: 1px solid #ddd;
border-radius: 6px;
position: relative;
overflow: hidden;
.el-image {
width: 100%;
height: 100%;
}
.tools {
position: absolute;
top:0px;
width: 100%;
height: 20px;
}
.shadow {
display: inline-block;
background-color: rgba(0,0,0,.5);
opacity: 0;
transition: opacity .3s;
color: #fff;
font-size: 20px;
line-height: 20px;
padding: 2px;
cursor: pointer;
}
&:hover {
.shadow {
opacity: 1;
}
}
}
&.hideShadow {
.shadow {
display: none;
}
}
&.single {
overflow: hidden;
position: relative;
.draggable-item {
position: absolute;
left: 0;
top: 0;
z-index: 1;
}
}
&.maxHidden {
.uploadBox {
display: none;
}
}
}
// el-image
.el-image-viewer__wrapper {
.el-image-viewer__mask {
opacity: .8;
}
.el-icon-circle-close {
color: #fff;
}
}
.cropper-content {
.cropper {
width: auto;
height: 300px;
}
}
</style>