feat: 组件

This commit is contained in:
2023-07-18 22:06:20 +08:00
parent bb673dfd8a
commit 30a7f03650
14 changed files with 613 additions and 0 deletions

View File

@ -0,0 +1,42 @@
<!--
* @Author: zhaojinfeng 121016171@qq.com
* @Date: 2023-07-18 12:22:11
* @LastEditors: zhaojinfeng 121016171@qq.com
* @LastEditTime: 2023-07-18 12:46:10
* @FilePath: \vue3\packages\preview-office-view\index.vue
* @Description:
*
-->
<template>
<div class="preview-office-view overflow-hidden h-full">
<template v-if="src">
<VueOfficeDocx v-if="extension === 'docx'" class="h-full" :src="src" />
<VueOfficeExcel v-else-if="extension === 'xlsx'" class="h-full" :src="src" />
<VueOfficePdf v-else-if="extension === 'pdf'" class="h-full" :src="src" />
</template>
<el-empty v-else>
<template #description>
暂不支持.{{ extension }}格式的文件
</template>
</el-empty>
</div>
</template>
<script lang="ts" setup name="ThPreviewOfficeView">
import VueOfficePdf from '@vue-office/pdf'
import VueOfficeDocx from '@vue-office/docx'
import VueOfficeExcel from '@vue-office/excel'
import '@vue-office/excel/lib/index.css'
import '@vue-office/docx/lib/index.css'
const route = useRoute()
const { url, extension } = route.query
const src = computed(() => {
if (typeof url === 'string')
return url
if (Array.isArray(url))
return url[0]
return ''
})
</script>

View File

@ -0,0 +1,37 @@
<!--
* @Author: zhaojinfeng 121016171@qq.com
* @Date: 2023-07-18 12:21:03
* @LastEditors: zhaojinfeng 121016171@qq.com
* @LastEditTime: 2023-07-18 13:02:44
* @FilePath: \vue3\packages\preview-office\index.vue
* @Description:
*
-->
<template>
<iframe v-if="src" class="h-full w-full" title="Office预览" :src="src" />
<el-empty v-else />
</template>
<script lang="ts" setup name="ThPreviewOffice">
const { file } = defineProps<{
/** 待预览的文件对象 */
file?: FileVO
}>()
const router = useRouter()
const ALLOW_EXTENSION: (string | undefined)[] = ['docx', 'xlsx', 'pdf']
const src = computed(() => {
if (!file || !ALLOW_EXTENSION.includes(file.extension))
return ''
return router.resolve({
name: 'preview-office',
query: {
url: file.url,
extension: file.extension,
},
}).href
})
</script>

View File

@ -0,0 +1,8 @@
<template>
<div>
SelectTableModal
</div>
</template>
<script lang="ts" setup name="ThSelectTableModal">
</script>

View File

