optimized image list interface

This commit is contained in:
2025-02-17 11:21:57 +08:00
parent d5fd626242
commit 5c0009f0b4
12 changed files with 482 additions and 32 deletions

View File

@@ -310,3 +310,22 @@ export const queryThingDetailListApi = (tag_name: string, provider: string, buck
hitSource: ["upload-file"], hitSource: ["upload-file"],
}); });
}; };
/**
* 获取单个照片url
* @param id
*/
export const getSingleImageApi = (id: number) => {
return service.Post('/api/auth/storage/image/url/single', {
id: id,
}, {
cacheFor: {
expire: 60 * 60 * 24 * 7,
mode: "restore",
},
meta: {
ignoreToken: false,
signature: false,
},
});
};

View File

@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<Props>(), {
iconPosition: 'top-left', iconPosition: 'top-left',
margin: '16px', margin: '16px',
borderRadius: '8px', borderRadius: '8px',
backgroundColor: '#e5eeff', backgroundColor: 'transparent',
showHoverCircle: true, // 默认显示悬停圆环 showHoverCircle: true, // 默认显示悬停圆环
iconSize: 24, // 默认图标大小 iconSize: 24, // 默认图标大小
showSelectedEffect: true, // 默认显示选中效果 showSelectedEffect: true, // 默认显示选中效果
@@ -100,6 +100,7 @@ function toggleSelection() {
.check-card.selected { .check-card.selected {
border: 1px solid rgba(125, 167, 255, 0.68); border: 1px solid rgba(125, 167, 255, 0.68);
box-shadow: 0 0 2px rgba(77, 167, 255, 0.89); box-shadow: 0 0 2px rgba(77, 167, 255, 0.89);
background-color: #e5eeff;
} }
.card-content { .card-content {

View File

@@ -10,6 +10,9 @@ interface UploadPredictResult {
height: number | null; height: number | null;
latitude: number | null; latitude: number | null;
longitude: number | null; longitude: number | null;
thumb_w: number | null;
thumb_h: number | null;
thumb_size: number | null;
} }
@@ -28,6 +31,9 @@ export const useUploadStore = defineStore(
height: null, height: null,
latitude: null, latitude: null,
longitude: null, longitude: null,
thumb_w: null,
thumb_h: null,
thumb_size: null,
}); });
/** /**
@@ -49,6 +55,10 @@ export const useUploadStore = defineStore(
predictResult.width = null; predictResult.width = null;
predictResult.height = null; predictResult.height = null;
predictResult.latitude = null; predictResult.latitude = null;
predictResult.longitude = null;
predictResult.thumb_w = null;
predictResult.thumb_h = null;
predictResult.thumb_size = null;
} }

View File

@@ -0,0 +1,59 @@
export function convertToImageBasedOnType(file: File): Promise<Blob> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.src = e.target?.result as string;
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
reject('Failed to get canvas context');
return;
}
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
// 根据文件的 MIME 类型判断是否需要转换
const mimeType = file.type;
if (mimeType === 'image/png') {
// 如果是 PNG 格式,转换为 JPEG 格式
canvas.toBlob((blob) => {
if (blob) {
resolve(blob); // 返回 JPEG 格式的图像
} else {
reject('Failed to convert to JPEG');
}
}, 'image/jpeg');
} else if (mimeType === 'image/jpeg') {
// 如果是 JPEG 格式,不需要转换
canvas.toBlob((blob) => {
if (blob) {
resolve(blob); // 返回 JPEG 格式的图像
} else {
reject('Failed to convert to JPEG');
}
}, 'image/jpeg');
} else {
reject('Unsupported image format');
}
};
img.onerror = () => {
reject('Failed to load image');
};
};
reader.onerror = () => {
reject('Failed to read file');
};
reader.readAsDataURL(file); // 读取文件为 DataURL
});
}

View File

@@ -0,0 +1,124 @@
// 定义返回数据的类型
interface ThumbnailResult {
binaryData: Blob | null;
width: number;
height: number;
size: number; // 添加 size 字段,表示缩略图的大小
}
// 工具函数生成视频或图片缩略图并返回二进制数据Blob及宽高和大小
export const generateThumbnail = (file: File): Promise<ThumbnailResult> => {
return new Promise((resolve, reject) => {
const fileType = file.type.toLowerCase();
const supportedImageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'];
const supportedVideoTypes = ['video/mp4', 'video/webm', 'video/ogg'];
// 判断文件类型
if (supportedImageTypes.includes(fileType)) {
// 如果是图片文件
createImageThumbnail(file)
.then((result) => resolve(result))
.catch(reject);
} else if (supportedVideoTypes.includes(fileType)) {
// 如果是视频文件
createVideoThumbnail(file)
.then((result) => resolve(result))
.catch(reject);
} else {
reject(new Error('不支持的文件类型'));
}
});
};
// 生成图片的缩略图
const createImageThumbnail = (file: File): Promise<ThumbnailResult> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const fixedHeight = 200;
const scaleFactor = fixedHeight / img.height;
const width = img.width * scaleFactor;
const height = fixedHeight;
canvas.width = width;
canvas.height = height;
ctx?.drawImage(img, 0, 0, width, height);
// 转换为二进制数据Blob
canvas.toBlob((blob) => {
if (blob) {
resolve({
binaryData: blob,
width: width,
height: height,
size: blob.size, // 获取缩略图的大小
});
} else {
reject(new Error('生成二进制数据失败'));
}
}, file.type);
};
img.src = event.target?.result as string;
};
reader.onerror = () => {
reject(new Error('读取文件失败'));
};
reader.readAsDataURL(file);
});
};
// 生成视频的缩略图
const createVideoThumbnail = (file: File): Promise<ThumbnailResult> => {
return new Promise((resolve, reject) => {
const videoElement = document.createElement('video');
const objectURL = URL.createObjectURL(file);
videoElement.src = objectURL;
videoElement.onloadeddata = () => {
// 在第一帧加载完成时获取图像
videoElement.currentTime = 2; // 设置视频播放位置为 2 秒(可调节)
videoElement.onseeked = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx) {
// 将视频帧绘制到 canvas 上
const fixedHeight = 200;
const scaleFactor = fixedHeight / videoElement.videoHeight;
const width = videoElement.videoWidth * scaleFactor;
const height = fixedHeight;
canvas.width = width;
canvas.height = height;
ctx.drawImage(videoElement, 0, 0, width, height);
// 将 canvas 转为图像 URL
canvas.toBlob((blob) => {
if (blob) {
resolve({
binaryData: blob,
width: width,
height: height,
size: blob.size, // 获取缩略图的大小
});
} else {
reject(new Error('生成视频缩略图失败'));
}
});
} else {
reject(new Error('无法获取 canvas 上下文'));
}
};
};
videoElement.onerror = () => {
reject(new Error('加载视频失败'));
};
});
};

View File

@@ -2,7 +2,7 @@
<div class="location-album-detail"> <div class="location-album-detail">
<div class="location-album-detail-header"> <div class="location-album-detail-header">
<div class="location-detail-content-nav"> <div class="location-detail-content-nav">
<AButton size="large" type="text" class="location-detail-content-nav-title">地点</AButton> <AButton size="large" type="text" class="location-detail-content-nav-title" @click="goBack">地点</AButton>
<span class="location-detail-content-nav-separator"> > </span> <span class="location-detail-content-nav-separator"> > </span>
<span class="location-detail-content-nav-name">乌鲁木齐</span> <span class="location-detail-content-nav-name">乌鲁木齐</span>
</div> </div>
@@ -11,13 +11,74 @@
<span style="font-size: 14px;color: #999999">共12张照片</span> <span style="font-size: 14px;color: #999999">共12张照片</span>
</div> </div>
<div class="location-album-detail-list"> <div class="location-album-detail-list">
<div style="width:100%;height:100%;">
<div v-for="(itemList, index) in albumList" :key="index">
<span style="margin-left: 10px;font-size: 13px">{{ itemList.date }}</span>
<AImagePreviewGroup>
<Vue3JustifiedLayout v-model:list="itemList.list" :options="options">
<template #default="{ item }">
<CheckCard :key="index"
class="photo-item"
margin="0"
border-radius="0"
v-model="selected"
:showHoverCircle="true"
:iconSize="20"
:showSelectedEffect="true"
:value="item.id">
<AImage :src="item.thumbnail"
:alt="item.file_name"
:key="index"
:height="200"
:previewMask="false"
:preview="{
src: item.url,
}"
loading="lazy"/>
</CheckCard>
</template>
</Vue3JustifiedLayout>
</AImagePreviewGroup>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Vue3JustifiedLayout from "vue3-justified-layout";
import 'vue3-justified-layout/dist/style.css';
import {queryLocationDetailListApi} from "@/api/storage";
const selected = ref<(string | number)[]>([]);
const albumList = ref<any[]>([]);
const route = useRoute();
const router = useRouter();
const options = reactive({
targetRowHeight: 200 // 高度
});
async function getImageList(id: number) {
const res: any = await queryLocationDetailListApi(id, "ali", "schisandra-album");
console.log(res);
if (res && res.code === 200) {
albumList.value = res.data.records;
}
}
onMounted(() => {
const idParam = route.params.id;
const albumId = Array.isArray(idParam) ? idParam[0] : idParam;
getImageList(parseInt(albumId, 10));
});
/**
* 返回上一页
*/
function goBack(): void {
router.go(-1);
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.location-album-detail { .location-album-detail {
@@ -47,7 +108,7 @@
justify-content: flex-start; justify-content: flex-start;
width: 1000%; width: 1000%;
height: 100%; height: 100%;
gap: 10px; gap: 5px;
.location-detail-content-nav-title { .location-detail-content-nav-title {
font-size: 20px; font-size: 20px;
@@ -75,7 +136,7 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
margin-left: 15px; margin-left: 30px;
width: 100%; width: 100%;
height: 22px; height: 22px;
} }
@@ -83,8 +144,6 @@
.location-album-detail-list { .location-album-detail-list {
width: 99%; width: 99%;
height: 100%; height: 100%;
margin-left: 5px;
background: #e2e2e2;
} }
} }

View File

@@ -2,7 +2,7 @@
<div class="people-album-detail"> <div class="people-album-detail">
<div class="people-album-detail-header"> <div class="people-album-detail-header">
<div class="people-album-detail-nav"> <div class="people-album-detail-nav">
<AButton type="text" size="large" class="people-album-detail-nav-button"> <AButton type="text" size="large" class="people-album-detail-nav-button" @click="goBack">
<template #icon> <template #icon>
<LeftOutlined style="font-size: 13px;font-weight: bolder"/> <LeftOutlined style="font-size: 13px;font-weight: bolder"/>
</template> </template>
@@ -14,16 +14,79 @@
<span style="font-size: 14px;color: #333333">张皓扬</span> <span style="font-size: 14px;color: #333333">张皓扬</span>
</div> </div>
</div> </div>
<ImageToolbar :selected="selected"/>
<div class="people-album-detail-info"> <div class="people-album-detail-info">
<span style="font-size: 14px;color: #999999">共12张照片</span> <span style="font-size: 14px;color: #999999">共12张照片</span>
</div> </div>
<div class="people-album-detail-list"> <div class="people-album-detail-list">
<div style="width:100%;height:100%;">
<div v-for="(itemList, index) in albumList" :key="index">
<span style="margin-left: 10px;font-size: 13px">{{ itemList.date }}</span>
<AImagePreviewGroup>
<Vue3JustifiedLayout v-model:list="itemList.list" :options="options">
<template #default="{ item }">
<CheckCard :key="index"
class="photo-item"
margin="0"
border-radius="0"
v-model="selected"
:showHoverCircle="true"
:iconSize="20"
:showSelectedEffect="true"
:value="item.id">
<AImage :src="item.thumbnail"
:alt="item.file_name"
:key="index"
:height="200"
:previewMask="false"
:preview="{
src: item.url,
}"
loading="lazy"/>
</CheckCard>
</template>
</Vue3JustifiedLayout>
</AImagePreviewGroup>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Vue3JustifiedLayout from "vue3-justified-layout";
import 'vue3-justified-layout/dist/style.css';
import {getFaceSamplesDetailList} from "@/api/storage";
import ImageToolbar from "@/views/Photograph/ImageToolbar/ImageToolbar.vue";
const selected = ref<(string | number)[]>([]);
const albumList = ref<any[]>([]);
const route = useRoute();
const router = useRouter();
const options = reactive({
targetRowHeight: 200 // 高度
});
async function getAlbumList(id: number) {
const res: any = await getFaceSamplesDetailList(id, "ali", "schisandra-album");
if (res && res.code === 200) {
albumList.value = res.data.records;
}
}
onMounted(() => {
const idParam = route.params.id;
const albumId = Array.isArray(idParam) ? idParam[0] : idParam;
getAlbumList(parseInt(albumId, 10));
});
/**
* 返回上一页
*/
function goBack(): void {
router.go(-1);
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.people-album-detail { .people-album-detail {
@@ -62,6 +125,7 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 8px;
} }
} }
@@ -89,8 +153,6 @@
.people-album-detail-list { .people-album-detail-list {
width: 99%; width: 99%;
height: 100%; height: 100%;
margin-left: 10px;
background: #e2e2e2;
} }
} }

