add coordinate map

This commit is contained in:
2025-03-16 23:11:32 +08:00
parent 9e6dd55087
commit 0068d36ac2
29 changed files with 55278 additions and 40 deletions

5
components.d.ts vendored
View File

@@ -133,6 +133,7 @@ declare module 'vue' {
LocationAlbumDetail: typeof import('./src/views/Album/LocationAlbum/LocationAlbumDetail.vue')['default'] LocationAlbumDetail: typeof import('./src/views/Album/LocationAlbum/LocationAlbumDetail.vue')['default']
LocationAlbumIndex: typeof import('./src/views/Album/LocationAlbum/LocationAlbumIndex.vue')['default'] LocationAlbumIndex: typeof import('./src/views/Album/LocationAlbum/LocationAlbumIndex.vue')['default']
LocationAlbumList: typeof import('./src/views/Album/LocationAlbum/LocationAlbumList.vue')['default'] LocationAlbumList: typeof import('./src/views/Album/LocationAlbum/LocationAlbumList.vue')['default']
LocationCoordinateMap: typeof import('./src/views/Album/LocationAlbum/Components/LocationCoordinateMap.vue')['default']
LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined'] LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined']
Login: typeof import('./src/views/Admin/Auth/Login.vue')['default'] Login: typeof import('./src/views/Admin/Auth/Login.vue')['default']
LoginFooter: typeof import('./src/views/Login/LoginFooter.vue')['default'] LoginFooter: typeof import('./src/views/Login/LoginFooter.vue')['default']
@@ -158,7 +159,9 @@ declare module 'vue' {
PlusOutlined: typeof import('@ant-design/icons-vue')['PlusOutlined'] PlusOutlined: typeof import('@ant-design/icons-vue')['PlusOutlined']
PlusSquareOutlined: typeof import('@ant-design/icons-vue')['PlusSquareOutlined'] PlusSquareOutlined: typeof import('@ant-design/icons-vue')['PlusSquareOutlined']
Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default'] Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default']
PreviewBlurDetect: typeof import('./src/views/Preview/PreviewBlurDetect.vue')['default'] PreviewBlurDetect: typeof import('./src/views/Preview/PreviewBlurDetect/PreviewBlurDetect.vue')['default']
PreviewOCR: typeof import('./src/views/Preview/PreviewOCR/PreviewOCR.vue')['default']
PrivacySpace: typeof import('./src/views/Photograph/PrivacySpace/PrivacySpace.vue')['default']
QrcodeOutlined: typeof import('@ant-design/icons-vue')['QrcodeOutlined'] QrcodeOutlined: typeof import('@ant-design/icons-vue')['QrcodeOutlined']
QRLogin: typeof import('./src/views/QRLogin/QRLogin.vue')['default'] QRLogin: typeof import('./src/views/QRLogin/QRLogin.vue')['default']
QRLoginFooter: typeof import('./src/views/QRLogin/QRLoginFooter.vue')['default'] QRLoginFooter: typeof import('./src/views/QRLogin/QRLoginFooter.vue')['default']

View File

@@ -1,6 +1,7 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<script>var Module;</script>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/logo.svg"/> <link rel="icon" type="image/svg+xml" href="/logo.svg"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0,user-scalable=no"/> <meta name="viewport" content="width=device-width, initial-scale=1.0,user-scalable=no"/>

View File

@@ -15,6 +15,8 @@
"@intlify/eslint-plugin-vue-i18n": "^4.0.0", "@intlify/eslint-plugin-vue-i18n": "^4.0.0",
"@mediapipe/face_detection": "^0.4.1646425229", "@mediapipe/face_detection": "^0.4.1646425229",
"@mediapipe/face_mesh": "^0.4.1633559619", "@mediapipe/face_mesh": "^0.4.1633559619",
"@paddlejs-models/ocr": "^1.2.4",
"@paddlejs-models/ocrdet": "^1.1.1",
"@tensorflow-models/coco-ssd": "^2.2.3", "@tensorflow-models/coco-ssd": "^2.2.3",
"@tensorflow-models/face-detection": "^1.0.3", "@tensorflow-models/face-detection": "^1.0.3",
"@tensorflow-models/face-landmarks-detection": "^1.0.6", "@tensorflow-models/face-landmarks-detection": "^1.0.6",
@@ -31,6 +33,7 @@
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/json-stringify-safe": "^5.0.3", "@types/json-stringify-safe": "^5.0.3",
"@types/leaflet": "^1.9.16",
"@types/node": "^22.13.10", "@types/node": "^22.13.10",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@vladmandic/face-api": "^1.7.15", "@vladmandic/face-api": "^1.7.15",
@@ -54,6 +57,7 @@
"json-stringify-safe": "^5.0.1", "json-stringify-safe": "^5.0.1",
"jsonc-eslint-parser": "^2.4.0", "jsonc-eslint-parser": "^2.4.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"leaflet": "^1.9.4",
"less": "^4.2.2", "less": "^4.2.2",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@@ -89,7 +93,7 @@
"typescript": "^5.8.2", "typescript": "^5.8.2",
"typescript-eslint": "^8.26.1", "typescript-eslint": "^8.26.1",
"unplugin-vue-components": "^28.4.1", "unplugin-vue-components": "^28.4.1",
"vite": "^6.2.1", "vite": "^6.2.2",
"vite-plugin-bundle-obfuscator": "1.4.2", "vite-plugin-bundle-obfuscator": "1.4.2",
"vite-plugin-chunk-split": "^0.5.0", "vite-plugin-chunk-split": "^0.5.0",
"vue-tsc": "2.2.8" "vue-tsc": "2.2.8"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -16,3 +16,15 @@ export const checkSecuritySettingApi = () => {
name: "check-security-setting", name: "check-security-setting",
}); });
}; };
/**
* 退出登录
*/
export const userLogoutApi = () => {
return service.Post('/api/auth/user/logout', {}, {
meta: {
ignoreToken: false,
signature: false,
},
name: "user-logout",
});
};

View File

@@ -24,7 +24,7 @@ export const queryShareImageApi = (invite_code: string, access_password: string)
access_password: access_password, access_password: access_password,
}, { }, {
cacheFor: { cacheFor: {
expire: 60 * 60 * 24 * 7, expire: 60 * 5,
mode: "restore", mode: "restore",
}, },
meta: { meta: {

View File

@@ -595,3 +595,44 @@ export const getShareStatisticsInfoApi = () => {
hitSource: ["upload-file", "upload-share-image"], hitSource: ["upload-file", "upload-share-image"],
}); });
}; };
/**
* 获取私密相册列表
* @param provider
* @param bucket
* @param password
*/
export const getPrivateImageListApi = (provider: string, bucket: string, password: string) => {
return service.Post('/api/auth/storage/image/private/list', {
provider: provider,
bucket: bucket,
password: password,
}, {
cacheFor: {
expire: 60 * 5,
mode: "restore",
},
meta: {
ignoreToken: false,
signature: false,
},
name: "get-private-image-list",
hitSource: ["upload-file", "delete-images"],
});
};
/**
* 获取坐标列表
*/
export const getCoordinateListApi = () => {
return service.Post('/api/auth/storage/coordinate/list', {}, {
cacheFor: {
expire:60 * 60 * 24 * 7,
mode: "restore",
},
meta: {
ignoreToken: false,
signature: false,
},
name: "get-coordinate-list",
hitSource: ["upload-file", "delete-images"],
});
};

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

