添加灯箱预览功能

This commit is contained in:
2026-01-21 20:54:04 +08:00
parent 6ecc3beaef
commit 46d0be7750
6 changed files with 123 additions and 526 deletions
+11 -2
View File
@@ -126,7 +126,16 @@ func ApiFiles(r *gin.RouterGroup) {
models.DB.Create(&fund_file_info) // 传入指针 models.DB.Create(&fund_file_info) // 传入指针
} }
ReturnJson(ctx, "apiOK", nil) //返回后台存储的URL
download_URL := path.Join("/api/files/download/", hash_str)
get_URL := path.Join("/api/files/get/", hash_str)
re := map[string]interface{}{
"download": download_URL,
"get": get_URL,
"hash": hash_str,
}
ReturnJson(ctx, "apiOK", re)
} else { } else {
ReturnJson(ctx, "file_hash_err", nil) ReturnJson(ctx, "file_hash_err", nil)
@@ -155,7 +164,7 @@ func ApiFiles(r *gin.RouterGroup) {
ReturnJson(ctx, "userCookieError", nil) ReturnJson(ctx, "userCookieError", nil)
} }
ReturnJson(ctx, "apiErr", nil) //ReturnJson(ctx, "apiErr", nil)
}) })
// r.GET("/upload", func(ctx *gin.Context) { // r.GET("/upload", func(ctx *gin.Context) {
+17
View File
@@ -38,6 +38,8 @@
"filepond-plugin-image-preview": "^4.6.12", "filepond-plugin-image-preview": "^4.6.12",
"filepond-plugin-image-resize": "^2.0.10", "filepond-plugin-image-resize": "^2.0.10",
"flatpickr": "^4.6.13", "flatpickr": "^4.6.13",
"fslightbox": "^3.7.4",
"fslightbox-vue": "^3.0.3",
"litepicker": "^2.0.12", "litepicker": "^2.0.12",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"tom-select": "^2.4.3", "tom-select": "^2.4.3",
@@ -2833,6 +2835,21 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/fslightbox": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/fslightbox/-/fslightbox-3.7.4.tgz",
"integrity": "sha512-zQqMHxiYkR0W/xrWQlchoO626C5KCM6rabpMWiJsy+MZCMHo7zlywsGAOGeOahRUqBZzXT9OeMddiVSfW77gaA==",
"license": "MIT"
},
"node_modules/fslightbox-vue": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/fslightbox-vue/-/fslightbox-vue-3.0.3.tgz",
"integrity": "sha512-+INqEhEqQ6U2380GrMHcO5KVRNjeYLWGd0l+EOIgXklY8V94gwgF9lXLMtgp0sHxMl6LSfm56IV/LcKbldPf9A==",
"license": "MIT",
"peerDependencies": {
"vue": ">=2.5.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+2
View File
@@ -42,6 +42,8 @@
"filepond-plugin-image-preview": "^4.6.12", "filepond-plugin-image-preview": "^4.6.12",
"filepond-plugin-image-resize": "^2.0.10", "filepond-plugin-image-resize": "^2.0.10",
"flatpickr": "^4.6.13", "flatpickr": "^4.6.13",
"fslightbox": "^3.7.4",
"fslightbox-vue": "^3.0.3",
"litepicker": "^2.0.12", "litepicker": "^2.0.12",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"tom-select": "^2.4.3", "tom-select": "^2.4.3",
@@ -1,219 +0,0 @@
<script setup>
import "cropperjs";
import { computed, ref } from "vue";
const fileObj = ref({});
const croppercanvas = ref();
const cropperimage = ref();
const cropperselection = ref();
/**
* 选区逻辑
*/
// 是否正在开始选区
const isCropperSelection = ref(false);
const isCropperMove = ref(true);
// 判断当前是移动还是选区
const currentType = computed(() => (isCropperMove.value ? "move" : "select"));
/**
* 按钮方法
*/
// 旋转
function handleRotate() {
cropperimage.value.$rotate("90deg");
cropperimage.value.$center("contain");
}
// 裁剪
function handleCropper() {
isCropperMove.value = false;
if (isCropperMove.value) {
cropperselection.value.$clear();
} else {
const cropperCanvas = croppercanvas.value;
const cropperCanvasRect = cropperCanvas.getBoundingClientRect();
const cropperImage = cropperimage.value;
const cropperImageRect = cropperImage.getBoundingClientRect();
const maxSelection = {
x: cropperImageRect.left - cropperCanvasRect.left,
y: cropperImageRect.top - cropperCanvasRect.top,
width: cropperImageRect.width,
height: cropperImageRect.height,
};
cropperselection.value.$change(
maxSelection.x,
maxSelection.y,
maxSelection.width,
maxSelection.height
);
}
}
// 移动
function handleMove() {
if (!isCropperMove.value) {
isCropperMove.value = true;
// 如果想要点击移动,清除选区,可以打开下面的代码注释
// cropperselection.value.$clear();
}
}
/**
* 监听选择区变化
* @param event
*/
function onCropperSelectionChange(event) {
if (event.detail.width && event.detail.height) {
isCropperSelection.value = true;
} else {
isCropperSelection.value = false;
}
}
/**
* 确认裁剪
*/
const emit = defineEmits(["success"]);
async function handleConfirm() {
if (isCropperSelection.value) {
const res = await cropperselection.value.$toCanvas();
const dataImage = res.toDataURL("image/png");
const file = dataURLtoFile(dataImage, fileObj.value.name);
emit("success", {
...fileObj.value,
file: file,
fileShow: dataImage,
});
}
}
// 将data:image转成新的file
function dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(","),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const blob = new Blob([u8arr], { type: mime });
const file = new File([blob], filename, { type: mime });
return file;
}
/**
* 文件上传
*/
const input_form = ref();
function handleUploadSuccess() {
const files = input_form.value.files;
if (files.length) {
fileObj.value = {
name: files[0].name,
file: files[0],
fileShow: URL.createObjectURL(files[0]),
};
}
}
</script>
<template>
<div class="basic_container">
<div class="tool_wrap">
<button :class="{ active: isCropperMove }" @click="handleMove">
移动
</button>
<button @click="handleRotate">旋转</button>
<button :class="{ active: isCropperSelection }" @click="handleCropper">
{{ isCropperSelection ? "重置选区" : "裁剪" }}
</button>
</div>
<div class="dialog_wrap">
<div class="image_wrap" ref="imageWrap">
<cropper-canvas ref="croppercanvas" background>
<cropper-image
:src="fileObj.fileShow"
alt="Picture"
ref="cropperimage"
rotatable
scalable
skewable
translatable
></cropper-image>
<cropper-shade hidden ref="cropperShade"></cropper-shade>
<cropper-handle :action="currentType" plain></cropper-handle>
<cropper-selection
id="cropperSelection"
ref="cropperselection"
movable
resizable
hidden
outlined
@change="onCropperSelectionChange"
>
<cropper-crosshair centered />
<cropper-handle
action="move"
theme-color="rgba(255, 255, 255, 0.35)"
/>
<cropper-handle action="n-resize" />
<cropper-handle action="e-resize" />
<cropper-handle action="s-resize" />
<cropper-handle action="w-resize" />
<cropper-handle action="ne-resize" />
<cropper-handle action="nw-resize" />
<cropper-handle action="se-resize" />
<cropper-handle action="sw-resize" />
</cropper-selection>
</cropper-canvas>
</div>
<div class="info_wrap">
<div class="cropper_preview">
<cropper-viewer
selection="#cropperSelection"
style="width: 200px"
></cropper-viewer>
</div>
<div class="btn_wrap">
<input type="file" ref="input_form" @change="handleUploadSuccess" />
<button type="primary" @click="handleConfirm"> </button>
</div>
点击确认后看控制台有信息
</div>
</div>
</div>
</template>
<style scoped>
.dialog_wrap {
display: flex;
.image_wrap {
width: 400px;
height: 300px;
flex-shrink: 0;
cropper-canvas {
width: 100%;
height: 100%;
}
}
.info_wrap {
margin-left: 20px;
}
}
button {
& + button {
margin-left: 20px;
}
}
button.active {
background-color: #c6dff8;
border-color: #409eff;
}
</style>
@@ -1,297 +0,0 @@
<script setup>
import { Modal } from "@tabler/core";
import { onMounted, ref } from "vue";
import "@/libs/cropper.min.js"
const avata_toolt=ref()
const avatar_toolt_moda=ref()
const cropper=ref()
const image = document.getElementById('cropper-image');
// 初始化Cropper
function initCropper(imageSrc) {
if (cropper.value) {
cropper.value.destroy();
}
image.src = imageSrc;
cropper.value = new Cropper(image, {
aspectRatio: 1,
viewMode: 2,
autoCropArea: 0.8,
zoomable: true,
zoomOnWheel: true,
zoomOnTouch: true,
wheelZoomRatio: 0.1,
//minCanvasWidth: 400,
//minCanvasHeight: 400,
crop: updatePreview,
ready() {
}
});
}
function inputfile(e){
const file = e.target.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
showMessage('⚠️ 请选择有效的图片文件', 'error');
return;
}
const reader = new FileReader();
reader.onload = () => {
initCropper(reader.result);
currentScale = 1;
};
reader.readAsDataURL(file);
}
onMounted(()=>{
//console.log(avata_toolt)
avatar_toolt_moda.value=new Modal(avata_toolt.value)
avatar_toolt_moda.value.show()
})
</script>
<template>
<div class="modal modal-blur fade" ref="avata_toolt" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content">
<div class="container">
<h1>头像裁剪工具</h1>
<div class="flex-wrapper">
<!-- 左侧裁剪区 -->
<div class="crop-section">
<div id="image-wrapper">
<img id="cropper-image"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
</div>
<!-- 上传进度 -->
<div class="progress-container">
<div class="progress-bar"></div>
</div>
<!-- 消息提示 -->
<div id="message" class="alertavater"></div>
<div class="preview-stats">
<!-- <p>当前缩放: <span id="zoomValue">100%</span></p> -->
<p>图片尺寸: <span id="imageSize">0 x 0</span></p>
</div>
</div>
<!-- 右侧预览区 -->
<div class="preview-section">
<h3>实时预览</h3>
<div class="preview-box">
<img id="preview-img"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
</div>
<!-- 控制按钮 -->
<div class="controls">
<label class="btn btn-primary">
📁 选择图片
<input type="file" accept="image/*" @change="inputfile">
</label>
<button class="btn btn-secondary " onclick="rotateImage(-90)"> 左旋</button>
<button class="btn btn-success " id="uploadBtn"> 裁剪头像</button>
<button class="btn btn-danger " style="margin-top: 150px;" onclick="avatar_toolt.hide()"> 取消</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* 头像裁剪器样式*/
.container {
width: 95%;
/* 改为百分比宽度 */
margin: 20px auto;
/* 增加上下边距 */
max-width: 1200px;
/* 保留最大宽度 */
background: white;
padding: 30px;
border-radius: 12px;
}
.flex-wrapper {
display: flex;
gap: 30px;
margin-top: 20px;
flex-wrap: wrap;
/* 添加换行支持 */
}
/* 裁剪区域 */
.crop-section {
flex: 1 1 60%;
/* 弹性布局基础宽度 */
min-width: 300px;
/* 降低最小宽度 */
height: auto;
/* 移除固定高度 */
min-height: 400px;
/* 设置最小高度 */
}
#image-wrapper {
width: 100%;
height: 60vh;
/* 改用视窗单位 */
max-height: 600px;
/* 设置最大高度 */
background: #f8f9fa;
border: 2px dashed #ddd;
border-radius: 8px;
overflow: hidden;
}
/* 预览区域自适应 */
.preview-section {
flex: 1 1 35%;
/* 弹性布局基础宽度 */
min-width: 250px;
/* 设置合理最小宽度 */
}
/* 移动端适配 */
@media (max-width: 768px) {
.container {
padding: 15px;
/* 减少内边距 */
}
.flex-wrapper {
flex-direction: column;
/* 垂直排列 */
}
.crop-section,
.preview-section {
width: 100% !important;
/* 强制全宽 */
min-width: unset;
/* 移除最小宽度 */
}
#image-wrapper {
height: 50vh;
/* 调整移动端高度 */
}
.preview-box {
width: 120px;
/* 缩小预览区域 */
height: 120px;
}
.controls {
flex-direction: column;
/* 垂直排列按钮 */
margin-top: 10px;
}
}
#cropper-image {
max-width: none !important;
max-height: none !important;
}
/* 控制区域 */
.controls {
margin-top: 20px;
display: flex;
flex-direction: column;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
/* 预览区域 */
.preview-section {
flex: 1;
min-width: 50px;
}
.preview-box {
width: 150px;
height: 150px;
/* border-radius: 50%; */
border: 3px solid var(--primary-color);
overflow: hidden;
margin: 0 auto 20px;
}
#preview-img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 上传进度 */
.progress-container {
height: 8px;
background: #eee;
border-radius: 4px;
margin-top: 20px;
overflow: hidden;
display: none;
}
.progress-bar {
width: 0%;
height: 100%;
background: var(--primary-color);
transition: width 0.3s ease;
}
/* 消息提示 */
.alertavater {
padding: 15px;
border-radius: 6px;
margin-top: 20px;
display: none;
}
.alertavater-success {
background: #dff0d8;
color: #3c763d;
}
.alertavater-error {
background: #f2dede;
color: var(--error-color);
}
input[type="file"] {
display: none;
}
</style>
@@ -1,16 +1,41 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted,defineProps } from "vue"; import { ref, onMounted, onUnmounted, defineProps, reactive } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
const { t, locale } = useI18n(); const { t, locale } = useI18n();
import Dropzone from "dropzone"; import Dropzone from "dropzone";
import "dropzone/dist/dropzone.css"; import "dropzone/dist/dropzone.css";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import "fslightbox";
const lightbox = new FsLightbox();
const userStore = useUserStore(); const userStore = useUserStore();
const dropzoneElement = ref(null); const dropzoneElement = ref(null);
let dropzoneInstance = null; let dropzoneInstance = null;
const files = ref([]);
const files = reactive([]);
function get_file_from_uuid(uuid) {
if (files.length != 0) {
for(let i=0;i<files.length;i++){
if(files[i].uuid==uuid){
return i;
}
}
}
return -1;
}
function remove_file_from_uuie(uuid) {
//delete files[uuid]
var id=get_file_from_uuid(uuid)
if(id>=0){
files.splice(id, 1)
}
}
const prop = defineProps({ const prop = defineProps({
maxFiles: { maxFiles: {
@@ -89,11 +114,60 @@ const initDropzone = () => {
// 处理点击事件 // 处理点击事件
console.log("缩略图被点击", file); console.log("缩略图被点击", file);
//动态把文件载入灯箱
//先移除原有数据
lightbox.props.sources.splice(0,lightbox.props.sources.length)
var dis_id=0;
var dis_id_t=0;
for (let i=0;i<files.length;i++){
if(files[i]["is_upload"]==true){
lightbox.props.sources.push(files[i]["get_url"])
if(files[i]["uuid"]==file.upload.uuid){
dis_id=dis_id_t;
}
}
dis_id_t+=1;
}
lightbox.open(dis_id);
// 可以在这里实现 // 可以在这里实现
// 1. 预览大图 // 1. 预览大图
// 2. 显示文件详情 // 2. 显示文件详情
// 3. 触发自定义操作 // 3. 触发自定义操作
}); });
//将后台接收到的url添加到文件列表
// var t = {
// //uuid:file.upload.uuid,
// hash: response.return.hash,
// get_url: response.return.get,
// download_url: response.return.download,
// name: file.name,
// size: file.size,
// };
var file_id=get_file_from_uuid(file.upload.uuid)
if(file_id>=0)
{
files[file_id]["hash"]=response.return.hash;
files[file_id]["get_url"]=response.return.get;
files[file_id]["download_url"]=response.return.download;
files[file_id]["file_name"]=file.name;
files[file_id]["file_size"]=file.size;
files[file_id]["is_upload"]=true;
console.log(files)
}
//files.push(t)
// files[file.upload.uuid]=t
// console.log(files)
// lightbox.props.sources.push(t.get_url)
// console.log(lightbox)
}); });
this.on("error", (file, errorMessage) => { this.on("error", (file, errorMessage) => {
console.error("上传失败:", file.name, errorMessage); console.error("上传失败:", file.name, errorMessage);
@@ -101,15 +175,24 @@ const initDropzone = () => {
this.on("removedfile", (file) => { this.on("removedfile", (file) => {
console.log("remove:", file); console.log("remove:", file);
//files.value = files.value.filter(f => f.name !== file.name) //files.value = files.value.filter(f => f.name !== file.name)
remove_file_from_uuie(file.upload.uuid)
console.log(files)
}); });
this.on("addedfile", (file) => { this.on("addedfile", (file) => {
//添加文件
console.log("addfile", file); console.log("addfile", file);
//控制排序 需要从添加文件开始操作
var t = {
uuid: file.upload.uuid,
is_upload: false,
};
files.push(t);
console.log(files);
}); });
this.on("sending", function (file, xhr, formData) { this.on("sending", function (file, xhr, formData) {
// 获取表单值并添加到 FormData // 获取表单值并添加到 FormData
//console.log(userStore.userCookie.Value) //console.log(userStore.userCookie.Value)
formData.append("cookie", userStore.userCookie.Value); formData.append("cookie", userStore.userCookie.Value);
}); });
}, },
}); });
@@ -149,6 +232,8 @@ const initDropzone = () => {
// 组件挂载时初始化 // 组件挂载时初始化
onMounted(() => { onMounted(() => {
initDropzone(); initDropzone();
//console.log(lightbox)
}); });
// 组件卸载时销毁 // 组件卸载时销毁