View File

@@ -73,7 +73,7 @@
<AAvatar :size="86" shape="circle" :src="item.face_image"/> <AAvatar :size="86" shape="circle" :src="item.face_image"/>
</div> </div>
<div class="people-album-item-name" v-show="!item.face_name"> <div class="people-album-item-name" v-show="!item.face_name">
<AButton @click="showAddNameInput(index)" class="people-album-add-name" <AButton @click.stop="showAddNameInput(index)" class="people-album-add-name"
v-show="item.showButton && !item.showInput" v-show="item.showButton && !item.showInput"
type="link" type="link"
size="small"> size="small">
@@ -82,11 +82,12 @@
<AInput ref="addNameInput" v-model:value="addNameInputValue" v-show="item.showInput" <AInput ref="addNameInput" v-model:value="addNameInputValue" v-show="item.showInput"
@blur="hideAddNameInput(index)" size="small" @blur="hideAddNameInput(index)" size="small"
:maxlength="10" :maxlength="10"
@click.stop
:placeholder="item.face_name" :placeholder="item.face_name"
class="people-album-add-input"> class="people-album-add-input">
<template #suffix> <template #suffix>
<AButton type="link" style="font-size: 12px;" size="small" @mousedown.prevent <AButton type="link" style="font-size: 12px;" size="small" @mousedown.prevent
@click="modifyFaceName(item.id,index)">完成 @click.stop="modifyFaceName(item.id,index)">完成
</AButton> </AButton>
</template> </template>
</AInput> </AInput>