@ -0,0 +1,239 @@
<template>
<div class="upload-avatar">
<div class="el-upload el-upload--picture-card" @click="showDialog = true">
<image-avatar
v-if="avatarUrl"
:file-id="avatarUrl"
:size="146"
/>
<el-icon v-else class="avatar-uploader-icon">
<plus />
</el-icon>
</div>
<slot name="text">
<div class="el-upload__tip w-400px">
<slot name="tip">
单张图片不大于{{ fileSize }}MB且格式为jpegjpgpng或bmp只可上传1张
</slot>
</div>
</slot>
</div>
<el-dialog
v-model="showDialog"
title="上传头像"
width="800px"
append-to-body
@opened="modalOpened"
@closed="closeDialog"
>
<el-row>
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
<VueCropper
v-if="visible"
ref="cropperRef"
:img="cropperURL"
:info="true"
:auto-crop="options.autoCrop"
:auto-crop-width="autoCropWidth"
:auto-crop-height="autoCropHeight"
:fixed-box="options.fixedBox"
:output-type="options.outputType"
@real-time="realTime"
/>
</el-col>
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
<div
class="avatar-upload-preview"
:style="imageStyle"
>
<img
v-show="options.previews.url"
:src="options.previews.url"
:style="options.previews.img"
alt="实时预览图片"
>
</div>
</el-col>
</el-row>
<br>
<el-row>
<el-col :lg="2" :md="2">
<el-button @click="selectImage">
选择
<el-icon class="el-icon--right">
<Upload />
</el-icon>
</el-button>
</el-col>
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
<el-button icon="Plus" @click="changeScale(1)" />
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button icon="Minus" @click="changeScale(-1)" />
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button icon="RefreshLeft" @click="rotateLeft()" />
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button icon="RefreshRight" @click="rotateRight()" />
</el-col>
<el-col :lg="{ span: 2, offset: 6 }" :md="2">
<el-button type="primary" :loading="loading" @click="confirmUpload">
</el-button>
</el-col>
</el-row>
</el-dialog>
</template>
<script lang="ts" setup name="ThUploadAvatar">
import { ElMessage } from 'element-plus'
import type { CSSProperties } from 'vue'
import { VueCropper } from 'vue-cropper'
const props = withDefaults(defineProps<{
accpet?: string
modelValue?: string
/**
* 图片大小单位MB
*
* @default 2
*/
fileSize?: number
/** 默认生成截图框宽度 */
autoCropHeight?: number
/** 默认生成截图框高度 */
autoCropWidth?: number
uploadFunction?: (...args: any) => Promise<FileVO>
}>(), {
accpet: '.jpeg,.jpg,.png,.bmp',
fileSize: 2,
autoCropHeight: 200,
autoCropWidth: 200,
uploadFunction: () => Promise.resolve({}),
})
const emit = defineEmits<{
(event: 'update:modelValue', modelValue?: string): void
(event: 'onSuccess', file: FileVO): void
(event: 'onError', error: Error): void
}>()
const avatarUrl = useVModel(props, 'modelValue', emit)
const imageStyle = computed(() => ({
height: `${props.autoCropHeight}px`,
width: `${props.autoCropWidth}px`,
}))
let loading = $ref(false)
interface PreviewData {
div: CSSProperties
h: number
html: string
img: CSSProperties
url: string
w: number
}
interface Option {
/** 裁剪图片的地址 */
img: any
/** 是否默认生成截图框 */
autoCrop: boolean
/** 固定截图框大小 不允许改变 */
fixedBox: boolean
/** 默认生成截图为PNG格式 */
outputType: string
/** 预览数据 */
previews: Partial<PreviewData>
visible: boolean
}
// 图片裁剪数据
const options = reactive<Option>({
img: '',
autoCrop: true,
fixedBox: true,
outputType: 'png',
previews: {},
visible: false,
})
const visible = ref(false)
const cropperRef = $ref<InstanceType<typeof VueCropper>>()
const { open, reset, onChange } = useFileDialog()
let showDialog = $ref(false)
/** 向左旋转 */
function rotateLeft() {
cropperRef?.rotateLeft()
}
/** 向右旋转 */
function rotateRight() {
cropperRef?.rotateRight()
}
/** 打开弹出层结束时的回调 */
function modalOpened() {
visible.value = true
}
/** 图片缩放 */
function changeScale(num?: number) {
cropperRef?.changeScale(num || 1)
}
/** 实时预览 */
function realTime(data: PreviewData) {
options.previews = data
}
const file = shallowRef()
const cropperURL = useObjectUrl(file)
onChange((param) => {
file.value = param?.item(0)
})
function selectImage() {
open({
accept: props.accpet,
multiple: false,
})
}
async function uploadImg(blob: Blob) {
if (blob.size > props.fileSize * 1024 * 1024) {
ElMessage.error('您的图片经过剪裁依然很大,请重新选择图片')
return
}
loading = true
const formData = new FormData()
formData.append('file', blob)
try {
const res = await props.uploadFunction(formData)
avatarUrl.value = res.url
showDialog = false
emit('onSuccess', res)
}
catch (err: any) {
emit('onError', err)
loading = false
}
}
function confirmUpload() {
cropperRef?.getCropBlob(uploadImg)
}
/** 关闭窗口 */
function closeDialog() {
options.visible = false
reset()
loading = false
}
</script>
<style>
@import url('vue-cropper/dist/index.css');
</style>