1
src/assets/svgs/map.svg Normal file
View File

@@ -0,0 +1 @@
<svg t="1742131739959" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="21495" width="200" height="200"><path d="M293.12 912.725333h512V298.666667h-57.6c-92.458667 233.216-153.898667 349.824-184.32 349.824-52.224 0-108.458667-116.608-168.661333-349.824H293.077333v614.058666z" fill="#E7E8FF" p-id="21496"></path><path d="M452.096 907.776a28.714667 28.714667 0 0 1-20.48-8.704 29.653333 29.653333 0 0 1 0-41.472l181.248-181.248a29.44 29.44 0 0 1 36.352-4.096l78.848 50.688 113.152-86.528a29.312 29.312 0 0 1 40.96 5.632c9.728 12.8 7.168 31.232-5.632 40.96l-129.024 98.816a29.226667 29.226667 0 0 1-33.792 1.536l-76.288-49.152-164.864 164.864a28.714667 28.714667 0 0 1-20.48 8.704zM563.2 385.536a29.184 29.184 0 0 1-29.184-29.184v-58.88a29.184 29.184 0 1 1 58.368 0V355.84a29.610667 29.610667 0 0 1-29.184 29.696z m258.048 522.24H305.152a73.216 73.216 0 0 1-73.216-73.216V305.664c0-40.448 32.768-73.216 73.216-73.216H348.16a29.184 29.184 0 1 1 0 58.368H305.152a14.848 14.848 0 0 0-14.848 14.848V834.56c0 8.192 6.656 14.848 14.848 14.848h515.584a14.848 14.848 0 0 0 14.848-14.848V305.664a14.848 14.848 0 0 0-14.848-14.848h-36.864a29.184 29.184 0 1 1 0-58.368h36.864c40.448 0 73.216 32.768 73.216 73.216V834.56a72.448 72.448 0 0 1-72.704 73.216zM142.336 848.384a29.184 29.184 0 0 1-29.184-29.184V321.024c0-16.384 13.312-29.184 29.184-29.184 15.872 0 29.184 13.312 29.184 29.184V819.2a28.544 28.544 0 0 1-29.184 29.184zM695.808 414.72a34.474667 34.474667 0 0 1-15.36-4.096 29.184 29.184 0 0 1-9.728-40.448 125.653333 125.653333 0 0 0-107.52-190.464c-69.12 0-125.44 56.32-125.44 125.44 0 22.528 6.144 44.032 16.896 63.488a28.928 28.928 0 0 1-10.752 39.936 28.928 28.928 0 0 1-39.936-10.752 180.48 180.48 0 0 1-25.088-92.16 184.533333 184.533333 0 0 1 184.32-184.32 184.533333 184.533333 0 0 1 184.32 184.32 183.04 183.04 0 0 1-26.624 95.232 29.098667 29.098667 0 0 1-25.088 13.824zM563.2 648.704a29.226667 29.226667 0 0 1-25.6-14.848L408.064 404.48a28.970667 28.970667 0 0 1 11.264-39.936 28.970667 28.970667 0 0 1 39.936 11.264l129.536 229.376a28.970667 28.970667 0 0 1-25.6 43.52z m0 0a28.416 28.416 0 0 1-14.336-3.584 29.354667 29.354667 0 0 1-10.752-39.936l130.56-229.376a29.354667 29.354667 0 0 1 39.936-10.752 29.354667 29.354667 0 0 1 10.752 39.936l-130.56 228.864a29.226667 29.226667 0 0 1-25.6 14.848z" fill="#595959" p-id="21497"></path></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
<svg t="1741802223847" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7367" width="200" height="200"><path d="M974.138979 369.303113C945.574097 244.841843 863.960149 142.824408 754.801494 88.755168V46.92802c0-21.423661-17.342964-38.766625-38.766625-39.7868-21.423661 0-38.766625 18.363138-38.766625 39.7868v13.262266c-109.158655-27.544707-223.418182-27.544707-331.556662 0V46.92802c0-21.423661-17.342964-38.766625-38.766626-39.7868-21.423661 0-38.766625 18.363138-38.766625 39.7868v41.827148C161.060025 142.824408 79.446077 244.841843 50.881196 369.303113 26.397011 476.42142 26.397011 586.600249 50.881196 693.718555c35.706102 154.046326 153.026152 275.447073 302.99178 311.153176 104.057783 25.504359 212.196264 25.504359 316.254048 0C821.112827 969.165629 938.432877 848.785056 974.138979 693.718555c24.484184-107.118306 24.484184-217.297136 0-324.415442zM430.386052 469.280199H216.14944c-17.342964 0-30.60523-14.282441-30.605231-30.60523 0-17.342964 13.262267-30.60523 30.605231-30.605231h214.236612c17.342964 0 30.60523 13.262267 30.605231 30.605231 0 16.32279-13.262267 30.60523-30.605231 30.60523z m367.262765 0H583.412204c-17.342964 0-30.60523-14.282441-30.60523-30.60523 0-17.342964 13.262267-30.60523 30.60523-30.605231h214.236613c17.342964 0 30.60523 13.262267 30.60523 30.605231 0 16.32279-13.262267 30.60523-30.60523 30.60523z" fill="#363853" p-id="7368"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,971 @@
<template>
<div class="enhancer-container">
<AFlex class="enhancer-content" :vertical="false" align="center" justify="flex-start">
<div class="enhancer-content-left">
<ACard class="enhancer-content-left-container">
<!-- 上传区域 -->
<div class="enhancer-content-left-upload">
<div class="enhancer-upload-container" ref="containerRef">
<div class="enhancer-upload-content" ref="uploadDraggerRef">
<Spin :spinning="enhancer.uploading" indicator="magic-ring">
<AUploadDragger
name="image"
accept="image/*"
:multiple="false"
:directory="false"
:maxCount="1"
:beforeUpload="enhancer.beforeUpload"
:custom-request="enhancer.customUploadRequest"
:disabled="enhancer.uploading || enhancer.isProcessing"
:showUploadList="false">
<div class="enhancer-upload-content-main">
<ABadge :offset="[-10, 10]">
<template #count>
<AAvatar :size="25" :src="successIcon" v-if="enhancer.imageData"/>
<AAvatar :size="25" :src="warnIcon" v-if="!enhancer.imageData"/>
</template>
<AAvatar shape="square" :size="70" :src="fileIcon"/>
</ABadge>
<span class="enhancer-upload-text">
点击或拖拽上传图片
</span>
</div>
</AUploadDragger>
</Spin>
</div>
</div>
</div>
<!-- 功能选择区 -->
<ADivider orientation="center" :plain="true">
<span class="enhancer-divider-title">增强功能</span>
</ADivider>
<div class="enhancer-function-selector">
<ARadioGroup v-model:value="enhancer.selectedFunction" button-style="solid" size="small"
style="width: 100%">
<ARadioButton value="upscale">图像升级</ARadioButton>
<ARadioButton value="deblur">去模糊</ARadioButton>
<ARadioButton value="denoise">去噪</ARadioButton>
<ARadioButton value="lowlight">弱光增强</ARadioButton>
<ARadioButton value="beautify">修饰</ARadioButton>
<ARadioButton value="derain">除雨</ARadioButton>
<ARadioButton value="defog">除雾</ARadioButton>
</ARadioGroup>
</div>
<!-- 参数设置区 -->
<ADivider orientation="center" :plain="true">
<span class="enhancer-divider-title">参数设置</span>
</ADivider>
<!-- 图像升级参数 -->
<div v-if="enhancer.selectedFunction === 'upscale'" class="enhancer-params">
<div class="enhancer-params-item">
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">模型:</span>
<ASelect style="width: 100%" size="large"
v-model:value="enhancer.upscaleParams.model"
:options="enhancer.upscaleModels">
</ASelect>
</div>
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">比例:</span>
<ASelect style="width: 100%" size="large"
v-model:value="enhancer.upscaleParams.scale"
:options="enhancer.upscaleScales">
</ASelect>
</div>
</div>
</div>
<!-- 去模糊参数 -->
<div v-if="enhancer.selectedFunction === 'deblur'" class="enhancer-params">
<div class="enhancer-params-item">
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">强度:</span>
<ASlider v-model:value="enhancer.deblurParams.strength" :min="0" :max="100" :step="1"/>
</div>
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">半径:</span>
<ASlider v-model:value="enhancer.deblurParams.radius" :min="1" :max="20" :step="1"/>
</div>
</div>
</div>
<!-- 去噪参数 -->
<div v-if="enhancer.selectedFunction === 'denoise'" class="enhancer-params">
<div class="enhancer-params-item">
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">强度:</span>
<ASlider v-model:value="enhancer.denoiseParams.strength" :min="0" :max="100" :step="1"/>
</div>
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">保留细节:</span>
<ASlider v-model:value="enhancer.denoiseParams.preserveDetail" :min="0" :max="100" :step="1"/>
</div>
</div>
</div>
<!-- 弱光增强参数 -->
<div v-if="enhancer.selectedFunction === 'lowlight'" class="enhancer-params">
<div class="enhancer-params-item">
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">亮度:</span>
<ASlider v-model:value="enhancer.lowlightParams.brightness" :min="0" :max="100" :step="1"/>
</div>
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">对比度:</span>
<ASlider v-model:value="enhancer.lowlightParams.contrast" :min="0" :max="100" :step="1"/>
</div>
</div>
</div>
<!-- 修饰参数 -->
<div v-if="enhancer.selectedFunction === 'beautify'" class="enhancer-params">
<div class="enhancer-params-item">
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">平滑度:</span>
<ASlider v-model:value="enhancer.beautifyParams.smoothness" :min="0" :max="100" :step="1"/>
</div>
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">饱和度:</span>
<ASlider v-model:value="enhancer.beautifyParams.saturation" :min="0" :max="100" :step="1"/>
</div>
</div>
</div>
<!-- 除雨参数 -->
<div v-if="enhancer.selectedFunction === 'derain'" class="enhancer-params">
<div class="enhancer-params-item">
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">强度:</span>
<ASlider v-model:value="enhancer.derainParams.strength" :min="0" :max="100" :step="1"/>
</div>
</div>
</div>
<!-- 除雾参数 -->
<div v-if="enhancer.selectedFunction === 'defog'" class="enhancer-params">
<div class="enhancer-params-item">
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">强度:</span>
<ASlider v-model:value="enhancer.defogParams.strength" :min="0" :max="100" :step="1"/>
</div>
<div class="enhancer-params-item-content">
<span class="enhancer-params-title">色彩恢复:</span>
<ASlider v-model:value="enhancer.defogParams.colorRecovery" :min="0" :max="100" :step="1"/>
</div>
</div>
</div>
<!-- 处理按钮 -->
<ADivider></ADivider>
<AButton style="width: 100%;" size="large" shape="default" type="default" :loading="enhancer.isProcessing"
:disabled="!enhancer.imageData"
@click="startEnhance">
<template #icon>
<AAvatar shape="square" :size="25" :src="runIcon"/>
</template>
<span class="enhancer-params-btn">开始处理</span>
</AButton>
</ACard>
</div>
<!-- 图像预览区 -->
<div class="enhancer-content-right">
<div
ref="canvasContainer"
class="canvas-container bg"
@mousedown="startDragging"
@mouseup="stopDragging"
@mouseleave="stopDragging"
@mousemove="dragImage"
@wheel="resizeImage"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
>
<!-- 进度条 -->
<div class="canvas-progressbar">
<span class="canvas-progressbar-text">
{{ enhancer.msg }}
</span>
<AProgress
v-if="enhancer.isProcessing"
:stroke-color="{
'0%': '#108ee9',
'100%': '#87d068',}"
:percent="enhancer.progressBar"
:showInfo="false"
status="active"
/>
</div>
<!-- 图片 -->
<canvas ref="canvas"></canvas>
<!-- 拖动条 -->
<div
class="dragLine"
v-if="enhancer.isDone"
ref="dragLine">
<div class="dragBall"
@mousedown.stop="startDraggingLine"
@mousemove.stop="dragLineFn"
@mouseup.stop="stopDraggingLine"
>
<svg width="30" viewBox="0 0 27 20">
<path fill="#ff3484" d="M9.6 0L0 9.6l9.6 9.6z"></path>
<path fill="#5fb3e5" d="M17 19.2l9.5-9.6L16.9 0z"></path>
</svg>
</div>
</div>
<!-- 菜单 -->
<div class="floating-menu" @mousedown.stop v-if="enhancer.isDone && enhancer.processedImg">
<AFlex :vertical="false" align="center" justify="space-between" :gap="12">
<ATooltip placement="top" title="下载图片">
<AButton type="text" size="large" @click="downloadImage" class="menu-btn">
<template #icon>
<AAvatar :src="downloadIcon" class="menu-icon"/>
</template>
</AButton>
</ATooltip>
<ATooltip placement="top" title="分享图片">
<AButton type="text" size="large" class="menu-btn">
<template #icon>
<AAvatar :src="shareIcon" :size="28" class="menu-icon"/>
</template>
</AButton>
</ATooltip>
<ATooltip placement="top" title="保存图片">
<AButton type="text" size="large" class="menu-btn">
<template #icon>
<AAvatar :src="saveIcon" :size="30" class="menu-icon"/>
</template>
</AButton>
</ATooltip>
<ATooltip placement="top" title="删除图片">
<AButton type="text" size="large" danger class="menu-btn" @click="deleteImage">
<template #icon>
<AAvatar :src="deleteIcon" :size="28" class="menu-icon"/>
</template>
</AButton>
</ATooltip>
</AFlex>
</div>
<!-- 图片信息 -->
<div class="image-info">
<ATag color="cyan" :bordered="false" v-if="enhancer.imageData">原图: {{ originalImageSize }}</ATag>
<ATag color="purple" :bordered="false" v-if="enhancer.processedImg">处理后: {{ processedImageSize }}</ATag>
</div>
</div>
</div>
</AFlex>
</div>
</template>
<script setup lang="ts">
import {ref, reactive, computed, onMounted, onUnmounted} from 'vue';
import {message} from "ant-design-vue";
import Spin from "@/components/MyUI/Spin/Spin.vue";
import Upscaler from 'upscaler';
import {initNSFWJs, predictNSFW} from "@/utils/tfjs/nsfw.ts";
import {NSFWJS} from "nsfwjs";
// 图标导入
import fileIcon from "@/assets/svgs/file.svg";
import successIcon from '@/assets/svgs/success.svg';
import warnIcon from '@/assets/svgs/warn.svg';
import runIcon from '@/assets/svgs/run.svg';
import downloadIcon from '@/assets/svgs/download.svg';
import shareIcon from '@/assets/svgs/share.svg';
import saveIcon from '@/assets/svgs/save.svg';
import deleteIcon from '@/assets/svgs/deleted.svg';
// DOM引用
const containerRef = ref<HTMLDivElement | null>(null);
const uploadDraggerRef = ref<HTMLDivElement | null>(null);
const canvasContainer = ref<HTMLDivElement | null>(null);
const canvas = ref<HTMLCanvasElement | null>(null);
const dragLine = ref<HTMLDivElement | null>(null);
// 图像处理实例
let upscaler: any = null;
// 状态管理
const enhancer = reactive({
// 基本状态
uploading: false,
isProcessing: false,
isDone: false,
msg: "",
progressBar: 0,
// 图像数据
imageData: "",
fileData: "",
processedImg: "",
input: null as any,
// 拖拽状态
dragging: false,
linePosition: 0,
draggingLine: false,
// 功能选择
selectedFunction: "upscale",
// 图像升级参数
upscaleParams: {
model: "x4",
scale: 4
},
upscaleModels: [
{label: "通用模型 x2", value: "x2"},
{label: "通用模型 x4", value: "x4"},
{label: "照片增强", value: "photo"},
{label: "动漫风格", value: "anime"}
],
upscaleScales: [
{label: "2x", value: 2},
{label: "4x", value: 4}
],
// 去模糊参数
deblurParams: {
strength: 50,
radius: 5
},
// 去噪参数
denoiseParams: {
strength: 50,
preserveDetail: 70
},
// 弱光增强参数
lowlightParams: {
brightness: 60,
contrast: 50
},
// 修饰参数
beautifyParams: {
smoothness: 30,
saturation: 50
},
// 除雨参数
derainParams: {
strength: 70
},
// 除雾参数
defogParams: {
strength: 70,
colorRecovery: 60
},
// 图片上传前的校验
async beforeUpload(file: File) {
enhancer.uploading = true;
const urlData = URL.createObjectURL(file);
const image = new Image();
image.src = urlData;
// 等待图片加载完成
await new Promise(resolve => {
image.onload = resolve;
});
// 图片 NSFW 检测
try {
const nsfw: NSFWJS = await initNSFWJs();
const isNSFW: boolean = await predictNSFW(nsfw, image);
if (isNSFW) {
message.error("检测到不适当的图片内容,请更换图片");
enhancer.fileData = '';
enhancer.uploading = false;
return false;
}
} catch (error) {
console.error("NSFW检测失败", error);
}
await clear();
enhancer.fileData = urlData;
enhancer.uploading = false;
return true;
},
// 自定义上传图片请求
async customUploadRequest(_file: any) {
enhancer.imageData = enhancer.fileData;
await loadImage();
}
});
// 图片尺寸信息
const originalImageSize = computed(() => {
if (!enhancer.imageData) return "";
const img = new Image();
img.src = enhancer.imageData;
return `${img.width}x${img.height}`;
});
const processedImageSize = computed(() => {
if (!enhancer.processedImg) return "";
const img = new Image();
img.src = enhancer.processedImg;
return `${img.width}x${img.height}`;
});
// 清空数据
async function clear() {
enhancer.imageData = "";
enhancer.processedImg = "";
enhancer.isDone = false;
enhancer.msg = "";
enhancer.progressBar = 0;
enhancer.isProcessing = false;
enhancer.dragging = false;
enhancer.linePosition = 0;
enhancer.draggingLine = false;
enhancer.input = null;
}
// 加载图片
async function loadImage() {
if (!canvas.value || !enhancer.imageData) return;
const ctx = canvas.value.getContext('2d');
if (!ctx) return;
const img = new Image();
img.src = enhancer.imageData;
await new Promise(resolve => {
img.onload = resolve;
});
canvas.value.width = img.width;
canvas.value.height = img.height;
ctx.drawImage(img, 0, 0);
// 初始化拖动线位置
enhancer.linePosition = img.width / 2;
// 初始化Upscaler
if (!upscaler) {
upscaler = new Upscaler();
}
}
// 开始图像增强处理
async function startEnhance() {
if (!enhancer.imageData || !canvas.value) {
message.warning("请先上传图片");
return;
}
enhancer.isProcessing = true;
enhancer.msg = "正在处理图片...";
const start = Date.now();
try {
const img = new Image();
img.src = enhancer.imageData;
await new Promise(resolve => {
img.onload = resolve;
});
let processedImage;
// 根据选择的功能进行不同的处理
switch (enhancer.selectedFunction) {
case 'upscale':
enhancer.msg = "正在进行图像升级...";
processedImage = await upscaler.upscale(img, {
model: enhancer.upscaleParams.model,
scale: enhancer.upscaleParams.scale,
progress: (progress: number) => {
enhancer.progressBar = Math.round(progress * 100);
}
});
break;
case 'deblur':
enhancer.msg = "正在进行去模糊处理...";
// 这里使用模拟进度实际应使用UpscalerJS的去模糊功能
simulateProgress();
processedImage = await simulateImageProcessing(img, 'deblur');
break;
case 'denoise':
enhancer.msg = "正在进行去噪处理...";
simulateProgress();
processedImage = await simulateImageProcessing(img, 'denoise');
break;
case 'lowlight':
enhancer.msg = "正在进行弱光增强...";
simulateProgress();
processedImage = await simulateImageProcessing(img, 'lowlight');
break;
case 'beautify':
enhancer.msg = "正在进行图像修饰...";
simulateProgress();
processedImage = await simulateImageProcessing(img, 'beautify');
break;
case 'derain':
enhancer.msg = "正在进行除雨处理...";
simulateProgress();
processedImage = await simulateImageProcessing(img, 'derain');
break;
case 'defog':
enhancer.msg = "正在进行除雾处理...";
simulateProgress();
processedImage = await simulateImageProcessing(img, 'defog');
break;
}
if (processedImage) {
enhancer.processedImg = processedImage.src || processedImage;
enhancer.isDone = true;
enhancer.msg = `处理完成! 用时: ${((Date.now() - start) / 1000).toFixed(2)}`;
// 更新画布显示处理后的图片
updateCanvasWithProcessedImage();
}
} catch (error) {
console.error("图像处理失败", error);
message.error("图像处理失败,请重试");
enhancer.msg = "处理失败";
} finally {
enhancer.isProcessing = false;
}
}
// 模拟进度更新
function simulateProgress() {
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 5;
enhancer.progressBar = Math.min(Math.round(progress), 99);
if (progress >= 100) {
clearInterval(interval);
}
}, 200);
}
// 模拟图像处理(在实际实现中应替换为真实的处理逻辑)
async function simulateImageProcessing(img: HTMLImageElement, _type: string) {
// 这里应该是实际的图像处理逻辑
// 目前使用模拟的方式返回原图
return new Promise<string>(resolve => {
setTimeout(() => {
resolve(img.src);
}, 2000);
});
}
// 更新画布显示处理后的图片
function updateCanvasWithProcessedImage() {
if (!canvas.value || !enhancer.processedImg) return;
const ctx = canvas.value.getContext('2d');
if (!ctx) return;
const img = new Image();
img.src = enhancer.processedImg;
img.onload = () => {
canvas.value!.width = img.width;
canvas.value!.height = img.height;
ctx.drawImage(img, 0, 0);
// 重置拖动线位置
enhancer.linePosition = img.width / 2;
// 绘制对比效果
drawComparisonView();
};
}
// 绘制对比视图
function drawComparisonView() {
if (!canvas.value || !enhancer.imageData || !enhancer.processedImg || !enhancer.isDone) return;
const ctx = canvas.value.getContext('2d');
if (!ctx) return;
const originalImg = new Image();
originalImg.src = enhancer.imageData;
const processedImg = new Image();
processedImg.src = enhancer.processedImg;
originalImg.onload = () => {
processedImg.onload = () => {
// 清除画布
ctx.clearRect(0, 0, canvas.value!.width, canvas.value!.height);
// 绘制处理后的图片
ctx.drawImage(processedImg, 0, 0, canvas.value!.width, canvas.value!.height);
// 绘制原图(左侧部分)
ctx.drawImage(
originalImg,
0, 0, originalImg.width, originalImg.height,
0, 0, enhancer.linePosition, canvas.value!.height
);
// 绘制分割线
ctx.beginPath();
ctx.moveTo(enhancer.linePosition, 0);
ctx.lineTo(enhancer.linePosition, canvas.value!.height);
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();
};
};
}
// 拖动相关函数
function startDragging(_e: MouseEvent) {
enhancer.dragging = true;
}
function stopDragging() {
enhancer.dragging = false;
}
function dragImage(_e: MouseEvent) {
if (!enhancer.dragging || !canvasContainer.value) return;
// 实现图像拖动逻辑
}
function startDraggingLine(e: MouseEvent) {
e.preventDefault();
enhancer.draggingLine = true;
}
function stopDraggingLine() {
enhancer.draggingLine = false;
}
function dragLineFn(e: MouseEvent) {
if (!enhancer.draggingLine || !canvas.value || !canvasContainer.value) return;
const rect = canvasContainer.value.getBoundingClientRect();
const x = e.clientX - rect.left;
// 限制拖动范围在画布内
enhancer.linePosition = Math.max(0, Math.min(canvas.value.width, x));
// 重新绘制对比视图
drawComparisonView();
}
// 缩放相关函数
function resizeImage(e: WheelEvent) {
e.preventDefault();
// 实现图像缩放逻辑
}
// 触摸事件处理
function touchStart(e: TouchEvent) {
if (e.touches.length === 1) {
enhancer.dragging = true;
}
}
function touchMove(e: TouchEvent) {
if (!enhancer.dragging || !canvasContainer.value || e.touches.length !== 1) return;
const touch = e.touches[0];
const rect = canvasContainer.value.getBoundingClientRect();
const x = touch.clientX - rect.left;
if (enhancer.isDone) {
// 更新拖动线位置
enhancer.linePosition = Math.max(0, Math.min(canvas.value?.width || 0, x));
drawComparisonView();
}
}
function touchEnd() {
enhancer.dragging = false;
}
// 下载图片
function downloadImage() {
if (!enhancer.processedImg) return;
const link = document.createElement('a');
link.href = enhancer.processedImg;
link.download = `enhanced_image_${Date.now()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
message.success('图片下载成功');
}
// 删除图片
function deleteImage() {
clear();
message.success('图片已删除');
}
// 组件挂载和卸载
onMounted(() => {
// 初始化Upscaler
upscaler = new Upscaler();
});
onUnmounted(() => {
// 清理资源
if (enhancer.processedImg) {
URL.revokeObjectURL(enhancer.processedImg);
}
if (enhancer.imageData) {
URL.revokeObjectURL(enhancer.imageData);
}
});
</script>
<style scoped lang="scss">
.enhancer-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
.enhancer-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.enhancer-content-left {
width: 29%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
.enhancer-content-left-container {
width: 100%;
height: 100%;
overflow: auto;
.enhancer-divider-title {
font-size: 13px;
color: rgba(126, 126, 135, 0.99);
}
}
}
.enhancer-content-right {
width: 70%;
height: 100%;
border-radius: 10px;
}
}
}
.enhancer-upload-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.enhancer-upload-content {
width: 100%;
height: 30vh;
.enhancer-upload-content-main {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
overflow: scroll;
.enhancer-upload-text {
font-size: 20px;
font-weight: bold;
}
}
}
}
.enhancer-function-selector {
margin-bottom: 15px;
}
.enhancer-params {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 15px;
.enhancer-params-item {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
.enhancer-params-item-content {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
.enhancer-params-title {
width: 20%;
font-size: 14px;
font-weight: bold;
}
}
}
.enhancer-params-btn {
font-size: 16px;
font-weight: bold;
}
}
.canvas-container {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 10px;
background-color: #f0f2f5;
canvas {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.canvas-progressbar {
position: absolute;
top: 10px;
left: 10px;
right: 10px;
z-index: 10;
background-color: rgba(255, 255, 255, 0.8);
padding: 10px;
border-radius: 5px;
.canvas-progressbar-text {
font-size: 14px;
font-weight: bold;
margin-bottom: 5px;
display: block;
}
}
.dragLine {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 2px;
background-color: #ffffff;
z-index: 5;
transform: translateX(var(--line-position));
.dragBall {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 30px;
height: 30px;
background-color: #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: ew-resize;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
}
.floating-menu {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(255, 255, 255, 0.9);
padding: 10px 20px;
border-radius: 50px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
transition: all 0.5s ease;
user-select: none;
}
.menu-btn {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.menu-btn:hover {
transform: scale(1.2);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.menu-icon {
transition: transform 0.2s ease;
}
.menu-icon:hover {
transform: scale(1.1);
}
.image-info {
position: absolute;
opacity: 0.8;
border-radius: 10px;
top: 0;
right: 0;
padding: 10px;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 10px;
align-items: flex-start;
transition: all 0.5s ease;
user-select: none;
z-index: 3;
}
</style>

View File

@@ -0,0 +1,3 @@
import ImageEnhancer from './ImageEnhancer.vue';
export default ImageEnhancer;

View File

@@ -13,30 +13,46 @@
placeholder="选择存储桶"> placeholder="选择存储桶">
</ACascader> </ACascader>
</template> </template>
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn"> <ATooltip title="选择存储桶" color="orange">
<template #icon> <AButton type="text" shape="circle" size="large" class="header-menu-item-btn">
<AAvatar size="default" shape="circle" :src="ProviderIcon[uploadStore.storageSelected?.[0]]? ProviderIcon[uploadStore.storageSelected?.[0]] : wenhao"/> <template #icon>
</template> <AAvatar size="default" shape="circle"
</AButton> :src="ProviderIcon[uploadStore.storageSelected?.[0]]? ProviderIcon[uploadStore.storageSelected?.[0]] : wenhao"/>
</template>
</AButton>
</ATooltip>
</APopover> </APopover>
</div> </div>
<!-- 社区按钮 --> <!-- 社区按钮 -->
<!-- <div class="button-wrapper">--> <!-- <div class="button-wrapper">-->
<!-- <AButton type="text" shape="circle" size="large" class="header-menu-item-btn">--> <!-- <AButton type="text" shape="circle" size="large" class="header-menu-item-btn">-->
<!-- <template #icon>--> <!-- <template #icon>-->
<!-- <AAvatar size="default" shape="circle" :src="community"/>--> <!-- <AAvatar size="default" shape="circle" :src="community"/>-->
<!-- </template>--> <!-- </template>-->
<!-- </AButton>--> <!-- </AButton>-->
<!-- </div>--> <!-- </div>-->
<!-- 上传按钮 --> <!-- 上传按钮 -->
<div class="button-wrapper"> <div class="button-wrapper">
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn" <ATooltip title="隐私空间" color="geekblue">
@click="uploadStore.openUploadDrawerFn"> <AButton type="text" shape="circle" size="large" class="header-menu-item-btn">
<template #icon> <template #icon>
<AAvatar size="default" shape="circle" :src="upload"/> <AAvatar size="default" shape="circle" :src="privacy"/>
</template> </template>
</AButton> </AButton>
</ATooltip>
</div>
<div class="button-wrapper">
<ATooltip title="上传" color="cyan">
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn"
@click="uploadStore.openUploadDrawerFn">
<template #icon>
<AAvatar size="default" shape="circle" :src="upload"/>
</template>
</AButton>
</ATooltip>
</div> </div>
<!-- 通知按钮 --> <!-- 通知按钮 -->
@@ -47,11 +63,13 @@
}"> }">
<div class="button-wrapper"> <div class="button-wrapper">
<ADropdown :trigger="['click']"> <ADropdown :trigger="['click']">
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn bouncing-button"> <ATooltip title="通知" color="lime">
<template #icon> <AButton type="text" shape="circle" size="large" class="header-menu-item-btn bouncing-button">
<AAvatar size="small" shape="circle" :src="notice"/> <template #icon>
</template> <AAvatar size="small" shape="circle" :src="notice"/>
</AButton> </template>
</AButton>
</ATooltip>
<template #overlay> <template #overlay>
<AMenu> <AMenu>
<AMenuItem key="reply" :style="menuCSSStyle"> <AMenuItem key="reply" :style="menuCSSStyle">
@@ -82,7 +100,9 @@
<AFlex :vertical="false" align="center" justify="flex-start" class="header-user-container"> <AFlex :vertical="false" align="center" justify="flex-start" class="header-user-container">
<APopover :arrow="false" trigger="click" placement="bottomRight"> <APopover :arrow="false" trigger="click" placement="bottomRight">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
<AAvatar :size="40" class="header-user-avatar" :src="user.user.avatar"/> <ATooltip :title="user.user.nickname" color="#108ee9">
<AAvatar :size="40" class="header-user-avatar" :src="user.user.avatar"/>
</ATooltip>
</div> </div>
<template #content> <template #content>
<div class="avatar-content"> <div class="avatar-content">
@@ -151,7 +171,7 @@ import personalCenter from "@/assets/svgs/personal-center.svg";
import accountSetting from "@/assets/svgs/setting.svg"; import accountSetting from "@/assets/svgs/setting.svg";
import logout from "@/assets/svgs/logout.svg"; import logout from "@/assets/svgs/logout.svg";
import wenhao from "@/assets/svgs/wenhao.svg"; import wenhao from "@/assets/svgs/wenhao.svg";
import privacy from "@/assets/svgs/privacy.svg";
import useStore from "@/store"; import useStore from "@/store";
import ImageUpload from "@/components/ImageUpload/ImageUpload.vue"; import ImageUpload from "@/components/ImageUpload/ImageUpload.vue";
import {getStorageConfigListApi} from "@/api/storage"; import {getStorageConfigListApi} from "@/api/storage";
@@ -178,6 +198,7 @@ const menuCSSStyle: any = reactive({
}); });
onMounted(() => { onMounted(() => {
getUserConfigList(); getUserConfigList();
}); });

View File

@@ -10,6 +10,7 @@ import PhoalbumDetail from "@/views/Album/Phoalbum/PhoalbumDetail.vue";
import PeopleAlbumDetail from "@/views/Album/PeopleAlbum/PeopleAlbumDetail.vue"; import PeopleAlbumDetail from "@/views/Album/PeopleAlbum/PeopleAlbumDetail.vue";
import LocationAlbumDetail from "@/views/Album/LocationAlbum/LocationAlbumDetail.vue"; import LocationAlbumDetail from "@/views/Album/LocationAlbum/LocationAlbumDetail.vue";
import ThingAlbumDetail from "@/views/Album/ThingAlbum/ThingAlbumDetail.vue"; import ThingAlbumDetail from "@/views/Album/ThingAlbum/ThingAlbumDetail.vue";
import LocationCoordinateMap from "@/views/Album/LocationAlbum/Components/LocationCoordinateMap.vue";
export default [ export default [
{ {
@@ -112,4 +113,13 @@ export default [
} }
] ]
}, },
{
path: '/main/album/location/map',
component: LocationCoordinateMap,
name: 'locationMap',
meta: {
requiresAuth: true,
title: '地点地图'
},
},
]; ];

View File

@@ -1,5 +1,6 @@
import AllPhoto from "@/views/Photograph/AllPhoto/AllPhoto.vue"; import AllPhoto from "@/views/Photograph/AllPhoto/AllPhoto.vue";
import RecentUpload from "@/views/Photograph/RecentUpload/RecentUpload.vue"; import RecentUpload from "@/views/Photograph/RecentUpload/RecentUpload.vue";
import PrivacySpace from "@/views/Photograph/PrivacySpace/PrivacySpace.vue";
export default [ export default [
{ {
@@ -20,4 +21,13 @@ export default [
title: '最近上传' title: '最近上传'
}, },
}, },
{
path: '/main/photo/privacy/space',
name: 'privacy',
component: PrivacySpace,
meta: {
requiresAuth: true,
title: '隐私空间'
},
},
]; ];

View File

@@ -2,10 +2,19 @@ export default [
{ {
path: '/preview/blur-detect', path: '/preview/blur-detect',
name: 'PreviewBlurDetect', name: 'PreviewBlurDetect',
component: () => import('@/views/Preview/PreviewBlurDetect.vue'), component: () => import('@/views/Preview/PreviewBlurDetect/PreviewBlurDetect.vue'),
meta: { meta: {
requiresAuth: false, requiresAuth: false,
title: 'PreviewBlurDetect', title: 'PreviewBlurDetect',
} }
}, },
{
path: '/preview/ocr',
name: 'PreviewOcr',
component: () => import('@/views/Preview/PreviewOCR/PreviewOCR.vue'),
meta: {
requiresAuth: false,
title: 'PreviewOcr',
}
},
]; ];

View File

@@ -3,6 +3,7 @@ import {generateClientId} from "@/api/client";
import {message} from "ant-design-vue"; import {message} from "ant-design-vue";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import {getGiteeUrl, getGithubUrl, getQQUrl} from "@/api/oauth"; import {getGiteeUrl, getGithubUrl, getQQUrl} from "@/api/oauth";
import {userLogoutApi} from "@/api/auth";
export const useAuthStore = defineStore( export const useAuthStore = defineStore(
'user', 'user',
@@ -145,6 +146,19 @@ export const useAuthStore = defineStore(
user.status = ""; user.status = "";
} }
/**
* Logout the user and clear the token and user info
*/
async function logout() {
const res: any = await userLogoutApi();
if (res && res.code === 200) {
localStorage.removeItem('STORE-USER');
setTimeout(() => {
router.push('/login');
}, 1000);
}
}
return { return {
user, user,
@@ -157,7 +171,8 @@ export const useAuthStore = defineStore(
openGithubUrl, openGithubUrl,
openGiteeUrl, openGiteeUrl,
openQQUrl, openQQUrl,
clear clear,
logout,
}; };
}, },
{ {

View File

@@ -68,9 +68,10 @@ export const service = createAlova({
if (response.data instanceof Blob) { if (response.data instanceof Blob) {
return response; return response;
} }
const userStore = useStore().user;
const {code} = response.data; const {code} = response.data;
if (code === 403) { if (code === 403) {
localStorage.removeItem('user'); await userStore.logout();
Modal.warning({ Modal.warning({
title: i18n.global.t('error.loginExpired'), title: i18n.global.t('error.loginExpired'),
content: i18n.global.t('error.authTokenExpired'), content: i18n.global.t('error.authTokenExpired'),

View File

@@ -68,9 +68,10 @@ export const service = createAlova({
if (response.data instanceof Blob) { if (response.data instanceof Blob) {
return response; return response;
} }
const userStore = useStore().user;
const {code} = response.data; const {code} = response.data;
if (code === 403) { if (code === 403) {
localStorage.removeItem('user'); await userStore.logout();
Modal.warning({ Modal.warning({
title: i18n.global.t('error.loginExpired'), title: i18n.global.t('error.loginExpired'),
content: i18n.global.t('error.authTokenExpired'), content: i18n.global.t('error.authTokenExpired'),

View File

@@ -0,0 +1,115 @@
<template>
<div class="location-map">
<div class="location-map-header">
<AButton size="large" type="text" shape="round" class="location-map-header-button" @click="goBack">
<template #icon>
<LeftOutlined/>
</template>
返回
</AButton>
</div>
<div class="location-map-container" id="location-map-container">
</div>
</div>
</template>
<script setup lang="ts">
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import useStore from "@/store";
import {getCoordinateListApi} from "@/api/storage";
const userStore = useStore().user;
const router = useRouter();
const customIcon = L.icon({
iconUrl: userStore.user.avatar,
iconSize: [38, 95], // size of the icon
shadowSize: [50, 64], // size of the shadow
iconAnchor: [22, 94], // point of the icon which will correspond to marker's location
shadowAnchor: [4, 62], // the same for the shadow
popupAnchor: [-3, -76] // point from which the popup should open relative to the iconAnchor
});
async function initMap() {
const map = L.map('location-map-container', {
attributionControl: false,
center: [34.3237, 108.5525],
}).setView([34.3237, 108.5525], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 8,
}).addTo(map);
const res: any = await getCoordinateListApi();
if (res && res.code === 200) {
const list: any[] = res.data.records;
for (const item of list) {
L.marker([item.latitude, item.longitude], {icon: customIcon}).addTo(map)
.bindPopup(`<h3 style="text-align: center;">
${item.country} ${item.province} ${item.city}
<br>
<a href="${'/main/album/location' + `/${item.id}` + `?name=${item.city}`}">
${item.image_count}张照片
</a>
<br>
<span style="color: #999;font-size: 12px;">点击查看</span>
</h3>`
)
.openPopup();
}
}
}
function goBack() {
return router.go(-1);
}
onMounted(() => {
initMap();
});
</script>
<style scoped lang="scss">
.location-map {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
gap: 10px;
position: relative;
.location-map-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
.location-map-header-button {
font-size: 20px;
color: rgb(59, 117, 255);
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
}
}
.location-map-container {
width: 100%;
height: calc(100% - 55px);
}
}
</style>

View File

@@ -3,6 +3,14 @@
<div class="location-album-header"> <div class="location-album-header">
<AButton type="link" size="large" class="location-album-button">地点</AButton> <AButton type="link" size="large" class="location-album-button">地点</AButton>
<span class="location-album-count">你一共在{{ locationAlbums ? locationAlbums.length : 0 }}个地点留下足迹</span> <span class="location-album-count">你一共在{{ locationAlbums ? locationAlbums.length : 0 }}个地点留下足迹</span>
<ATooltip title="点击查看地图" placement="bottom">
<AButton type="text" size="large" shape="default" class="location-album-button" @click="toMap()">
<template #icon>
<AAvatar shape="square" size="default" :src="map"></AAvatar>
</template>
</AButton>
</ATooltip>
</div> </div>
<div class="location-album-content" v-if="locationAlbums && locationAlbums.length>0 "> <div class="location-album-content" v-if="locationAlbums && locationAlbums.length>0 ">
<div class="location-album-content-item" v-for="(item, index) in locationAlbums" :key="index"> <div class="location-album-content-item" v-for="(item, index) in locationAlbums" :key="index">
@@ -35,6 +43,7 @@
import {queryLocationAlbumApi} from "@/api/storage"; import {queryLocationAlbumApi} from "@/api/storage";
import useStore from "@/store"; import useStore from "@/store";
import empty from "@/assets/svgs/empty.svg"; import empty from "@/assets/svgs/empty.svg";
import map from "@/assets/svgs/map.svg";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -53,6 +62,10 @@ async function getLocationAlbums(provider: string, bucket: string) {
} }
} }
function toMap() {
router.push({path: route.path + "/map"});
}
onMounted(() => { onMounted(() => {
getLocationAlbums(upload.storageSelected?.[0], upload.storageSelected?.[1]); getLocationAlbums(upload.storageSelected?.[0], upload.storageSelected?.[1]);
}); });

View File

@@ -61,19 +61,19 @@
</AInput> </AInput>
</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" v-show="!item.showInput" <AButton @click.stop="showAddNameInput(index)" class="people-album-add-name" v-show="!item.showInput"
type="link" type="link"
size="small"> size="small">
{{ item.face_name }} {{ item.face_name }}
</AButton> </AButton>
<AInput v-model:value="addNameInputValue" autofocus v-show="item.showInput" <AInput v-model:value="addNameInputValue" autofocus @click.stop v-show="item.showInput"
@blur="hideAddNameInput(index)" size="small" @blur="hideAddNameInput(index)" size="small"
:maxlength="10" :maxlength="10"
: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

@@ -0,0 +1,24 @@
<template>
</template>
<script setup lang="ts">
import useStore from "@/store";
import {getPrivateImageListApi} from "@/api/storage";
const uploadStore = useStore().upload;
async function getPrivateImageList() {
const res: any = await getPrivateImageListApi(uploadStore.storageSelected?.[0], uploadStore.storageSelected?.[1], "111");
console.log(res);
if (res && res.code === 200) { /* empty */
}
}
onMounted(() => {
getPrivateImageList();
});
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,205 @@
<template>
<div class="ocr-detection">
<a-card class="main-card">
<template #title>
<div class="card-title">
<scan-outlined/>
<span>OCR文字识别</span>
</div>
</template>
<a-row :gutter="[16, 16]">
<a-col :span="24">
<a-alert type="info" show-icon>
<template #message>上传图片进行OCR文字识别</template>
<template #description>支持JPGPNG等常见图片格式将自动识别图片中的文字内容</template>
</a-alert>
</a-col>
</a-row>
<div class="upload-container">
<a-upload
v-model:file-list="fileList"
list-type="picture-card"
:before-upload="beforeUpload"
:multiple="false"
@remove="handleRemove"
>
<div v-if="!fileList.length">
<plus-outlined/>
<div style="margin-top: 8px">上传图片</div>
</div>
</a-upload>
</div>
<div class="preview-container" v-if="imageUrl">
<a-row :gutter="[16, 16]">
<a-col :span="12">
<div class="canvas-wrapper">
<canvas ref="canvasRef" style="max-width: 100%; height: auto;"></canvas>
</div>
</a-col>
<a-col :span="12">
<a-card title="识别结果" :bordered="false">
<template #extra>
<a-button type="primary" :loading="recognizing" @click="handleRecognize">
{{ recognizing ? '识别中...' : '开始识别' }}
</a-button>
</template>
<div class="result-content">
<a-empty v-if="!recognizedText" description="暂无识别结果"/>
<div v-else class="text-result">
<p>{{ recognizedText }}</p>
</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
import {ref, onMounted} from 'vue';
import {PlusOutlined, ScanOutlined} from '@ant-design/icons-vue';
import {message} from 'ant-design-vue';
import type {UploadProps} from 'ant-design-vue';
import * as ocr from '@paddlejs-models/ocr';
// 状态变量
const fileList = ref<any[]>([]);
const imageUrl = ref<string>('');
const recognizing = ref<boolean>(false);
const recognizedText = ref<string>('');
const canvasRef = ref<HTMLCanvasElement | null>(null);
// 初始化OCR模型
onMounted(async () => {
try {
// '/tfjs/ocr/ch_PP-OCRv2_det_fuse_activation/model.json', '/tfjs/ocr/ch_PP-OCRv2_rec_fuse_activation/model.json'
await ocr.init();
message.success('OCR模型初始化成功');
} catch (error) {
message.error('OCR模型初始化失败');
console.error(error);
}
});
// 上传前检查文件
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件!');
return false;
}
// 创建图片URL
imageUrl.value = URL.createObjectURL(file);
return false; // 阻止自动上传
};
// 处理文件移除
const handleRemove = () => {
imageUrl.value = '';
recognizedText.value = '';
if (canvasRef.value) {
const ctx = canvasRef.value.getContext('2d');
ctx?.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
}
return true;
};
// 执行OCR识别
const handleRecognize = async () => {
if (!imageUrl.value) {
message.warning('请先上传图片');
return;
}
recognizing.value = true;
try {
const img = new Image();
img.src = imageUrl.value;
await new Promise((resolve) => (img.onload = resolve));
// 设置canvas尺寸
if (canvasRef.value) {
canvasRef.value.width = img.width;
canvasRef.value.height = img.height;
const ctx = canvasRef.value.getContext('2d');
ctx?.drawImage(img, 0, 0);
// 执行OCR识别
const result = await ocr.recognize(img, {
canvas: canvasRef.value,
style: {
strokeStyle: '#FF4D4F',
lineWidth: 2,
fillStyle: 'rgba(255, 77, 79, 0.1)'
}
});
recognizedText.value = result.text;
message.success('识别完成');
}
} catch (error) {
console.error(error);
message.error('识别失败,请重试');
} finally {
recognizing.value = false;
}
};
</script>
<style scoped lang="scss">
.ocr-detection {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
.main-card {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border-radius: 8px;
overflow: hidden;
}
.card-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
color: var(--primary-color);
}
.upload-container {
margin: 20px 0;
background-color: #fafafa;
padding: 20px;
border-radius: 4px;
border: 1px dashed #d9d9d9;
}
.preview-container {
margin-top: 20px;
}
.canvas-wrapper {
background-color: #fafafa;
padding: 16px;
border-radius: 4px;
border: 1px solid #d9d9d9;
}
.result-content {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
}
.text-result {
white-space: pre-wrap;
word-break: break-all;
}
}
</style>

View File

@@ -14,14 +14,14 @@
<div class="share-content-verify" <div class="share-content-verify"
v-if="imageList && imageList.length === 0 && !getPassword"> v-if="imageList && imageList.length === 0 && !getPassword">
<AInputPassword size="large" placeholder="请输入访问密码" style="width: 20%" <AInputPassword size="large" placeholder="请输入访问密码" style="width: 20%"
@pressEnter="(e)=>getShareImages(e.target.value)"/> @keyup.enter="(e)=>getShareImages(e.target.value)"/>
<p style="font-size: 12px;color: #999;">回车后可查看图片列表</p> <p style="font-size: 12px;color: #999;">回车后可查看图片列表</p>
</div> </div>
<div v-else class="share-content-list"> <div v-else class="share-content-list">
<ImageWaterfallList :image-list="imageList"/> <ImageWaterfallList :image-list="imageList"/>
</div> </div>
</div> </div>
<AFloatButton tooltip="评论" :badge="{ count: 5, color: 'green' }" <AFloatButton v-if="imageList && imageList.length > 0" tooltip="评论" :badge="{ count: 0, color: 'green' }"
@click="shareStore.setOpenCommentDrawer(true)" @click="shareStore.setOpenCommentDrawer(true)"
> >
<template #icon> <template #icon>
@@ -42,6 +42,7 @@ import {queryShareImageApi, queryShareInfoApi} from "@/api/share";
import ImageWaterfallList from "@/components/ImageWaterfallList/ImageWaterfallList.vue"; import ImageWaterfallList from "@/components/ImageWaterfallList/ImageWaterfallList.vue";
import useStore from "@/store"; import useStore from "@/store";
import CommentModal from "@/views/Share/ShareViewList/CommentModal.vue"; import CommentModal from "@/views/Share/ShareViewList/CommentModal.vue";
import {message} from "ant-design-vue";
const imageList = ref<any[]>([]); const imageList = ref<any[]>([]);
@@ -65,6 +66,9 @@ async function getShareImages(password: string) {
imageList.value = res.data.records; imageList.value = res.data.records;
shareStore.addPassword(code, password); shareStore.addPassword(code, password);
await getShareInfo(code, password); await getShareInfo(code, password);
} else {
imageList.value = [];
message.warning(res.msg);
} }
imageStore.imageListLoading = false; imageStore.imageListLoading = false;
} }

View File

@@ -8,7 +8,6 @@ import {predictLandscapeImageData} from '@/utils/tfjs/landscape_recognition';
import {cocoSsdPredict} from '@/utils/tfjs/mobilenet'; import {cocoSsdPredict} from '@/utils/tfjs/mobilenet';
import {getCategoryByLabel} from '@/constant/coco_ssd_label_category'; import {getCategoryByLabel} from '@/constant/coco_ssd_label_category';
// 初始化TensorFlow后端
tf.setBackend('webgl').then(); tf.setBackend('webgl').then();
// 定义消息接口 // 定义消息接口
@@ -33,8 +32,6 @@ interface ImageAnalysisResponse {
error?: string; error?: string;
} }
// 注意不再需要将ArrayBuffer转换为张量的函数因为我们直接使用ImageData对象
// 主要的处理函数 // 主要的处理函数
async function processImage(data: ImageAnalysisRequest): Promise<ImageAnalysisResponse> { async function processImage(data: ImageAnalysisRequest): Promise<ImageAnalysisResponse> {
const {imageData, width, height, settings} = data; const {imageData, width, height, settings} = data;