新增
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<component :is="type" v-bind="linkProps">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* @description 兼容第三方页面的跳转
|
||||
*/
|
||||
import { isExternal } from '@/utils/validate'
|
||||
|
||||
interface Props {
|
||||
to: string | Record<string, string>
|
||||
replace?: boolean
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const isExternalLink = computed(() => {
|
||||
return typeof props.to !== 'object' && isExternal(props.to)
|
||||
})
|
||||
|
||||
const type = computed(() => {
|
||||
if (isExternalLink.value) {
|
||||
return 'a'
|
||||
}
|
||||
return 'router-link'
|
||||
})
|
||||
|
||||
const linkProps = computed(() => {
|
||||
if (isExternalLink.value) {
|
||||
return {
|
||||
href: props.to,
|
||||
target: '_blank'
|
||||
}
|
||||
}
|
||||
return props
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="color-select flex flex-1">
|
||||
<el-color-picker v-model="color" :predefine="predefineColors"></el-color-picker>
|
||||
<el-input v-model="color" class="mx-[10px] flex-1" type="text" readonly></el-input>
|
||||
<el-button type="text" @click="reset">重置</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
resetColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: number): void
|
||||
}>()
|
||||
|
||||
const color = useVModel(props, 'modelValue', emit)
|
||||
const predefineColors = ['#FF2C3C', '#f7971e', '#fa444d', '#e0a356', '#2f80ed', '#2ec840']
|
||||
|
||||
const reset = () => {
|
||||
color.value = props.resetColor
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<el-date-picker
|
||||
v-model="content"
|
||||
type="datetimerange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
></el-date-picker>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
}>(),
|
||||
{
|
||||
startTime: '',
|
||||
endTime: ''
|
||||
}
|
||||
)
|
||||
const emit = defineEmits(['update:startTime', 'update:endTime'])
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="del-wrap">
|
||||
<slot></slot>
|
||||
<div v-if="showClose" class="icon-close" @click.stop="handleClose">
|
||||
<icon :size="12" name="el-icon-CloseBold" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
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>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-for="(item, index) in getOptions" :key="index">
|
||||
<span>{{ index != 0 ? '、' : '' }}{{ item[config.label] }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
options: any[]
|
||||
value: any
|
||||
config?: Record<string, string>
|
||||
}>(),
|
||||
{
|
||||
options: () => [],
|
||||
config: () => ({
|
||||
label: 'name',
|
||||
value: 'value'
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const values = computed(() => {
|
||||
if (props.value !== null && typeof props.value !== 'undefined') {
|
||||
return Array.isArray(props.value) ? props.value : String(props.value).split(',')
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
const getOptions = computed(() => {
|
||||
return props.options.filter((item) => values.value.includes(item[props.config.value]))
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="border border-br flex flex-col" :style="styles">
|
||||
<toolbar
|
||||
class="border-b border-br"
|
||||
:editor="editorRef"
|
||||
:defaultConfig="toolbarConfig"
|
||||
:mode="mode"
|
||||
/>
|
||||
<w-editor
|
||||
class="flex-1 overflow-hidden"
|
||||
v-model="valueHtml"
|
||||
:defaultConfig="editorConfig"
|
||||
:mode="mode"
|
||||
@onCreated="handleCreated"
|
||||
/>
|
||||
<material-picker
|
||||
ref="materialPickerRef"
|
||||
:type="fileType"
|
||||
:limit="-1"
|
||||
hidden-upload
|
||||
@change="selectChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import '@wangeditor/editor/dist/css/style.css' // 引入 css
|
||||
|
||||
import type { IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
|
||||
import { Editor as WEditor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
import MaterialPicker from '@/components/material/picker.vue'
|
||||
import { addUnit } from '@/utils/util'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string
|
||||
mode?: 'default' | 'simple'
|
||||
height?: string | number
|
||||
width?: string | number
|
||||
toolbarConfig?: Partial<IToolbarConfig>
|
||||
}>(),
|
||||
{
|
||||
modelValue: '',
|
||||
mode: 'default',
|
||||
height: '100%',
|
||||
width: 'auto',
|
||||
toolbarConfig: () => ({})
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
// 编辑器实例,必须用 shallowRef
|
||||
const editorRef = shallowRef()
|
||||
const materialPickerRef = shallowRef<InstanceType<typeof MaterialPicker>>()
|
||||
const fileType = ref('')
|
||||
|
||||
let insertFn: any
|
||||
|
||||
const editorConfig: Partial<IEditorConfig> = {
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
customBrowseAndUpload(insert: any) {
|
||||
fileType.value = 'image'
|
||||
materialPickerRef.value?.showPopup(-1)
|
||||
insertFn = insert
|
||||
}
|
||||
},
|
||||
uploadVideo: {
|
||||
customBrowseAndUpload(insert: any) {
|
||||
fileType.value = 'video'
|
||||
materialPickerRef.value?.showPopup(-1)
|
||||
insertFn = insert
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const styles = computed<CSSProperties>(() => ({
|
||||
height: addUnit(props.height),
|
||||
width: addUnit(props.width)
|
||||
}))
|
||||
const valueHtml = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const selectChange = (fileUrl: string[]) => {
|
||||
fileUrl.forEach((url) => {
|
||||
insertFn(url)
|
||||
})
|
||||
}
|
||||
|
||||
// 组件销毁时,也及时销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value
|
||||
if (editor == null) return
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
const handleCreated = (editor: any) => {
|
||||
editorRef.value = editor // 记录 editor 实例,重要!
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.w-e-full-screen-container {
|
||||
z-index: 999;
|
||||
}
|
||||
.w-e-text-container [data-slate-editor] ul {
|
||||
list-style: disc;
|
||||
}
|
||||
.w-e-text-container [data-slate-editor] ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.17em;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
h5 {
|
||||
font-size: 0.83em;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,151 @@
|
||||
<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>
|
||||
<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
|
||||
}
|
||||
})
|
||||
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>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="footer-btns">
|
||||
<div class="footer-btns__content" :style="fixed ? 'position: fixed' : ''">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.footer-btns {
|
||||
height: 60px;
|
||||
&__content {
|
||||
bottom: 0;
|
||||
height: 60px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
@apply flex justify-center items-center shadow bg-body;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as ElementPlusIcons from '@element-plus/icons-vue'
|
||||
//@ts-ignore
|
||||
import localIconsName from 'virtual:svg-icons-names'
|
||||
|
||||
export const LOCAL_ICON_PREFIX = 'local-icon-'
|
||||
export const EL_ICON_PREFIX = 'el-icon-'
|
||||
|
||||
const elIconsName: string[] = []
|
||||
|
||||
for (const [, component] of Object.entries(ElementPlusIcons)) {
|
||||
elIconsName.push(`${EL_ICON_PREFIX}${component.name}`)
|
||||
}
|
||||
|
||||
export function getElementPlusIconNames() {
|
||||
return elIconsName
|
||||
}
|
||||
export function getLocalIconNames() {
|
||||
return localIconsName
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import { ElIcon } from 'element-plus'
|
||||
import { createVNode, defineComponent, h, resolveComponent } from 'vue'
|
||||
|
||||
import { EL_ICON_PREFIX, LOCAL_ICON_PREFIX } from './index'
|
||||
import svgIcon from './svg-icon.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Icon',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '14px'
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'inherit'
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
return () => {
|
||||
if (props.name.indexOf(EL_ICON_PREFIX) === 0) {
|
||||
// el-icon
|
||||
return createVNode(
|
||||
ElIcon,
|
||||
{
|
||||
size: props.size,
|
||||
color: props.color
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
createVNode(resolveComponent(props.name.replace(EL_ICON_PREFIX, '')))
|
||||
}
|
||||
)
|
||||
}
|
||||
if (props.name.indexOf(LOCAL_ICON_PREFIX) === 0) {
|
||||
// 本地icon
|
||||
return h(
|
||||
'i',
|
||||
{
|
||||
class: ['local-icon']
|
||||
},
|
||||
createVNode(svgIcon, { ...props })
|
||||
)
|
||||
}
|
||||
// 如果name不符合预期的前缀,返回null
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="icon-select">
|
||||
<el-popover
|
||||
trigger="contextmenu"
|
||||
v-model:visible="state.popoverVisible"
|
||||
:width="state.popoverWidth"
|
||||
>
|
||||
<div
|
||||
@mouseover.stop="state.mouseoverSelect = true"
|
||||
@mouseout.stop="state.mouseoverSelect = false"
|
||||
>
|
||||
<div>
|
||||
<div class="flex justify-between">
|
||||
<div class="mb-3">请选择图标</div>
|
||||
<div>
|
||||
<span
|
||||
v-for="(item, index) in iconTabsMap"
|
||||
:key="index"
|
||||
class="cursor-pointer text-sm ml-2"
|
||||
:class="{
|
||||
'text-primary': index == tabIndex
|
||||
}"
|
||||
@click="tabIndex = index"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-[280px]">
|
||||
<el-scrollbar>
|
||||
<div class="flex flex-wrap">
|
||||
<div v-for="item in iconNamesFliter" :key="item" class="m-1">
|
||||
<el-button @click="handleSelect(item)">
|
||||
<icon :name="item" :size="18" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-input
|
||||
ref="inputRef"
|
||||
v-model.trim="state.inputValue"
|
||||
placeholder="搜索图标"
|
||||
:autofocus="false"
|
||||
:disabled="disabled"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
clearable
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="flex items-center" v-if="modelValue">
|
||||
<el-tooltip class="flex-1 w-20" :content="modelValue" placement="top">
|
||||
<icon
|
||||
class="mr-1"
|
||||
:key="modelValue"
|
||||
:name="modelValue"
|
||||
:size="16"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<template v-else>无</template>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button>
|
||||
<icon name="el-icon-Close" :size="18" @click="handleClear" />
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { ElInput } from 'element-plus'
|
||||
import { computed, nextTick, onMounted, reactive, shallowRef, watch } from 'vue'
|
||||
|
||||
import { getElementPlusIconNames, getLocalIconNames } from './index'
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
disabled?: boolean
|
||||
}
|
||||
withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
disabled: false
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'change', value: string): void
|
||||
}>()
|
||||
|
||||
const tabIndex = ref(0)
|
||||
const iconTabsMap = [
|
||||
{
|
||||
name: 'element图标',
|
||||
icons: getElementPlusIconNames()
|
||||
},
|
||||
{
|
||||
name: '本地图标',
|
||||
icons: getLocalIconNames()
|
||||
}
|
||||
]
|
||||
|
||||
const inputRef = shallowRef<InstanceType<typeof ElInput>>()
|
||||
|
||||
const state = reactive({
|
||||
inputValue: '',
|
||||
popoverVisible: false,
|
||||
popoverWidth: 0,
|
||||
mouseoverSelect: false,
|
||||
inputFocus: false
|
||||
})
|
||||
|
||||
// input 框聚焦
|
||||
const handleFocus = () => {
|
||||
state.inputFocus = state.popoverVisible = true
|
||||
}
|
||||
|
||||
// input 框失去焦点
|
||||
const handleBlur = () => {
|
||||
state.inputFocus = false
|
||||
state.popoverVisible = state.mouseoverSelect
|
||||
}
|
||||
|
||||
// 选中图标
|
||||
const handleSelect = (icon: string) => {
|
||||
state.mouseoverSelect = state.popoverVisible = false
|
||||
emits('update:modelValue', icon)
|
||||
emits('change', icon)
|
||||
}
|
||||
//取消选中
|
||||
const handleClear = () => {
|
||||
emits('update:modelValue', '')
|
||||
emits('change', '')
|
||||
}
|
||||
|
||||
//根据输入框内容塞选
|
||||
const iconNamesFliter = computed(() => {
|
||||
const iconNames = iconTabsMap[tabIndex.value]?.icons ?? []
|
||||
if (!state.inputValue) {
|
||||
return iconNames
|
||||
}
|
||||
const inputValue = state.inputValue.toLowerCase()
|
||||
return iconNames.filter((icon: string) => {
|
||||
if (icon.toLowerCase().indexOf(inputValue) !== -1) {
|
||||
return icon
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 获取 input 的宽度
|
||||
const getInputWidth = () => {
|
||||
nextTick(() => {
|
||||
const inputWidth = inputRef.value?.$el.offsetWidth
|
||||
state.popoverWidth = inputWidth < 300 ? 300 : inputWidth
|
||||
})
|
||||
}
|
||||
|
||||
//监听body点击事件
|
||||
useEventListener(document.body, 'click', () => {
|
||||
state.popoverVisible = state.inputFocus || state.mouseoverSelect ? true : false
|
||||
})
|
||||
|
||||
watch(
|
||||
() => state.popoverVisible,
|
||||
async (value) => {
|
||||
await nextTick()
|
||||
if (value) {
|
||||
inputRef.value?.focus()
|
||||
} else {
|
||||
inputRef.value?.blur()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
getInputWidth()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<svg aria-hidden="true" :style="styles">
|
||||
<use :xlink:href="symbolId" fill="currentColor" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
import { addUnit } from '@/utils/util'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 16
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'inherit'
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const symbolId = computed(() => `#${props.name}`)
|
||||
const styles = computed<CSSProperties>(() => {
|
||||
return {
|
||||
width: addUnit(props.size),
|
||||
height: addUnit(props.size),
|
||||
color: props.color
|
||||
}
|
||||
})
|
||||
return { symbolId, styles }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<el-image :style="styles" v-bind="props"> </el-image>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { imageProps } from 'element-plus'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { addUnit } from '@/utils/util'
|
||||
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: 'auto'
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: 'auto'
|
||||
},
|
||||
radius: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
...imageProps
|
||||
})
|
||||
|
||||
const styles = computed<CSSProperties>(() => {
|
||||
return {
|
||||
width: addUnit(props.width),
|
||||
height: addUnit(props.height),
|
||||
borderRadius: addUnit(props.radius)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-image {
|
||||
display: block;
|
||||
.el-image__error {
|
||||
@apply text-xs;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="article-list">
|
||||
<el-form ref="formRef" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="文章名称">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
>
|
||||
</el-input>
|
||||
<el-button type="primary" class="ml-4" :icon="Search" @click="resetPage" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table
|
||||
size="large"
|
||||
v-loading="pager.loading"
|
||||
:data="pager.lists"
|
||||
height="432px"
|
||||
@row-click="handleSelectItem"
|
||||
>
|
||||
<el-table-column label="选择" min-width="50">
|
||||
<template #default="{ row }">
|
||||
<div class="flex row-center">
|
||||
<el-checkbox
|
||||
:model-value="getSelectItem(row.id)"
|
||||
size="large"
|
||||
@change="handleSelectItem(row)"
|
||||
></el-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文章名称" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center">
|
||||
<el-image
|
||||
fit="cover"
|
||||
:src="row.image"
|
||||
class="flex-none w-[58px] h-[58px]"
|
||||
/>
|
||||
<div class="ml-4 overflow-hidden">
|
||||
<el-tooltip effect="dark" :content="row.title" placement="top">
|
||||
<div class="text-base line-clamp-2">
|
||||
{{ row.title }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="create_time" min-width="140" />
|
||||
</el-table>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import { articleLists } from '@/api/article'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
|
||||
import { LinkTypeEnum } from '.'
|
||||
|
||||
//TODO TODO
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: any): void
|
||||
}>()
|
||||
|
||||
const selectData = ref<any>({
|
||||
path: '/pages/news_detail/news_detail',
|
||||
name: '',
|
||||
query: {},
|
||||
type: LinkTypeEnum.ARTICLE_LIST
|
||||
})
|
||||
|
||||
const queryParams = reactive<any>({
|
||||
name: '',
|
||||
is_show: 1
|
||||
})
|
||||
|
||||
const { pager, getLists, resetPage } = usePaging({
|
||||
fetchFun: articleLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const getSelectItem = (id: number) => {
|
||||
return id == Number(selectData.value.id)
|
||||
}
|
||||
/**
|
||||
* @description 选择
|
||||
*/
|
||||
const handleSelectItem = (event: any) => {
|
||||
selectData.value = {
|
||||
id: event.id,
|
||||
name: event.title,
|
||||
path: '/pages/news_detail/news_detail',
|
||||
query: {
|
||||
id: event.id
|
||||
},
|
||||
type: LinkTypeEnum.ARTICLE_LIST
|
||||
}
|
||||
|
||||
emit('update:modelValue', selectData.value)
|
||||
}
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (value.type != LinkTypeEnum.ARTICLE_LIST) {
|
||||
return (selectData.value = {
|
||||
id: '',
|
||||
name: '',
|
||||
path: '/pages/news_detail/news_detail',
|
||||
type: LinkTypeEnum.SHOP_PAGES
|
||||
})
|
||||
}
|
||||
selectData.value = value
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-input-group__append) {
|
||||
.el-button {
|
||||
margin: 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="custom-link h-[530px]">
|
||||
<div class="text-xl font-medium">自定义链接</div>
|
||||
<div class="flex flex-wrap items-center mt-4">
|
||||
<div class="w-[86px] text-right">自定义链接</div>
|
||||
<div class="ml-4 flex-1 min-w-[100px]">
|
||||
<el-input
|
||||
class="max-w-[320px]"
|
||||
:model-value="modelValue.query?.url"
|
||||
placeholder="请输入链接地址"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-tips ml-[101px] max-w-[320px]">
|
||||
请填写完整的带有“https://”或“http://”的链接地址,链接的域名必须在微信公众平台设置业务域名
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import { type Link, LinkTypeEnum } from '.'
|
||||
|
||||
defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<Link>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: Link): void
|
||||
}>()
|
||||
|
||||
const handleInput = (value: string) => {
|
||||
emit('update:modelValue', {
|
||||
path: '/pages/webview/webview',
|
||||
query: {
|
||||
url: value
|
||||
},
|
||||
type: LinkTypeEnum.CUSTOM_LINK
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,19 @@
|
||||
export enum MenuTypeEnum {
|
||||
'SHOP_PAGES' = 'shop',
|
||||
'APPTOOL' = 'application_tool',
|
||||
'OTHER_LINK' = 'other_link'
|
||||
}
|
||||
|
||||
export enum LinkTypeEnum {
|
||||
'SHOP_PAGES' = 'shop',
|
||||
'ARTICLE_LIST' = 'article',
|
||||
'CUSTOM_LINK' = 'custom',
|
||||
'MINI_PROGRAM' = 'mini_program'
|
||||
}
|
||||
|
||||
export interface Link {
|
||||
path: string
|
||||
name?: string
|
||||
type: string
|
||||
query?: Record<string, any>
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="link flex">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
class="flex-none w-[180px] min-h-[350px] link-menu"
|
||||
:default-openeds="[
|
||||
MenuTypeEnum.SHOP_PAGES,
|
||||
MenuTypeEnum.APPTOOL,
|
||||
MenuTypeEnum.OTHER_LINK
|
||||
]"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<el-sub-menu v-for="(item, index) in menus" :index="item.type" :key="index">
|
||||
<template #title>
|
||||
<span>{{ item.name }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="(sitem, sindex) in item.children"
|
||||
:index="sitem.type"
|
||||
:key="sindex"
|
||||
style="min-width: 160px"
|
||||
>
|
||||
<span>{{ sitem.name }}</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
<div class="flex-1 ml-4 link-content">
|
||||
<shop-pages v-model="activeLink" v-if="LinkTypeEnum.SHOP_PAGES == activeMenu" />
|
||||
<article-list v-model="activeLink" v-if="LinkTypeEnum.ARTICLE_LIST == activeMenu" />
|
||||
<custom-link v-model="activeLink" v-if="LinkTypeEnum.CUSTOM_LINK == activeMenu" />
|
||||
<mini-program v-model="activeLink" v-if="LinkTypeEnum.MINI_PROGRAM == activeMenu" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import { type Link, LinkTypeEnum, MenuTypeEnum } from '.'
|
||||
import ArticleList from './article-list.vue'
|
||||
import CustomLink from './custom-link.vue'
|
||||
import MiniProgram from './mini-program.vue'
|
||||
import ShopPages from './shop-pages.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<Link>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: any): void
|
||||
}>()
|
||||
|
||||
const menus = ref([
|
||||
{
|
||||
name: '商城页面',
|
||||
type: MenuTypeEnum.SHOP_PAGES,
|
||||
children: [
|
||||
{
|
||||
name: '基础页面',
|
||||
type: LinkTypeEnum.SHOP_PAGES,
|
||||
link: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '应用工具',
|
||||
type: MenuTypeEnum.APPTOOL,
|
||||
children: [
|
||||
{
|
||||
name: '文章资讯',
|
||||
type: LinkTypeEnum.ARTICLE_LIST,
|
||||
link: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '其他',
|
||||
type: MenuTypeEnum.OTHER_LINK,
|
||||
children: [
|
||||
{
|
||||
name: '自定义链接',
|
||||
type: LinkTypeEnum.CUSTOM_LINK,
|
||||
link: {}
|
||||
},
|
||||
{
|
||||
name: '跳转小程序',
|
||||
type: LinkTypeEnum.MINI_PROGRAM,
|
||||
link: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const activeLink = computed({
|
||||
get() {
|
||||
let linkStoreage: any = {}
|
||||
menus.value.forEach((item) => {
|
||||
const res = item.children.find((citem) => citem.type == activeMenu.value)
|
||||
if (res) linkStoreage = res
|
||||
})
|
||||
return linkStoreage.link
|
||||
},
|
||||
set(value) {
|
||||
menus.value.forEach((item) => {
|
||||
item.children.forEach((citem) => {
|
||||
if (citem.type == activeMenu.value) {
|
||||
citem.link = value
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const activeMenu = ref<string>(LinkTypeEnum.SHOP_PAGES)
|
||||
|
||||
const handleSelect = (index: string) => {
|
||||
activeMenu.value = index
|
||||
}
|
||||
|
||||
watch(
|
||||
activeLink,
|
||||
(value) => {
|
||||
if (!value.type) return
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
activeMenu.value = value.type
|
||||
activeLink.value = value
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.link {
|
||||
.link-menu {
|
||||
--el-menu-item-height: 40px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
border-color: transparent;
|
||||
|
||||
&.is-active {
|
||||
border-right-width: 2px;
|
||||
border-color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-content {
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="mini-program h-[530px]">
|
||||
<div class="text-xl font-medium">跳转小程序</div>
|
||||
<div class="flex flex-wrap items-center mt-4">
|
||||
<div class="w-[86px] text-right">小程序APPID</div>
|
||||
<div class="ml-4 flex-1 min-w-[100px]">
|
||||
<el-input
|
||||
class="max-w-[320px]"
|
||||
:model-value="modelValue.query?.appId"
|
||||
placeholder="请输入小程序appId"
|
||||
@input="(value) => handleInput('appId', value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center mt-4">
|
||||
<div class="w-[86px] text-right">小程序路径</div>
|
||||
<div class="ml-4 flex-1 min-w-[100px]">
|
||||
<el-input
|
||||
class="max-w-[320px]"
|
||||
:model-value="modelValue.query?.path"
|
||||
placeholder="请输入小程序路径链接地址"
|
||||
@input="(value) => handleInput('path', value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center mt-4">
|
||||
<div class="w-[86px] text-right">传递参数</div>
|
||||
<div class="ml-4 flex-1 min-w-[100px]">
|
||||
<el-input
|
||||
class="max-w-[320px]"
|
||||
:model-value="modelValue.query?.query"
|
||||
placeholder="请输入小程序跳转参数(选填)"
|
||||
@input="(value) => handleInput('query', value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-tips ml-[100px] max-w-[320px]">
|
||||
<div>示例:id=2&ustm=jiny&name=234</div>
|
||||
<div class="text-error">
|
||||
注意:不允许输入中文、特殊字符等。如果出现对不起,当前页面无法访问,大概率是跳转参数的问题!!
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center mt-4">
|
||||
<div class="w-[86px] text-right">小程序版本</div>
|
||||
<div class="ml-4 flex-1 min-w-[100px]">
|
||||
<el-radio-group
|
||||
:model-value="modelValue.query?.env_version"
|
||||
@change="(value) => handleInput('env_version', value as string)"
|
||||
>
|
||||
<el-radio value="release">正式版</el-radio>
|
||||
<el-radio value="trial">体验版</el-radio>
|
||||
<el-radio value="develop">开发版</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="form-tips ml-[100px] max-w-[320px]">
|
||||
<div class="mt-4">
|
||||
1.
|
||||
小程序APPID和小程序路径链接地址,小程序路径链接地址请填写小程序的页面路径,如:pages/index/index
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span>2. 如果是H5(浏览器)中需要跳转到小程序,则需要以下配置---></span>
|
||||
<a
|
||||
href="https://mp.weixin.qq.com/"
|
||||
class="text-primary"
|
||||
target="_blank"
|
||||
rel="nofollow"
|
||||
>
|
||||
小程序管理后台 -> 设置 -> 隐私与安全 -> 明文 scheme 拉起此小程序
|
||||
(点击跳转去配置)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import { type Link, LinkTypeEnum } from '.'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<Link>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: Link): void
|
||||
}>()
|
||||
|
||||
const handleInput = (key: 'appId' | 'path' | 'env_version' | 'query', value: string) => {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
name: '小程序跳转',
|
||||
query: {
|
||||
...props.modelValue.query,
|
||||
[key]: value
|
||||
},
|
||||
type: LinkTypeEnum.MINI_PROGRAM
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (!value.query?.env_version) {
|
||||
handleInput('env_version', 'release')
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="link-picker flex-1" @click="!disabled && popupRef?.open()">
|
||||
<el-input :model-value="getLink" placeholder="请选择链接" readonly :disabled="disabled">
|
||||
<template #suffix>
|
||||
<icon v-if="!modelValue?.path" name="el-icon-ArrowRight" />
|
||||
<icon
|
||||
v-else
|
||||
name="el-icon-Close"
|
||||
@click.stop="!disabled && emit('update:modelValue', {})"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
<popup ref="popupRef" width="1050px" title="链接选择" @confirm="handleConfirm">
|
||||
<link-content v-model="activeLink" />
|
||||
</popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
|
||||
import { type Link, LinkTypeEnum } from '.'
|
||||
import LinkContent from './index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: any): void
|
||||
}>()
|
||||
|
||||
const popupRef = shallowRef<InstanceType<typeof Popup>>()
|
||||
const activeLink = ref<Link>({ path: '', type: LinkTypeEnum.SHOP_PAGES })
|
||||
const handleConfirm = () => {
|
||||
emit('update:modelValue', activeLink.value)
|
||||
}
|
||||
|
||||
const getLink = computed(() => {
|
||||
switch (props.modelValue?.type) {
|
||||
case LinkTypeEnum.SHOP_PAGES:
|
||||
return props.modelValue.name
|
||||
case LinkTypeEnum.ARTICLE_LIST:
|
||||
return props.modelValue.name
|
||||
case LinkTypeEnum.CUSTOM_LINK:
|
||||
return props.modelValue.query?.url
|
||||
default:
|
||||
return props.modelValue?.name
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (value?.type) {
|
||||
activeLink.value = value as Link
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.link-picker {
|
||||
:deep(.el-input) {
|
||||
&.is-disabled {
|
||||
.el-input__inner {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.el-input__suffix {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.el-input__inner {
|
||||
cursor: pointer;
|
||||
}
|
||||
.el-input__suffix {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="shop-pages h-[530px]">
|
||||
<div class="link-list flex flex-wrap">
|
||||
<div
|
||||
class="link-item border border-br px-5 py-[5px] rounded-[3px] cursor-pointer mr-[10px] mb-[10px]"
|
||||
v-for="(item, index) in linkList"
|
||||
:class="{
|
||||
'border-primary text-primary':
|
||||
modelValue.path == item.path && modelValue.name == item.name
|
||||
}"
|
||||
:key="index"
|
||||
@click="handleSelect(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import { type Link, LinkTypeEnum } from '.'
|
||||
|
||||
defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<Link>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: Link): void
|
||||
}>()
|
||||
|
||||
const linkList = ref([
|
||||
{
|
||||
path: '/pages/index/index',
|
||||
name: '商城首页',
|
||||
type: LinkTypeEnum.SHOP_PAGES,
|
||||
canTab: true
|
||||
},
|
||||
{
|
||||
path: '/pages/news/news',
|
||||
name: '文章资讯',
|
||||
type: LinkTypeEnum.SHOP_PAGES,
|
||||
canTab: true
|
||||
},
|
||||
{
|
||||
path: '/pages/user/user',
|
||||
name: '个人中心',
|
||||
type: LinkTypeEnum.SHOP_PAGES,
|
||||
canTab: true
|
||||
},
|
||||
{
|
||||
path: '/pages/collection/collection',
|
||||
name: '我的收藏',
|
||||
type: LinkTypeEnum.SHOP_PAGES
|
||||
},
|
||||
{
|
||||
path: '/pages/customer_service/customer_service',
|
||||
name: '联系客服',
|
||||
type: LinkTypeEnum.SHOP_PAGES
|
||||
},
|
||||
{
|
||||
path: '/pages/user_set/user_set',
|
||||
name: '个人设置',
|
||||
type: LinkTypeEnum.SHOP_PAGES
|
||||
},
|
||||
{
|
||||
path: '/pages/as_us/as_us',
|
||||
name: '关于我们',
|
||||
type: LinkTypeEnum.SHOP_PAGES
|
||||
},
|
||||
{
|
||||
path: '/pages/user_data/user_data',
|
||||
name: '个人资料',
|
||||
type: LinkTypeEnum.SHOP_PAGES
|
||||
},
|
||||
{
|
||||
path: '/pages/agreement/agreement',
|
||||
name: '隐私政策',
|
||||
query: {
|
||||
type: 'privacy'
|
||||
},
|
||||
type: LinkTypeEnum.SHOP_PAGES
|
||||
},
|
||||
{
|
||||
path: '/pages/agreement/agreement',
|
||||
name: '服务协议',
|
||||
query: {
|
||||
type: 'service'
|
||||
},
|
||||
type: LinkTypeEnum.SHOP_PAGES
|
||||
},
|
||||
{
|
||||
path: '/pages/search/search',
|
||||
name: '搜索',
|
||||
type: LinkTypeEnum.SHOP_PAGES
|
||||
},
|
||||
{
|
||||
path: '/packages/pages/user_wallet/user_wallet',
|
||||
name: '我的钱包',
|
||||
type: LinkTypeEnum.SHOP_PAGES
|
||||
}
|
||||
])
|
||||
|
||||
const handleSelect = (value: Link) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="file-item relative"
|
||||
:style="{ height: height || fileSize, width: width || fileSize }"
|
||||
>
|
||||
<el-image class="image" v-if="type == 'image'" fit="contain" :src="uri"></el-image>
|
||||
<video class="video" v-else-if="type == 'video'" :src="uri"></video>
|
||||
<el-image
|
||||
class="image"
|
||||
v-else
|
||||
src="https://img95.699pic.com/element/40103/3946.png_860.png"
|
||||
></el-image>
|
||||
<div
|
||||
v-if="type == 'video'"
|
||||
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] rounded-full w-5 h-5 flex justify-center items-center bg-[rgba(0,0,0,0.3)]"
|
||||
>
|
||||
<icon name="el-icon-CaretRight" :size="18" color="#fff" />
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
// 图片地址
|
||||
uri: {
|
||||
type: String
|
||||
},
|
||||
// 图片尺寸
|
||||
fileSize: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
// 选择器尺寸-宽度(不传则是使用size
|
||||
width: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 选择器尺寸-高度(不传则是使用size
|
||||
height: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 文件类型
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image'
|
||||
}
|
||||
},
|
||||
emits: ['close']
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.file-item {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
@apply bg-br-extra-light border border-br-extra-light;
|
||||
.image,
|
||||
.video {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,225 @@
|
||||
import { type CheckboxValueType, ElMessage, ElTree } from 'element-plus'
|
||||
import { type Ref, shallowRef } from 'vue'
|
||||
|
||||
import {
|
||||
fileCateAdd,
|
||||
fileCateDelete,
|
||||
fileCateEdit,
|
||||
fileCateLists,
|
||||
fileDelete,
|
||||
fileList,
|
||||
fileMove,
|
||||
fileRename
|
||||
} from '@/api/file'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
// 左侧分组的钩子函数
|
||||
export function useCate(type: number) {
|
||||
const treeRef = shallowRef<InstanceType<typeof ElTree>>()
|
||||
// 分组列表
|
||||
const cateLists = ref<any[]>([])
|
||||
|
||||
// 选中的分组id
|
||||
const cateId = ref<number | string>('')
|
||||
|
||||
// 获取分组列表
|
||||
const getCateLists = async () => {
|
||||
const data = await fileCateLists({
|
||||
page_type: 0,
|
||||
type
|
||||
})
|
||||
const item: any[] = [
|
||||
{
|
||||
name: '全部',
|
||||
id: ''
|
||||
},
|
||||
{
|
||||
name: '未分组',
|
||||
id: 0
|
||||
}
|
||||
]
|
||||
cateLists.value = data.lists
|
||||
cateLists.value.unshift(...item)
|
||||
setTimeout(() => {
|
||||
treeRef.value?.setCurrentKey(cateId.value)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 添加分组
|
||||
const handleAddCate = async (value: string) => {
|
||||
await fileCateAdd({
|
||||
type,
|
||||
name: value,
|
||||
pid: 0
|
||||
})
|
||||
getCateLists()
|
||||
}
|
||||
|
||||
const handleAddChildCate = async (value: string, pid: number) => {
|
||||
await fileCateAdd({
|
||||
type,
|
||||
name: value,
|
||||
pid: pid
|
||||
})
|
||||
getCateLists()
|
||||
}
|
||||
|
||||
// 编辑分组
|
||||
const handleEditCate = async (value: string, id: number) => {
|
||||
await fileCateEdit({
|
||||
id,
|
||||
name: value
|
||||
})
|
||||
getCateLists()
|
||||
}
|
||||
|
||||
// 删除分组
|
||||
const handleDeleteCate = async (id: number, children?: number) => {
|
||||
if (children) {
|
||||
await feedback.confirm('删除文件夹将会永久删除文件夹及其所有内容。您确定要继续吗?')
|
||||
} else {
|
||||
await feedback.confirm('确定要删除?')
|
||||
}
|
||||
await fileCateDelete({ id })
|
||||
cateId.value = ''
|
||||
getCateLists()
|
||||
}
|
||||
|
||||
//选中分类
|
||||
const handleCatSelect = (item: any) => {
|
||||
cateId.value = item.id
|
||||
}
|
||||
|
||||
return {
|
||||
treeRef,
|
||||
cateId,
|
||||
cateLists,
|
||||
handleAddCate,
|
||||
handleAddChildCate,
|
||||
handleEditCate,
|
||||
handleDeleteCate,
|
||||
getCateLists,
|
||||
handleCatSelect
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件的钩子函数
|
||||
export function useFile(
|
||||
cateId: Ref<string | number>,
|
||||
type: Ref<number>,
|
||||
limit: Ref<number>,
|
||||
size: number
|
||||
) {
|
||||
const tableRef = shallowRef()
|
||||
const listShowType = ref('normal')
|
||||
const moveId = ref(0)
|
||||
const select = ref<any[]>([])
|
||||
const isCheckAll = ref(false)
|
||||
const isIndeterminate = ref(false)
|
||||
const fileParams = reactive({
|
||||
name: '',
|
||||
type: type,
|
||||
cid: cateId,
|
||||
source: ''
|
||||
})
|
||||
const { pager, getLists, resetPage } = usePaging({
|
||||
fetchFun: fileList,
|
||||
params: fileParams,
|
||||
firstLoading: true,
|
||||
size
|
||||
})
|
||||
|
||||
const getFileList = () => {
|
||||
getLists()
|
||||
}
|
||||
const refresh = () => {
|
||||
resetPage()
|
||||
}
|
||||
|
||||
const isSelect = (id: number) => {
|
||||
return !!select.value.find((item: any) => item.id == id)
|
||||
}
|
||||
|
||||
const batchFileDelete = async (id?: number[]) => {
|
||||
await feedback.confirm(
|
||||
'确认删除后,本地或云存储文件也将同步删除,如文件已被使用,请谨慎操作!'
|
||||
)
|
||||
const ids = id ? id : select.value.map((item: any) => item.id)
|
||||
await fileDelete({ ids })
|
||||
getFileList()
|
||||
clearSelect()
|
||||
}
|
||||
|
||||
const batchFileMove = async () => {
|
||||
const ids = select.value.map((item: any) => item.id)
|
||||
await fileMove({ ids, cid: moveId.value })
|
||||
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: any) => item.id != id)
|
||||
}
|
||||
|
||||
const selectAll = (value: CheckboxValueType) => {
|
||||
isIndeterminate.value = false
|
||||
tableRef.value?.toggleAllSelection()
|
||||
if (value) {
|
||||
select.value = [...pager.lists]
|
||||
return
|
||||
}
|
||||
clearSelect()
|
||||
}
|
||||
|
||||
const handleFileRename = async (name: string, id: number) => {
|
||||
await fileRename({
|
||||
id,
|
||||
name
|
||||
})
|
||||
getFileList()
|
||||
}
|
||||
return {
|
||||
listShowType,
|
||||
tableRef,
|
||||
moveId,
|
||||
pager,
|
||||
fileParams,
|
||||
select,
|
||||
isCheckAll,
|
||||
isIndeterminate,
|
||||
getFileList,
|
||||
refresh,
|
||||
batchFileDelete,
|
||||
batchFileMove,
|
||||
selectFile,
|
||||
isSelect,
|
||||
clearSelect,
|
||||
cancelSelete,
|
||||
selectAll,
|
||||
handleFileRename
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,664 @@
|
||||
<template>
|
||||
<div class="material" v-loading="pager.loading">
|
||||
<div class="material__left">
|
||||
<div class="flex-1 min-h-0">
|
||||
<el-scrollbar>
|
||||
<div class="material-left__content pt-4 p-b-4">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
node-key="id"
|
||||
:data="cateLists"
|
||||
empty-text=""
|
||||
:highlight-current="true"
|
||||
:expand-on-click-node="false"
|
||||
:current-node-key="cateId"
|
||||
@node-click="handleCatSelect"
|
||||
>
|
||||
<template v-slot="{ data }">
|
||||
<div class="flex flex-1 items-center min-w-0 pr-4">
|
||||
<img
|
||||
class="w-[20px] h-[16px] mr-3"
|
||||
src="@/assets/images/icon_folder.png"
|
||||
/>
|
||||
<span class="flex-1 truncate mr-2">
|
||||
<overflow-tooltip :content="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>
|
||||
<popover-input
|
||||
@confirm="handleEditCate($event, data.id)"
|
||||
size="default"
|
||||
:value="data.name"
|
||||
width="400px"
|
||||
:limit="20"
|
||||
show-limit
|
||||
teleported
|
||||
>
|
||||
<div>
|
||||
<el-dropdown-item>
|
||||
命名分组
|
||||
</el-dropdown-item>
|
||||
</div>
|
||||
</popover-input>
|
||||
<popover-input
|
||||
@confirm="handleAddChildCate($event, data.id)"
|
||||
size="default"
|
||||
width="400px"
|
||||
:limit="20"
|
||||
show-limit
|
||||
teleported
|
||||
>
|
||||
<div>
|
||||
<el-dropdown-item>
|
||||
添加分组
|
||||
</el-dropdown-item>
|
||||
</div>
|
||||
</popover-input>
|
||||
<div
|
||||
@click="
|
||||
handleDeleteCate(
|
||||
data.id,
|
||||
data?.children?.length
|
||||
)
|
||||
"
|
||||
>
|
||||
<el-dropdown-item>删除分组</el-dropdown-item>
|
||||
</div>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center p-2 border-t border-br">
|
||||
<popover-input
|
||||
@confirm="handleAddCate"
|
||||
size="default"
|
||||
width="400px"
|
||||
:limit="20"
|
||||
show-limit
|
||||
teleported
|
||||
>
|
||||
<el-button> 添加分组 </el-button>
|
||||
</popover-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="material__center flex flex-col">
|
||||
<div class="operate-btn flex">
|
||||
<div class="flex-1 flex">
|
||||
<upload
|
||||
v-if="type == 'image'"
|
||||
class="mr-3"
|
||||
:data="{ cid: cateId }"
|
||||
:type="type"
|
||||
:show-progress="true"
|
||||
@change="refresh"
|
||||
>
|
||||
<el-button type="primary">本地上传</el-button>
|
||||
</upload>
|
||||
<upload
|
||||
v-if="type == 'video'"
|
||||
class="mr-3"
|
||||
:data="{ cid: cateId }"
|
||||
:type="type"
|
||||
:show-progress="true"
|
||||
@allSuccess="refresh"
|
||||
>
|
||||
<el-button type="primary">本地上传</el-button>
|
||||
</upload>
|
||||
<upload
|
||||
v-if="type == 'file'"
|
||||
class="mr-3"
|
||||
:data="{ cid: cateId }"
|
||||
:type="type"
|
||||
:show-progress="true"
|
||||
@allSuccess="refresh"
|
||||
>
|
||||
<el-button type="primary">本地上传</el-button>
|
||||
</upload>
|
||||
<el-button
|
||||
v-if="mode == 'page'"
|
||||
:disabled="!select.length"
|
||||
@click.stop="batchFileDelete()"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
|
||||
<popup
|
||||
v-if="mode == 'page'"
|
||||
class="ml-3"
|
||||
@confirm="batchFileMove"
|
||||
:disabled="!select.length"
|
||||
title="移动文件"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button :disabled="!select.length">移动</el-button>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<span class="mr-5">移动文件至</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-select
|
||||
v-model="fileParams.source"
|
||||
placeholder="请选择文件来源"
|
||||
clearable
|
||||
style="margin-right: 20px"
|
||||
class="max-w-52 ml-3"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
class="w-60"
|
||||
placeholder="请输入名称"
|
||||
v-model="fileParams.name"
|
||||
@keyup.enter="refresh"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="refresh">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Search" />
|
||||
</template>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<div class="flex items-center ml-2">
|
||||
<el-tooltip content="列表视图" placement="top">
|
||||
<div
|
||||
class="list-icon"
|
||||
:class="{
|
||||
select: listShowType == 'table'
|
||||
}"
|
||||
@click="listShowType = 'table'"
|
||||
>
|
||||
<icon name="local-icon-list-2" :size="18" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="平铺视图" placement="top">
|
||||
<div
|
||||
class="list-icon"
|
||||
:class="{
|
||||
select: listShowType == 'normal'
|
||||
}"
|
||||
@click="listShowType = 'normal'"
|
||||
>
|
||||
<icon name="el-icon-Menu" :size="18" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3" v-if="mode == 'page'">
|
||||
<el-checkbox
|
||||
:disabled="!pager.lists.length"
|
||||
v-model="isCheckAll"
|
||||
@change="selectAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
>
|
||||
当页全选
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div class="material-center__content flex flex-col flex-1 mb-1 min-h-0">
|
||||
<el-scrollbar v-if="pager.lists.length" v-show="listShowType == 'normal'">
|
||||
<ul class="file-list flex flex-wrap mt-4">
|
||||
<li
|
||||
class="file-item-wrap"
|
||||
v-for="item in pager.lists"
|
||||
:key="item.id"
|
||||
:style="{ width: fileSize }"
|
||||
>
|
||||
<del-wrap @close="batchFileDelete([item.id])">
|
||||
<file-item
|
||||
:uri="item.url"
|
||||
:file-size="fileSize"
|
||||
:type="type"
|
||||
@click="selectFile(item)"
|
||||
>
|
||||
<div class="item-selected" v-if="isSelect(item.id)">
|
||||
<icon :size="24" name="el-icon-Check" color="#fff" />
|
||||
</div>
|
||||
</file-item>
|
||||
</del-wrap>
|
||||
|
||||
<overflow-tooltip class="mt-1" :content="item.name" />
|
||||
<div class="operation-btns flex items-center">
|
||||
<popover-input
|
||||
@confirm="handleFileRename($event, item.id)"
|
||||
size="default"
|
||||
:value="item.name"
|
||||
width="400px"
|
||||
:limit="50"
|
||||
show-limit
|
||||
teleported
|
||||
>
|
||||
<el-button type="primary" link> 重命名 </el-button>
|
||||
</popover-input>
|
||||
|
||||
<el-button
|
||||
v-if="item.type === 10 || item.type === 20"
|
||||
type="primary"
|
||||
link
|
||||
@click="handlePreview(item.url)"
|
||||
>
|
||||
查看
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="item.type === 10 || item.type === 20"
|
||||
type="primary"
|
||||
link
|
||||
@click="textCopy(item.url)"
|
||||
style="margin-left: 1px"
|
||||
>地址</el-button
|
||||
>
|
||||
<el-link
|
||||
v-else
|
||||
type="primary"
|
||||
:underline="false"
|
||||
style="margin-left: 25px"
|
||||
:href="item.url"
|
||||
>下载</el-link
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
class="mt-4"
|
||||
v-show="listShowType == 'table'"
|
||||
:data="pager.lists"
|
||||
width="100%"
|
||||
height="100%"
|
||||
size="large"
|
||||
@row-click="selectFile"
|
||||
>
|
||||
<el-table-column width="55">
|
||||
<template #default="{ row }">
|
||||
<el-checkbox :modelValue="isSelect(row.id)" @change="selectFile(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="图片" width="100">
|
||||
<template #default="{ row }">
|
||||
<file-item :uri="row.url" file-size="50px" :type="type"></file-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="名称" min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link @click.stop="handlePreview(row.url)" :underline="false">
|
||||
{{ row.name }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="上传时间" min-width="100" />
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="inline-block">
|
||||
<popover-input
|
||||
@confirm="handleFileRename($event, row.id)"
|
||||
size="default"
|
||||
:value="row.name"
|
||||
width="400px"
|
||||
:limit="50"
|
||||
show-limit
|
||||
teleported
|
||||
>
|
||||
<el-button type="primary" link> 重命名 </el-button>
|
||||
</popover-input>
|
||||
</div>
|
||||
<div class="inline-block">
|
||||
<el-button type="primary" link @click.stop="handlePreview(row.url)">
|
||||
查看
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="inline-block">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click.stop="batchFileDelete([row.id])"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div
|
||||
class="flex flex-1 justify-center items-center"
|
||||
v-if="!pager.loading && !pager.lists.length"
|
||||
>
|
||||
暂无数据~
|
||||
</div>
|
||||
</div>
|
||||
<div class="material-center__footer flex justify-between items-center mt-2">
|
||||
<div class="flex">
|
||||
<template v-if="mode == 'page'">
|
||||
<span class="mr-3">
|
||||
<el-checkbox
|
||||
:disabled="!pager.lists.length"
|
||||
v-model="isCheckAll"
|
||||
@change="selectAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
>
|
||||
当页全选
|
||||
</el-checkbox>
|
||||
</span>
|
||||
<el-button :disabled="!select.length" @click="batchFileDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
<popup
|
||||
class="ml-3 inline"
|
||||
@confirm="batchFileMove"
|
||||
:disabled="!select.length"
|
||||
title="移动文件"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button :disabled="!select.length">移动</el-button>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<span class="mr-5">移动文件至</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>
|
||||
</template>
|
||||
</div>
|
||||
<pagination
|
||||
v-model="pager"
|
||||
@change="getFileList"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="material__right" v-if="mode == 'picker'">
|
||||
<div class="flex justify-between p-2 flex-wrap">
|
||||
<div class="sm flex items-center">
|
||||
已选择 {{ select.length }}
|
||||
<span v-if="limit">/{{ limit }}</span>
|
||||
</div>
|
||||
<el-button type="primary" link @click="clearSelect">清空</el-button>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<el-scrollbar class="ls-scrollbar">
|
||||
<ul class="select-lists flex flex-col p-t-3">
|
||||
<li class="mb-4" v-for="item in select" :key="item.id">
|
||||
<div class="select-item">
|
||||
<del-wrap @close="cancelSelete(item.id)">
|
||||
<file-item
|
||||
:uri="item.url"
|
||||
file-size="100px"
|
||||
:type="type"
|
||||
></file-item>
|
||||
</del-wrap>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
<preview v-model="showPreview" :url="previewUrl" :type="type" />
|
||||
<input ref="textCopys" id="textCopys" value="" style="opacity: 0; position: absolute" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import FileItem from './file.vue'
|
||||
import { useCate, useFile } from './hook'
|
||||
import Preview from './preview.vue'
|
||||
|
||||
const props = defineProps({
|
||||
fileSize: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image'
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'picker'
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 15
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['change'])
|
||||
const { limit } = toRefs(props)
|
||||
const typeValue = computed<number>(() => {
|
||||
switch (props.type) {
|
||||
case 'image':
|
||||
return 10
|
||||
case 'video':
|
||||
return 20
|
||||
case 'file':
|
||||
return 30
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
const options = [
|
||||
{
|
||||
value: '0',
|
||||
label: '后台上传'
|
||||
},
|
||||
{
|
||||
value: '1',
|
||||
label: '前端上传'
|
||||
}
|
||||
]
|
||||
|
||||
const visible: Ref<boolean> = inject('visible', ref<boolean>(false))!
|
||||
const previewUrl = ref('')
|
||||
const showPreview = ref(false)
|
||||
const {
|
||||
treeRef,
|
||||
cateId,
|
||||
cateLists,
|
||||
handleAddCate,
|
||||
handleAddChildCate,
|
||||
handleEditCate,
|
||||
handleDeleteCate,
|
||||
getCateLists,
|
||||
handleCatSelect
|
||||
} = useCate(typeValue.value)
|
||||
|
||||
const {
|
||||
tableRef,
|
||||
listShowType,
|
||||
moveId,
|
||||
pager,
|
||||
fileParams,
|
||||
select,
|
||||
isCheckAll,
|
||||
isIndeterminate,
|
||||
getFileList,
|
||||
refresh,
|
||||
batchFileDelete,
|
||||
batchFileMove,
|
||||
selectFile,
|
||||
isSelect,
|
||||
clearSelect,
|
||||
cancelSelete,
|
||||
selectAll,
|
||||
handleFileRename
|
||||
} = useFile(cateId, typeValue, limit, props.pageSize)
|
||||
|
||||
const getData = async () => {
|
||||
await getCateLists()
|
||||
treeRef.value?.setCurrentKey(cateId.value)
|
||||
getFileList()
|
||||
}
|
||||
|
||||
const handlePreview = (url: string) => {
|
||||
previewUrl.value = url
|
||||
showPreview.value = true
|
||||
}
|
||||
watch(
|
||||
() => visible.value,
|
||||
async (val: boolean) => {
|
||||
if (val) {
|
||||
getData()
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
watch(cateId, () => {
|
||||
fileParams.name = ''
|
||||
refresh()
|
||||
})
|
||||
|
||||
watch(
|
||||
select,
|
||||
(val: any[]) => {
|
||||
emit('change', val)
|
||||
if (val.length == pager.lists.length && val.length !== 0) {
|
||||
isIndeterminate.value = false
|
||||
isCheckAll.value = true
|
||||
return
|
||||
}
|
||||
if (val.length > 0) {
|
||||
isIndeterminate.value = true
|
||||
} else {
|
||||
isCheckAll.value = false
|
||||
isIndeterminate.value = false
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
props.mode == 'page' && getData()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
clearSelect
|
||||
})
|
||||
|
||||
const textCopys = ref()
|
||||
const textCopy = (uri: string) => {
|
||||
const text = uri // 复制文本内容
|
||||
const input = textCopys.value
|
||||
input.value = text // 修改input的内容
|
||||
input.select() // 选中文本
|
||||
document.execCommand('copy') // 浏览器复制
|
||||
ElMessage({
|
||||
message: '地址复制成功',
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.material {
|
||||
@apply h-full min-h-0 flex flex-1;
|
||||
&__left {
|
||||
@apply border-r border-br flex flex-col w-[200px];
|
||||
:deep(.el-tree-node__content) {
|
||||
height: 36px;
|
||||
.el-tree-node__label {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__center {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 16px 16px 0;
|
||||
.list-icon {
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
&.select {
|
||||
@apply text-primary bg-primary-light-8;
|
||||
}
|
||||
}
|
||||
.file-list {
|
||||
.file-item-wrap {
|
||||
margin-right: 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 {
|
||||
@apply border-l border-br flex flex-col;
|
||||
width: 130px;
|
||||
.select-lists {
|
||||
padding: 10px;
|
||||
|
||||
.select-item {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,317 @@
|
||||
<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" @click.stop>
|
||||
<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 Popup from '@/components/popup/index.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
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 { disabled, limit, modelValue } = toRefs(props)
|
||||
const { getImageUrl } = useAppStore()
|
||||
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 = 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
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.material-select {
|
||||
.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>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div v-show="modelValue">
|
||||
<div v-if="type == 'image'">
|
||||
<el-image-viewer
|
||||
v-if="previewLists.length"
|
||||
:url-list="previewLists"
|
||||
hide-on-click-modal
|
||||
@close="handleClose"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="type == 'video'">
|
||||
<el-dialog v-model="visible" width="740px" title="视频预览" :before-close="handleClose">
|
||||
<div class="aspect-video overflow-hidden">
|
||||
<video class="size-full" controls>
|
||||
<source :src="url" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image'
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const playerRef = shallowRef()
|
||||
|
||||
const visible = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
const previewLists = ref<any[]>([])
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (value) {
|
||||
nextTick(() => {
|
||||
previewLists.value = [props.url]
|
||||
playerRef.value?.play()
|
||||
})
|
||||
} else {
|
||||
nextTick(() => {
|
||||
previewLists.value = []
|
||||
playerRef.value?.pause()
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tooltip v-bind="props" :disabled="disabled">
|
||||
<div
|
||||
ref="textRef"
|
||||
class="overflow-text truncate"
|
||||
:style="{ textOverflow: overfloType }"
|
||||
>
|
||||
{{ content }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { type Placement, useTooltipContentProps } from 'element-plus'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
...useTooltipContentProps,
|
||||
teleported: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placement: {
|
||||
type: String as PropType<Placement>,
|
||||
default: 'top'
|
||||
},
|
||||
overfloType: {
|
||||
type: String as PropType<'ellipsis' | 'unset' | 'clip'>,
|
||||
default: 'ellipsis'
|
||||
}
|
||||
})
|
||||
const textRef = shallowRef<HTMLElement>()
|
||||
const disabled = ref(false)
|
||||
|
||||
useEventListener(textRef, 'mouseenter', () => {
|
||||
if (textRef.value?.scrollWidth! > textRef.value?.offsetWidth!) {
|
||||
disabled.value = false
|
||||
} else {
|
||||
disabled.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-bind="props"
|
||||
:pager-count="5"
|
||||
v-model:currentPage="pager.page"
|
||||
v-model:pageSize="pager.size"
|
||||
:page-sizes="pageSizes"
|
||||
:layout="layout"
|
||||
:total="pager.count"
|
||||
:hide-on-single-page="false"
|
||||
@size-change="sizeChange"
|
||||
@current-change="pageChange"
|
||||
></el-pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
modelValue?: Record<string, any>
|
||||
pageSizes?: number[]
|
||||
layout?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => ({}),
|
||||
pageSizes: () => [15, 20, 30, 40],
|
||||
layout: 'total, sizes, prev, pager, next, jumper'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'change'): void
|
||||
(event: 'update:modelValue', value: any): void
|
||||
}>()
|
||||
|
||||
const pager = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
const sizeChange = () => {
|
||||
pager.value.page = 1
|
||||
emit('change')
|
||||
}
|
||||
const pageChange = () => {
|
||||
emit('change')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div @mouseenter="inPopover = true" @mouseleave="inPopover = false">
|
||||
<el-popover
|
||||
placement="top"
|
||||
v-model:visible="visible"
|
||||
:width="width"
|
||||
trigger="contextmenu"
|
||||
class="popover-input"
|
||||
:teleported="teleported"
|
||||
:persistent="false"
|
||||
popper-class="!p-0"
|
||||
>
|
||||
<div class="flex p-3" @click.stop="">
|
||||
<div class="popover-input__input mr-[10px] flex-1">
|
||||
<el-select
|
||||
class="flex-1"
|
||||
:size="size"
|
||||
v-if="type == 'select'"
|
||||
v-model="inputValue"
|
||||
:teleported="teleported"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-else
|
||||
v-model.trim="inputValue"
|
||||
:maxlength="limit"
|
||||
:show-word-limit="showLimit"
|
||||
:type="type"
|
||||
:size="size"
|
||||
clearable
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
</div>
|
||||
<div class="popover-input__btns flex-none">
|
||||
<el-button link @click="close">取消</el-button>
|
||||
<el-button type="primary" :size="size" @click="handleConfirm">确定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<template #reference>
|
||||
<div class="inline" @click.stop="handleOpen">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: '300px'
|
||||
},
|
||||
placeholder: String,
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<'default' | 'small' | 'large'>,
|
||||
default: 'default'
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
showLimit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
teleported: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['confirm'])
|
||||
const visible = ref(false)
|
||||
const inPopover = ref(false)
|
||||
const inputValue = ref()
|
||||
const handleConfirm = () => {
|
||||
close()
|
||||
emit('confirm', inputValue.value)
|
||||
}
|
||||
const handleOpen = () => {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(value) => {
|
||||
inputValue.value = value
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
useEventListener(document.documentElement, 'click', () => {
|
||||
if (inPopover.value) return
|
||||
close()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="dialog">
|
||||
<div class="dialog__trigger" @click="open">
|
||||
<!-- 触发弹窗 -->
|
||||
<slot name="trigger"></slot>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:class="customClass"
|
||||
:center="center"
|
||||
:append-to-body="true"
|
||||
:width="width"
|
||||
:close-on-click-modal="clickModalClose"
|
||||
@closed="close"
|
||||
>
|
||||
<!-- 弹窗内容 -->
|
||||
<template v-if="title" #header>{{ title }}</template>
|
||||
|
||||
<!-- 自定义内容 -->
|
||||
<slot>{{ content }}</slot>
|
||||
<!-- 底部弹窗页脚 -->
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button v-if="cancelButtonText" @click="handleEvent('cancel')">
|
||||
{{ cancelButtonText }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="confirmButtonText"
|
||||
type="primary"
|
||||
@click="handleEvent('confirm')"
|
||||
>
|
||||
{{ confirmButtonText }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
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: false
|
||||
},
|
||||
center: {
|
||||
// 是否居中布局
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['confirm', 'cancel', 'close', 'open'],
|
||||
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
|
||||
nextTick(() => {
|
||||
emit('close')
|
||||
})
|
||||
}
|
||||
const open = () => {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
emit('open')
|
||||
visible.value = true
|
||||
}
|
||||
provide('visible', visible)
|
||||
return {
|
||||
visible,
|
||||
handleEvent,
|
||||
close,
|
||||
open
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dialog-body {
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="upload">
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
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"
|
||||
:accept="getAccept"
|
||||
>
|
||||
<slot />
|
||||
</el-upload>
|
||||
<el-dialog
|
||||
v-if="showProgress && fileList.length"
|
||||
v-model="visible"
|
||||
title="上传进度"
|
||||
:close-on-click-modal="false"
|
||||
width="500px"
|
||||
:modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="file-list p-4">
|
||||
<template v-for="(item, index) in fileList" :key="index">
|
||||
<div class="mb-5">
|
||||
<div>{{ item.name }}</div>
|
||||
<div class="flex-1">
|
||||
<el-progress :percentage="parseInt(item.percentage)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { ElUpload } from 'element-plus'
|
||||
import { computed, defineComponent, ref, shallowRef } from 'vue'
|
||||
|
||||
import config from '@/config'
|
||||
import { RequestCodeEnum } from '@/enums/requestEnums'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
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', 'success', 'allSuccess'],
|
||||
setup(props, { emit }) {
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
const uploadRefs = shallowRef<InstanceType<typeof ElUpload>>()
|
||||
const action = ref(`${config.baseUrl}${config.urlPrefix}/upload/${props.type}`)
|
||||
const headers = computed(() => ({
|
||||
token: userStore.token,
|
||||
version: appStore.config.version
|
||||
}))
|
||||
const visible = ref(false)
|
||||
const fileList = ref<any[]>([])
|
||||
|
||||
const handleProgress = () => {
|
||||
visible.value = true
|
||||
}
|
||||
let uploadLen = 0
|
||||
const handleSuccess = (response: any, file: any) => {
|
||||
uploadLen++
|
||||
if (uploadLen == fileList.value.length) {
|
||||
uploadLen = 0
|
||||
fileList.value = []
|
||||
emit('allSuccess')
|
||||
}
|
||||
emit('change', file)
|
||||
if (response.code == RequestCodeEnum.SUCCESS) {
|
||||
emit('success', response)
|
||||
}
|
||||
if (response.code == RequestCodeEnum.FAIL && response.msg) {
|
||||
feedback.msgError(response.msg)
|
||||
}
|
||||
}
|
||||
const handleError = (event: any, file: any) => {
|
||||
uploadLen++
|
||||
if (uploadLen == fileList.value.length) {
|
||||
uploadLen = 0
|
||||
fileList.value = []
|
||||
emit('allSuccess')
|
||||
}
|
||||
feedback.msgError(`${file.name}文件上传失败`)
|
||||
uploadRefs.value?.abort(file)
|
||||
visible.value = false
|
||||
emit('change', file)
|
||||
emit('error', file)
|
||||
}
|
||||
const handleExceed = () => {
|
||||
feedback.msgError(`超出上传上限${props.limit},请重新上传`)
|
||||
}
|
||||
const handleClose = () => {
|
||||
fileList.value = []
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const getAccept = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'image':
|
||||
return '.jpg,.png,.gif,.jpeg,.ico'
|
||||
case 'video':
|
||||
return '.wmv,.avi,.mpg,.mpeg,.3gp,.mov,.mp4,.flv,.rmvb,.mkv'
|
||||
default:
|
||||
return '*'
|
||||
}
|
||||
})
|
||||
return {
|
||||
uploadRefs,
|
||||
action,
|
||||
headers,
|
||||
visible,
|
||||
fileList,
|
||||
getAccept,
|
||||
handleProgress,
|
||||
handleSuccess,
|
||||
handleError,
|
||||
handleExceed,
|
||||
handleClose
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
Reference in New Issue
Block a user