mirror of
https://gitee.com/likeadmin/likeadmin_java.git
synced 2026-06-23 18:42:52 +08:00
初始化admin pc端
This commit is contained in:
53
admin/src/components/del-wrap/index.vue
Normal file
53
admin/src/components/del-wrap/index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="del-wrap">
|
||||
<slot></slot>
|
||||
<div v-if="showClose" class="icon-close" @click.stop="handleClose">
|
||||
<el-icon :size="12"><close-bold /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
components: {},
|
||||
props: {
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
setup(props, { emit }) {
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
return {
|
||||
handleClose
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.del-wrap {
|
||||
position: relative;
|
||||
&:hover > .icon-close {
|
||||
display: flex;
|
||||
}
|
||||
.icon-close {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
237
admin/src/components/editor/index copy.vue
Normal file
237
admin/src/components/editor/index copy.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="cl-editor-quill">
|
||||
<div ref="editorRef" class="editor" :style="style"></div>
|
||||
<material-select ref="materialRef" :hidden="true" :limit="-1" @change="filesChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, onMounted, onUnmounted, ref, toRefs, watch } from 'vue'
|
||||
import Quill from 'quill'
|
||||
import 'quill/dist/quill.snow.css'
|
||||
import MaterialSelect from '@/components/material-select/index.vue'
|
||||
interface Props {
|
||||
// 绑定的值
|
||||
modelValue: string
|
||||
// 编辑器的参数
|
||||
options?: Record<any, any>
|
||||
// 编辑器的宽度
|
||||
width?: number | string
|
||||
// 编辑器的高
|
||||
height?: number | string
|
||||
minHeight?: number | string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
options: () => ({}),
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', val: string): void
|
||||
(event: 'load', quill: any): void
|
||||
}>()
|
||||
|
||||
let quill: any = null
|
||||
|
||||
const editorRef = ref()
|
||||
const materialRef = ref()
|
||||
|
||||
// 文本内容
|
||||
const content = ref('')
|
||||
|
||||
// 光标位置
|
||||
const cursorIndex = ref(0)
|
||||
|
||||
// 图片上传处理
|
||||
const uploadFileHandler = () => {
|
||||
const selection = quill.getSelection()
|
||||
if (selection) {
|
||||
cursorIndex.value = selection.index
|
||||
}
|
||||
|
||||
materialRef.value.showPopup()
|
||||
}
|
||||
// 图片插入
|
||||
const filesChange = (files: any[]) => {
|
||||
if (files.length > 0) {
|
||||
// 批量插入图片
|
||||
files.forEach((file, i) => {
|
||||
quill.insertEmbed(cursorIndex.value + i, 'image', file, Quill.sources.USER)
|
||||
})
|
||||
|
||||
// 移动光标到图片后一位
|
||||
quill.setSelection(cursorIndex.value + files.length)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置内容
|
||||
const setContent = (val: string) => {
|
||||
quill.root.innerHTML = val || ''
|
||||
}
|
||||
|
||||
// 编辑框样式
|
||||
const style = computed<any>(() => {
|
||||
return {
|
||||
height: typeof props.height == 'string' ? props.height : `${props.height}px`,
|
||||
width: typeof props.width == 'string' ? props.width : `${props.width}px`,
|
||||
}
|
||||
})
|
||||
|
||||
// 监听绑定值
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val: string) => {
|
||||
if (val) {
|
||||
if (val !== content.value) {
|
||||
setContent(val)
|
||||
}
|
||||
} else {
|
||||
setContent('')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
// 实例化
|
||||
quill = new Quill(editorRef.value, {
|
||||
theme: 'snow',
|
||||
placeholder: '输入内容',
|
||||
modules: {
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
['blockquote', 'code-block'],
|
||||
[{ header: 1 }, { header: 2 }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
[{ indent: '-1' }, { indent: '+1' }],
|
||||
[{ direction: 'rtl' }],
|
||||
[{ size: ['small', false, 'large', 'huge'] }],
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||
[{ color: [] }, { background: [] }],
|
||||
[{ font: [] }],
|
||||
[{ align: [] }],
|
||||
['clean'],
|
||||
['link', 'image'],
|
||||
],
|
||||
},
|
||||
...props.options,
|
||||
})
|
||||
|
||||
// 添加图片工具
|
||||
quill.getModule('toolbar').addHandler('image', uploadFileHandler)
|
||||
|
||||
// 监听输入
|
||||
quill.on('text-change', () => {
|
||||
content.value = quill.root.innerHTML
|
||||
emit('update:modelValue', content.value)
|
||||
})
|
||||
|
||||
setContent(props.modelValue)
|
||||
|
||||
// 加载回调
|
||||
emit('load', quill)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.cl-editor-quill {
|
||||
background-color: #fff;
|
||||
|
||||
.ql-snow {
|
||||
line-height: 22px !important;
|
||||
}
|
||||
#quill-upload-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ql-snow {
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.ql-snow .ql-tooltip[data-mode='link']::before {
|
||||
content: '请输入链接地址:';
|
||||
}
|
||||
|
||||
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
|
||||
border-right: 0px;
|
||||
content: '保存';
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.ql-snow .ql-tooltip[data-mode='video']::before {
|
||||
content: '请输入视频地址:';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
|
||||
content: '14px';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
|
||||
content: '10px';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
|
||||
content: '18px';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
|
||||
content: '32px';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
|
||||
content: '文本';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
|
||||
content: '标题1';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
|
||||
content: '标题2';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
|
||||
content: '标题3';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
|
||||
content: '标题4';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
|
||||
content: '标题5';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
|
||||
content: '标题6';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
|
||||
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
|
||||
content: '标准字体';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
|
||||
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
|
||||
content: '衬线字体';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
|
||||
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
|
||||
content: '等宽字体';
|
||||
}
|
||||
}
|
||||
</style>
|
||||
124
admin/src/components/editor/index.vue
Normal file
124
admin/src/components/editor/index.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div
|
||||
class="tinymce-boxz"
|
||||
:style="{
|
||||
'max-width': width + 'px',
|
||||
'max-height': height + 'px'
|
||||
}"
|
||||
>
|
||||
<!-- tinymce-vue -->
|
||||
<Editor v-model="content" :api-key="tiny.apiKey" :init="tiny.init" />
|
||||
|
||||
<!-- 文件管理器 -->
|
||||
<material-select
|
||||
:hiddenUpload="true"
|
||||
:type="tinymce.type"
|
||||
ref="materialRef"
|
||||
@change="handleMaterialFile"
|
||||
></material-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Editor from "@tinymce/tinymce-vue";
|
||||
import { reactive, ref, withDefaults, computed } from "vue";
|
||||
import materialSelect from '@/components/material-select/index.vue'
|
||||
|
||||
/** Props Start **/
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: string
|
||||
width?: string
|
||||
height?: string
|
||||
}>(), {
|
||||
modelValue: '',
|
||||
width: '1000',
|
||||
height: '1000'
|
||||
})
|
||||
|
||||
/** Props End **/
|
||||
|
||||
/** Emit Start **/
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
/** Emit End **/
|
||||
|
||||
/** Computed Start **/
|
||||
let content = computed({
|
||||
get: () => {
|
||||
return props.modelValue || ''
|
||||
},
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
/** Computed End **/
|
||||
|
||||
/** Data Start **/
|
||||
const materialRef = ref<InstanceType<typeof materialSelect> | null>(null)
|
||||
|
||||
const tinymce = ref<any>({
|
||||
callback: null,//回调函数
|
||||
type: 'image' //选择文件类型 /image:图片/video:视频
|
||||
})
|
||||
|
||||
// 富文本基础配置
|
||||
const tiny = reactive({
|
||||
apiKey: "mejzqiqf65aswd278mtojz1w7g3zysvdhg3sjen77zf7f6e9",
|
||||
init: {
|
||||
language: "zh_CN", //语言类型
|
||||
placeholder: "在这里输入文字", //textarea中的提示信息
|
||||
min_width: props.width,
|
||||
min_height: props.height,
|
||||
height: props.height, //注:引入autoresize插件时,此属性失效
|
||||
resize: "both", //编辑器宽高是否可变,false-否,true-高可变,'both'-宽高均可,注意引号
|
||||
branding: false, //tiny技术支持信息是否显
|
||||
font_formats:
|
||||
"微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;", //字体样式
|
||||
plugins:
|
||||
"preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media template code codesample table charmap hr pagebreak nonbreaking anchor insertdatetime advlist lists wordcount textpattern autosave", //插件配置 axupimgs indent2em
|
||||
toolbar: [
|
||||
"fullscreen undo redo restoredraft | cut copy paste pastetext | forecolor backcolor bold italic underline strikethrough link anchor | alignleft aligncenter alignright alignjustify outdent indent | bullist numlist | blockquote subscript superscript removeformat ",
|
||||
"styleselect formatselect fontselect fontsizeselect | table image axupimgs media charmap hr pagebreak insertdatetime selectall visualblocks searchreplace | code print preview | indent2em lineheight formatpainter",
|
||||
], //工具栏配置,设为false则隐藏
|
||||
paste_data_images: true, //图片是否可粘贴
|
||||
file_picker_types: "file image media",
|
||||
// 文件上传处理函数
|
||||
file_picker_callback: (callback: any, value: any, meta: any) => {
|
||||
if (meta.filetype == "image") {
|
||||
tinymce.value.type = 'image';
|
||||
} else if (meta.filetype == "media") {
|
||||
tinymce.value.type = 'video';
|
||||
} else {
|
||||
tinymce.value.type = 'file';
|
||||
}
|
||||
// 打开资源管理器
|
||||
materialRef.value.showPopup(1)
|
||||
// 保存回调到全局
|
||||
tinymce.value.callback = callback
|
||||
}
|
||||
}
|
||||
})
|
||||
/** Data End **/
|
||||
|
||||
/** Methods Start **/
|
||||
// 确认选择文件时
|
||||
const handleMaterialFile = (event: Event) => {
|
||||
tinymce.value.callback(event);
|
||||
materialRef.value.fileList = []
|
||||
}
|
||||
/** Methods End **/
|
||||
</script>
|
||||
<style scoped>
|
||||
.tinymce-boxz > textarea {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
/* 隐藏apikey没有绑定当前域名的提示 */
|
||||
.tox-notifications-container .tox-notification--warning {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.tox.tox-tinymce {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
29
admin/src/components/footer-btns/index.vue
Normal file
29
admin/src/components/footer-btns/index.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="footer-wrap">
|
||||
<div class="footer-content">
|
||||
<div class="flex flex-center">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
export default defineComponent({})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.footer-wrap {
|
||||
height: 60px;
|
||||
.footer-content {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding-left: $layout-aside-width;
|
||||
height: 60px;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
admin/src/components/material-select/file-item.vue
Normal file
57
admin/src/components/material-select/file-item.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div>
|
||||
<del-wrap @close="$emit('close')">
|
||||
<div class="file-item" :style="{ height: fileSize, width: fileSize }">
|
||||
<el-image v-if="type == 'image'" class="image" fit="contain" :src="uri"></el-image>
|
||||
<video v-else-if="type == 'video'" class="video" :src="uri"></video>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</del-wrap>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import DelWrap from '@/components/del-wrap/index.vue'
|
||||
import { defineComponent, inject } from 'vue'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
DelWrap
|
||||
},
|
||||
props: {
|
||||
// 图片地址
|
||||
uri: {
|
||||
type: String
|
||||
},
|
||||
// 图片尺寸
|
||||
fileSize: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
setup() {
|
||||
const type = inject('type')
|
||||
return {
|
||||
type
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.file-item {
|
||||
background-color: $border-color-light;
|
||||
border: 1px solid $border-color-light;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
.image,
|
||||
.video {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
162
admin/src/components/material-select/hook.ts
Normal file
162
admin/src/components/material-select/hook.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import {
|
||||
apiFileCateAdd,
|
||||
apiFileCateDelete,
|
||||
apiFileCateEdit,
|
||||
apiFileCateLists,
|
||||
apiFileDelete,
|
||||
apiFileList,
|
||||
apiFileMove
|
||||
} from '@/api/app'
|
||||
import { usePages } from '@/core/hooks/pages'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, inject, reactive, ref, Ref } from 'vue'
|
||||
|
||||
// 左侧分组的钩子函数
|
||||
export function useCate(typeValue: Ref<any>) {
|
||||
// 分组列表
|
||||
const cateLists: Ref<any[]> = ref([])
|
||||
// 选中的分组id
|
||||
const cateId = ref('')
|
||||
// 添加分组
|
||||
const handleAddCate = (val: string) => {
|
||||
apiFileCateAdd({
|
||||
type: typeValue.value,
|
||||
pid: 0,
|
||||
name: val
|
||||
}).then(() => {
|
||||
getCateLists()
|
||||
})
|
||||
}
|
||||
// 编辑分组
|
||||
const handleEditCate = (val: string, id: number) => {
|
||||
apiFileCateEdit({
|
||||
id,
|
||||
name: val
|
||||
}).then(() => {
|
||||
getCateLists()
|
||||
})
|
||||
}
|
||||
// 删除分组
|
||||
const handleDeleteCate = (id: number) => {
|
||||
apiFileCateDelete({
|
||||
id
|
||||
}).then(() => {
|
||||
getCateLists()
|
||||
})
|
||||
}
|
||||
// 获取分组列表
|
||||
const getCateLists = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFileCateLists({
|
||||
type: typeValue.value,
|
||||
page_type: 1
|
||||
}).then((res: any) => {
|
||||
const item: any[] = [
|
||||
{
|
||||
name: '全部',
|
||||
id: ''
|
||||
},
|
||||
{
|
||||
name: '未分组',
|
||||
id: 0
|
||||
}
|
||||
]
|
||||
cateLists.value = res?.lists
|
||||
cateLists.value.unshift(...item)
|
||||
resolve(cateLists)
|
||||
})
|
||||
})
|
||||
}
|
||||
return {
|
||||
cateId,
|
||||
cateLists,
|
||||
handleAddCate,
|
||||
handleEditCate,
|
||||
handleDeleteCate,
|
||||
getCateLists
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件的钩子函数
|
||||
export function useFile(cateId: Ref<string>, type: Ref<any>, limit: Ref<number>) {
|
||||
const moveId = ref(0)
|
||||
const select: Ref<any[]> = ref([])
|
||||
const fileParams = reactive({
|
||||
name: '',
|
||||
type: type,
|
||||
cid: cateId
|
||||
})
|
||||
const { pager, requestApi, resetPage } = usePages({
|
||||
callback: apiFileList,
|
||||
params: fileParams
|
||||
})
|
||||
|
||||
const selectStatus = computed(
|
||||
() => (id: number) => select.value.find((item: any) => item.id == id)
|
||||
)
|
||||
|
||||
const getFileList = () => {
|
||||
requestApi()
|
||||
}
|
||||
const refresh = () => {
|
||||
resetPage()
|
||||
}
|
||||
const batchFileDelete = (id?: number[]) => {
|
||||
const ids = id ? id : select.value.map((item: any) => item.id)
|
||||
apiFileDelete({
|
||||
ids
|
||||
}).then(res => {
|
||||
getFileList()
|
||||
clearSelect()
|
||||
})
|
||||
}
|
||||
const batchFileMove = () => {
|
||||
const ids = select.value.map((item: any) => item.id)
|
||||
apiFileMove({
|
||||
ids,
|
||||
cid: moveId.value
|
||||
}).then(res => {
|
||||
moveId.value = 0
|
||||
getFileList()
|
||||
clearSelect()
|
||||
})
|
||||
}
|
||||
|
||||
const selectFile = (item: any) => {
|
||||
const index = select.value.findIndex((items: any) => items.id == item.id)
|
||||
if (index != -1) {
|
||||
select.value.splice(index, 1)
|
||||
return
|
||||
}
|
||||
if (select.value.length == limit.value) {
|
||||
if (limit.value == 1) {
|
||||
select.value = []
|
||||
select.value.push(item)
|
||||
return
|
||||
}
|
||||
ElMessage.warning('已达到选择上限')
|
||||
return
|
||||
}
|
||||
select.value.push(item)
|
||||
}
|
||||
const clearSelect = () => {
|
||||
select.value = []
|
||||
}
|
||||
const cancelSelete = (id: number) => {
|
||||
select.value = select.value.filter(item => item.id != id)
|
||||
}
|
||||
return {
|
||||
moveId,
|
||||
pager,
|
||||
fileParams,
|
||||
select,
|
||||
getFileList,
|
||||
refresh,
|
||||
batchFileDelete,
|
||||
batchFileMove,
|
||||
selectFile,
|
||||
selectStatus,
|
||||
clearSelect,
|
||||
cancelSelete
|
||||
}
|
||||
}
|
||||
255
admin/src/components/material-select/index.vue
Normal file
255
admin/src/components/material-select/index.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="material-select">
|
||||
<popup
|
||||
ref="popupRef"
|
||||
width="950px"
|
||||
custom-class="body-padding"
|
||||
:title="`选择${tipsText}`"
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
<template #trigger>
|
||||
<div class="material-select__trigger clearfix" @click.stop>
|
||||
<draggable v-model="fileList" class="draggable" animation="300" item-key="id">
|
||||
<template #item="{ element, index }">
|
||||
<div
|
||||
class="material-preview"
|
||||
:class="{
|
||||
'is-disabled': disabled,
|
||||
'is-one': limit == 1
|
||||
}"
|
||||
@click="showPopup(index)"
|
||||
>
|
||||
<file-item
|
||||
:uri="element"
|
||||
:file-size="size"
|
||||
@close="deleteImg(index)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
v-show="showUpload"
|
||||
class="material-upload"
|
||||
:class="{
|
||||
'is-disabled': disabled,
|
||||
'is-one': limit == 1
|
||||
}"
|
||||
@click="showPopup(-1)"
|
||||
>
|
||||
<slot name="upload">
|
||||
<div
|
||||
class="upload-btn flex flex-col flex-center"
|
||||
:style="{
|
||||
width: size,
|
||||
height: size
|
||||
}"
|
||||
>
|
||||
<el-icon :size="25"><plus /></el-icon>
|
||||
<span>添加</span>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="material-wrap">
|
||||
<material
|
||||
ref="materialRefs"
|
||||
:file-size="fileSize"
|
||||
:limit="meterialLimit"
|
||||
@change="selectChange"
|
||||
/>
|
||||
</div>
|
||||
</popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
provide,
|
||||
reactive,
|
||||
defineComponent,
|
||||
computed,
|
||||
ref,
|
||||
Ref,
|
||||
toRef,
|
||||
toRefs,
|
||||
watch,
|
||||
nextTick
|
||||
} from 'vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
import FileItem from './file-item.vue'
|
||||
import Material from './material.vue'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Popup,
|
||||
Draggable,
|
||||
FileItem,
|
||||
Material
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Array],
|
||||
default: () => []
|
||||
},
|
||||
// 文件类型
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image'
|
||||
},
|
||||
// 选择器尺寸
|
||||
size: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
// 文件尺寸
|
||||
fileSize: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
// 选择数量限制
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
// 禁用选择
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['change', 'update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const popupRef: Ref<typeof Popup | null> = ref(null)
|
||||
const materialRefs: Ref<typeof Material | null> = ref(null)
|
||||
const fileList: Ref<any[]> = ref([])
|
||||
const select: Ref<any[]> = ref([])
|
||||
const isAdd = ref(true)
|
||||
const currentIndex = ref(-1)
|
||||
const { disabled, limit, modelValue } = toRefs(props)
|
||||
const tipsText = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'image':
|
||||
return '图片'
|
||||
case 'video':
|
||||
return '视频'
|
||||
}
|
||||
})
|
||||
const typeValue = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'image':
|
||||
return 10
|
||||
case 'video':
|
||||
return 20
|
||||
case 'file':
|
||||
return 30
|
||||
}
|
||||
})
|
||||
const showUpload = computed(() => {
|
||||
return props.limit - fileList.value.length > 0
|
||||
})
|
||||
const meterialLimit = computed(() => {
|
||||
if (!isAdd.value) {
|
||||
return 1
|
||||
}
|
||||
if (!limit.value) {
|
||||
return null
|
||||
}
|
||||
return limit.value - fileList.value.length
|
||||
})
|
||||
const handleConfirm = () => {
|
||||
const selectUri = select.value.map(item => item.uri)
|
||||
if (!isAdd.value) {
|
||||
fileList.value.splice(currentIndex.value, 1, selectUri.shift())
|
||||
} else {
|
||||
fileList.value = [...fileList.value, ...selectUri]
|
||||
}
|
||||
handleChange()
|
||||
}
|
||||
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)
|
||||
nextTick(() => {
|
||||
materialRefs.value?.clearSelect()
|
||||
})
|
||||
}
|
||||
|
||||
const deleteImg = (index: number) => {
|
||||
fileList.value.splice(index, 1)
|
||||
handleChange()
|
||||
}
|
||||
|
||||
watch(modelValue, (val: any[] | string) => {
|
||||
console.log(val)
|
||||
fileList.value = Array.isArray(val) ? val : val == '' ? [] : [val]
|
||||
})
|
||||
provide('type', props.type)
|
||||
provide('fileSize', props.fileSize)
|
||||
provide('limit', props.limit)
|
||||
provide('typeValue', typeValue)
|
||||
return {
|
||||
popupRef,
|
||||
materialRefs,
|
||||
fileList,
|
||||
tipsText,
|
||||
handleConfirm,
|
||||
meterialLimit,
|
||||
showUpload,
|
||||
showPopup,
|
||||
selectChange,
|
||||
deleteImg
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.material-select {
|
||||
.material-upload,
|
||||
.material-preview {
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: $color-text-secondary;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
box-sizing: border-box;
|
||||
float: left;
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&.is-one {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.material-upload {
|
||||
.upload-btn {
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #d7d7d7;
|
||||
}
|
||||
}
|
||||
}
|
||||
.material-wrap {
|
||||
height: 540px;
|
||||
border-top: 1px solid $border-color-base;
|
||||
border-bottom: 1px solid $border-color-base;
|
||||
}
|
||||
</style>
|
||||
347
admin/src/components/material-select/material.vue
Normal file
347
admin/src/components/material-select/material.vue
Normal file
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<div v-loading="pager.loading" class="material flex col-stretch">
|
||||
<div class="material__left">
|
||||
<el-scrollbar class="ls-scrollbar" style="height: calc(100% - 40px)">
|
||||
<div class="material-left__content p-t-16 p-b-16">
|
||||
<el-tree
|
||||
ref="treeRefs"
|
||||
node-key="id"
|
||||
:data="cateLists"
|
||||
empty-text
|
||||
:highlight-current="true"
|
||||
:expand-on-click-node="false"
|
||||
icon-class="el-icon-arrow-right"
|
||||
:current-node-key="cateId"
|
||||
@node-click="currentChange"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="flex flex-1 flex-center" style="min-width: 0">
|
||||
<img
|
||||
style="width: 20px; height: 16px"
|
||||
src="@/assets/images/icon_folder.png"
|
||||
alt
|
||||
class="m-r-10"
|
||||
/>
|
||||
<span class="flex-1 line-1 m-r-10">
|
||||
{{ data.name }}
|
||||
</span>
|
||||
<el-dropdown v-if="data.id > 0" :hide-on-click="false">
|
||||
<span class="muted m-r-10">···</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<div>
|
||||
<popover-input
|
||||
type="text"
|
||||
tips="分类名称"
|
||||
@confirm="handleEditCate($event, data.id)"
|
||||
>
|
||||
<el-dropdown-item>命名分组</el-dropdown-item>
|
||||
</popover-input>
|
||||
</div>
|
||||
<div @click="handleDeleteCate(data.id)">
|
||||
<el-dropdown-item>删除分组</el-dropdown-item>
|
||||
</div>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<div class="flex flex-center">
|
||||
<popover-input tips="分类名称" type="text" @confirm="handleAddCate">
|
||||
<el-button size="small">添加分组</el-button>
|
||||
</popover-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="material__center flex flex-col">
|
||||
<div class="operate-btn flex">
|
||||
<div class="flex-1 flex">
|
||||
<upload
|
||||
class="m-r-10"
|
||||
:data="{ cid: cateId }"
|
||||
:type="type"
|
||||
:show-progress="true"
|
||||
@change="refresh"
|
||||
>
|
||||
<el-button size="small" type="primary">本地上传</el-button>
|
||||
</upload>
|
||||
<popup
|
||||
class="m-r-10 inline"
|
||||
content="确定删除选中的文件?"
|
||||
:disabled="!select.length"
|
||||
@confirm="batchFileDelete()"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button size="small" :disabled="!select.length">删除</el-button>
|
||||
</template>
|
||||
</popup>
|
||||
<popup
|
||||
class="m-r-10 inline"
|
||||
:disabled="!select.length"
|
||||
title="移动文件"
|
||||
@confirm="batchFileMove"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button size="small" :disabled="!select.length">移动</el-button>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<span class="m-r-20">移动文件至</span>
|
||||
<el-select v-model="moveId" placeholder="请选择">
|
||||
<template v-for="item in cateLists" :key="item.id">
|
||||
<el-option
|
||||
v-if="item.id !== ''"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
></el-option>
|
||||
</template>
|
||||
</el-select>
|
||||
</div>
|
||||
</popup>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="fileParams.name"
|
||||
size="small"
|
||||
placeholder="请输入名字"
|
||||
style="width: 280px"
|
||||
@keyup.enter="refresh"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="refresh"></el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div class="material-center__content flex flex-col flex-1">
|
||||
<ul class="file-list flex flex-wrap m-t-14">
|
||||
<li
|
||||
v-for="item in pager.lists"
|
||||
:key="item.id"
|
||||
class="file-item-wrap"
|
||||
:style="{ width: fileSize }"
|
||||
@click="selectFile(item)"
|
||||
>
|
||||
<file-item
|
||||
:uri="item.uri"
|
||||
:file-size="fileSize"
|
||||
@close="batchFileDelete([item.id])"
|
||||
>
|
||||
<div v-if="selectStatus(item.id)" class="item-selected">
|
||||
<el-icon color="#fff" size="24">
|
||||
<check />
|
||||
</el-icon>
|
||||
</div>
|
||||
</file-item>
|
||||
|
||||
<div class="item-name line-1 xs p-t-10">{{ item.name }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
v-if="!pager.loading && !pager.lists.length"
|
||||
class="flex flex-1 row-center col-center"
|
||||
>
|
||||
暂无数据~
|
||||
</div>
|
||||
</div>
|
||||
<div class="material-center__footer flex row-right">
|
||||
<pagination
|
||||
v-model="pager"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
@change="getFileList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="material__right">
|
||||
<div class="flex row-between p-l-10 p-r-10">
|
||||
<div class="sm flex flex-center">
|
||||
已选择 {{ select.length }}
|
||||
<span v-if="limit">/{{ limit }}</span>
|
||||
</div>
|
||||
<el-button type="text" size="small" @click="clearSelect">清空</el-button>
|
||||
</div>
|
||||
|
||||
<el-scrollbar class="ls-scrollbar" style="height: calc(100% - 32px)">
|
||||
<ul class="select-lists flex-col p-t-10">
|
||||
<li v-for="item in select" :key="item.id" class="m-b-16">
|
||||
<div class="select-item">
|
||||
<file-item
|
||||
:uri="item.uri"
|
||||
file-size="100px"
|
||||
@close="cancelSelete(item.id)"
|
||||
></file-item>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, Ref, ref, toRefs, watch } from 'vue'
|
||||
import { useCate, useFile } from './hook'
|
||||
import PopoverInput from '@/components/popover-input/index.vue'
|
||||
import Pagination from '@/components/pagination/index.vue'
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
import Upload from '@/components/upload/index.vue'
|
||||
import FileItem from './file-item.vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ElTree } from 'element-plus'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
PopoverInput,
|
||||
Pagination,
|
||||
Popup,
|
||||
Upload,
|
||||
FileItem
|
||||
},
|
||||
props: {
|
||||
fileSize: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
emits: ['change'],
|
||||
setup(props, { emit }) {
|
||||
const treeRefs: Ref<typeof ElTree | null> = ref(null)
|
||||
const type = inject('type') as Ref<string>
|
||||
const { limit } = toRefs(props)
|
||||
const typeValue = inject('typeValue') as Ref<10 | 20 | 30>
|
||||
const visible = inject('visible') as Ref<boolean>
|
||||
const { cateId, cateLists, handleAddCate, handleEditCate, handleDeleteCate, getCateLists } =
|
||||
useCate(typeValue)
|
||||
const {
|
||||
moveId,
|
||||
pager,
|
||||
fileParams,
|
||||
select,
|
||||
getFileList,
|
||||
refresh,
|
||||
batchFileDelete,
|
||||
batchFileMove,
|
||||
selectFile,
|
||||
selectStatus,
|
||||
clearSelect,
|
||||
cancelSelete
|
||||
} = useFile(cateId, typeValue, limit)
|
||||
|
||||
const currentChange = (item: any) => {
|
||||
cateId.value = item.id
|
||||
}
|
||||
|
||||
watch(
|
||||
visible,
|
||||
async (val: boolean) => {
|
||||
if (val) {
|
||||
await getCateLists()
|
||||
treeRefs.value?.setCurrentKey(cateId.value)
|
||||
getFileList()
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
watch(cateId, (val: string) => {
|
||||
fileParams.name = ''
|
||||
refresh()
|
||||
})
|
||||
watch(
|
||||
select,
|
||||
(val: any[]) => {
|
||||
emit('change', val)
|
||||
},
|
||||
{
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
return {
|
||||
treeRefs,
|
||||
Search,
|
||||
type,
|
||||
limit,
|
||||
cateId,
|
||||
cateLists,
|
||||
handleAddCate,
|
||||
handleEditCate,
|
||||
handleDeleteCate,
|
||||
currentChange,
|
||||
moveId,
|
||||
pager,
|
||||
fileParams,
|
||||
select,
|
||||
getFileList,
|
||||
refresh,
|
||||
batchFileDelete,
|
||||
batchFileMove,
|
||||
selectFile,
|
||||
selectStatus,
|
||||
clearSelect,
|
||||
cancelSelete
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.material {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
&__left {
|
||||
width: 170px;
|
||||
:deep(.el-tree-node__content) {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
&__center {
|
||||
flex: 1;
|
||||
border-left: 1px solid $border-color-base;
|
||||
padding: 16px;
|
||||
.file-list {
|
||||
.file-item-wrap {
|
||||
margin-right: 16px;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.3;
|
||||
cursor: pointer;
|
||||
.item-selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.operation-btns {
|
||||
height: 28px;
|
||||
visibility: hidden;
|
||||
}
|
||||
&:hover .operation-btns {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__right {
|
||||
border-left: 1px solid $border-color-base;
|
||||
width: 150px;
|
||||
.select-lists {
|
||||
padding: 10px;
|
||||
|
||||
.select-item {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
64
admin/src/components/pagination/index.vue
Normal file
64
admin/src/components/pagination/index.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:currentPage="modelValue.page"
|
||||
v-model:pageSize="modelValue.size"
|
||||
:page-sizes="pageSizes"
|
||||
:layout="layout"
|
||||
:total="modelValue.count"
|
||||
hide-on-single-page
|
||||
@size-change="sizeChange"
|
||||
@current-change="pageChange"
|
||||
>
|
||||
</el-pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, reactive, toRefs } from 'vue'
|
||||
export default defineComponent({
|
||||
components: {},
|
||||
props: {
|
||||
// 每一页条数
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
// 允许选择的每一页条数
|
||||
pageSizes: {
|
||||
type: Array,
|
||||
default: () => [10, 20, 30, 40]
|
||||
},
|
||||
// 分页的布局(参考element的分页组件)
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'total, sizes, prev, pager, next, jumper'
|
||||
}
|
||||
},
|
||||
emits: ['change'],
|
||||
setup(props, { emit }) {
|
||||
const sizeChange = () => {
|
||||
props.modelValue.page = 1
|
||||
emit('change')
|
||||
}
|
||||
const pageChange = () => {
|
||||
emit('change')
|
||||
}
|
||||
return {
|
||||
sizeChange,
|
||||
pageChange
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.pagination {
|
||||
height: 100%;
|
||||
.pagination-footer {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
78
admin/src/components/popover-input/index.vue
Normal file
78
admin/src/components/popover-input/index.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="popover-input">
|
||||
<el-popover v-model:visible="visible" placement="top" :width="width" trigger="manual">
|
||||
<div class="flex">
|
||||
<div class="popover-input__input m-r-10 flex-1">
|
||||
<el-input
|
||||
v-model="value"
|
||||
:type="type"
|
||||
size="mini"
|
||||
:placeholder="placeholder"
|
||||
></el-input>
|
||||
</div>
|
||||
<div class="popover-input__btns flex-none">
|
||||
<el-button type="text" size="mini" @click="close">取消</el-button>
|
||||
<el-button type="primary" size="mini" @click="handleConfirm">确定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<template #reference>
|
||||
<div class="inline" type="text" @click="open">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
export default defineComponent({
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'number'
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 250
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['confirm'],
|
||||
setup(props, { emit }) {
|
||||
const visible = ref(false)
|
||||
const value = ref('')
|
||||
const open = () => {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (value.value) {
|
||||
emit('confirm', value.value)
|
||||
value.value = ''
|
||||
}
|
||||
close()
|
||||
}
|
||||
return {
|
||||
visible,
|
||||
value,
|
||||
open,
|
||||
close,
|
||||
handleConfirm
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
136
admin/src/components/popup/index.vue
Normal file
136
admin/src/components/popup/index.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="dialog">
|
||||
<div class="dialog__trigger" @click="open">
|
||||
<!-- 触发弹窗 -->
|
||||
<slot name="trigger"></slot>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:custom-class="customClass"
|
||||
:append-to-body="true"
|
||||
:width="width"
|
||||
:close-on-click-modal="clickModalClose"
|
||||
>
|
||||
<!-- 弹窗内容 -->
|
||||
<template v-if="title" #title>
|
||||
{{ title }}
|
||||
</template>
|
||||
<template v-else #title>
|
||||
<div class="flex col-center">
|
||||
<el-icon :size="25" :color="$variables.color_warning"
|
||||
><warning-filled
|
||||
/></el-icon>
|
||||
<span class="m-l-6">温馨提示</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 自定义内容 -->
|
||||
<slot>
|
||||
{{ content }}
|
||||
</slot>
|
||||
<!-- 底部弹窗页脚 -->
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button v-if="cancelButtonText" size="small" @click="handleEvent('cancel')">
|
||||
{{ cancelButtonText }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="confirmButtonText"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleEvent('confirm')"
|
||||
>
|
||||
{{ confirmButtonText }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, provide, ref } from 'vue'
|
||||
export default defineComponent({
|
||||
components: {},
|
||||
props: {
|
||||
title: {
|
||||
// 弹窗标题
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
content: {
|
||||
// 弹窗内容
|
||||
type: String,
|
||||
default: '确认要删除?'
|
||||
},
|
||||
confirmButtonText: {
|
||||
// 确认按钮内容
|
||||
type: [String, Boolean],
|
||||
default: '确认'
|
||||
},
|
||||
cancelButtonText: {
|
||||
// 取消按钮内容
|
||||
type: [String, Boolean],
|
||||
default: '取消'
|
||||
},
|
||||
width: {
|
||||
// 弹窗的宽度
|
||||
type: String,
|
||||
default: '400px'
|
||||
},
|
||||
disabled: {
|
||||
// 是否禁用
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
async: {
|
||||
// 是否开启异步关闭
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
clickModalClose: {
|
||||
// 点击遮罩层关闭对话窗口
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['confirm', 'cancel'],
|
||||
setup(props, { emit }) {
|
||||
const visible = ref(false)
|
||||
|
||||
const handleEvent = (type: 'confirm' | 'cancel') => {
|
||||
emit(type)
|
||||
if (!props.async || type === 'cancel') {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
}
|
||||
const open = () => {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
provide('visible', visible)
|
||||
return {
|
||||
visible,
|
||||
handleEvent,
|
||||
close,
|
||||
open
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dialog-body {
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
131
admin/src/components/upload/index.vue
Normal file
131
admin/src/components/upload/index.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="upload">
|
||||
<el-upload
|
||||
ref="uploadRefs"
|
||||
:action="action"
|
||||
:multiple="multiple"
|
||||
:limit="limit"
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:data="data"
|
||||
:on-progress="handleProgress"
|
||||
:on-success="handleSuccess"
|
||||
:on-exceed="handleExceed"
|
||||
:on-error="handleError"
|
||||
>
|
||||
<slot></slot>
|
||||
</el-upload>
|
||||
<el-dialog
|
||||
v-if="showProgress && fileList.length"
|
||||
v-model="visible"
|
||||
title="上传进度"
|
||||
:close-on-click-modal="false"
|
||||
width="500px"
|
||||
:modal="false"
|
||||
:before-close="handleClose"
|
||||
>
|
||||
<div class="file-list">
|
||||
<template v-for="(item, index) in fileList" :key="index">
|
||||
<div class="m-b-20">
|
||||
<div>{{ item.name }}</div>
|
||||
<div class="flex-1">
|
||||
<el-progress :percentage="parseInt(item.percentage)"></el-progress>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, Ref, ref } from 'vue'
|
||||
import { ElMessage, ElUpload } from 'element-plus'
|
||||
import { useStore } from '@/store'
|
||||
import { version } from '@/config/app'
|
||||
export default defineComponent({
|
||||
components: {},
|
||||
props: {
|
||||
// 上传文件类型
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image'
|
||||
},
|
||||
// 是否支持多选
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 多选时最多选择几条
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
// 上传时的额外参数
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
// 是否显示上传进度
|
||||
showProgress: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['change', 'error'],
|
||||
setup(props, { emit }) {
|
||||
const store = useStore()
|
||||
const uploadRefs: Ref<typeof ElUpload | null> = ref(null)
|
||||
const action = ref(`${import.meta.env.VITE_APP_BASE_URL}/adminapi/upload/${props.type}`)
|
||||
const headers = computed(() => ({
|
||||
token: store.getters.token,
|
||||
version: version
|
||||
}))
|
||||
const visible = ref(false)
|
||||
const fileList: Ref<any[]> = ref([])
|
||||
|
||||
const handleProgress = (event: any, file: any, fileLists: any[]) => {
|
||||
visible.value = true
|
||||
fileList.value = fileLists
|
||||
}
|
||||
|
||||
const handleSuccess = (event: any, file: any, fileLists: any[]) => {
|
||||
const allSuccess = fileLists.every(item => item.status == 'success')
|
||||
if (allSuccess) {
|
||||
uploadRefs.value?.clearFiles()
|
||||
visible.value = false
|
||||
emit('change')
|
||||
}
|
||||
}
|
||||
const handleError = (event: any, file: any, fileLists: any[]) => {
|
||||
ElMessage.error(`${file.name}文件上传失败`)
|
||||
uploadRefs.value?.abort()
|
||||
visible.value = false
|
||||
emit('change')
|
||||
emit('error')
|
||||
}
|
||||
const handleExceed = () => {
|
||||
ElMessage.error('超出上传上限,请重新上传')
|
||||
}
|
||||
const handleClose = () => {
|
||||
uploadRefs.value?.abort()
|
||||
uploadRefs.value?.clearFiles()
|
||||
visible.value = false
|
||||
}
|
||||
return {
|
||||
uploadRefs,
|
||||
action,
|
||||
headers,
|
||||
visible,
|
||||
fileList,
|
||||
handleProgress,
|
||||
handleSuccess,
|
||||
handleError,
|
||||
handleExceed,
|
||||
handleClose
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
Reference in New Issue
Block a user