mirror of
https://gitee.com/ssssssss-team/magic-boot.git
synced 2026-08-16 00:00:34 +08:00
mb-list mb-form mb-xxx等 统一请求方法 列表(post) 保存(post) 删除(delete) 详情(get) 等
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user