新增
This commit is contained in:
@@ -1,49 +1,49 @@
|
||||
<template>
|
||||
<el-date-picker
|
||||
v-model="content"
|
||||
:type="pickerType"
|
||||
range-separator="-"
|
||||
:start-placeholder="startPlaceholder"
|
||||
:end-placeholder="endPlaceholder"
|
||||
:value-format="valueFormat"
|
||||
clearable
|
||||
@change="emit('change', $event)"
|
||||
></el-date-picker>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
pickerType?: 'daterange' | 'datetimerange'
|
||||
valueFormat?: string
|
||||
startPlaceholder?: string
|
||||
endPlaceholder?: string
|
||||
}>(),
|
||||
{
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
pickerType: 'datetimerange',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
}
|
||||
)
|
||||
const emit = defineEmits(['update:startTime', 'update:endTime', 'change'])
|
||||
|
||||
const content = computed<any>({
|
||||
get: () => {
|
||||
return [props.startTime, props.endTime]
|
||||
},
|
||||
set: (value: Event | any) => {
|
||||
if (value === null) {
|
||||
emit('update:startTime', '')
|
||||
emit('update:endTime', '')
|
||||
} else {
|
||||
emit('update:startTime', value[0])
|
||||
emit('update:endTime', value[1])
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<el-date-picker
|
||||
v-model="content"
|
||||
:type="pickerType"
|
||||
range-separator="-"
|
||||
:start-placeholder="startPlaceholder"
|
||||
:end-placeholder="endPlaceholder"
|
||||
:value-format="valueFormat"
|
||||
clearable
|
||||
@change="emit('change', $event)"
|
||||
></el-date-picker>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
pickerType?: 'daterange' | 'datetimerange'
|
||||
valueFormat?: string
|
||||
startPlaceholder?: string
|
||||
endPlaceholder?: string
|
||||
}>(),
|
||||
{
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
pickerType: 'datetimerange',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
}
|
||||
)
|
||||
const emit = defineEmits(['update:startTime', 'update:endTime', 'change'])
|
||||
|
||||
const content = computed<any>({
|
||||
get: () => {
|
||||
return [props.startTime, props.endTime]
|
||||
},
|
||||
set: (value: Event | any) => {
|
||||
if (value === null) {
|
||||
emit('update:startTime', '')
|
||||
emit('update:endTime', '')
|
||||
} else {
|
||||
emit('update:startTime', value[0])
|
||||
emit('update:endTime', value[1])
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,157 +1,157 @@
|
||||
<template>
|
||||
<div class="export-data">
|
||||
<popup
|
||||
ref="popupRef"
|
||||
title="导出设置"
|
||||
width="500px"
|
||||
confirm-button-text="确认导出"
|
||||
@confirm="handleConfirm"
|
||||
:async="true"
|
||||
@open="getData"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button>导出</el-button>
|
||||
</template>
|
||||
<div>
|
||||
<p v-if="props.exportHint" class="text-sm text-gray-500 mb-3 leading-relaxed">{{ props.exportHint }}</p>
|
||||
<el-form ref="formRef" :model="formData" label-width="120px" :rules="formRules">
|
||||
<el-form-item label="数据量:">
|
||||
预计导出{{ exportData.count }}条数据, 共{{ exportData.sum_page }}页,每页{{
|
||||
exportData.page_size
|
||||
}}条数据
|
||||
</el-form-item>
|
||||
<el-form-item label="导出限制:">
|
||||
每次导出最大允许{{ exportData.max_page }}页,共{{
|
||||
exportData.all_max_size
|
||||
}}条数据
|
||||
</el-form-item>
|
||||
<el-form-item prop="page_type" label="导出范围:" required>
|
||||
<el-radio-group v-model="formData.page_type">
|
||||
<el-radio :value="0">全部导出</el-radio>
|
||||
<el-radio :value="1">分页导出</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="分页范围:" v-if="formData.page_type == 1">
|
||||
<div class="flex">
|
||||
<el-form-item prop="page_start">
|
||||
<el-input
|
||||
style="width: 140px"
|
||||
v-model.number="formData.page_start"
|
||||
placeholder=""
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<span class="flex-none ml-2 mr-2">页,至</span>
|
||||
<el-form-item prop="page_end">
|
||||
<el-input
|
||||
style="width: 140px"
|
||||
v-model.number="formData.page_end"
|
||||
placeholder=""
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="导出文件名称:" prop="file_name">
|
||||
<el-input
|
||||
v-model="formData.file_name"
|
||||
placeholder="请输入导出文件名称"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</popup>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const props = defineProps({
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 25
|
||||
},
|
||||
fetchFun: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
/** 可选:导出弹窗内提示文案(如说明与列表筛选一致) */
|
||||
exportHint: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const popupRef = shallowRef<InstanceType<typeof Popup>>()
|
||||
const formData = reactive({
|
||||
page_type: 0,
|
||||
page_start: 1,
|
||||
page_end: 200,
|
||||
file_name: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
page_start: [
|
||||
{ required: true, message: '请输入起始页码' },
|
||||
{ type: 'number', message: '页码必须是整数' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (value <= 0) return callback(new Error('页码必须大于0'))
|
||||
callback()
|
||||
}
|
||||
}
|
||||
],
|
||||
page_end: [
|
||||
{ required: true, message: '请输入结束页码' },
|
||||
{ type: 'number', message: '页码必须是整数' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (value <= 0) return callback(new Error('页码必须大于0'))
|
||||
callback()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const exportData = reactive({
|
||||
count: 0,
|
||||
sum_page: 0,
|
||||
page_size: 0,
|
||||
max_page: 0,
|
||||
all_max_size: 0
|
||||
})
|
||||
|
||||
const getData = async () => {
|
||||
const res = await props.fetchFun({
|
||||
...props.params,
|
||||
page_size: props.pageSize,
|
||||
export: 1
|
||||
})
|
||||
Object.assign(exportData, res)
|
||||
formData.file_name = res.file_name
|
||||
formData.page_end = res.page_end
|
||||
formData.page_start = res.page_start
|
||||
}
|
||||
const handleConfirm = async () => {
|
||||
await formRef.value?.validate()
|
||||
feedback.loading('正在导出中...')
|
||||
try {
|
||||
await props.fetchFun({
|
||||
...props.params,
|
||||
...formData,
|
||||
page_size: props.pageSize,
|
||||
export: 2
|
||||
})
|
||||
popupRef.value?.close()
|
||||
feedback.closeLoading()
|
||||
} catch (error) {
|
||||
feedback.closeLoading()
|
||||
}
|
||||
}
|
||||
getData()
|
||||
</script>
|
||||
<template>
|
||||
<div class="export-data">
|
||||
<popup
|
||||
ref="popupRef"
|
||||
title="导出设置"
|
||||
width="500px"
|
||||
confirm-button-text="确认导出"
|
||||
@confirm="handleConfirm"
|
||||
:async="true"
|
||||
@open="getData"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button>导出</el-button>
|
||||
</template>
|
||||
<div>
|
||||
<p v-if="props.exportHint" class="text-sm text-gray-500 mb-3 leading-relaxed">{{ props.exportHint }}</p>
|
||||
<el-form ref="formRef" :model="formData" label-width="120px" :rules="formRules">
|
||||
<el-form-item label="数据量:">
|
||||
预计导出{{ exportData.count }}条数据, 共{{ exportData.sum_page }}页,每页{{
|
||||
exportData.page_size
|
||||
}}条数据
|
||||
</el-form-item>
|
||||
<el-form-item label="导出限制:">
|
||||
每次导出最大允许{{ exportData.max_page }}页,共{{
|
||||
exportData.all_max_size
|
||||
}}条数据
|
||||
</el-form-item>
|
||||
<el-form-item prop="page_type" label="导出范围:" required>
|
||||
<el-radio-group v-model="formData.page_type">
|
||||
<el-radio :value="0">全部导出</el-radio>
|
||||
<el-radio :value="1">分页导出</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="分页范围:" v-if="formData.page_type == 1">
|
||||
<div class="flex">
|
||||
<el-form-item prop="page_start">
|
||||
<el-input
|
||||
style="width: 140px"
|
||||
v-model.number="formData.page_start"
|
||||
placeholder=""
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<span class="flex-none ml-2 mr-2">页,至</span>
|
||||
<el-form-item prop="page_end">
|
||||
<el-input
|
||||
style="width: 140px"
|
||||
v-model.number="formData.page_end"
|
||||
placeholder=""
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="导出文件名称:" prop="file_name">
|
||||
<el-input
|
||||
v-model="formData.file_name"
|
||||
placeholder="请输入导出文件名称"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</popup>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const props = defineProps({
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 25
|
||||
},
|
||||
fetchFun: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
/** 可选:导出弹窗内提示文案(如说明与列表筛选一致) */
|
||||
exportHint: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const popupRef = shallowRef<InstanceType<typeof Popup>>()
|
||||
const formData = reactive({
|
||||
page_type: 0,
|
||||
page_start: 1,
|
||||
page_end: 200,
|
||||
file_name: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
page_start: [
|
||||
{ required: true, message: '请输入起始页码' },
|
||||
{ type: 'number', message: '页码必须是整数' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (value <= 0) return callback(new Error('页码必须大于0'))
|
||||
callback()
|
||||
}
|
||||
}
|
||||
],
|
||||
page_end: [
|
||||
{ required: true, message: '请输入结束页码' },
|
||||
{ type: 'number', message: '页码必须是整数' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (value <= 0) return callback(new Error('页码必须大于0'))
|
||||
callback()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const exportData = reactive({
|
||||
count: 0,
|
||||
sum_page: 0,
|
||||
page_size: 0,
|
||||
max_page: 0,
|
||||
all_max_size: 0
|
||||
})
|
||||
|
||||
const getData = async () => {
|
||||
const res = await props.fetchFun({
|
||||
...props.params,
|
||||
page_size: props.pageSize,
|
||||
export: 1
|
||||
})
|
||||
Object.assign(exportData, res)
|
||||
formData.file_name = res.file_name
|
||||
formData.page_end = res.page_end
|
||||
formData.page_start = res.page_start
|
||||
}
|
||||
const handleConfirm = async () => {
|
||||
await formRef.value?.validate()
|
||||
feedback.loading('正在导出中...')
|
||||
try {
|
||||
await props.fetchFun({
|
||||
...props.params,
|
||||
...formData,
|
||||
page_size: props.pageSize,
|
||||
export: 2
|
||||
})
|
||||
popupRef.value?.close()
|
||||
feedback.closeLoading()
|
||||
} catch (error) {
|
||||
feedback.closeLoading()
|
||||
}
|
||||
}
|
||||
getData()
|
||||
</script>
|
||||
|
||||
@@ -1,488 +1,488 @@
|
||||
<template>
|
||||
<div class="material-select">
|
||||
<popup
|
||||
ref="popupRef"
|
||||
width="1050px"
|
||||
custom-class="body-padding"
|
||||
:title="`选择${tipsText}`"
|
||||
@confirm="handleConfirm"
|
||||
@close="handleClose"
|
||||
>
|
||||
<template v-if="!hiddenUpload" #trigger>
|
||||
<div
|
||||
class="material-select__trigger clearfix"
|
||||
:class="{ 'is-drop-hover': dropActive }"
|
||||
tabindex="0"
|
||||
@click.stop
|
||||
@dragenter.prevent.stop="onTriggerDragEnter"
|
||||
@dragleave.prevent.stop="onTriggerDragLeave"
|
||||
@dragover.prevent.stop="onTriggerDragOver"
|
||||
@drop.prevent.stop="onTriggerDrop"
|
||||
@paste="onTriggerPaste"
|
||||
>
|
||||
<draggable class="draggable" v-model="fileList" animation="300" item-key="id">
|
||||
<template v-slot:item="{ element, index }">
|
||||
<div
|
||||
class="material-preview"
|
||||
:class="{
|
||||
'is-disabled': disabled,
|
||||
'is-one': limit == 1
|
||||
}"
|
||||
@click="showPopup(index)"
|
||||
>
|
||||
<del-wrap @close="deleteImg(index)">
|
||||
<file-item
|
||||
:uri="excludeDomain ? getImageUrl(element) : element"
|
||||
:file-size="size"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:type="type"
|
||||
></file-item>
|
||||
</del-wrap>
|
||||
<div class="operation-btns text-xs text-center">
|
||||
<span>修改</span>
|
||||
|
|
||||
<span @click.stop="handlePreview(element)">查看</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
class="material-upload"
|
||||
@click="showPopup(-1)"
|
||||
v-show="showUpload"
|
||||
:class="{
|
||||
'is-disabled': disabled,
|
||||
'is-one': limit == 1,
|
||||
[uploadClass]: true
|
||||
}"
|
||||
>
|
||||
<slot name="upload">
|
||||
<div
|
||||
class="upload-btn"
|
||||
:style="{
|
||||
width: width || size,
|
||||
height: height || size
|
||||
}"
|
||||
>
|
||||
<icon :size="25" name="el-icon-Plus" />
|
||||
<span>添加</span>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar>
|
||||
<div class="material-wrap">
|
||||
<material
|
||||
ref="materialRef"
|
||||
:type="type"
|
||||
:file-size="fileSize"
|
||||
:limit="meterialLimit"
|
||||
@change="selectChange"
|
||||
/>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</popup>
|
||||
<preview v-model="showPreview" :url="previewUrl" :type="type" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import Draggable from 'vuedraggable'
|
||||
|
||||
import { uploadMaterialFile } from '@/api/file'
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
import FileItem from './file.vue'
|
||||
import Material from './index.vue'
|
||||
import Preview from './preview.vue'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Popup,
|
||||
Draggable,
|
||||
FileItem,
|
||||
Material,
|
||||
Preview
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Array],
|
||||
default: () => []
|
||||
},
|
||||
// 文件类型
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image'
|
||||
},
|
||||
// 选择器尺寸
|
||||
size: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
// 选择器尺寸-宽度(不传则是使用size
|
||||
width: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 选择器尺寸-高度(不传则是使用size
|
||||
height: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 文件尺寸
|
||||
fileSize: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
// 选择数量限制
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
// 禁用选择
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 隐藏上传框*(目前在富文本中使用到)
|
||||
hiddenUpload: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
uploadClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
//选择的url排出域名
|
||||
excludeDomain: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 拖拽/粘贴直传时的素材分组 id(与弹窗内「本地上传」一致,默认未分组) */
|
||||
uploadCid: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['change', 'update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const popupRef = ref<InstanceType<typeof Popup>>()
|
||||
const materialRef = ref<InstanceType<typeof Material>>()
|
||||
const previewUrl = ref('')
|
||||
const showPreview = ref(false)
|
||||
const fileList = ref<any[]>([])
|
||||
const select = ref<any[]>([])
|
||||
const isAdd = ref(true)
|
||||
const currentIndex = ref(-1)
|
||||
const dragDepth = ref(0)
|
||||
const externalUploading = ref(false)
|
||||
const { disabled, limit, modelValue } = toRefs(props)
|
||||
const appStore = useAppStore()
|
||||
const getImageUrl = (url: string) => appStore.getImageUrl(url)
|
||||
|
||||
const allowDropPaste = computed(
|
||||
() =>
|
||||
!props.hiddenUpload &&
|
||||
(props.type === 'image' || props.type === 'video' || props.type === 'file')
|
||||
)
|
||||
|
||||
const dropActive = computed(() => dragDepth.value > 0)
|
||||
|
||||
function hasFileDrag(dt: DataTransfer | null) {
|
||||
if (!dt) return false
|
||||
const types = Array.from(dt.types as unknown as string[])
|
||||
return types.includes('Files') || types.includes('application/x-moz-file')
|
||||
}
|
||||
|
||||
function isAcceptedExtension(fileName: string, uploadType: string): boolean {
|
||||
if (uploadType === 'file') return true
|
||||
const ext = fileName.includes('.')
|
||||
? fileName.split('.').pop()?.toLowerCase() || ''
|
||||
: ''
|
||||
if (uploadType === 'image') {
|
||||
if (!ext) return true
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'ico'].includes(ext)
|
||||
}
|
||||
if (uploadType === 'video') {
|
||||
if (!ext) return true
|
||||
return ['wmv', 'avi', 'mpg', 'mpeg', '3gp', 'mov', 'mp4', 'flv', 'rmvb', 'mkv'].includes(
|
||||
ext
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function toRelativeStoredUrl(fullUrl: string): string {
|
||||
if (!fullUrl || !fullUrl.startsWith('http')) return fullUrl
|
||||
const oss = String(appStore.config?.oss_domain || '')
|
||||
.trim()
|
||||
.replace(/\/$/, '')
|
||||
if (oss && fullUrl.startsWith(oss)) {
|
||||
return fullUrl.slice(oss.length).replace(/^\//, '')
|
||||
}
|
||||
const origin =
|
||||
typeof window !== 'undefined' ? window.location.origin.replace(/\/$/, '') : ''
|
||||
if (origin && fullUrl.startsWith(origin)) {
|
||||
return fullUrl.slice(origin.length).replace(/^\//, '')
|
||||
}
|
||||
try {
|
||||
return new URL(fullUrl).pathname.replace(/^\//, '')
|
||||
} catch {
|
||||
return fullUrl
|
||||
}
|
||||
}
|
||||
|
||||
function clipboardFiles(e: ClipboardEvent): File[] {
|
||||
const uploadType = props.type
|
||||
const fromFiles = Array.from(e.clipboardData?.files || []).filter((f) =>
|
||||
isAcceptedExtension(f.name, uploadType)
|
||||
)
|
||||
if (fromFiles.length) return fromFiles
|
||||
if (uploadType !== 'image') return []
|
||||
const items = e.clipboardData?.items
|
||||
if (!items) return []
|
||||
const out: File[] = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const it = items[i]
|
||||
if (it.kind === 'file' && it.type.startsWith('image/')) {
|
||||
const f = it.getAsFile()
|
||||
if (f) out.push(f)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function runExternalUpload(files: File[]) {
|
||||
if (externalUploading.value) return
|
||||
const maxSlots =
|
||||
limit.value === -1 ? Infinity : Math.max(0, limit.value - fileList.value.length)
|
||||
if (!maxSlots) {
|
||||
feedback.msgWarning('已达到上传数量上限')
|
||||
return
|
||||
}
|
||||
const queue = files.slice(0, maxSlots)
|
||||
if (!queue.length) return
|
||||
|
||||
const uploadType =
|
||||
props.type === 'video' ? 'video' : props.type === 'file' ? 'file' : 'image'
|
||||
externalUploading.value = true
|
||||
try {
|
||||
const paths: string[] = []
|
||||
for (const file of queue) {
|
||||
const data = await uploadMaterialFile(file, uploadType, props.uploadCid)
|
||||
paths.push(props.excludeDomain ? toRelativeStoredUrl(data.url) : data.url)
|
||||
}
|
||||
fileList.value = [...fileList.value, ...paths]
|
||||
const valueImg = limit.value != 1 ? fileList.value : fileList.value[0] || ''
|
||||
emit('update:modelValue', valueImg)
|
||||
emit('change', valueImg)
|
||||
handleClose()
|
||||
} catch (err: any) {
|
||||
feedback.msgError(err?.message || '上传失败')
|
||||
} finally {
|
||||
externalUploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onTriggerDragEnter = (e: DragEvent) => {
|
||||
if (!allowDropPaste.value || disabled.value) return
|
||||
if (!hasFileDrag(e.dataTransfer)) return
|
||||
dragDepth.value++
|
||||
}
|
||||
|
||||
const onTriggerDragLeave = () => {
|
||||
if (!allowDropPaste.value) return
|
||||
dragDepth.value = Math.max(0, dragDepth.value - 1)
|
||||
}
|
||||
|
||||
const onTriggerDragOver = (e: DragEvent) => {
|
||||
if (!allowDropPaste.value || disabled.value) return
|
||||
if (hasFileDrag(e.dataTransfer)) e.preventDefault()
|
||||
}
|
||||
|
||||
const onTriggerDrop = async (e: DragEvent) => {
|
||||
dragDepth.value = 0
|
||||
if (!allowDropPaste.value || disabled.value) return
|
||||
const files = Array.from(e.dataTransfer?.files || []).filter((f) =>
|
||||
isAcceptedExtension(f.name, props.type)
|
||||
)
|
||||
if (!files.length) return
|
||||
await runExternalUpload(files)
|
||||
}
|
||||
|
||||
const onTriggerPaste = async (e: ClipboardEvent) => {
|
||||
if (!allowDropPaste.value || disabled.value) return
|
||||
const files = clipboardFiles(e)
|
||||
if (!files.length) return
|
||||
e.preventDefault()
|
||||
await runExternalUpload(files)
|
||||
}
|
||||
const tipsText = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'image':
|
||||
return '图片'
|
||||
case 'video':
|
||||
return '视频'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const showUpload = computed(() => {
|
||||
return props.limit - fileList.value.length > 0
|
||||
})
|
||||
const meterialLimit: any = computed(() => {
|
||||
if (!isAdd.value) {
|
||||
return 1
|
||||
}
|
||||
if (limit.value == -1) return null
|
||||
return limit.value - fileList.value.length
|
||||
})
|
||||
const handleConfirm = useThrottleFn(
|
||||
() => {
|
||||
const selectUri = select.value.map((item) =>
|
||||
props.excludeDomain ? item.uri : item.url
|
||||
)
|
||||
if (!isAdd.value) {
|
||||
fileList.value.splice(currentIndex.value, 1, selectUri.shift())
|
||||
} else {
|
||||
fileList.value = [...fileList.value, ...selectUri]
|
||||
}
|
||||
handleChange()
|
||||
},
|
||||
1000,
|
||||
false
|
||||
)
|
||||
const showPopup = (index: number) => {
|
||||
if (disabled.value) return
|
||||
if (index >= 0) {
|
||||
isAdd.value = false
|
||||
currentIndex.value = index
|
||||
} else {
|
||||
isAdd.value = true
|
||||
}
|
||||
popupRef.value?.open()
|
||||
}
|
||||
|
||||
const selectChange = (val: any[]) => {
|
||||
select.value = val
|
||||
}
|
||||
const handleChange = () => {
|
||||
const valueImg = limit.value != 1 ? fileList.value : fileList.value[0] || ''
|
||||
emit('update:modelValue', valueImg)
|
||||
emit('change', valueImg)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const deleteImg = (index: number) => {
|
||||
fileList.value.splice(index, 1)
|
||||
handleChange()
|
||||
}
|
||||
|
||||
const handlePreview = (url: string) => {
|
||||
previewUrl.value = props.excludeDomain ? getImageUrl(url) : url
|
||||
showPreview.value = true
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
nextTick(() => {
|
||||
if (props.hiddenUpload) fileList.value = []
|
||||
materialRef.value?.clearSelect()
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
modelValue,
|
||||
(val: any[] | string) => {
|
||||
fileList.value = Array.isArray(val) ? val : val == '' ? [] : [val]
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
provide('limit', props.limit)
|
||||
provide('hiddenUpload', props.hiddenUpload)
|
||||
return {
|
||||
popupRef,
|
||||
materialRef,
|
||||
fileList,
|
||||
tipsText,
|
||||
handleConfirm,
|
||||
meterialLimit,
|
||||
showUpload,
|
||||
showPopup,
|
||||
selectChange,
|
||||
deleteImg,
|
||||
previewUrl,
|
||||
showPreview,
|
||||
handlePreview,
|
||||
handleClose,
|
||||
getImageUrl,
|
||||
dropActive,
|
||||
onTriggerDragEnter,
|
||||
onTriggerDragLeave,
|
||||
onTriggerDragOver,
|
||||
onTriggerDrop,
|
||||
onTriggerPaste
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.material-select {
|
||||
.material-select__trigger {
|
||||
outline: none;
|
||||
&.is-drop-hover {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary-light-5);
|
||||
}
|
||||
}
|
||||
.material-upload,
|
||||
.material-preview {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
box-sizing: border-box;
|
||||
float: left;
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&.is-one {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&:hover {
|
||||
.operation-btns {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.operation-btns {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
line-height: 2;
|
||||
color: #fff;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
.material-upload {
|
||||
:deep(.upload-btn) {
|
||||
@apply text-tx-secondary box-border rounded border-br border-dashed border flex flex-col justify-center items-center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.material-wrap {
|
||||
min-width: 720px;
|
||||
height: 560px;
|
||||
@apply border-t border-b border-br;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="material-select">
|
||||
<popup
|
||||
ref="popupRef"
|
||||
width="1050px"
|
||||
custom-class="body-padding"
|
||||
:title="`选择${tipsText}`"
|
||||
@confirm="handleConfirm"
|
||||
@close="handleClose"
|
||||
>
|
||||
<template v-if="!hiddenUpload" #trigger>
|
||||
<div
|
||||
class="material-select__trigger clearfix"
|
||||
:class="{ 'is-drop-hover': dropActive }"
|
||||
tabindex="0"
|
||||
@click.stop
|
||||
@dragenter.prevent.stop="onTriggerDragEnter"
|
||||
@dragleave.prevent.stop="onTriggerDragLeave"
|
||||
@dragover.prevent.stop="onTriggerDragOver"
|
||||
@drop.prevent.stop="onTriggerDrop"
|
||||
@paste="onTriggerPaste"
|
||||
>
|
||||
<draggable class="draggable" v-model="fileList" animation="300" item-key="id">
|
||||
<template v-slot:item="{ element, index }">
|
||||
<div
|
||||
class="material-preview"
|
||||
:class="{
|
||||
'is-disabled': disabled,
|
||||
'is-one': limit == 1
|
||||
}"
|
||||
@click="showPopup(index)"
|
||||
>
|
||||
<del-wrap @close="deleteImg(index)">
|
||||
<file-item
|
||||
:uri="excludeDomain ? getImageUrl(element) : element"
|
||||
:file-size="size"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:type="type"
|
||||
></file-item>
|
||||
</del-wrap>
|
||||
<div class="operation-btns text-xs text-center">
|
||||
<span>修改</span>
|
||||
|
|
||||
<span @click.stop="handlePreview(element)">查看</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
class="material-upload"
|
||||
@click="showPopup(-1)"
|
||||
v-show="showUpload"
|
||||
:class="{
|
||||
'is-disabled': disabled,
|
||||
'is-one': limit == 1,
|
||||
[uploadClass]: true
|
||||
}"
|
||||
>
|
||||
<slot name="upload">
|
||||
<div
|
||||
class="upload-btn"
|
||||
:style="{
|
||||
width: width || size,
|
||||
height: height || size
|
||||
}"
|
||||
>
|
||||
<icon :size="25" name="el-icon-Plus" />
|
||||
<span>添加</span>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar>
|
||||
<div class="material-wrap">
|
||||
<material
|
||||
ref="materialRef"
|
||||
:type="type"
|
||||
:file-size="fileSize"
|
||||
:limit="meterialLimit"
|
||||
@change="selectChange"
|
||||
/>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</popup>
|
||||
<preview v-model="showPreview" :url="previewUrl" :type="type" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import Draggable from 'vuedraggable'
|
||||
|
||||
import { uploadMaterialFile } from '@/api/file'
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
import FileItem from './file.vue'
|
||||
import Material from './index.vue'
|
||||
import Preview from './preview.vue'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Popup,
|
||||
Draggable,
|
||||
FileItem,
|
||||
Material,
|
||||
Preview
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Array],
|
||||
default: () => []
|
||||
},
|
||||
// 文件类型
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image'
|
||||
},
|
||||
// 选择器尺寸
|
||||
size: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
// 选择器尺寸-宽度(不传则是使用size
|
||||
width: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 选择器尺寸-高度(不传则是使用size
|
||||
height: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 文件尺寸
|
||||
fileSize: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
// 选择数量限制
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
// 禁用选择
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 隐藏上传框*(目前在富文本中使用到)
|
||||
hiddenUpload: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
uploadClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
//选择的url排出域名
|
||||
excludeDomain: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 拖拽/粘贴直传时的素材分组 id(与弹窗内「本地上传」一致,默认未分组) */
|
||||
uploadCid: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['change', 'update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const popupRef = ref<InstanceType<typeof Popup>>()
|
||||
const materialRef = ref<InstanceType<typeof Material>>()
|
||||
const previewUrl = ref('')
|
||||
const showPreview = ref(false)
|
||||
const fileList = ref<any[]>([])
|
||||
const select = ref<any[]>([])
|
||||
const isAdd = ref(true)
|
||||
const currentIndex = ref(-1)
|
||||
const dragDepth = ref(0)
|
||||
const externalUploading = ref(false)
|
||||
const { disabled, limit, modelValue } = toRefs(props)
|
||||
const appStore = useAppStore()
|
||||
const getImageUrl = (url: string) => appStore.getImageUrl(url)
|
||||
|
||||
const allowDropPaste = computed(
|
||||
() =>
|
||||
!props.hiddenUpload &&
|
||||
(props.type === 'image' || props.type === 'video' || props.type === 'file')
|
||||
)
|
||||
|
||||
const dropActive = computed(() => dragDepth.value > 0)
|
||||
|
||||
function hasFileDrag(dt: DataTransfer | null) {
|
||||
if (!dt) return false
|
||||
const types = Array.from(dt.types as unknown as string[])
|
||||
return types.includes('Files') || types.includes('application/x-moz-file')
|
||||
}
|
||||
|
||||
function isAcceptedExtension(fileName: string, uploadType: string): boolean {
|
||||
if (uploadType === 'file') return true
|
||||
const ext = fileName.includes('.')
|
||||
? fileName.split('.').pop()?.toLowerCase() || ''
|
||||
: ''
|
||||
if (uploadType === 'image') {
|
||||
if (!ext) return true
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'ico'].includes(ext)
|
||||
}
|
||||
if (uploadType === 'video') {
|
||||
if (!ext) return true
|
||||
return ['wmv', 'avi', 'mpg', 'mpeg', '3gp', 'mov', 'mp4', 'flv', 'rmvb', 'mkv'].includes(
|
||||
ext
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function toRelativeStoredUrl(fullUrl: string): string {
|
||||
if (!fullUrl || !fullUrl.startsWith('http')) return fullUrl
|
||||
const oss = String(appStore.config?.oss_domain || '')
|
||||
.trim()
|
||||
.replace(/\/$/, '')
|
||||
if (oss && fullUrl.startsWith(oss)) {
|
||||
return fullUrl.slice(oss.length).replace(/^\//, '')
|
||||
}
|
||||
const origin =
|
||||
typeof window !== 'undefined' ? window.location.origin.replace(/\/$/, '') : ''
|
||||
if (origin && fullUrl.startsWith(origin)) {
|
||||
return fullUrl.slice(origin.length).replace(/^\//, '')
|
||||
}
|
||||
try {
|
||||
return new URL(fullUrl).pathname.replace(/^\//, '')
|
||||
} catch {
|
||||
return fullUrl
|
||||
}
|
||||
}
|
||||
|
||||
function clipboardFiles(e: ClipboardEvent): File[] {
|
||||
const uploadType = props.type
|
||||
const fromFiles = Array.from(e.clipboardData?.files || []).filter((f) =>
|
||||
isAcceptedExtension(f.name, uploadType)
|
||||
)
|
||||
if (fromFiles.length) return fromFiles
|
||||
if (uploadType !== 'image') return []
|
||||
const items = e.clipboardData?.items
|
||||
if (!items) return []
|
||||
const out: File[] = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const it = items[i]
|
||||
if (it.kind === 'file' && it.type.startsWith('image/')) {
|
||||
const f = it.getAsFile()
|
||||
if (f) out.push(f)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function runExternalUpload(files: File[]) {
|
||||
if (externalUploading.value) return
|
||||
const maxSlots =
|
||||
limit.value === -1 ? Infinity : Math.max(0, limit.value - fileList.value.length)
|
||||
if (!maxSlots) {
|
||||
feedback.msgWarning('已达到上传数量上限')
|
||||
return
|
||||
}
|
||||
const queue = files.slice(0, maxSlots)
|
||||
if (!queue.length) return
|
||||
|
||||
const uploadType =
|
||||
props.type === 'video' ? 'video' : props.type === 'file' ? 'file' : 'image'
|
||||
externalUploading.value = true
|
||||
try {
|
||||
const paths: string[] = []
|
||||
for (const file of queue) {
|
||||
const data = await uploadMaterialFile(file, uploadType, props.uploadCid)
|
||||
paths.push(props.excludeDomain ? toRelativeStoredUrl(data.url) : data.url)
|
||||
}
|
||||
fileList.value = [...fileList.value, ...paths]
|
||||
const valueImg = limit.value != 1 ? fileList.value : fileList.value[0] || ''
|
||||
emit('update:modelValue', valueImg)
|
||||
emit('change', valueImg)
|
||||
handleClose()
|
||||
} catch (err: any) {
|
||||
feedback.msgError(err?.message || '上传失败')
|
||||
} finally {
|
||||
externalUploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onTriggerDragEnter = (e: DragEvent) => {
|
||||
if (!allowDropPaste.value || disabled.value) return
|
||||
if (!hasFileDrag(e.dataTransfer)) return
|
||||
dragDepth.value++
|
||||
}
|
||||
|
||||
const onTriggerDragLeave = () => {
|
||||
if (!allowDropPaste.value) return
|
||||
dragDepth.value = Math.max(0, dragDepth.value - 1)
|
||||
}
|
||||
|
||||
const onTriggerDragOver = (e: DragEvent) => {
|
||||
if (!allowDropPaste.value || disabled.value) return
|
||||
if (hasFileDrag(e.dataTransfer)) e.preventDefault()
|
||||
}
|
||||
|
||||
const onTriggerDrop = async (e: DragEvent) => {
|
||||
dragDepth.value = 0
|
||||
if (!allowDropPaste.value || disabled.value) return
|
||||
const files = Array.from(e.dataTransfer?.files || []).filter((f) =>
|
||||
isAcceptedExtension(f.name, props.type)
|
||||
)
|
||||
if (!files.length) return
|
||||
await runExternalUpload(files)
|
||||
}
|
||||
|
||||
const onTriggerPaste = async (e: ClipboardEvent) => {
|
||||
if (!allowDropPaste.value || disabled.value) return
|
||||
const files = clipboardFiles(e)
|
||||
if (!files.length) return
|
||||
e.preventDefault()
|
||||
await runExternalUpload(files)
|
||||
}
|
||||
const tipsText = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'image':
|
||||
return '图片'
|
||||
case 'video':
|
||||
return '视频'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const showUpload = computed(() => {
|
||||
return props.limit - fileList.value.length > 0
|
||||
})
|
||||
const meterialLimit: any = computed(() => {
|
||||
if (!isAdd.value) {
|
||||
return 1
|
||||
}
|
||||
if (limit.value == -1) return null
|
||||
return limit.value - fileList.value.length
|
||||
})
|
||||
const handleConfirm = useThrottleFn(
|
||||
() => {
|
||||
const selectUri = select.value.map((item) =>
|
||||
props.excludeDomain ? item.uri : item.url
|
||||
)
|
||||
if (!isAdd.value) {
|
||||
fileList.value.splice(currentIndex.value, 1, selectUri.shift())
|
||||
} else {
|
||||
fileList.value = [...fileList.value, ...selectUri]
|
||||
}
|
||||
handleChange()
|
||||
},
|
||||
1000,
|
||||
false
|
||||
)
|
||||
const showPopup = (index: number) => {
|
||||
if (disabled.value) return
|
||||
if (index >= 0) {
|
||||
isAdd.value = false
|
||||
currentIndex.value = index
|
||||
} else {
|
||||
isAdd.value = true
|
||||
}
|
||||
popupRef.value?.open()
|
||||
}
|
||||
|
||||
const selectChange = (val: any[]) => {
|
||||
select.value = val
|
||||
}
|
||||
const handleChange = () => {
|
||||
const valueImg = limit.value != 1 ? fileList.value : fileList.value[0] || ''
|
||||
emit('update:modelValue', valueImg)
|
||||
emit('change', valueImg)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const deleteImg = (index: number) => {
|
||||
fileList.value.splice(index, 1)
|
||||
handleChange()
|
||||
}
|
||||
|
||||
const handlePreview = (url: string) => {
|
||||
previewUrl.value = props.excludeDomain ? getImageUrl(url) : url
|
||||
showPreview.value = true
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
nextTick(() => {
|
||||
if (props.hiddenUpload) fileList.value = []
|
||||
materialRef.value?.clearSelect()
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
modelValue,
|
||||
(val: any[] | string) => {
|
||||
fileList.value = Array.isArray(val) ? val : val == '' ? [] : [val]
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
provide('limit', props.limit)
|
||||
provide('hiddenUpload', props.hiddenUpload)
|
||||
return {
|
||||
popupRef,
|
||||
materialRef,
|
||||
fileList,
|
||||
tipsText,
|
||||
handleConfirm,
|
||||
meterialLimit,
|
||||
showUpload,
|
||||
showPopup,
|
||||
selectChange,
|
||||
deleteImg,
|
||||
previewUrl,
|
||||
showPreview,
|
||||
handlePreview,
|
||||
handleClose,
|
||||
getImageUrl,
|
||||
dropActive,
|
||||
onTriggerDragEnter,
|
||||
onTriggerDragLeave,
|
||||
onTriggerDragOver,
|
||||
onTriggerDrop,
|
||||
onTriggerPaste
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.material-select {
|
||||
.material-select__trigger {
|
||||
outline: none;
|
||||
&.is-drop-hover {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary-light-5);
|
||||
}
|
||||
}
|
||||
.material-upload,
|
||||
.material-preview {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
box-sizing: border-box;
|
||||
float: left;
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&.is-one {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&:hover {
|
||||
.operation-btns {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.operation-btns {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
line-height: 2;
|
||||
color: #fff;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
.material-upload {
|
||||
:deep(.upload-btn) {
|
||||
@apply text-tx-secondary box-border rounded border-br border-dashed border flex flex-col justify-center items-center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.material-wrap {
|
||||
min-width: 720px;
|
||||
height: 560px;
|
||||
@apply border-t border-b border-br;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user