View File

@@ -59,12 +59,14 @@
:iconSize="20" :iconSize="20"
:showSelectedEffect="true" :showSelectedEffect="true"
:value="item.id"> :value="item.id">
<AImage :src="item.url" <AImage :src="item.thumbnail"
:alt="item.file_name" :alt="item.file_name"
:key="index" :key="index"
style="height: 200px" :height="200"
:previewMask="false" :previewMask="false"
fallback="data:image/png;base64,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" :preview="{
src: item.url,
}"
loading="lazy"/> loading="lazy"/>
</CheckCard> </CheckCard>
</template> </template>

View File

@@ -2,22 +2,87 @@
<div class="thing-album-detail"> <div class="thing-album-detail">
<div class="thing-album-detail-header"> <div class="thing-album-detail-header">
<div class="thing-detail-content-nav"> <div class="thing-detail-content-nav">
<AButton size="large" type="text" class="thing-detail-content-nav-title">人物</AButton> <AButton size="large" type="text" class="thing-detail-content-nav-title" @click="goBack">人物</AButton>
<span class="thing-detail-content-nav-separator"> > </span> <span class="thing-detail-content-nav-separator"> > </span>
<span class="thing-detail-content-nav-name">人物</span> <span class="thing-detail-content-nav-name">人物</span>
</div> </div>
</div> </div>
<ImageToolbar :selected="selected"/>
<div class="thing-album-detail-info"> <div class="thing-album-detail-info">
<span style="font-size: 14px;color: #999999">共12张照片</span> <span style="font-size: 14px;color: #999999">共12张照片</span>
</div> </div>
<div class="thing-album-detail-list"> <div class="thing-album-detail-list">
<div style="width:100%;height:100%;">
<div v-for="(itemList, index) in albumList" :key="index">
<span style="margin-left: 10px;font-size: 13px">{{ itemList.date }}</span>
<AImagePreviewGroup>
<Vue3JustifiedLayout v-model:list="itemList.list" :options="options">
<template #default="{ item }">
<CheckCard :key="index"
class="photo-item"
margin="0"
border-radius="0"
v-model="selected"
:showHoverCircle="true"
:iconSize="20"
:showSelectedEffect="true"
:value="item.id">
<AImage :src="item.thumbnail"
:alt="item.file_name"
:key="index"
:height="200"
:previewMask="false"
:preview="{
src: item.url,
}"
loading="lazy"/>
</CheckCard>
</template>
</Vue3JustifiedLayout>
</AImagePreviewGroup>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Vue3JustifiedLayout from "vue3-justified-layout";
import 'vue3-justified-layout/dist/style.css';
import {queryThingDetailListApi} from "@/api/storage";
import ImageToolbar from "@/views/Photograph/ImageToolbar/ImageToolbar.vue";
const selected = ref<(string | number)[]>([]);
const albumList = ref<any[]>([]);
const route = useRoute();
const router = useRouter();
const options = reactive({
targetRowHeight: 200 // 高度
});
async function getImageList(tag_name: string) {
const res: any = await queryThingDetailListApi(tag_name, "ali", "schisandra-album");
if (res && res.code === 200) {
albumList.value = res.data.records;
}
}
onMounted(() => {
const idParam = route.params.id;
const tag_name = Array.isArray(idParam) ? idParam[0] : idParam;
getImageList(tag_name);
});
/**
* 返回上一页
*/
function goBack(): void {
router.go(-1);
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.thing-album-detail { .thing-album-detail {
@@ -75,7 +140,7 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
margin-left: 15px; margin-left: 30px;
width: 100%; width: 100%;
height: 22px; height: 22px;
} }
@@ -83,8 +148,7 @@
.thing-album-detail-list { .thing-album-detail-list {
width: 99%; width: 99%;
height: 100%; height: 100%;
margin-left: 5px; //margin-left: 5px;
background: #e2e2e2;
} }
} }