View File

@ -0,0 +1,122 @@
<template>
<el-table border :data="fileList">
<el-table-column align="center" :label="label1">
<template #default="{ row }">
<template v-if="row.createdBy">
{{ row.name }}
</template>
<el-input
v-else v-model.trim="allName[`${row.id}`]" clearable :loading="allLoading[`${row.id}`]"
placeholder="不填则为默认文件名称"
/>
</template>
</el-table-column>
<el-table-column align="center" :label="label2">
<template #default="{ row, $index }">
<upload-file-link v-if="row.createdBy" :file-id="row.id" :file-name="row.name">
{{ row.name }}
</upload-file-link>
<upload-file-single
v-else :accept="accept" :auto-upload="false" :index-key="`${row.id}`" :max-size="20"
:file-name="allName[`${row.id}`]" :loading="allLoading[`${row.id}`]"
@on-change="$event => onChange($event, row.id)" @on-success="$event => onSuccess($event, row, $index)"
@on-error="onError(row.id)"
/>
</template>
</el-table-column>
<el-table-column align="center" :label="label3" width="200px">
<template #default="{ row, $index }">
<el-popconfirm v-if="row.createdBy" :title="popconfirm" :disabled="disabled" @confirm="remove($index)">
<template #reference>
<el-button type="danger" :disabled="disabled" link>
删除
</el-button>
</template>
</el-popconfirm>
<template v-else>
<el-button type="primary" :loading="allLoading[`${row.id}`]" link @click="save(row.id)">
保存
</el-button>
<el-button type="danger" :loading="allLoading[`${row.id}`]" link @click="remove($index)">
取消
</el-button>
</template>
</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup name="ThUploadTable">
import type { UploadFile } from 'element-plus'
const props = withDefaults(defineProps<{
/** 上传格式 */
accept?: string
/** 列1的文字 */
label1?: string
/** 列2的文字 */
label2?: string
/** 列3的文字 */
label3?: string
disabled?: boolean
/** 文件列表支持v-model */
fileList?: FileVO[]
/** 删除按钮提示文字 */
popconfirm?: string
}>(), {
accept: '.jpeg,.jpg,.png,.bmp',
label1: '文件名称',
label2: '图片上传',
label3: '操作',
fileList: () => [],
popconfirm: '是否删除本行?',
})
const emit = defineEmits<{
(event: 'update:fileList', fileList?: FileVO[]): void
}>()
const allName = reactive<Record<string, string | undefined>>({})
const allLoading = reactive<Record<string, boolean | undefined>>({})
function onSuccess(file: FileVO, row: FileVO, index: number) {
const indexKey = `${row.id}`
const newList = [...props.fileList.slice(0, index), file, ...props.fileList.slice(index + 1)]
emit('update:fileList', newList)
allLoading[indexKey] = false
}
function onError(id: number) {
const indexKey = `${id}`
allLoading[indexKey] = false
}
function onChange(uploadFile: UploadFile, id: number) {
const indexKey = `${id}`
if (uploadFile.status !== 'ready') {
allLoading[indexKey] = false
return
}
if (allName[indexKey])
return
const arr = uploadFile.name.split('.')
arr.pop()
/** 去除拓展名的文件名 */
const newFilename = arr.join('.')
// 特殊情况时例如:.npmrc 使用原始文件名
allName[indexKey] = newFilename || uploadFile.name
}
function save(id: number) {
const indexKey = `${id}`
allLoading[indexKey] = true
}
function remove(index: number) {
const newList = [...props.fileList.slice(0, index), ...props.fileList.slice(index + 1)]
emit('update:fileList', newList)
}
</script>