improve image sharing function

This commit is contained in:
2025-02-20 23:03:25 +08:00
parent 3995884adc
commit 2063a99c83
20 changed files with 1150 additions and 550 deletions

10
components.d.ts vendored
View File

@@ -2,6 +2,7 @@
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
@@ -39,6 +40,7 @@ declare module 'vue' {
AModal: typeof import('ant-design-vue/es')['Modal']
AnimatedNature: typeof import('./src/components/AnimatedNature/AnimatedNature.vue')['default']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
APopover: typeof import('ant-design-vue/es')['Popover']
AProgress: typeof import('ant-design-vue/es')['Progress']
AQrcode: typeof import('ant-design-vue/es')['QRCode']
@@ -64,6 +66,7 @@ declare module 'vue' {
BoxDog: typeof import('./src/components/BoxDog/BoxDog.vue')['default']
Card3D: typeof import('./src/components/Card3D/Card3D.vue')['default']
CheckCard: typeof import('./src/components/CheckCard/CheckCard.vue')['default']
CheckCircleOutlined: typeof import('@ant-design/icons-vue')['CheckCircleOutlined']
CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined']
CloseOutlined: typeof import('@ant-design/icons-vue')['CloseOutlined']
Clouds: typeof import('./src/components/Clouds/Clouds.vue')['default']
@@ -71,6 +74,7 @@ declare module 'vue' {
CommentList: typeof import('./src/components/CommentReply/src/CommentList/CommentList.vue')['default']
CommentReply: typeof import('./src/components/CommentReply/index.vue')['default']
CompareImage: typeof import('./src/views/Upscale/CompareImage.vue')['default']
CopyOutlined: typeof import('@ant-design/icons-vue')['CopyOutlined']
DeleteOutlined: typeof import('@ant-design/icons-vue')['DeleteOutlined']
DownloadOutlined: typeof import('@ant-design/icons-vue')['DownloadOutlined']
DownOutlined: typeof import('@ant-design/icons-vue')['DownOutlined']
@@ -87,6 +91,7 @@ declare module 'vue' {
InboxOutlined: typeof import('@ant-design/icons-vue')['InboxOutlined']
LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default']
LeftOutlined: typeof import('@ant-design/icons-vue')['LeftOutlined']
LinkOutlined: typeof import('@ant-design/icons-vue')['LinkOutlined']
LoadingGraphic: typeof import('./src/components/LoadingGraphic/LoadingGraphic.vue')['default']
LocationAlbum: typeof import('./src/views/Album/LocationAlbum/LocationAlbum.vue')['default']
LocationAlbumDetail: typeof import('./src/views/Album/LocationAlbum/LocationAlbumDetail.vue')['default']
@@ -105,7 +110,7 @@ declare module 'vue' {
Phoalbum: typeof import('./src/views/Album/Phoalbum/Phoalbum.vue')['default']
PhoalbumDetail: typeof import('./src/views/Album/Phoalbum/PhoalbumDetail.vue')['default']
PhoalbumList: typeof import('./src/views/Album/Phoalbum/PhoalbumList.vue')['default']
PhoneUpload: typeof import('./src/views/PhoneUpload/PhoneUpload.vue')['default']
PhoneUpload: typeof import('./src/views/Phone/UpscalePhoneUpload/PhoneUpload.vue')['default']
PhotoStack: typeof import('./src/components/PhotoStack/PhotoStack.vue')['default']
PlusOutlined: typeof import('@ant-design/icons-vue')['PlusOutlined']
PlusSquareOutlined: typeof import('@ant-design/icons-vue')['PlusSquareOutlined']
@@ -123,6 +128,8 @@ declare module 'vue' {
SearchOutlined: typeof import('@ant-design/icons-vue')['SearchOutlined']
SendOutlined: typeof import('@ant-design/icons-vue')['SendOutlined']
ShareAltOutlined: typeof import('@ant-design/icons-vue')['ShareAltOutlined']
SharePhoneUpload: typeof import('./src/views/Phone/SharePhoneUpload/SharePhoneUpload.vue')['default']
ShareUpload: typeof import('./src/views/ImageShare/ShareUpload.vue')['default']
Spin: typeof import('./src/components/MyUI/Spin/Spin.vue')['default']
StarButton: typeof import('./src/components/StarButton/StarButton.vue')['default']
TabletOutlined: typeof import('@ant-design/icons-vue')['TabletOutlined']
@@ -132,6 +139,7 @@ declare module 'vue' {
Tooltip: typeof import('./src/components/MyUI/Tooltip/Tooltip.vue')['default']
UploadImage: typeof import('./src/views/Upscale/UploadImage.vue')['default']
Upscale: typeof import('./src/views/Upscale/index.vue')['default']
UpscalePhoneUpload: typeof import('./src/views/Phone/UpscalePhoneUpload/UpscalePhoneUpload.vue')['default']
UserInfoCard: typeof import('./src/components/CommentReply/src/UserInfoCard/UserInfoCard.vue')['default']
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
VueCompareImage: typeof import('./src/components/VueCompareImage/VueCompareImage.vue')['default']

View File

@@ -52,6 +52,7 @@
"json-stringify-safe": "^5.0.1",
"less": "^4.2.2",
"localforage": "^1.10.0",
"moment": "^2.30.1",
"nprogress": "^0.2.0",
"nsfwjs": "^4.2.1",
"pinia": "^3.0.1",
@@ -79,15 +80,15 @@
"sass": "^1.85.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"unplugin-vue-components": "^28.2.0",
"vite": "^6.1.0",
"unplugin-vue-components": "^28.4.0",
"vite": "^6.1.1",
"vite-plugin-bundle-obfuscator": "1.4.1",
"vite-plugin-chunk-split": "^0.5.0",
"vue-tsc": "2.2.2"
},
"overrides": {
"vite-plugin-chunk-split": {
"vite": "^6.1.0"
"vite": "^6.1.1"
}
}
}

43
src/api/share/index.ts Normal file
View File

@@ -0,0 +1,43 @@
import {service} from "@/utils/alova/service.ts";
/**
* 上传分享图片
* @param formData
*/
export const shareImageUploadApi = (formData) => {
return service.Post('/api/auth/share/upload', {...formData}, {
meta: {
ignoreToken: false,
signature: false,
},
});
};
/**
* 查询分享图片列表
* @param share_code
* @param access_password
*/
export const queryShareImageApi = (share_code: string, access_password: string) => {
return service.Post('/api/auth/share/image/list', {
share_code: share_code,
access_password: access_password,
}, {
meta: {
ignoreToken: false,
signature: false,
},
});
};
/**
* 查询分享记录列表
*/
export const queryShareRecordListApi = (dataRequest: string[]) => {
return service.Post('/api/auth/share/record/list', {
date_range: dataRequest,
}, {
meta: {
ignoreToken: false,
signature: false,
},
});
};

View File

@@ -110,7 +110,7 @@ export const createAlbumApi = (name: string) => {
* @param type
* @param sort
*/
export const albumListApi = (type: string, sort: boolean) => {
export const albumListApi = (type: number, sort: boolean) => {
return service.Post('/api/auth/storage/album/list', {
type: type,
sort: sort,
@@ -344,3 +344,5 @@ export const getStorageConfigListApi = () => {
},
});
};

View File

@@ -2,7 +2,7 @@ export default [
{
path: '/upscale/app',
name: 'upscaleApp',
component: () => import('@/views/PhoneUpload/PhoneUpload.vue'),
component: () => import('@/views/Phone/UpscalePhoneUpload/UpscalePhoneUpload.vue'),
meta: {
requiresAuth: false,
title: '手机上传'

View File

@@ -62,7 +62,7 @@ const options = reactive({
});
async function getImageList(id: number) {
const res: any = await queryLocationDetailListApi(id, upload.storageSelected[0], upload.storageSelected[1]);
const res: any = await queryLocationDetailListApi(id, upload.storageSelected?.[0], upload.storageSelected?.[1]);
console.log(res);
if (res && res.code === 200) {
albumList.value = res.data.records;

View File

@@ -44,7 +44,7 @@ async function getLocationAlbums(provider: string, bucket: string) {
}
onMounted(() => {
getLocationAlbums(upload.storageSelected[0], upload.storageSelected[1]);
getLocationAlbums(upload.storageSelected?.[0], upload.storageSelected?.[1]);
});
</script>
<style scoped lang="scss">

View File

@@ -72,7 +72,7 @@ const options = reactive({
});
async function getAlbumList(id: number) {
const res: any = await getFaceSamplesDetailList(id, upload.storageSelected[0], upload.storageSelected[1]);
const res: any = await getFaceSamplesDetailList(id, upload.storageSelected?.[0], upload.storageSelected?.[1]);
if (res && res.code === 200) {
albumList.value = res.data.records;
}

View File

@@ -98,7 +98,7 @@ const upload = useStore().upload;
async function getAlbumList(id: number) {
const res: any = await queryAlbumDetailListApi(id, upload.storageSelected[0], upload.storageSelected[1]);
const res: any = await queryAlbumDetailListApi(id, upload.storageSelected?.[0], upload.storageSelected?.[1]);
if (res && res.code === 200) {
albumList.value = res.data.records;
}

View File

@@ -54,7 +54,7 @@
<span
style="color: #999; font-size: 12px;">已全部加载 {{ albumList ? albumList.length : 0 }} 个相册</span>
</template>
<ATabPane key="0" tab="全部相册">
<ATabPane key="-1" tab="全部相册">
<ASpin tip="Loading..." :spinning="loading" size="large">
<div class="phoalbum-item-container">
<div class="phoalbum-item"
@@ -63,7 +63,8 @@
@click.prevent="handleClick(album.id)"
@mouseover="isHovered = index"
@mouseleave="isHovered = null">
<PhotoStack :src="album.cover_image" :default-src="default_cover"/>
<PhotoStack :src="album.cover_image ?`data:image/png;base64,`+album.cover_image: ``"
:default-src="default_cover"/>
<div class="phoalbum-item-info">
<span class="phoalbum-item-name">{{ album.name }}</span>
<span class="phoalbum-item-date">{{ album.created_at }}</span>
@@ -103,11 +104,155 @@
</div>
</ASpin>
</ATabPane>
<ATabPane key="1" tab="我的相册">
<ATabPane key="0" tab="我的相册">
<ASpin tip="Loading..." :spinning="loading" size="large">
<div class="phoalbum-item-container">
<div class="phoalbum-item"
v-for="(album, index) in albumList"
:key="album.id"
@click.prevent="handleClick(album.id)"
@mouseover="isHovered = index"
@mouseleave="isHovered = null">
<PhotoStack :src="album.cover_image ?`data:image/png;base64,`+album.cover_image: ``"
:default-src="default_cover"/>
<div class="phoalbum-item-info">
<span class="phoalbum-item-name">{{ album.name }}</span>
<span class="phoalbum-item-date">{{ album.created_at }}</span>
</div>
<div class="phoalbum-item-operation"
:class="{ 'fade-in': isHovered === index, 'fade-out': isHovered !== index }">
<ADropdown trigger="click" @click.stop>
<AButton type="text" shape="circle" size="small" @click.prevent>
<template #icon>
<AAvatar shape="circle" size="small" :src="more"/>
</template>
</AButton>
<template #overlay>
<AMenu>
<APopover placement="left" trigger="click">
<AMenuItem key="1">重命名相册</AMenuItem>
<template #content>
<AInput :placeholder="album.name" class="phoalbum-create-input"
v-model:value="albumRenameValue">
<template #suffix>
<AButton type="text" @click.prevent size="small" style="color: #0e87cc"
@click="renameAlbum(album.id, albumRenameValue)">
确认
</AButton>
</template>
</AInput>
</template>
</APopover>
<AMenuItem key="2">分享相册</AMenuItem>
<AMenuItem key="3" @click.prevent="deleteAlbum(album.id)">删除相册</AMenuItem>
<AMenuItem key="4">下载相册</AMenuItem>
</AMenu>
</template>
</ADropdown>
</div>
</div>
</div>
</ASpin>
</ATabPane>
<ATabPane key="1" tab="我的分享">
<ASpin tip="Loading..." :spinning="loading" size="large">
<div class="phoalbum-item-container">
<div class="phoalbum-item"
v-for="(album, index) in albumList"
:key="album.id"
@click.prevent="handleClick(album.id)"
@mouseover="isHovered = index"
@mouseleave="isHovered = null">
<PhotoStack :src="album.cover_image ?`data:image/png;base64,`+album.cover_image: ``"
:default-src="default_cover"/>
<div class="phoalbum-item-info">
<span class="phoalbum-item-name">{{ album.name }}</span>
<span class="phoalbum-item-date">{{ album.created_at }}</span>
</div>
<div class="phoalbum-item-operation"
:class="{ 'fade-in': isHovered === index, 'fade-out': isHovered !== index }">
<ADropdown trigger="click" @click.stop>
<AButton type="text" shape="circle" size="small" @click.prevent>
<template #icon>
<AAvatar shape="circle" size="small" :src="more"/>
</template>
</AButton>
<template #overlay>
<AMenu>
<APopover placement="left" trigger="click">
<AMenuItem key="1">重命名相册</AMenuItem>
<template #content>
<AInput :placeholder="album.name" class="phoalbum-create-input"
v-model:value="albumRenameValue">
<template #suffix>
<AButton type="text" @click.prevent size="small" style="color: #0e87cc"
@click="renameAlbum(album.id, albumRenameValue)">
确认
</AButton>
</template>
</AInput>
</template>
</APopover>
<AMenuItem key="2">分享相册</AMenuItem>
<AMenuItem key="3" @click.prevent="deleteAlbum(album.id)">删除相册</AMenuItem>
<AMenuItem key="4">下载相册</AMenuItem>
</AMenu>
</template>
</ADropdown>
</div>
</div>
</div>
</ASpin>
</ATabPane>
<ATabPane key="2" tab="收藏相册">
<ASpin tip="Loading..." :spinning="loading" size="large">
<div class="phoalbum-item-container">
<div class="phoalbum-item"
v-for="(album, index) in albumList"
:key="album.id"
@click.prevent="handleClick(album.id)"
@mouseover="isHovered = index"
@mouseleave="isHovered = null">
<PhotoStack :src="album.cover_image ?`data:image/png;base64,`+album.cover_image: ``"
:default-src="default_cover"/>
<div class="phoalbum-item-info">
<span class="phoalbum-item-name">{{ album.name }}</span>
<span class="phoalbum-item-date">{{ album.created_at }}</span>
</div>
<div class="phoalbum-item-operation"
:class="{ 'fade-in': isHovered === index, 'fade-out': isHovered !== index }">
<ADropdown trigger="click" @click.stop>
<AButton type="text" shape="circle" size="small" @click.prevent>
<template #icon>
<AAvatar shape="circle" size="small" :src="more"/>
</template>
</AButton>
<template #overlay>
<AMenu>
<APopover placement="left" trigger="click">
<AMenuItem key="1">重命名相册</AMenuItem>
<template #content>
<AInput :placeholder="album.name" class="phoalbum-create-input"
v-model:value="albumRenameValue">
<template #suffix>
<AButton type="text" @click.prevent size="small" style="color: #0e87cc"
@click="renameAlbum(album.id, albumRenameValue)">
确认
</AButton>
</template>
</AInput>
</template>
</APopover>
<AMenuItem key="2">分享相册</AMenuItem>
<AMenuItem key="3" @click.prevent="deleteAlbum(album.id)">删除相册</AMenuItem>
<AMenuItem key="4">下载相册</AMenuItem>
</AMenu>
</template>
</ADropdown>
</div>
</div>
</div>
</ASpin>
</ATabPane>
</ATabs>
</div>
@@ -141,7 +286,7 @@ async function createAlbumSubmit() {
const res: any = await createAlbumApi(albumNameValue.value);
if (res && res.code === 200) {
albumNameValue.value = "未命名相册";
await getAlbumList("0", selecetedKey.value);
await getAlbumList(0, selecetedKey.value);
} else {
message.error("创建相册失败");
}
@@ -153,7 +298,7 @@ async function createAlbumSubmit() {
*/
async function handleSelect({key}) {
selecetedKey.value = key;
await getAlbumList("0", key);
await getAlbumList(0, key);
}
/**
@@ -161,7 +306,7 @@ async function handleSelect({key}) {
* @param activeKey
*/
async function handleTabChange(activeKey: string) {
await getAlbumList(activeKey, selecetedKey.value);
await getAlbumList(parseInt(activeKey), selecetedKey.value);
}
/**
@@ -169,7 +314,7 @@ async function handleTabChange(activeKey: string) {
* @param type
* @param sort
*/
async function getAlbumList(type: string = "0", sort: boolean = true) {
async function getAlbumList(type: number = 0, sort: boolean = true) {
albumList.value = [];
loading.value = true;
const res: any = await albumListApi(type, sort);
@@ -192,7 +337,7 @@ async function renameAlbum(id: number, name: string) {
const res: any = await renameAlbumApi(id, name);
if (res && res.code === 200) {
albumRenameValue.value = "";
await getAlbumList("0", selecetedKey.value);
await getAlbumList(0, selecetedKey.value);
}
}
@@ -203,7 +348,7 @@ async function renameAlbum(id: number, name: string) {
async function deleteAlbum(id: number) {
const res: any = await deleteAlbumApi(id);
if (res && res.code === 200) {
await getAlbumList("0", selecetedKey.value);
await getAlbumList(0, selecetedKey.value);
} else {
message.error("删除相册失败");
}
@@ -221,7 +366,7 @@ function handleClick(id: number) {
}
onMounted(() => {
getAlbumList("0", selecetedKey.value);
getAlbumList(0, selecetedKey.value);
});
</script>

View File

@@ -52,11 +52,12 @@ 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";
import useStore from "@/store";
const selected = ref<(string | number)[]>([]);
const albumList = ref<any[]>([]);
const upload = useStore().upload;
const route = useRoute();
const router = useRouter();
const options = reactive({
@@ -64,7 +65,7 @@ const options = reactive({
});
async function getImageList(tag_name: string) {
const res: any = await queryThingDetailListApi(tag_name, "ali", "schisandra-album");
const res: any = await queryThingDetailListApi(tag_name, upload.storageSelected?.[0], upload.storageSelected?.[1]);
if (res && res.code === 200) {
albumList.value = res.data.records;
}

View File

@@ -34,7 +34,6 @@ const thingAlbumList = ref<any[]>([]);
async function getThingAlbumList(provider: string, bucket: string) {
const res: any = await queryThingAlbumApi(provider, bucket);
console.log(res);
if (res && res.code === 200) {
thingAlbumList.value = res.data.records;
}
@@ -53,7 +52,7 @@ function handleClick(id: string) {
}
onMounted(() => {
getThingAlbumList(upload.storageSelected[0], upload.storageSelected[1]);
getThingAlbumList(upload.storageSelected?.[0], upload.storageSelected?.[1]);
});
</script>

View File

@@ -45,8 +45,9 @@
<div class="image-share-left-bottom-title">
<h3>快传管理</h3>
<ARangePicker
:value="hackValue || value"
:value="selectedDateRange"
:disabled-date="disabledDate"
format="YYYY-MM-DD"
@change="onChange"
@openChange="onOpenChange"
@calendarChange="onCalendarChange"
@@ -55,167 +56,82 @@
/>
</div>
<div class="image-share-left-bottom-content">
<ACard style="width: 100%;height: 100%;" :bodyStyle="{padding: 0}">
<ATable :columns="columns" size="large" style="width: 100%;height: 100%;" :bordered="false">
<ACard style="width: 100%;height: 100%;"
:bodyStyle="{padding: 0, overflow: 'auto', display: 'flex', flexDirection: 'column'}">
<ATable :columns="columns" :data-source="dataSources" size="large"
style="flex: 1"
:pagination="false"
:loading="loading"
:scroll="{ y: '40vh',x:true }"
:bordered="false" @resizeColumn="handleResizeColumn">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'cover_image'">
<AAvatar shape="square" size="large" :src="`data:image/png;base64,`+record.cover_image"/>
</template>
<!-- 访问密码 -->
<template v-else-if="column.key === 'access_password'">
<AInputPassword
v-if="record.access_password"
v-model:value="record.access_password"
type="password"
:visibilityToggle="true"
:bordered="false"
size="small"
readOnly
style="width: 100px;"
/>
<p v-else style="color: #999">无密码</p>
</template>
<!-- 有效期 -->
<template v-else-if="column.key === 'validity_period'">
</template>
<template v-else-if="column.key === 'action'">
<ATooltip title="复制分享链接">
<AButton type="text" size="small" @click="copyToClipboard(record.share_code)">
<LinkOutlined />
</AButton>
</ATooltip>
<ATooltip title="删除快传记录">
<APopconfirm
title="确定删除该快传记录?"
ok-text="确定"
cancel-text="取消"
>
<AButton type="text" size="small" danger>
<DeleteOutlined/>
</AButton>
</APopconfirm>
</ATooltip>
</template>
</template>
</ATable>
</ACard>
</div>
</div>
</div>
<div class="image-share-right">
<div class="image-share-right-top">
<h3>照片快传</h3>
</div>
<div class="image-share-right-bottom">
<div class="image-share-right-bottom-content">
<div class="image-share-right-bottom-upload" ref="qrContainer" v-if="fileList.length<=0">
<AUploadDragger
name="file"
:multiple="true"
:showUploadList="false"
:beforeUpload="beforeUpload"
v-model:fileList="fileList"
class="image-share-right-upload"
>
<div class="image-share-right-upload-item">
<p class="ant-upload-drag-icon">
<ABadge :offset="[-15, 20]" :count="fileList.length">
<AAvatar shape="square" :size="folderIconSize" :src="folder"/>
</ABadge>
</p>
<p class="ant-upload-text" style="font-size: 2.6vh;font-weight: bolder">单击或拖动文件到此区域以上传</p>
<AButton type="primary" size="large" shape="round" style="width: 70%"> </AButton>
<ShareUpload/>
<div class="qr">
<AQrcode :bordered="false" color="rgba(126, 126, 135, 0.48)"
:size="qrcodeSize"
:value="`git.landaiqing.cneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjI1MTEyMjE3MzQyMDIxIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTczOTg3ODIyOCwibmJmIjoxNzM5ODcxMDI4LCJpYXQiOjE3Mzk4NzEwMjh9.EUiZsVjhGqHx1V5o90S3W5li6nIqucxy9eEY9LWgqXY`"
:icon="phone"
:iconSize="iconSize"
:status="`active`"
/>
<span style="font-size: 2vh;color: #999999">手机扫码上传</span>
</div>
</div>
</AUploadDragger>
</div>
<div class="image-share-right-bottom-container" v-else>
<div class="image-share-right-bottom-container-header">
<AInput v-model:value="titleName" :bordered="false" size="large" placeholder="给快传起个标题"/>
<ADropdown placement="bottomLeft" :trigger="['click']">
<template #overlay>
<AMenu>
<AMenuItem key="1">
<AUpload
name="file"
:multiple="true"
:showUploadList="false"
:beforeUpload="beforeUpload"
v-model:fileList="fileList"
>
上传文件
</AUpload>
</AMenuItem>
<AMenuItem key="2">
<APopover placement="bottomLeft" trigger="hover">
<template #content>
<AQrcode :bordered="false" color="rgba(126, 126, 135, 0.48)"
:size="qrcodeSize"
:value="`git.landaiqing.cneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjI1MTEyMjE3MzQyMDIxIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTczOTg3ODIyOCwibmJmIjoxNzM5ODcxMDI4LCJpYXQiOjE3Mzk4NzEwMjh9.EUiZsVjhGqHx1V5o90S3W5li6nIqucxy9eEY9LWgqXY`"
:icon="phone"
:iconSize="iconSize"
:status="`active`"
/>
</template>
手机上传
</APopover>
</AMenuItem>
</AMenu>
</template>
<AButton size="middle" shape="circle">
<template #icon>
<PlusOutlined/>
</template>
</AButton>
</ADropdown>
</div>
<div class="image-share-right-bottom-content-list">
<p style="font-size: 2.0vh;color: #999999;cursor: default">{{ fileList.length }}个文件
{{ calculateTotalSize(fileList) }}</p>
<div class="image-share-right-bottom-content-list-wrapper">
<div class="image-share-right-bottom-content-list-item" v-for="(item, index) in fileList" :key="index">
<div class="file-thumbnail">
<AImage :width="50" :height="50" :src="convertFileToUrl(item.originFileObj)">
<template #previewMask>
</template>
</AImage>
</div>
<div class="file-info">
<p style="font-size: 2.0vh;color: #333333;cursor: default;font-weight: bold">{{ item.name }}</p>
<p style="font-size: 1.5vh;color: #999999;cursor: default">{{
formatByteSize(item.size)
}}</p>
</div>
<div class="file-operation">
<AButton size="middle" shape="circle" type="text" @click="removeBase64Image(index)">
<template #icon>
<CloseOutlined/>
</template>
</AButton>
</div>
</div>
</div>
</div>
<ADivider/>
<div class="image-share-right-bottom-operation">
<div class="image-share-right-operation-select">
<div class="image-share-right-operation-item">
<span class="label-text">访问时效</span>
<ASelect style="width: 50%">
<ASelectOption value="1">1</ASelectOption>
<ASelectOption value="3">3</ASelectOption>
<ASelectOption value="7">7</ASelectOption>
<ASelectOption value="15">15</ASelectOption>
<ASelectOption value="30">30</ASelectOption>
<ASelectOption value="0">永久</ASelectOption>
</ASelect>
</div>
<div class="image-share-right-operation-item">
<span class="label-text">访问密码</span>
<AInputPassword style="width: 50%"></AInputPassword>
</div>
<div class="image-share-right-operation-item">
<span class="label-text">访问限制</span>
<AInputNumber style="width: 50%" :defaultValue="100" :min="1"></AInputNumber>
</div>
</div>
<div class="image-share-right-bottom-operation-btn">
<AButton type="primary" size="large" shape="default" style="width: 100%">开始上传</AButton>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {Dayjs} from 'dayjs';
import folder from "@/assets/svgs/folder.svg";
import {NSFWJS} from "nsfwjs";
import {initNSFWJs, predictNSFW} from "@/utils/tfjs/nsfw.ts";
import dayjs from 'dayjs';
import ShareUpload from "@/views/ImageShare/ShareUpload.vue";
import {queryShareRecordListApi} from "@/api/share";
import {message} from "ant-design-vue";
import i18n from "@/locales";
import phone from "@/assets/svgs/qr-phone.svg";
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
type RangeValue = [Dayjs, Dayjs];
const dates = ref<RangeValue>();
const value = ref<RangeValue>();
const selectedDateRange = ref<RangeValue>();
const hackValue = ref<RangeValue>();
const titleName = ref<string>("");
const qrContainer = ref<HTMLDivElement | null>(null);
const loading = ref<boolean>(false);
const disabledDate = (current: Dayjs) => {
@@ -236,81 +152,65 @@ const onOpenChange = (open: boolean) => {
}
};
const onChange = (val: RangeValue) => {
value.value = val;
const onChange = async (val: RangeValue) => {
selectedDateRange.value = val;
// 将日期范围处理成一个数组
if (val && val.length === 2) {
const startDate = val[0].format('YYYY-MM-DD'); // 格式化开始日期
const endDate = val[1].format('YYYY-MM-DD'); // 格式化结束日期
const dateRangeArray = [startDate, endDate]; // 将日期范围存入数组
await getShareRecords(dateRangeArray);
} else {
console.log('No date range selected');
}
};
const onCalendarChange = (val: RangeValue) => {
dates.value = val;
};
/**
* 格式化字节大小
* @param bytes
*/
function formatByteSize(bytes) {
if (bytes < 1024) {
return `${bytes} Bytes`;
} else if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(2)} KB`;
} else if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
} else {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
}
/**
* 格式化字节大小
* @param bytes
* @param decimals
*/
function formatBytes(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
/**
* 计算文件总大小
* @param fileDataArray
*/
function calculateTotalSize(fileDataArray: { size: number }[]): string {
const totalSize = fileDataArray.reduce((acc, file) => acc + file.size, 0);
return formatBytes(totalSize);
}
const columns = [
{
title: '快传记录',
dataIndex: 'name',
key: 'name',
dataIndex: 'cover_image',
key: 'cover_image',
ellipsis: true,
width: 90,
},
{
title: '上传时间',
dataIndex: 'created_at',
key: 'created_at',
ellipsis: true,
customRender: ({text}) => {
return dayjs(text).format('YYYY-MM-DD'); // 格式化时间
},
},
{
title: '浏览次数',
dataIndex: 'views',
key: 'views',
title: '访问密码',
key: 'access_password',
dataIndex: 'access_password',
ellipsis: true,
},
{
title: '浏览人数',
key: 'viewers',
dataIndex: 'viewers',
title: '访问限制',
key: 'visit_limit',
dataIndex: 'visit_limit',
ellipsis: true,
customRender: ({text}) => {
return `${text}`;
},
},
{
title: '传输状态',
key: 'status',
dataIndex: 'status',
title: '有效期',
key: 'validity_period',
dataIndex: 'validity_period',
ellipsis: true,
customRender: ({text}) => {
return formatValidityPeriod(text);
},
},
{
title: '操作',
@@ -319,339 +219,44 @@ const columns = [
},
];
const qrcodeSize = ref<number>(220);
const iconSize = ref<number>(30);
const folderIconSize = ref<number>(100);
function handleResizeColumn(w, col) {
col.width = w;
}
/**
* 更新二维码大小
*/
const updateQrcodeSize = () => {
if (qrContainer.value) {
// 设置 QRCode 大小
const containerWidth = qrContainer.value.clientWidth;
qrcodeSize.value = containerWidth * 0.5; // 设置 QRCode 为父盒子宽度的80%
folderIconSize.value = containerWidth * 0.3; // 设置文件夹图标大小为父盒子宽度的10%
iconSize.value = Math.min(containerWidth * 0.1, 40); // 设置 icon 大小为父盒子宽度的10%
}
const formatValidityPeriod = (period: number) => {
return period === 0 ? '永久' : `${period}`;
};
const fileList = ref<any[]>([]);
/**
* 上传文件前置
* @param file
*/
async function beforeUpload(file: any) {
if (!window.FileReader) return false; // 判断是否支持FileReader
const reader = new FileReader();
reader.readAsDataURL(file); // 文件转换
reader.onloadend = async function () {
const img: HTMLImageElement = document.createElement('img');
img.src = reader.result as string;
img.onload = async () => {
// 图片 NSFW 检测
const nsfw: NSFWJS = await initNSFWJs();
const isNSFW: boolean = await predictNSFW(nsfw, img);
if (isNSFW) {
message.error(i18n.global.t('comment.illegalImage'));
return false;
}
};
};
return true;
// 复制功能
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text).then(() => {
message.success('复制成功');
}).catch(() => {
message.error('复制失败');
});
}
/**
* 删除 base64 图片
* @param index
*/
async function removeBase64Image(index: number) {
fileList.value.splice(index, 1);
}
const dataSources = ref<any[]>([]);
/**
* 转换文件为 URL
* @param file
* 获取分享记录
*/
function convertFileToUrl(file: any) {
return URL.createObjectURL(file);
async function getShareRecords(dateRange: string[]) {
loading.value = true;
const res: any = await queryShareRecordListApi(dateRange);
if (res && res.code === 200) {
dataSources.value = res.data.records;
}
loading.value = false;
}
onMounted(() => {
window.addEventListener("resize", updateQrcodeSize);
onMounted(async () => {
const endDate = dayjs().format('YYYY-MM-DD'); // 当前日期
const startDate = dayjs().subtract(30, 'day').format('YYYY-MM-DD'); // 30 天前的日期
await getShareRecords([startDate, endDate]);
});
</script>
<style scoped lang="scss">
.image-share {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
width: 100%;
height: 100%;
gap: 20px;
.image-share-left {
height: 100%;
width: 65%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
.image-share-left-top {
width: 100%;
height: 30%;
display: flex;
flex-direction: column;
gap: 10px;
.image-share-left-title {
width: 100%;
height: 20%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.image-share-left-content {
width: 100%;
height: 80%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.image-share-left-content-item {
height: 100%;
width: 30%;
color: #fff;
overflow: auto;
.image-share-left-item-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
overflow: hidden;
}
}
}
}
.image-share-left-bottom {
width: 100%;
height: 70%;
display: flex;
flex-direction: column;
.image-share-left-bottom-title {
width: 100%;
height: 20%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.image-share-left-bottom-content {
width: 100%;
height: 80%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
}
}
.image-share-right {
height: 100%;
width: 35%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
.image-share-right-top {
width: 100%;
height: 6%;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.image-share-right-bottom {
width: 100%;
height: 94%;
display: flex;
flex-direction: column;
.image-share-right-bottom-content {
width: 90%;
height: 100%;
padding: 20px;
background: #ffffff;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
.image-share-right-bottom-upload {
width: 100%;
height: 100%;
overflow: auto;
.image-share-right-upload {
width: 100%;
height: 100%;
}
}
.image-share-right-bottom-container {
width: 100%;
height: 100%;
.image-share-right-bottom-container-header {
width: 100%;
height: 10%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.image-share-right-bottom-content-list {
width: 95%;
height: 40%;
display: flex;
flex-direction: column;
align-content: flex-start;
justify-content: flex-start;
gap: 15px;
flex-wrap: nowrap;
padding: 10px;
overflow: auto;
background-color: #f5f5f5;
border-radius: 10px;
.image-share-right-bottom-content-list-wrapper {
width: 100%;
height: 27vh;
display: flex;
flex-direction: column;
align-content: flex-start;
justify-content: flex-start;
overflow: auto;
gap: 10px;
.image-share-right-bottom-content-list-item {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.file-thumbnail {
height: 100%;
width: 17%;
}
.file-info {
height: 100%;
width: 63%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
}
.file-operation {
height: 100%;
width: 20%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
}
}
.image-share-right-bottom-operation {
width: 100%;
height: 40%;
display: flex;
flex-direction: column;
align-items: center;
.image-share-right-operation-select {
width: 100%;
height: 75%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
flex-wrap: nowrap;
.image-share-right-operation-item {
width: 100%;
height: 5vh;
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
.label-text {
width: 50%;
color: #999999;
font-size: 2.2vh;
}
}
}
.image-share-right-bottom-operation-btn {
width: 100%;
height: 35%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
}
}
}
.image-share-right-upload-item {
//width: 100% !important;
//height: 100% !important;
display: flex;
flex-direction: column;
align-items: center;
//justify-content: center;
gap: 2vh;
}
<style scoped lang="scss" src="./index.scss">
</style>

View File

@@ -0,0 +1,679 @@
<template>
<div class="image-share-right">
<div class="image-share-right-top">
<h3>照片快传</h3>
</div>
<div class="image-share-right-bottom">
<div class="image-share-right-bottom-content">
<div class="image-share-right-bottom-upload" ref="qrContainer"
v-if="fileList.length <= 0 && !uploadSuccess">
<AUploadDragger
name="file"
:multiple="true"
:showUploadList="false"
:beforeUpload="beforeUpload"
v-model:fileList="fileList"
:customRequest="customRequest"
class="image-share-right-upload"
>
<div class="image-share-right-upload-item">
<p class="ant-upload-drag-icon">
<ABadge :offset="[-15, 20]" :count="fileList.length">
<AAvatar shape="square" :size="folderIconSize" :src="folder"/>
</ABadge>
</p>
<p class="ant-upload-text" style="font-size: 2.6vh;font-weight: bolder">单击或拖动文件到此区域以上传</p>
<AButton type="primary" size="large" shape="round" style="width: 70%"> </AButton>
<div class="qr">
<AQrcode :bordered="false" color="rgba(126, 126, 135, 0.48)"
:size="qrcodeSize"
:value="`git.landaiqing.cneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjI1MTEyMjE3MzQyMDIxIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTczOTg3ODIyOCwibmJmIjoxNzM5ODcxMDI4LCJpYXQiOjE3Mzk4NzEwMjh9.EUiZsVjhGqHx1V5o90S3W5li6nIqucxy9eEY9LWgqXY`"
:icon="phone"
:iconSize="iconSize"
:status="`active`"
/>
<span style="font-size: 2vh;color: #999999">手机扫码上传</span>
</div>
</div>
</AUploadDragger>
</div>
<div class="image-share-right-bottom-container" v-if="fileList.length > 0 && !loading">
<div class="image-share-right-bottom-container-header">
<AInput v-model:value="titleName" :bordered="false" size="large" placeholder="给快传起个标题"/>
<ADropdown placement="bottomLeft" :trigger="['click']">
<template #overlay>
<AMenu>
<AMenuItem key="1">
<AUpload
name="file"
:multiple="true"
:showUploadList="false"
:beforeUpload="beforeUpload"
v-model:fileList="fileList"
>
上传文件
</AUpload>
</AMenuItem>
<AMenuItem key="2">
<APopover placement="bottomLeft" trigger="hover">
<template #content>
<AQrcode :bordered="false" color="rgba(126, 126, 135, 0.48)"
:size="qrcodeSize"
:value="`git.landaiqing.cneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjI1MTEyMjE3MzQyMDIxIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTczOTg3ODIyOCwibmJmIjoxNzM5ODcxMDI4LCJpYXQiOjE3Mzk4NzEwMjh9.EUiZsVjhGqHx1V5o90S3W5li6nIqucxy9eEY9LWgqXY`"
:icon="phone"
:iconSize="iconSize"
:status="`active`"
/>
</template>
手机上传
</APopover>
</AMenuItem>
</AMenu>
</template>
<AButton size="middle" shape="circle">
<template #icon>
<PlusOutlined/>
</template>
</AButton>
</ADropdown>
</div>
<div class="image-share-right-bottom-content-list">
<p style="font-size: 2.0vh;color: #999999;cursor: default">{{ fileList.length }}个文件
{{ calculateTotalSize(fileList) }}</p>
<div class="image-share-right-bottom-content-list-wrapper">
<div class="image-share-right-bottom-content-list-item"
v-for="(item, index) in fileList" :key="index">
<div class="file-thumbnail" ref="fileContainer">
<AImage :width="50" :height="50"
:src="convertFileToUrl(item.originFileObj)">
<template #previewMask>
</template>
</AImage>
</div>
<div class="file-info">
<p style="font-size: 2.0vh;color: #333333;cursor: default;font-weight: bold">{{ item.name }}</p>
<p style="font-size: 1.5vh;color: #999999;cursor: default">{{
formatByteSize(item.size)
}}</p>
</div>
<div class="file-operation">
<AButton size="middle" shape="circle" type="text" @click="removeImage(index)">
<template #icon>
<CloseOutlined/>
</template>
</AButton>
</div>
</div>
</div>
</div>
<ADivider/>
<div class="image-share-right-bottom-operation">
<div class="image-share-right-operation-select">
<div class="image-share-right-operation-item">
<span class="label-text">访问时效</span>
<ASelect style="width: 50%" placeholder="请选择" :defaultValue="1" v-model:value="expire_date">
<ASelectOption value="1">1</ASelectOption>
<ASelectOption value="3">3</ASelectOption>
<ASelectOption value="7">7</ASelectOption>
<ASelectOption value="15">15</ASelectOption>
<ASelectOption value="30">30</ASelectOption>
<ASelectOption value="0">永久</ASelectOption>
</ASelect>
</div>
<div class="image-share-right-operation-item">
<span class="label-text">访问密码</span>
<AInputPassword style="width: 50%" v-model:value="access_password" :maxlength="10"
:showCount="true"></AInputPassword>
</div>
<div class="image-share-right-operation-item">
<span class="label-text">访问限制</span>
<AInputNumber style="width: 50%" :defaultValue="100" :min="1"
v-model:value="access_limit"></AInputNumber>
</div>
</div>
<div class="image-share-right-bottom-operation-btn">
<AButton type="primary" size="middle" shape="round" style="width: 100%"
:loading="loading"
@click="customUploader">
开始上传
</AButton>
</div>
</div>
</div>
<div v-if="loading && !uploadSuccess" class="image-share-right-bottom-loading">
<div class="image-share-right-bottom-loading-content">
<a-progress
type="circle"
:stroke-color="{
'0%': '#108ee9',
'100%': '#87d068',
}"
:percent="percent"
:size="180"
/>
<p>{{ fileList.length }} 个文件 / {{ calculateTotalSize(fileList) }}</p>
</div>
<div class="image-share-right-bottom-loading-footer">
<AButton type="primary" size="large" shape="round" style="width: 80%" @click="abort">
</AButton>
</div>
</div>
<div v-if="uploadSuccess" class="image-share-right-bottom-success">
<div class="image-share-right-bottom-success-header">
<div class="image-share-right-bottom-success-header-title">
<CheckCircleOutlined style="font-size: 3vh;color: #52c41a"/>
<h3> </h3>
</div>
<div class="image-share-right-bottom-success-header-close">
<AButton type="text" size="middle" shape="circle" @click="uploadSuccess = false">
<CloseOutlined/>
</AButton>
</div>
</div>
<div class="image-share-right-bottom-success-content">
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import folder from "@/assets/svgs/folder.svg";
import phone from "@/assets/svgs/qr-phone.svg";
import useStore from "@/store";
import {NSFWJS} from "nsfwjs";
import {initNSFWJs, predictNSFW} from "@/utils/tfjs/nsfw.ts";
import {message} from "ant-design-vue";
import i18n from "@/locales";
import {useRequest} from "alova/client";
import {shareImageUploadApi} from "@/api/share";
import imageCompression from "browser-image-compression";
import {generateThumbnail} from "@/utils/imageUtils/generateThumb.ts";
const titleName = ref<string>("");
const upload = useStore().upload;
const percent = ref<number>(0);
const uploadSuccess = ref<boolean>(false);
const qrContainer = ref<HTMLDivElement | null>(null);
/**
* 格式化字节大小
* @param bytes
*/
function formatByteSize(bytes: number) {
if (bytes < 1024) {
return `${bytes} Bytes`;
} else if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(2)} KB`;
} else if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
} else {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
}
/**
* 格式化字节大小
* @param bytes
* @param decimals
*/
function formatBytes(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
/**
* 计算文件总大小
* @param fileDataArray
*/
function calculateTotalSize(fileDataArray: { size: number }[]): string {
const totalSize = fileDataArray.reduce((acc, file) => acc + file.size, 0);
return formatBytes(totalSize);
}
const qrcodeSize = ref<number>(220);
const iconSize = ref<number>(30);
const folderIconSize = ref<number>(100);
const loading = ref<boolean>(false);
/**
* 更新二维码大小
*/
const updateQrcodeSize = () => {
if (qrContainer.value) {
// 设置 QRCode 大小
const containerWidth = qrContainer.value.clientWidth;
qrcodeSize.value = containerWidth * 0.5; // 设置 QRCode 为父盒子宽度的80%
folderIconSize.value = containerWidth * 0.3; // 设置文件夹图标大小为父盒子宽度的10%
iconSize.value = Math.min(containerWidth * 0.1, 40); // 设置 icon 大小为父盒子宽度的10%
}
};
const fileList = ref<any[]>([]);
const expire_date = ref<string>("1");
const access_limit = ref<number>(100);
const access_password = ref<string>("");
/**
* 上传文件前置
* @param file
*/
async function beforeUpload(file: any) {
if (!window.FileReader) return false; // 判断是否支持FileReader
const reader = new FileReader();
reader.readAsDataURL(file); // 文件转换
reader.onloadend = async function () {
const img: HTMLImageElement = document.createElement('img');
img.src = reader.result as string;
img.onload = async () => {
// 图片 NSFW 检测
const nsfw: NSFWJS = await initNSFWJs();
const isNSFW: boolean = await predictNSFW(nsfw, img);
if (isNSFW) {
message.error(i18n.global.t('comment.illegalImage'));
return false;
}
};
};
return true;
}
/**
* 自定义请求
* @param _file
* @param _fileList
*/
function customRequest(_file: any, _fileList: any) {
return;
}
const {uploading, send: submitFile, abort} = useRequest(shareImageUploadApi, {
immediate: false,
debounce: 500,
});
/**
* 自定义上传器
*/
async function customUploader() {
if (fileList.value.length <= 0) return;
loading.value = true;
uploadSuccess.value = false;
// 存储所有图片信息的数组
const images: any[] = [];
for (const file of fileList.value) {
// 压缩图片
const compressedFile = await imageCompression(file.originFileObj, {
maxSizeMB: 0.1,
maxWidthOrHeight: 750,
maxIteration: 10,
useWebWorker: true,
initialQuality: 0.6,
});
// 生成缩略图
const {binaryData, width, height, size} = await generateThumbnail(compressedFile);
// 将文件转换为 Base64 编码
const base64Thumbnail = binaryData ? await toBase64(binaryData) : '';
const base64Image = await toBase64(file.originFileObj);
// 创建文件的元数据对象
const fileObj = {
file_name: file.name, // 文件名
origin_image: base64Image, // 原始图片的 base64 数据
file_type: file.type, // 文件类型
thumbnail: base64Thumbnail, // 缩略图的 base64 数据
thumb_w: width, // 缩略图宽度
thumb_h: height, // 缩略图高度
thumb_size: size, // 缩略图文件大小
};
// 将文件对象添加到图片数组
images.push(fileObj);
}
// 准备发送给后端的请求数据
const requestData = {
title: titleName.value,
expire_date: expire_date.value,
access_limit: access_limit.value,
access_password: access_password.value,
provider: upload.storageSelected?.[0],
bucket: upload.storageSelected?.[1],
images: images,
};
watch(
() => uploading.value,
(upload) => {
if (upload && upload.loaded && upload.total) {
percent.value = Number(Math.round((upload.loaded / upload.total) * 100).toFixed(2));
}
},
);
const res: any = await submitFile(requestData);
if (res && res.code === 200) {
uploadSuccess.value = true;
fileList.value = [];
}
loading.value = false;
}
/**
* 将文件转换为 Base64 编码
* @param file
*/
function toBase64(file: Blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
// 确保 reader.result 是字符串类型
if (typeof reader.result === 'string') {
resolve(reader.result.split(',')[1]); // 只获取 base64 部分
} else {
reject(new Error('FileReader result is not a string.'));
}
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
/**
* 删除图片
* @param index
*/
async function removeImage(index: number) {
fileList.value.splice(index, 1);
}
/**
* 转换文件为 URL
* @param file
*/
function convertFileToUrl(file: any) {
return URL.createObjectURL(file);
}
const user = useStore().user;
const websocket = useStore().websocket;
const wsOptions = {
url: import.meta.env.VITE_FILE_SOCKET_URL + "?user_id=" + user.user.uid,
protocols: [user.token.accessToken],
};
onMounted(() => {
window.addEventListener("resize", updateQrcodeSize);
websocket.initialize(wsOptions);
websocket.on("message", async (res: any) => {
if (res && res.code === 200) {
const {data} = res;
console.log(data);
}
});
});
</script>
<style scoped lang="scss">
.image-share-right {
height: 100%;
width: 35%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
.image-share-right-top {
width: 100%;
height: 6%;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.image-share-right-bottom {
width: 100%;
height: 94%;
display: flex;
flex-direction: column;
.image-share-right-bottom-content {
width: 90%;
height: 100%;
padding: 20px;
background: #ffffff;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
.image-share-right-bottom-upload {
width: 100%;
height: 100%;
overflow: auto;
.image-share-right-upload {
width: 100%;
height: 100%;
}
}
.image-share-right-bottom-container {
width: 100%;
height: 100%;
.image-share-right-bottom-container-header {
width: 100%;
height: 10%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.image-share-right-bottom-content-list {
width: 95%;
height: 40%;
display: flex;
flex-direction: column;
align-content: flex-start;
justify-content: flex-start;
gap: 15px;
flex-wrap: nowrap;
padding: 10px;
overflow: auto;
background-color: #f5f5f5;
border-radius: 10px;
.image-share-right-bottom-content-list-wrapper {
width: 100%;
height: 27vh;
display: flex;
flex-direction: column;
align-content: flex-start;
justify-content: flex-start;
overflow: auto;
gap: 10px;
.image-share-right-bottom-content-list-item {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 10px;
.file-thumbnail {
height: 100%;
width: 17%;
}
.file-info {
height: 100%;
width: 63%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
}
.file-operation {
height: 100%;
width: 20%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
}
}
.image-share-right-bottom-operation {
width: 100%;
height: 40%;
display: flex;
flex-direction: column;
align-items: center;
.image-share-right-operation-select {
width: 100%;
height: 75%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
flex-wrap: nowrap;
.image-share-right-operation-item {
width: 100%;
height: 5vh;
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
.label-text {
width: 50%;
color: #999999;
font-size: 2.2vh;
}
}
}
.image-share-right-bottom-operation-btn {
width: 100%;
height: 35%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.image-share-right-bottom-loading {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
.image-share-right-bottom-loading-content {
width: 100%;
height: 80%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
}
.image-share-right-bottom-loading-footer {
width: 100%;
height: 20%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
}
.image-share-right-bottom-success {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
.image-share-right-bottom-success-header {
width: 100%;
height: 10%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.image-share-right-bottom-success-header-title {
height: 100%;
width: 90%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 10px;
}
.image-share-right-bottom-success-header-close {
height: 100%;
width: 10%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
}
.image-share-right-bottom-success-content {
width: 100%;
height: 90%;
display: flex;
flex-direction: row;
align-items: center;
}
}
}
}
}
.image-share-right-upload-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2vh;
}
</style>

View File

@@ -0,0 +1,100 @@
.image-share {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
width: 100%;
height: 100%;
gap: 20px;
.image-share-left {
height: 100%;
width: 65%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
.image-share-left-top {
width: 100%;
height: 30%;
display: flex;
flex-direction: column;
gap: 10px;
.image-share-left-title {
width: 100%;
height: 20%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.image-share-left-content {
width: 100%;
height: 80%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.image-share-left-content-item {
height: 100%;
width: 30%;
color: #fff;
overflow: auto;
.image-share-left-item-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
overflow: hidden;
}
}
}
}
.image-share-left-bottom {
width: 100%;
height: 70%;
display: flex;
flex-direction: column;
.image-share-left-bottom-title {
width: 100%;
height: 20%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.image-share-left-bottom-content {
width: 100%;
height: 80%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
.ant-card {
height: 100%;
.ant-table {
flex: 1; // 让 ATable 填满剩余空间
height: 100%;
}
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
<template>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
</style>

View File

@@ -96,7 +96,7 @@ const images = ref<any[]>([]);
* 获取所有图片
*/
async function getAllImages() {
const res: any = await queryAllImagesApi("image", false, upload.storageSelected[0], upload.storageSelected[1]);
const res: any = await queryAllImagesApi("image", false, upload.storageSelected?.[0], upload.storageSelected?.[1]);
if (res && res.code === 200) {
images.value = res.data.records;
}

View File

@@ -224,8 +224,8 @@ async function customUploadRequest(file: any) {
formData.append("thumbnail", binaryData);
}
formData.append("data", JSON.stringify({
provider: upload.storageSelected[0],
bucket: upload.storageSelected[1],
provider: upload.storageSelected?.[0],
bucket: upload.storageSelected?.[1],
fileType: file.file.type,
...upload.predictResult,
}));

View File

@@ -38,6 +38,7 @@
<script setup lang="ts">
import Vue3JustifiedLayout from "vue3-justified-layout";
import 'vue3-justified-layout/dist/style.css';
import {queryShareImageApi} from "@/api/share";
const selected = ref<(string | number)[]>([]);
@@ -47,6 +48,13 @@ const options = reactive({
});
async function getImages() {
const res = await queryShareImageApi("c09e3c571303448798c878095fbaa521", "123456");
console.log(res);
}
getImages();
</script>
<style scoped lang="scss">
.recycling-bin {