View File

@@ -14,7 +14,7 @@
创建相册 创建相册
</AButton> </AButton>
</div> </div>
<image-toolbar :selected="selected" /> <image-toolbar :selected="selected"/>
<div class="photo-list"> <div class="photo-list">
<ATabs size="small" :tabBarGutter="50" type="line" tabPosition="top" :tabBarStyle="{position:'unset'}" <ATabs size="small" :tabBarGutter="50" type="line" tabPosition="top" :tabBarStyle="{position:'unset'}"
style="width: 99%;"> style="width: 99%;">
@@ -37,11 +37,14 @@
:iconSize="20" :iconSize="20"
:showSelectedEffect="true" :showSelectedEffect="true"
:value="item.id"> :value="item.id">
<AImage :src="item.url" <AImage :src="item.thumbnail"
:alt="item.file_name" :alt="item.file_name"
:key="index" :key="index"
style="height: 200px" :height="200"
:previewMask="false" :previewMask="false"
:preview="{
src: item.url,
}"
loading="lazy"/> loading="lazy"/>
</CheckCard> </CheckCard>
</template> </template>
@@ -73,7 +76,7 @@ import Vue3JustifiedLayout from "vue3-justified-layout";
import 'vue3-justified-layout/dist/style.css'; import 'vue3-justified-layout/dist/style.css';
import ImageUpload from "@/views/Photograph/ImageUpload/ImageUpload.vue"; import ImageUpload from "@/views/Photograph/ImageUpload/ImageUpload.vue";
import useStore from "@/store"; import useStore from "@/store";
import {queryAllImagesApi} from "@/api/storage"; import {getSingleImageApi, queryAllImagesApi} from "@/api/storage";
import ImageToolbar from "@/views/Photograph/ImageToolbar/ImageToolbar.vue"; import ImageToolbar from "@/views/Photograph/ImageToolbar/ImageToolbar.vue";
@@ -87,7 +90,9 @@ const options = reactive({
const images = ref<any[]>([]); const images = ref<any[]>([]);
/**
* 获取所有图片
*/
async function getAllImages() { async function getAllImages() {
const res: any = await queryAllImagesApi("image", false, "ali", "schisandra-album"); const res: any = await queryAllImagesApi("image", false, "ali", "schisandra-album");
if (res && res.code === 200) { if (res && res.code === 200) {
@@ -95,6 +100,30 @@ async function getAllImages() {
} }
} }
// const previewUrl = ref<string>("");
//
// /**
// * 获取单张图片
// * @param id
// */
// async function getSingleImage(id: number) {
// previewUrl.value = "";
// const res: any = await getSingleImageApi(id);
// if (res && res.code === 200) {
// previewUrl.value = res.data;
// setVisible(true);
// return;
// }
// previewUrl.value = "data:image/png;base64,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";
// setVisible(true);
// }
//
// const visible = ref<boolean>(false);
//
// const setVisible = (value): void => {
// visible.value = value;
// };
onMounted(() => { onMounted(() => {
getAllImages(); getAllImages();

View File

@@ -62,6 +62,7 @@ import imageCompression from "browser-image-compression";
import exifr from 'exifr'; import exifr from 'exifr';
import isScreenshot from "@/utils/imageUtils/isScreenshot.ts"; import isScreenshot from "@/utils/imageUtils/isScreenshot.ts";
import {getCategoryByLabel} from "@/constant/coco_ssd_label_category.ts"; import {getCategoryByLabel} from "@/constant/coco_ssd_label_category.ts";
import {generateThumbnail} from "@/utils/imageUtils/generateThumb.ts";
const predicting = ref<boolean>(false); const predicting = ref<boolean>(false);
const progressPercent = ref<number>(0); const progressPercent = ref<number>(0);
@@ -145,7 +146,7 @@ async function beforeUpload(file: File, fileList: File[]) {
} }
// 提取 EXIF 数据 // 提取 EXIF 数据
const gpsData = await extractGPSExifData(file); const gpsData: any = await extractGPSExifData(file);
if (gpsData) { if (gpsData) {
upload.predictResult.longitude = gpsData.longitude; upload.predictResult.longitude = gpsData.longitude;
upload.predictResult.latitude = gpsData.latitude; upload.predictResult.latitude = gpsData.latitude;
@@ -210,12 +211,21 @@ const {uploading, send: submitFile, abort} = useRequest(uploadFile, {
* @param file * @param file
*/ */
async function customUploadRequest(file: any) { async function customUploadRequest(file: any) {
const compressedFile = await imageCompression(file.file, options);
// 生成缩略图
const {binaryData, width, height, size} = await generateThumbnail(compressedFile);
upload.predictResult.thumb_w = width;
upload.predictResult.thumb_h = height;
upload.predictResult.thumb_size = size;
const formData = new FormData(); const formData = new FormData();
formData.append("file", file.file); formData.append("file", file.file);
if (binaryData) {
formData.append("thumbnail", binaryData);
}
formData.append("data", JSON.stringify({ formData.append("data", JSON.stringify({
provider: 'ali', provider: 'ali',
bucket: 'schisandra', bucket: 'schisandra-album',
fileType: file.file.type, fileType: file.file.type,
...upload.predictResult, ...upload.predictResult,
})); }));
@@ -276,14 +286,24 @@ async function extractGPSExifData(file) {
if (!supportedFormats.includes(file.type)) { if (!supportedFormats.includes(file.type)) {
return null; return null;
} }
const options: any = {
ifd0: false,
exif: false,
gps: ['GPSLatitudeRef', 'GPSLatitude', 0x0003, 0x0004],
interop: false,
ifd1: false // thumbnail
};
// 提取GPS EXIF 数据 // 提取GPS EXIF 数据
let {latitude, longitude} = await exifr.gps(file); const gpsData = await exifr.parse(file, options);
if (latitude && longitude) { if (!gpsData) {
return {latitude, longitude};
} else {
return null; return null;
} }
const {latitude, longitude} = gpsData;
if (latitude && longitude) {
return {latitude, longitude};
}
return null;
} }
</script> </script>