初始化admin pc端

This commit is contained in:
Mrtangl
2022-04-08 10:42:44 +08:00
parent d9c9f27530
commit 19665b64fb
137 changed files with 11991 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>