7/17 添加公区收益

This commit is contained in:
Zy
2026-07-21 17:32:22 +08:00
parent 37970c547b
commit 0c4c9676b5
41 changed files with 1583 additions and 132 deletions

View File

@@ -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;

View File

@@ -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';

View File

@@ -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';