This commit is contained in:
oneao committed 2026-03-23 21:12:09 +08:00
1 parent 3e172a1d53
commit 401a6719f6
12 files changed
+487 -277

No files matched your search

@@ -56,11 +56,11 @@ public class FileController {
@PostMapping("/delete") @PostMapping("/delete")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Result<Void> deleteFile(@RequestBody Map<String, Object> params) { public Result<Void> deleteFile(@RequestBody Map<String, Object> params) {
ValidationUtils.checkRequiredFields(params, "id", "tableName", "bucketName"); ValidationUtils.checkRequiredFields(params, "subid", "tableName", "bucketName");
String tableName = params.get("tableName").toString(); String tableName = params.get("tableName").toString();
String bucketName = params.get("bucketName").toString(); String bucketName = params.get("bucketName").toString();
String id = params.get("id").toString(); String id = params.get("subid").toString();
// 查询数据 // 查询数据
List<Map<String, Object>> maps = dbUtils.loadData("SELECT * FROM " + tableName + " WHERE subid = ?", id); List<Map<String, Object>> maps = dbUtils.loadData("SELECT * FROM " + tableName + " WHERE subid = ?", id);
-1
View File
@@ -12,7 +12,6 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
Directory: typeof import('./src/components/wang-editor/modules/directory.vue')['default'] Directory: typeof import('./src/components/wang-editor/modules/directory.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
@@ -349,7 +349,11 @@ async function uploadLocalFiles(files) {
const tempUrl = URL.createObjectURL(blob); const tempUrl = URL.createObjectURL(blob);
if (file.size > 512000) { if (file.size > 512000) {
const { url, size, blob: compressedBlob } = await compressImage(tempUrl); const {
url,
size,
blob: compressedBlob,
} = await compressImage(tempUrl);
fileInfo.mx_mapfilename = url; fileInfo.mx_mapfilename = url;
fileInfo.mx_filesize = size; fileInfo.mx_filesize = size;
@@ -494,7 +498,9 @@ async function handleDownloadFile(file) {
function handleShowPreview(file) { function handleShowPreview(file) {
console.log(file); console.log(file);
if (isImage(file.mx_fileext)) { if (isImage(file.mx_fileext)) {
const index = previewImageList.value.findIndex((item) => item === file.mx_mapfilename); const index = previewImageList.value.findIndex(
(item) => item === file.mx_mapfilename,
);
if (index !== -1) { if (index !== -1) {
preViewImageIndex.value = index; preViewImageIndex.value = index;
@@ -507,7 +513,10 @@ function handleShowPreview(file) {
if (isExcel(file.mx_fileext)) { if (isExcel(file.mx_fileext)) {
// https://view.officeapps.live.com/op/view.aspx?src= // https://view.officeapps.live.com/op/view.aspx?src=
window.open(`https://view.officeapps.live.com/op/view.aspx?src=${fileUrl}`, "_blank"); window.open(
`https://view.officeapps.live.com/op/view.aspx?src=${fileUrl}`,
"_blank",
);
} else { } else {
window.open(fileUrl, "_blank"); window.open(fileUrl, "_blank");
} }
@@ -517,7 +526,9 @@ function handleShowPreview(file) {
function handleDownloadPreviewImage(index) { function handleDownloadPreviewImage(index) {
const previewFileUrl = previewImageList.value[index]; const previewFileUrl = previewImageList.value[index];
const previewFile = fileList.value.find((item) => item.mx_mapfilename === previewFileUrl); const previewFile = fileList.value.find(
(item) => item.mx_mapfilename === previewFileUrl,
);
if (previewFile) { if (previewFile) {
handleDownloadFile(previewFile); handleDownloadFile(previewFile);
@@ -558,7 +569,7 @@ async function deleteFiles(files) {
try { try {
for (const item of files) { for (const item of files) {
if (item.subid) { if (item.subid) {
await deleteFileApi("bf_file", item.subid); await deleteFileApi(item.subid);
} }
} }
@@ -587,12 +598,12 @@ async function handleSave() {
loading.value = true; loading.value = true;
loadingTip.value = "正在保存中 . . ."; loadingTip.value = "正在保存中 . . .";
const { insertdata, updatedata, deletedata, hasChanges } = compareAndClassifyData( const { insertdata, updatedata, deletedata, hasChanges } =
fileList.value, compareAndClassifyData(fileList.value, oldFileList.value, "subid", [
oldFileList.value, "_content",
"subid", "_status",
["_content", "_status", "checked"], "checked",
); ]);
// 处理新增的文件 // 处理新增的文件
for (const item of insertdata) { for (const item of insertdata) {
@@ -679,7 +690,9 @@ function handleTreeNodeClick(data) {
<div class="g3-title"> <div class="g3-title">
附件管理({{ fileList.length }}) 附件管理({{ fileList.length }})
<template v-if="!props.readonly"> 未上传 ({{ pendingFileList.length }}) </template> <template v-if="!props.readonly">
未上传 ({{ pendingFileList.length }})
</template>
</div> </div>
</template> </template>
<input <input
@@ -719,6 +732,9 @@ function handleTreeNodeClick(data) {
style="margin-left: 8px" style="margin-left: 8px"
@click="handleDownloadZip" @click="handleDownloadZip"
> >
<el-icon class="el-icon--left">
<G3Icon icon="lucide:download" />
</el-icon>
打包下载 打包下载
</el-button> </el-button>
@@ -729,6 +745,9 @@ function handleTreeNodeClick(data) {
:disabled="checkedFiles.length === 0 || loading" :disabled="checkedFiles.length === 0 || loading"
@click="handleBatchDelete()" @click="handleBatchDelete()"
> >
<el-icon class="el-icon--left">
<G3Icon icon="lucide:trash-2" />
</el-icon>
批量删除 批量删除
</el-button> </el-button>
</div> </div>
@@ -738,7 +757,7 @@ function handleTreeNodeClick(data) {
<span class="g3-mode"> <span class="g3-mode">
模式:{{ displayMode === 0 ? "卡片视图" : "列表视图" }} 模式:{{ displayMode === 0 ? "卡片视图" : "列表视图" }}
<el-icon class="el-icon--right"> <el-icon class="el-icon--right">
<ArrowDown /> <G3Icon icon="lucide:chevron-down" />
</el-icon> </el-icon>
</span> </span>
<template #dropdown> <template #dropdown>
@@ -767,13 +786,17 @@ function handleTreeNodeClick(data) {
> >
<div v-if="fileList.length === 0" class="g3-tip"> <div v-if="fileList.length === 0" class="g3-tip">
<el-icon size="64"> <el-icon size="64">
<UploadFilled /> <G3Icon icon="lucide:cloud-upload" />
</el-icon> </el-icon>
<div>支持 拖拽文件、粘贴文件、多文件上传</div> <div>支持 拖拽文件、粘贴文件、多文件上传</div>
</div> </div>
<div v-else-if="displayMode === 0" class="g3-files"> <div v-else-if="displayMode === 0" class="g3-files">
<div v-for="(file, index) in fileList" :key="index" class="g3-file-item"> <div
v-for="(file, index) in fileList"
:key="index"
class="g3-file-item"
>
<div class="g3-file-checkbox"> <div class="g3-file-checkbox">
<el-checkbox v-model="file._checked" size="large" /> <el-checkbox v-model="file._checked" size="large" />
</div> </div>
@@ -785,9 +808,15 @@ function handleTreeNodeClick(data) {
<span v-if="file._status === 'pending'">未上传</span> <span v-if="file._status === 'pending'">未上传</span>
<span v-if="file._status === 'failed'">上传失败</span> <span v-if="file._status === 'failed'">上传失败</span>
</div> </div>
<div class="g3-file-preview" @click.stop="handleShowPreview(file)"> <div
class="g3-file-preview"
@click.stop="handleShowPreview(file)"
>
<template v-if="getFileIcon(file.mx_fileext) === 'image'"> <template v-if="getFileIcon(file.mx_fileext) === 'image'">
<el-image class="g3-file-preview-img" :src="file.mx_mapfilename" /> <el-image
class="g3-file-preview-img"
:src="file.mx_mapfilename"
/>
</template> </template>
<template v-else> <template v-else>
<G3Icon :icon="getFileIcon(file.mx_fileext)" size="88" /> <G3Icon :icon="getFileIcon(file.mx_fileext)" size="88" />
@@ -803,8 +832,15 @@ function handleTreeNodeClick(data) {
<div>{{ formatFileSize(file.mx_filesize) }}</div> <div>{{ formatFileSize(file.mx_filesize) }}</div>
<div> <div>
{{ file.mx_updateuser_id || file.mx_inputuser_id }} {{ file.mx_updateuser_id || file.mx_inputuser_id }}
<template v-if="file.mx_inputdatetime || file.mx_updatedatetime"> <template
| {{ formatTime(file.mx_updatedatetime || file.mx_inputdatetime) }} v-if="file.mx_inputdatetime || file.mx_updatedatetime"
>
|
{{
formatTime(
file.mx_updatedatetime || file.mx_inputdatetime,
)
}}
</template> </template>
</div> </div>
</div> </div>
@@ -812,26 +848,33 @@ function handleTreeNodeClick(data) {
<el-button <el-button
v-if="!props.readonly" v-if="!props.readonly"
type="danger" type="danger"
:icon="Delete"
size="small" size="small"
circle circle
@click.stop="handleDeleteOne(file)" @click.stop="handleDeleteOne(file)"
/> >
<G3Icon icon="lucide:trash-2" />
</el-button>
<el-button <el-button
type="primary" type="primary"
:icon="Download"
size="small" size="small"
circle circle
@click.stop="handleDownloadFile(file)" @click.stop="handleDownloadFile(file)"
/> >
<el-dropdown v-if="!props.readonly" placement="top" trigger="click"> <G3Icon icon="lucide:download" />
</el-button>
<el-dropdown
v-if="!props.readonly"
placement="top"
trigger="click"
>
<el-button <el-button
type="info" type="info"
:icon="More"
size="small" size="small"
circle circle
style="margin-left: 12px" style="margin-left: 12px"
/> >
<G3Icon icon="lucide:ellipsis" />
</el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="handleShowUpdateDialog(file)"> <el-dropdown-item @click="handleShowUpdateDialog(file)">
@@ -855,9 +898,6 @@ function handleTreeNodeClick(data) {
height="100%" height="100%"
show-overflow show-overflow
show-header-overflow show-header-overflow
:cell-config="{
height: 26,
}"
:sort-config="{ :sort-config="{
trigger: 'cell', trigger: 'cell',
}" }"
@@ -872,14 +912,28 @@ function handleTreeNodeClick(data) {
<el-checkbox v-model="row._checked" size="large" /> <el-checkbox v-model="row._checked" size="large" />
</template> </template>
</vxe-column> </vxe-column>
<vxe-column field="mx_filename" title="文件名" sortable align="center"> <vxe-column
field="mx_filename"
title="文件名"
sortable
align="center"
>
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link @click.stop="handleShowPreview(row)"> <el-button
type="primary"
link
@click.stop="handleShowPreview(row)"
>
{{ row.mx_filename }} {{ row.mx_filename }}
</el-button> </el-button>
</template> </template>
</vxe-column> </vxe-column>
<vxe-column field="mx_inputdatetime" title="上传日期" sortable align="center"> <vxe-column
field="mx_inputdatetime"
title="上传日期"
sortable
align="center"
>
<template #default="{ row }"> <template #default="{ row }">
{{ formatTime(row.mx_inputdatetime) }} {{ formatTime(row.mx_inputdatetime) }}
</template> </template>
@@ -904,7 +958,14 @@ function handleTreeNodeClick(data) {
</vxe-column> </vxe-column>
<vxe-column width="180" title="操作"> <vxe-column width="180" title="操作">
<template #default="{ row }"> <template #default="{ row }">
<div style="display: flex; align-items: center; justify-content: center"> <div
style="
display: flex;
align-items: center;
justify-content: center;
height: 28px;
"
>
<el-button <el-button
v-if="!props.readonly" v-if="!props.readonly"
type="danger" type="danger"
@@ -913,14 +974,26 @@ function handleTreeNodeClick(data) {
> >
删除 删除
</el-button> </el-button>
<el-button type="primary" link @click.stop="handleDownloadFile(row)"> <el-button
type="primary"
link
@click.stop="handleDownloadFile(row)"
>
下载 下载
</el-button> </el-button>
<el-dropdown v-if="!props.readonly" placement="top" trigger="click"> <el-dropdown
<el-button type="info" link style="margin-left: 12px"> 更多 </el-button> v-if="!props.readonly"
placement="top"
trigger="click"
>
<el-button type="info" link style="margin-left: 12px">
更多
</el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="handleShowUpdateDialog(row)"> <el-dropdown-item
@click="handleShowUpdateDialog(row)"
>
修改文件信息 修改文件信息
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
@@ -937,10 +1010,16 @@ function handleTreeNodeClick(data) {
</div> </div>
<template v-if="!props.readonly" #footer> <template v-if="!props.readonly" #footer>
<el-button type="primary" :icon="HelpFilled" :disabled="loading" @click="handleClickUpload"> <el-button type="primary" :disabled="loading" @click="handleClickUpload">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:mouse-pointer" />
</el-icon>
选择 选择
</el-button> </el-button>
<el-button type="primary" :icon="UploadFilled" :disabled="loading" @click="handleSave"> <el-button type="primary" :disabled="loading" @click="handleSave">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:upload" />
</el-icon>
上传 上传
</el-button> </el-button>
</template> </template>
@@ -953,7 +1032,9 @@ function handleTreeNodeClick(data) {
preview-teleported preview-teleported
@close="showPreview = false" @close="showPreview = false"
> >
<template #toolbar="{ actions, prev, next, reset, activeIndex, setActiveItem }"> <template
#toolbar="{ actions, prev, next, reset, activeIndex, setActiveItem }"
>
<ElIcon @click="prev"> <ElIcon @click="prev">
<Back /> <Back />
</ElIcon> </ElIcon>
@@ -966,10 +1047,16 @@ function handleTreeNodeClick(data) {
<ElIcon @click="actions('zoomOut')"> <ElIcon @click="actions('zoomOut')">
<ZoomOut /> <ZoomOut />
</ElIcon> </ElIcon>
<ElIcon @click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })"> <ElIcon
@click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })"
>
<ZoomIn /> <ZoomIn />
</ElIcon> </ElIcon>
<ElIcon @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })"> <ElIcon
@click="
actions('clockwise', { rotateDeg: 180, enableTransition: false })
"
>
<RefreshRight /> <RefreshRight />
</ElIcon> </ElIcon>
<ElIcon @click="actions('anticlockwise')"> <ElIcon @click="actions('anticlockwise')">
@@ -999,7 +1086,11 @@ function handleTreeNodeClick(data) {
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="showUpdateDialog = false"> 取消 </el-button> <el-button @click="showUpdateDialog = false"> 取消 </el-button>
<el-button type="primary" :loading="updateLoading" @click="handleConfirmUpdate"> <el-button
type="primary"
:loading="updateLoading"
@click="handleConfirmUpdate"
>
确认 确认
</el-button> </el-button>
</div> </div>
@@ -83,58 +83,82 @@ function switchTab(tab, index) {
moveScroll(index); moveScroll(index);
} }
const HOME_PATH = "/";
// 关闭标签 // 关闭标签
async function closeTab(tab) { async function closeTab(tab) {
if (tab.fullPath === HOME_PATH) return; // ❗首页不能关
const activePath = await tabStore.closeTab(tab.fullPath); const activePath = await tabStore.closeTab(tab.fullPath);
if (activePath && route.fullPath !== activePath) { if (activePath && route.fullPath !== activePath) {
router.push(activePath); router.push(activePath);
} else if (!activePath) { } else if (!activePath) {
router.push("/"); router.push(HOME_PATH);
} }
} }
// 关闭左侧
function closeLeft(data) { function closeLeft(data) {
const currentIndex = tabStore.tabs.findIndex( const currentIndex = tabStore.tabs.findIndex(
(item) => item.fullPath === data.fullPath, (item) => item.fullPath === data.fullPath,
); );
if (currentIndex === -1) return; if (currentIndex === -1) return;
// 关闭左侧所有标签 tabStore.tabs
tabStore.tabs.slice(0, currentIndex).forEach((item) => { .slice(0, currentIndex)
const index = tabStore.tabs.findIndex((t) => t.fullPath === item.fullPath); .filter((item) => item.fullPath !== HOME_PATH) // ❗过滤首页
if (index !== -1) tabStore.tabs.splice(index, 1); .forEach((item) => {
if (item.fullPath === route.fullPath) { const index = tabStore.tabs.findIndex(
router.push(data.fullPath); (t) => t.fullPath === item.fullPath,
} );
}); if (index !== -1) tabStore.tabs.splice(index, 1);
if (item.fullPath === route.fullPath) {
router.push(data.fullPath);
}
});
} }
// 关闭右侧
function closeRight(data) { function closeRight(data) {
const currentIndex = tabStore.tabs.findIndex( const currentIndex = tabStore.tabs.findIndex(
(item) => item.fullPath === data.fullPath, (item) => item.fullPath === data.fullPath,
); );
if (currentIndex === -1) return; if (currentIndex === -1) return;
tabStore.tabs.slice(currentIndex + 1).forEach((item) => { tabStore.tabs
const index = tabStore.tabs.findIndex((t) => t.fullPath === item.fullPath); .slice(currentIndex + 1)
if (index !== -1) tabStore.tabs.splice(index, 1); .filter((item) => item.fullPath !== HOME_PATH) // ❗过滤首页
if (item.fullPath === route.fullPath) { .forEach((item) => {
router.push(data.fullPath); const index = tabStore.tabs.findIndex(
} (t) => t.fullPath === item.fullPath,
}); );
if (index !== -1) tabStore.tabs.splice(index, 1);
if (item.fullPath === route.fullPath) {
router.push(data.fullPath);
}
});
} }
// 关闭其他
function closeOther(tab) { function closeOther(tab) {
tabStore.tabs = tabStore.tabs.filter((t) => t.fullPath === tab.fullPath); tabStore.tabs = tabStore.tabs.filter(
(t) => t.fullPath === tab.fullPath || t.fullPath === HOME_PATH, // ❗保留首页
);
router.push(tab.fullPath); router.push(tab.fullPath);
} }
// 关闭全部
function closeAll() { function closeAll() {
tabStore.tabs = []; tabStore.tabs = tabStore.tabs.filter(
router.push("/"); (t) => t.fullPath === HOME_PATH, // ❗只保留首页
} );
router.push(HOME_PATH);
}
function toHome() { function toHome() {
router.push("/"); router.push("/");
} }
@@ -63,13 +63,22 @@ function logout() {
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="router.push('/user-center')" <el-dropdown-item @click="router.push('/user-center')">
>个人中心</el-dropdown-item <el-icon class="el-icon--left">
> <G3Icon icon="lucide:user-round-pen" />
</el-icon>
个人中心
</el-dropdown-item>
<el-dropdown-item @click="showUpdatePassword = !showUpdatePassword"> <el-dropdown-item @click="showUpdatePassword = !showUpdatePassword">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:lock-keyhole-open" />
</el-icon>
修改密码 修改密码
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item divided @click="logout"> <el-dropdown-item divided @click="logout">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:arrow-right-from-line" />
</el-icon>
退出登录 退出登录
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
@@ -154,10 +154,10 @@ export function insertFileApi(params) {
return httpPost("/file/insert", params); return httpPost("/file/insert", params);
} }
export function deleteFileApi({ id, tableName }) { export function deleteFileApi(subid) {
return httpPost("/file/delete", { return httpPost("/file/delete", {
id, subid,
tableName, tableName: "bf_file",
bucketName: import.meta.env.PUBLIC_ALIYUN_BUCKET, bucketName: import.meta.env.PUBLIC_ALIYUN_BUCKET,
}); });
} }
+163 -180
View File
@@ -1,42 +1,42 @@
import { getAliyunKeyApi } from "@/request/api"; import { getAliyunKeyApi, insertFileApi } from "@/request/api";
/** /**
* 选择并压缩图片 * 选择并压缩图片
*/ */
export function uploadImage(options = {}) { export function uploadImage(options = {}) {
const { quality = 0.9, maxSize = 512000 } = options const { quality = 0.9, maxSize = 512000 } = options;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (typeof document === 'undefined') { if (typeof document === "undefined") {
reject(new Error('Require browser environment')) reject(new Error("Require browser environment"));
return return;
} }
const input = document.createElement('input') const input = document.createElement("input");
input.type = 'file' input.type = "file";
input.accept = 'image/*' input.accept = "image/*";
input.style.display = 'none' input.style.display = "none";
input.onchange = async (e) => { input.onchange = async (e) => {
const file = e.target.files?.[0] const file = e.target.files?.[0];
if (!file) { if (!file) {
reject(new Error('No file selected')) reject(new Error("No file selected"));
return return;
} }
try { try {
if (!file.type.startsWith('image/')) { if (!file.type.startsWith("image/")) {
throw new Error('Invalid image file') throw new Error("Invalid image file");
} }
// 创建一个临时URL来加载图片 // 创建一个临时URL来加载图片
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file);
// 调用 compressImage 进行压缩 // 调用 compressImage 进行压缩
const compressResult = await compressImage(url, quality, maxSize) const compressResult = await compressImage(url, quality, maxSize);
// 释放临时URL // 释放临时URL
URL.revokeObjectURL(url) URL.revokeObjectURL(url);
resolve({ resolve({
blob: compressResult.blob, blob: compressResult.blob,
@@ -44,19 +44,17 @@ export function uploadImage(options = {}) {
fileName: file.name, fileName: file.name,
fileExt: getFileExtension(file.name), fileExt: getFileExtension(file.name),
fileSize: file.size, fileSize: file.size,
}) });
} catch (err) {
reject(err);
} finally {
document.body.removeChild(input);
} }
catch (err) { };
reject(err)
}
finally {
document.body.removeChild(input)
}
}
document.body.appendChild(input) document.body.appendChild(input);
input.click() input.click();
}) });
} }
/** /**
@@ -64,26 +62,26 @@ export function uploadImage(options = {}) {
*/ */
export function uploadVideo() { export function uploadVideo() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (typeof document === 'undefined') { if (typeof document === "undefined") {
reject(new Error('Require browser environment')) reject(new Error("Require browser environment"));
return return;
} }
const input = document.createElement('input') const input = document.createElement("input");
input.type = 'file' input.type = "file";
input.accept = 'video/*' input.accept = "video/*";
input.style.display = 'none' input.style.display = "none";
input.onchange = (e) => { input.onchange = (e) => {
const file = e.target.files?.[0] const file = e.target.files?.[0];
if (!file) { if (!file) {
reject(new Error('No file selected')) reject(new Error("No file selected"));
return return;
} }
if (!file.type.startsWith('video/')) { if (!file.type.startsWith("video/")) {
reject(new Error('Invalid video file')) reject(new Error("Invalid video file"));
return return;
} }
resolve({ resolve({
@@ -92,121 +90,114 @@ export function uploadVideo() {
fileName: file.name, fileName: file.name,
fileExt: getFileExtension(file.name), fileExt: getFileExtension(file.name),
fileSize: file.size, fileSize: file.size,
}) });
document.body.removeChild(input) document.body.removeChild(input);
} };
document.body.appendChild(input) document.body.appendChild(input);
input.click() input.click();
}) });
} }
// 压缩图片 // 压缩图片
export async function compressImage(url, quality = 0.9, maxSize = 512000) { export async function compressImage(url, quality = 0.9, maxSize = 512000) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const img = new Image() const img = new Image();
img.src = url img.src = url;
img.onload = async () => { img.onload = async () => {
try { try {
// 获取原图大小 // 获取原图大小
const imgSize = await fetch(url) const imgSize = await fetch(url)
.then(res => res.blob()) .then((res) => res.blob())
.then(blob => blob.size) .then((blob) => blob.size);
if (imgSize <= maxSize) { if (imgSize <= maxSize) {
return resolve({ url, size: imgSize, blob: await fetch(url).then(res => res.blob()) }) return resolve({ url, size: imgSize, blob: await fetch(url).then((res) => res.blob()) });
} }
const canvas = document.createElement('canvas') const canvas = document.createElement("canvas");
const ctx = canvas.getContext('2d') const ctx = canvas.getContext("2d");
if (!ctx) if (!ctx) return reject(new Error("无法获取canvas上下文"));
return reject(new Error('无法获取canvas上下文'))
let targetWidth = img.width let targetWidth = img.width;
let targetHeight = img.height let targetHeight = img.height;
// 最大边长限制 // 最大边长限制
const maxEdge = 2000 const maxEdge = 2000;
if (targetWidth > maxEdge || targetHeight > maxEdge) { if (targetWidth > maxEdge || targetHeight > maxEdge) {
if (targetWidth > targetHeight) { if (targetWidth > targetHeight) {
targetHeight = (maxEdge * targetHeight) / targetWidth targetHeight = (maxEdge * targetHeight) / targetWidth;
targetWidth = maxEdge targetWidth = maxEdge;
} } else {
else { targetWidth = (maxEdge * targetWidth) / targetHeight;
targetWidth = (maxEdge * targetWidth) / targetHeight targetHeight = maxEdge;
targetHeight = maxEdge
} }
} }
canvas.width = targetWidth canvas.width = targetWidth;
canvas.height = targetHeight canvas.height = targetHeight;
ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, targetWidth, targetHeight) ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
const getBlob = q => const getBlob = (q) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
canvas.toBlob( canvas.toBlob(
blob => (blob ? resolve(blob) : reject(new Error('Blob转换失败'))), (blob) => (blob ? resolve(blob) : reject(new Error("Blob转换失败"))),
'image/jpeg', "image/jpeg",
q, q,
) );
}) });
let blob = await getBlob(quality) let blob = await getBlob(quality);
// 调整质量直到接近最大文件大小 // 调整质量直到接近最大文件大小
let minQ = 0.5 let minQ = 0.5;
let maxQ = quality let maxQ = quality;
let attempts = 0 let attempts = 0;
while (blob.size > maxSize && maxQ - minQ > 0.01 && attempts < 5) { while (blob.size > maxSize && maxQ - minQ > 0.01 && attempts < 5) {
const midQ = (minQ + maxQ) / 2 const midQ = (minQ + maxQ) / 2;
blob = await getBlob(midQ) blob = await getBlob(midQ);
if (blob.size > maxSize) { if (blob.size > maxSize) {
maxQ = midQ maxQ = midQ;
} else {
minQ = midQ;
} }
else { attempts++;
minQ = midQ
}
attempts++
} }
resolve({ blob, size: blob.size, url: URL.createObjectURL(blob) }) resolve({ blob, size: blob.size, url: URL.createObjectURL(blob) });
} catch (err) {
reject(err);
} }
catch (err) { };
reject(err)
}
}
img.onerror = () => reject(new Error('图片加载失败')) img.onerror = () => reject(new Error("图片加载失败"));
}) });
} }
export function formatFileSize(size) { export function formatFileSize(size) {
if (size < 1024) if (size < 1024) return `${size} B`;
return `${size} B` else if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`;
else if (size < 1024 * 1024) else if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(2)} MB`;
return `${(size / 1024).toFixed(2)} KB` else return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`;
else if (size < 1024 * 1024 * 1024)
return `${(size / (1024 * 1024)).toFixed(2)} MB`
else return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`
} }
// 获取文件扩展名并进行分类 // 获取文件扩展名并进行分类
export function getFileExtension(fileName) { export function getFileExtension(fileName) {
// 获取文件扩展名 // 获取文件扩展名
const fileExt = fileName.includes('.') const fileExt = fileName.includes(".")
? fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase() // 获取扩展名并转为小写 ? fileName.slice(fileName.lastIndexOf(".") + 1).toLowerCase() // 获取扩展名并转为小写
: '' : "";
// 如果是office类文件,将其归类到 "office" // 如果是office类文件,将其归类到 "office"
if (['docx', 'docm', 'xls', 'xlsx'].includes(fileExt)) { if (["docx", "docm", "xls", "xlsx"].includes(fileExt)) {
return 'office' return "office";
} }
// 返回扩展名 // 返回扩展名
return fileExt return fileExt;
} }
// 上传到阿里云 // 上传到阿里云
@@ -216,29 +207,27 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath
getAliyunKeyApi() getAliyunKeyApi()
.then(async (res) => { .then(async (res) => {
try { try {
const aliyunKey = res.data const aliyunKey = res.data;
const formData = new FormData() const formData = new FormData();
let uniqueName = `${Date.now()}_${Math.random() let uniqueName = `${Date.now()}_${Math.random().toString(36).slice(2)}`;
.toString(36)
.slice(2)}`
if (fileExt) { if (fileExt) {
uniqueName += `.${fileExt}` uniqueName += `.${fileExt}`;
} }
formData.append('key', `${aliyunKey.dir}${uniqueName}`) formData.append("key", `${aliyunKey.dir}${uniqueName}`);
formData.append('OSSAccessKeyId', aliyunKey.accessid) formData.append("OSSAccessKeyId", aliyunKey.accessid);
formData.append('policy', aliyunKey.policy) formData.append("policy", aliyunKey.policy);
formData.append('Signature', aliyunKey.signature) formData.append("Signature", aliyunKey.signature);
formData.append('file', blob, uniqueName) formData.append("file", blob, uniqueName);
const xhr = new XMLHttpRequest() const xhr = new XMLHttpRequest();
xhr.open('POST', aliyunKey.host, true) xhr.open("POST", aliyunKey.host, true);
xhr.onload = async () => { xhr.onload = async () => {
if (xhr.status === 200 || xhr.status === 204) { if (xhr.status === 200 || xhr.status === 204) {
const allFilePath = `${aliyunKey.host}/${aliyunKey.dir}${uniqueName}` const allFilePath = `${aliyunKey.host}/${aliyunKey.dir}${uniqueName}`;
const insertRes = await insertFileApi({ const insertRes = await insertFileApi({
father, father,
@@ -248,8 +237,8 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath
mx_mapfilename: allFilePath, mx_mapfilename: allFilePath,
mx_filesize: fileSize, mx_filesize: fileSize,
mx_fileext: fileExt, mx_fileext: fileExt,
mx_cate_id: cateid || '', mx_cate_id: cateid || "",
}) });
if (insertRes.success) { if (insertRes.success) {
resolve({ resolve({
@@ -260,33 +249,30 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath
mx_mapfilename: allFilePath, mx_mapfilename: allFilePath,
mx_filesize: fileSize, mx_filesize: fileSize,
mx_fileext: fileExt, mx_fileext: fileExt,
mx_cate_id: cateid || '', mx_cate_id: cateid || "",
}) });
} } else {
else { reject(new Error(insertRes.data.message));
reject(new Error(insertRes.data.message))
} }
} else {
ElMessage.error("上传失败,请稍后再试");
reject(new Error(`OSS 上传失败: ${xhr.responseText}`));
} }
else { };
ElMessage.error('上传失败,请稍后再试')
reject(new Error(`OSS 上传失败: ${xhr.responseText}`))
}
}
xhr.onerror = () => { xhr.onerror = () => {
ElMessage.error('上传失败,请稍后再试') ElMessage.error("上传失败,请稍后再试");
reject(new Error('OSS 上传异常')) reject(new Error("OSS 上传异常"));
} };
xhr.send(formData) xhr.send(formData);
} } catch (err) {
catch (err) { ElMessage.error("上传失败,请稍后再试");
ElMessage.error('上传失败,请稍后再试') reject(err);
reject(err)
} }
}) })
.catch(reject) .catch(reject);
}) });
} }
export function getAliyunFileBlob(url) { export function getAliyunFileBlob(url) {
@@ -294,73 +280,70 @@ export function getAliyunFileBlob(url) {
// 创建一个独立的异步函数来处理 fetch 请求 // 创建一个独立的异步函数来处理 fetch 请求
async function fetchFile() { async function fetchFile() {
try { try {
const response = await fetch(url, { mode: 'cors' }) const response = await fetch(url, { mode: "cors" });
// 检查响应状态 // 检查响应状态
if (!response.ok) { if (!response.ok) {
ElMessage.error(`下载失败:${response.status}`) ElMessage.error(`下载失败:${response.status}`);
reject(new Error(`下载失败:${response.status}`)) reject(new Error(`下载失败:${response.status}`));
return return;
} }
// 获取文件内容 // 获取文件内容
const blob = await response.blob() const blob = await response.blob();
// 返回blob // 返回blob
resolve(blob) resolve(blob);
} } catch (error) {
catch (error) {
// 捕获网络请求失败或其他错误 // 捕获网络请求失败或其他错误
ElMessage.error('下载文件时发生错误') ElMessage.error("下载文件时发生错误");
reject(error) reject(error);
} }
} }
// 调用异步函数 // 调用异步函数
fetchFile() fetchFile();
}) });
} }
export function downloadZip( export function downloadZip(files, fileName = "文件合集", callback) {
files, const zip = new JSZip();
fileName = '文件合集', const addedFiles = new Set(); // 用来存储已添加的文件名
callback,
) {
const zip = new JSZip()
const addedFiles = new Set() // 用来存储已添加的文件名
let result = files.map((item) => { let result = files.map((item) => {
let promise = getAliyunFileBlob(item.mx_mapfilename).then((res) => { let promise = getAliyunFileBlob(item.mx_mapfilename).then((res) => {
// 处理文件名重复的情况 // 处理文件名重复的情况
let uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles) let uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles);
zip.file(uniqueFileName, res, { binary: true }) zip.file(uniqueFileName, res, { binary: true });
addedFiles.add(uniqueFileName) // 添加文件名到已添加的集合中 addedFiles.add(uniqueFileName); // 添加文件名到已添加的集合中
}) });
return promise return promise;
}) });
Promise.all(result).then(() => { Promise.all(result)
zip.generateAsync({ type: 'blob' }).then((res) => { .then(() => {
callback && callback() zip.generateAsync({ type: "blob" }).then((res) => {
saveAs(res, `${fileName}.zip`) callback && callback();
saveAs(res, `${fileName}.zip`);
});
}) })
}).catch(() => { .catch(() => {
callback && callback() callback && callback();
}) });
} }
// 处理文件名重复的函数 // 处理文件名重复的函数
function getUniqueFileName(fileName, addedFiles) { function getUniqueFileName(fileName, addedFiles) {
let uniqueFileName = fileName let uniqueFileName = fileName;
let counter = 1 let counter = 1;
// 如果文件名已经存在,则在文件名后加上 (1), (2), ... // 如果文件名已经存在,则在文件名后加上 (1), (2), ...
while (addedFiles.has(uniqueFileName)) { while (addedFiles.has(uniqueFileName)) {
const fileNameWithoutExtension = uniqueFileName.replace(/\.[^/.]+$/, '') const fileNameWithoutExtension = uniqueFileName.replace(/\.[^/.]+$/, "");
const extension = uniqueFileName.slice(uniqueFileName.lastIndexOf('.')) const extension = uniqueFileName.slice(uniqueFileName.lastIndexOf("."));
uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}` uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`;
counter++ counter++;
} }
return uniqueFileName return uniqueFileName;
} }
@@ -344,6 +344,8 @@ async function handleTreeLoad(node, resolve) {
} }
async function handleTreeNodeClick(nodeData) { async function handleTreeNodeClick(nodeData) {
console.log("===node===")
console.log(nodeData)
if (!nodeData || nodeData.b_id === "root" || !nodeData.b_moduleid) return; if (!nodeData || nodeData.b_id === "root" || !nodeData.b_moduleid) return;
currentNode.value = nodeData; currentNode.value = nodeData;
@@ -17,6 +17,8 @@ const loading = reactive({
const emits = defineEmits(["change", "closed"]); const emits = defineEmits(["change", "closed"]);
const showFile = ref(false);
const showVisible = defineModel(false); const showVisible = defineModel(false);
const form = ref({ const form = ref({
@@ -136,18 +138,41 @@ function onClosed() {
<div class="form-item"> <div class="form-item">
<div class="label">发布</div> <div class="label">发布</div>
<el-switch v-model="form.b_visible" active-value="1" inactive-value="0" /> <el-switch
v-model="form.b_visible"
active-value="1"
inactive-value="0"
/>
</div> </div>
<div class="form-item"> <div class="form-item">
<div class="label">公告内容</div> <div class="label">公告内容</div>
<WangEditor v-model="form.b_info" :father="form.b_id" module-id="oa_notices" /> <WangEditor
v-model="form.b_info"
:father="form.b_id"
module-id="oa_notices"
/>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<el-button @click="showVisible = false">取消</el-button> <div class="g3-footer">
<el-button type="primary" :loading="loading.save" @click="handleSave">确认</el-button> <el-button type="primary" @click="showFile = true">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:paperclip" />
</el-icon>
上传附件
</el-button>
<div>
<el-button @click="showVisible = false">取消</el-button>
<el-button type="primary" :loading="loading.save" @click="handleSave"
>确认</el-button
>
</div>
</div>
</template> </template>
<G3File v-model="showFile" :father="form.b_id" module-id="oa_notices" />
</el-dialog> </el-dialog>
</template> </template>
@@ -171,4 +196,10 @@ function onClosed() {
min-width: 70px; min-width: 70px;
white-space: nowrap; white-space: nowrap;
} }
.g3-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
</style> </style>
@@ -191,7 +191,12 @@ async function deleteRows(rows = []) {
<template> <template>
<div class="g3-flex-container"> <div class="g3-flex-container">
<div class="g3-header"> <div class="g3-header">
<el-button type="primary" @click="showCategory = true"> 分类 </el-button> <el-button type="primary" @click="showCategory = true">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:settings" />
</el-icon>
分类设置
</el-button>
</div> </div>
<div class="g3-container"> <div class="g3-container">
<el-splitter> <el-splitter>
@@ -218,8 +223,18 @@ async function deleteRows(rows = []) {
<div class="g3-container-right"> <div class="g3-container-right">
<div style="width: 100%"> <div style="width: 100%">
<div style="display: flex; height: 32px; margin-bottom: 8px"> <div style="display: flex; height: 32px; margin-bottom: 8px">
<el-button type="primary" @click="handleAdd"> 新增 </el-button> <el-button type="primary" @click="handleAdd">
<el-button type="danger" @click="handleDeleteRows"> 批量删除 </el-button> <el-icon class="el-icon--left">
<G3Icon icon="lucide:plus" />
</el-icon>
新增
</el-button>
<el-button type="danger" @click="handleDeleteRows">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:trash-2" />
</el-icon>
批量删除
</el-button>
</div> </div>
</div> </div>
<div class="g3-table"> <div class="g3-table">
@@ -279,11 +294,20 @@ async function deleteRows(rows = []) {
</template> </template>
</vxe-column> </vxe-column>
<vxe-column width="200" align="center" fixed="right" title="操作"> <vxe-column
width="200"
align="center"
fixed="right"
title="操作"
>
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link @click="handleEditRow(row)"> 编辑 </el-button> <el-button type="primary" link @click="handleEditRow(row)">
编辑
</el-button>
<el-button type="danger" link @click="handleDeleteRow(row)"> 删除 </el-button> <el-button type="danger" link @click="handleDeleteRow(row)">
删除
</el-button>
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
@@ -18,7 +18,14 @@ const loading = reactive({
let oldRowData = {}; let oldRowData = {};
const textareaFields = ["b_sql", "b_resultsql", "b_polyline", "b_polyline2", "b_graph_x", "b_img"]; const textareaFields = [
"b_sql",
"b_resultsql",
"b_polyline",
"b_polyline2",
"b_graph_x",
"b_img",
];
const validRules = ref({ const validRules = ref({
b_id: [ b_id: [
@@ -149,7 +156,9 @@ async function deleteRows(rows = []) {
return; return;
} }
const delectedIds = rows.filter((row) => !$table.isInsertByRow(row)).map((row) => row.b_id); const delectedIds = rows
.filter((row) => !$table.isInsertByRow(row))
.map((row) => row.b_id);
const tipMessage = `确定要删除选中的用户提醒吗?`; const tipMessage = `确定要删除选中的用户提醒吗?`;
@@ -248,7 +257,12 @@ async function saveRow(row) {
]; ];
} else { } else {
// 更新 // 更新
const { isChanged, result } = diffObject(row, oldRowData, ["b_id"], ["_X_ROW_KEY"]); const { isChanged, result } = diffObject(
row,
oldRowData,
["b_id"],
["_X_ROW_KEY"],
);
if (!isChanged) { if (!isChanged) {
ElMessage.info("没有数据变更"); ElMessage.info("没有数据变更");
@@ -297,7 +311,7 @@ onMounted(async () => {
await getsubtable(tableData, tableDataoriginal, ``); await getsubtable(tableData, tableDataoriginal, ``);
}); });
async function handleRefresh(){ async function handleRefresh() {
await getsubtable(tableData, tableDataoriginal, ``); await getsubtable(tableData, tableDataoriginal, ``);
} }
</script> </script>
@@ -306,11 +320,25 @@ async function handleRefresh(){
<div class="g3-flex-container"> <div class="g3-flex-container">
<div class="g3-header"> <div class="g3-header">
<div class="g3-header-left"> <div class="g3-header-left">
<el-button type="primary" @click="handleAdd"> 新建 </el-button> <el-button type="primary" @click="handleAdd">
<el-button type="danger" @click="handleDeleteRows"> 删除 </el-button> <el-icon class="el-icon--left">
<G3Icon icon="lucide:plus" />
</el-icon>
新建
</el-button>
<el-button type="danger" @click="handleDeleteRows">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:trash-2" />
</el-icon>
批量删除
</el-button>
</div> </div>
<div class="g3-header-right"> <div class="g3-header-right">
<div class="g3-svg-query" style="margin-right: 10px" @click="handleRefresh"> <div
class="g3-svg-query"
style="margin-right: 10px"
@click="handleRefresh"
>
<G3Icon icon="lucide:refresh-ccw" /> <G3Icon icon="lucide:refresh-ccw" />
</div> </div>
<el-dropdown> <el-dropdown>
@@ -398,7 +426,9 @@ async function handleRefresh(){
> >
保存 保存
</el-button> </el-button>
<el-button link @click="cancelEditRow(row)" @click.stop> 取消 </el-button> <el-button link @click="cancelEditRow(row)" @click.stop>
取消
</el-button>
</template> </template>
<template v-else> <template v-else>
<el-button <el-button
@@ -411,7 +441,12 @@ async function handleRefresh(){
编辑 编辑
</el-button> </el-button>
<el-button :disabled="loading.save" type="danger" link @click="handleDeleteRow(row)"> <el-button
:disabled="loading.save"
type="danger"
link
@click="handleDeleteRow(row)"
>
删除 删除
</el-button> </el-button>
</template> </template>
@@ -425,17 +425,26 @@ function handleShowBatchCopyDialog() {
</el-icon> </el-icon>
批量删除 批量删除
</el-button> </el-button>
<el-button type="primary" @click="showPasswordRow = !showPasswordRow" <el-button type="primary" @click="showPasswordRow = !showPasswordRow">
>查看密码</el-button <el-icon class="el-icon--left">
> <G3Icon icon="lucide:eye" />
<el-button type="primary" @click="handleShowBatchCopyDialog" </el-icon>
>批量复制权限</el-button 查看密码
> </el-button>
<el-button type="primary" @click="handleShowBatchCopyDialog">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:copy" />
</el-icon>
批量复制权限
</el-button>
<el-button <el-button
type="primary" type="primary"
@click="handleShowCopyDialog" @click="handleShowCopyDialog"
:disabled="!isSelectedRow" :disabled="!isSelectedRow"
> >
<el-icon class="el-icon--left">
<G3Icon icon="lucide:copy" />
</el-icon>
复制权限 复制权限
</el-button> </el-button>
<el-button <el-button
@@ -443,6 +452,9 @@ function handleShowBatchCopyDialog() {
:disabled="!isSelectedRow" :disabled="!isSelectedRow"
@click="showEmployee = true" @click="showEmployee = true"
> >
<el-icon class="el-icon--left">
<G3Icon icon="lucide:book-user" />
</el-icon>
员工资料 员工资料
</el-button> </el-button>
</div> </div>