7/17 添加公区收益
This commit is contained in:
@@ -1,17 +1,37 @@
|
||||
<template>
|
||||
<!-- 原有图片上传 -->
|
||||
<div>
|
||||
<el-upload
|
||||
v-if="type"
|
||||
action=""
|
||||
:action="uploadUrl"
|
||||
:headers="headerToken()"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:http-request="handleUploadRequest"
|
||||
class="editor-img-uploader"
|
||||
name="file"
|
||||
name="files"
|
||||
:show-file-list="false"
|
||||
>
|
||||
<i ref="uploadRef"></i>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<!-- 新增:文件附件上传(Word/PDF/Excel) -->
|
||||
<div>
|
||||
<el-upload
|
||||
v-if="type"
|
||||
:action="uploadUrl"
|
||||
:headers="headerToken()"
|
||||
:before-upload="handleBeforeFileUpload"
|
||||
:http-request="handleFileUploadRequest"
|
||||
class="editor-file-uploader"
|
||||
name="files"
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.zip"
|
||||
:show-file-list="false"
|
||||
>
|
||||
<i ref="fileUploadRef"></i>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<div class="editor">
|
||||
<quill-editor
|
||||
ref="quillEditorRef"
|
||||
@@ -19,7 +39,7 @@
|
||||
content-type="html"
|
||||
:options="options"
|
||||
:style="styles"
|
||||
@text-change="(e: any) => $emit('update:modelValue', content)"
|
||||
@text-change="() => $emit('update:modelValue', content)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -27,9 +47,11 @@
|
||||
<script setup lang="ts">
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||
|
||||
import { QuillEditor, Quill } from '@vueup/vue-quill';
|
||||
import { Quill, QuillEditor } from '@vueup/vue-quill';
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
import type { UploadRequestHandler, UploadRequestOptions } from 'element-plus';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import axios from 'axios';
|
||||
|
||||
defineEmits(['update:modelValue']);
|
||||
|
||||
@@ -45,14 +67,20 @@ const props = defineProps({
|
||||
/* 上传文件大小限制(MB) */
|
||||
fileSize: propTypes.number.def(5),
|
||||
/* 类型(base64格式、url格式) */
|
||||
type: propTypes.string.def('base64')
|
||||
type: propTypes.string.def('url')
|
||||
});
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const quillEditorRef = ref();
|
||||
const useStore = useUserStore();
|
||||
function headerToken() {
|
||||
return {
|
||||
Authorization: 'Bearer ' + useStore.token
|
||||
};
|
||||
}
|
||||
const uploadRef = ref<HTMLDivElement>();
|
||||
|
||||
const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/handleLog/uploadAttachments';
|
||||
const options = ref<any>({
|
||||
theme: 'snow',
|
||||
bounds: document.body,
|
||||
@@ -70,7 +98,8 @@ const options = ref<any>({
|
||||
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
|
||||
[{ align: [] }], // 对齐方式
|
||||
['clean'], // 清除文本格式
|
||||
['link', 'image', 'video'] // 链接、图片、视频
|
||||
// ['image', 'video', 'link'] // 图片、视频、链接、文件
|
||||
['image'] // 图片
|
||||
],
|
||||
handlers: {
|
||||
image: (value: boolean) => {
|
||||
@@ -83,8 +112,9 @@ const options = ref<any>({
|
||||
},
|
||||
link: function (value) {
|
||||
if (value) {
|
||||
const href = prompt('输入文件链接地址');
|
||||
Quill.format('link', href);
|
||||
// const href = prompt('输入网络文件链接地址');
|
||||
// Quill.format('link', href);
|
||||
fileUploadRef.value?.click();
|
||||
} else {
|
||||
Quill.format('link', false);
|
||||
}
|
||||
@@ -140,47 +170,218 @@ const handleBeforeUpload = (file: any) => {
|
||||
};
|
||||
|
||||
// base64 模式插入图片
|
||||
const handleUploadRequest: UploadRequestHandler = (options: UploadRequestOptions) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const file = options.file as File;
|
||||
const quill = toRaw(quillEditorRef.value)?.getQuill();
|
||||
if (!quill) {
|
||||
proxy?.$modal.msgError('编辑器未就绪');
|
||||
proxy?.$modal.closeLoading();
|
||||
reject(new Error('editor not ready'));
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result as string;
|
||||
const range = quill.selection?.savedRange;
|
||||
const length = range ? range.index : quill.getLength();
|
||||
quill.insertEmbed(length, 'image', base64);
|
||||
quill.setSelection(length + 1);
|
||||
proxy?.$modal.closeLoading();
|
||||
options.onSuccess?.({ url: base64 });
|
||||
resolve();
|
||||
};
|
||||
reader.onerror = () => {
|
||||
proxy?.$modal.msgError('图片插入失败');
|
||||
proxy?.$modal.closeLoading();
|
||||
const err = Object.assign(new Error('read image failed'), {
|
||||
status: 0,
|
||||
method: 'POST',
|
||||
url: options.action || ''
|
||||
});
|
||||
options.onError?.(err as any);
|
||||
reject(err);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
const handleUploadRequest: UploadRequestHandler = async (options: UploadRequestOptions) => {
|
||||
const file = options.file as File;
|
||||
const quill = toRaw(quillEditorRef.value)?.getQuill();
|
||||
if (!quill) {
|
||||
proxy?.$modal.msgError('编辑器未就绪');
|
||||
proxy?.$modal.closeLoading();
|
||||
return Promise.reject(new Error('editor not ready'));
|
||||
}
|
||||
const range = quill.selection?.savedRange;
|
||||
const insertIndex = range ? range.index : quill.getLength();
|
||||
|
||||
<style>
|
||||
.editor-img-uploader {
|
||||
display: none;
|
||||
if (props.type === 'base64') {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result as string;
|
||||
const range = quill.selection?.savedRange;
|
||||
const length = range ? range.index : quill.getLength();
|
||||
quill.insertEmbed(length, 'image', base64);
|
||||
quill.setSelection(length + 1);
|
||||
proxy?.$modal.closeLoading();
|
||||
options.onSuccess?.({ url: base64 });
|
||||
resolve();
|
||||
};
|
||||
reader.onerror = () => {
|
||||
proxy?.$modal.msgError('图片插入失败');
|
||||
proxy?.$modal.closeLoading();
|
||||
const err = Object.assign(new Error('read image failed'), {
|
||||
status: 0,
|
||||
method: 'POST',
|
||||
url: options.action || ''
|
||||
});
|
||||
options.onError?.(err as any);
|
||||
reject(err);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('files', file); // 和ImageExtend配置name保持一致 img
|
||||
const res = await axios.post(uploadUrl, formData, {
|
||||
headers: {
|
||||
...headerToken(),
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
const successList = res.data.data.success;
|
||||
if (successList.length > 0) {
|
||||
// 多张图片依次向后插入,不覆盖光标
|
||||
let index = insertIndex;
|
||||
for (let i = 0; i < successList.length; i++) {
|
||||
// 根据后端返回结构取图片地址
|
||||
const imgUrl = successList[i].url;
|
||||
// 插入真实图片地址到富文本
|
||||
quill.insertEmbed(index, 'image', imgUrl);
|
||||
index += 1;
|
||||
proxy?.$modal.closeLoading();
|
||||
options.onSuccess?.({ url: imgUrl });
|
||||
}
|
||||
} else {
|
||||
proxy?.$modal.msgError('图片上传服务器失败');
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
} catch (err) {
|
||||
proxy?.$modal.msgError('图片上传服务器失败');
|
||||
options.onError?.(err as any);
|
||||
return Promise.reject(err);
|
||||
} finally {
|
||||
proxy?.$modal.closeLoading();
|
||||
}
|
||||
};
|
||||
// 解决【粘贴图片自动base64】核心修复
|
||||
// 监听编辑器粘贴事件,拦截粘贴图片并调用上传接口
|
||||
let pasteHandler: ((e: ClipboardEvent) => Promise<void>) | null = null;
|
||||
watch(
|
||||
quillEditorRef,
|
||||
(val) => {
|
||||
if (!val || props.type !== 'url') return;
|
||||
const quill = toRaw(val).getQuill();
|
||||
pasteHandler = async (e: ClipboardEvent) => {
|
||||
const items = e.clipboardData?.items;
|
||||
if (!items) return;
|
||||
const fileItems = Array.from(items).filter((it) => it.kind === 'file');
|
||||
if (!fileItems.length) return;
|
||||
e.preventDefault();
|
||||
|
||||
proxy?.$modal.loading('正在上传粘贴图片...');
|
||||
for (const item of fileItems) {
|
||||
const file = item.getAsFile();
|
||||
if (!file) continue;
|
||||
const pass = handleBeforeUpload(file);
|
||||
if (!pass) {
|
||||
proxy?.$modal.closeLoading();
|
||||
return;
|
||||
}
|
||||
// 构造合法参数,去除不安全强转
|
||||
await handleUploadRequest({
|
||||
file,
|
||||
action: uploadUrl,
|
||||
onSuccess: () => {},
|
||||
onError: () => {},
|
||||
data: [],
|
||||
name: 'files'
|
||||
} as unknown as UploadRequestOptions);
|
||||
}
|
||||
};
|
||||
quill.container.addEventListener('paste', pasteHandler);
|
||||
},
|
||||
{ flush: 'post' }
|
||||
);
|
||||
|
||||
// 新增文件上传ref
|
||||
const fileUploadRef = ref<HTMLElement>();
|
||||
|
||||
// 文件上传前校验(限制文档格式)
|
||||
const handleBeforeFileUpload = (file: File) => {
|
||||
const allowTypes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/zip'
|
||||
];
|
||||
const ext = file.name.split('.').pop();
|
||||
const allowExt = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'zip'];
|
||||
if (!allowTypes.includes(file.type) && !allowExt.includes(ext)) {
|
||||
proxy?.$modal.msgError('仅支持PDF、Word、Excel、Zip文件');
|
||||
return false;
|
||||
}
|
||||
// 文件大小限制
|
||||
const maxByte = props.fileSize * 1024 * 1024;
|
||||
if (file.size > maxByte) {
|
||||
proxy?.$modal.msgError(`文件不能超过${props.fileSize}MB`);
|
||||
return false;
|
||||
}
|
||||
proxy?.$modal.loading('附件上传中...');
|
||||
return true;
|
||||
};
|
||||
|
||||
// 文件自定义上传逻辑,上传成功插入a链接
|
||||
const handleFileUploadRequest: UploadRequestHandler = async (options) => {
|
||||
const file = options.file as File;
|
||||
const quill = toRaw(quillEditorRef.value)?.getQuill();
|
||||
if (!quill) {
|
||||
proxy?.$modal.msgError('编辑器未就绪');
|
||||
proxy?.$modal.closeLoading();
|
||||
return Promise.reject();
|
||||
}
|
||||
const insertIndex = quill.selection?.savedRange?.index ?? quill.getLength();
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('files', file);
|
||||
const res = await axios.post(uploadUrl, formData, {
|
||||
headers: {
|
||||
...headerToken(),
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
const successList = res.data.data?.success ?? [];
|
||||
if (!successList.length) {
|
||||
throw new Error('上传无返回地址');
|
||||
}
|
||||
|
||||
// 循环插入下载链接 <a href="url" target="_blank">文件名</a>
|
||||
let curIndex = insertIndex;
|
||||
successList.forEach((item) => {
|
||||
// 拼接带下载属性的a标签HTML
|
||||
const linkHtml = `<a href="${item.url}" target="_blank" download="${file.name}" style="color:#409EFF;text-decoration:underline;margin:0 4px;">${file.name}</a>`;
|
||||
// 在光标位置插入HTML链接
|
||||
quill.clipboard.dangerouslyPasteHTML(curIndex, linkHtml);
|
||||
curIndex += linkHtml.length;
|
||||
});
|
||||
quill.setSelection(curIndex);
|
||||
options.onSuccess?.({ url: successList[0].url });
|
||||
} catch (err) {
|
||||
proxy?.$modal.msgError('附件上传失败');
|
||||
options.onError?.(err as any);
|
||||
return Promise.reject(err);
|
||||
} finally {
|
||||
proxy?.$modal.closeLoading();
|
||||
}
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
const quill = toRaw(quillEditorRef.value)?.getQuill();
|
||||
if (quill) {
|
||||
if (pasteHandler) {
|
||||
quill.container.removeEventListener('paste', pasteHandler);
|
||||
pasteHandler = null;
|
||||
}
|
||||
quill.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep(.ql-snow .ql-toolbar button.ql-file) {
|
||||
width: 40px;
|
||||
font-size: 12px;
|
||||
}
|
||||
:deep(.ql-snow .ql-toolbar button.ql-file::before) {
|
||||
all: unset !important;
|
||||
content: '附件';
|
||||
display: block;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.editor,
|
||||
.ql-toolbar {
|
||||
white-space: pre-wrap !important;
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { listResident } from '@/api/system/resident';
|
||||
import { ResidentVO } from '@/api/system/resident/type';
|
||||
import { listResident } from '@/api/system/residentReviewProcess/resident';
|
||||
import { ResidentVO } from '@/api/system/residentReviewProcess/resident/type';
|
||||
import { ref, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
|
||||
import { useHouseStore } from '@/store/modules/house';
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ResidentVO } from '@/api/system/resident/type';
|
||||
import { ResidentVO } from '@/api/system/residentReviewProcess/resident/type';
|
||||
import { getUserByRoleKey } from '@/api/system/user';
|
||||
import { ref, watch, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user