✨ add coordinate map
This commit is contained in:
5
components.d.ts
vendored
5
components.d.ts
vendored
@@ -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']
|
||||||
|
@@ -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"/>
|
||||||
|
@@ -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"
|
||||||
|
10669
public/tfjs/ocr/ch_PP-OCRv2_det_fuse_activation/model.json
Normal file
10669
public/tfjs/ocr/ch_PP-OCRv2_det_fuse_activation/model.json
Normal file
File diff suppressed because it is too large
Load Diff
41040
public/tfjs/ocr/ch_PP-OCRv2_rec_fuse_activation/model.json
Normal file
41040
public/tfjs/ocr/ch_PP-OCRv2_rec_fuse_activation/model.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@@ -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: {
|
||||||
|
@@ -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"],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
1
src/assets/json/basic-style.json
Normal file
1
src/assets/json/basic-style.json
Normal file
File diff suppressed because one or more lines are too long
2066
src/assets/json/map-style.json
Normal file
2066
src/assets/json/map-style.json
Normal file
File diff suppressed because it is too large
Load Diff
1
src/assets/svgs/map.svg
Normal file
1
src/assets/svgs/map.svg
Normal 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 |
1
src/assets/svgs/privacy.svg
Normal file
1
src/assets/svgs/privacy.svg
Normal 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 |
971
src/components/ImageEnhancer/ImageEnhancer.vue
Normal file
971
src/components/ImageEnhancer/ImageEnhancer.vue
Normal 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>
|
3
src/components/ImageEnhancer/index.ts
Normal file
3
src/components/ImageEnhancer/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import ImageEnhancer from './ImageEnhancer.vue';
|
||||||
|
|
||||||
|
export default ImageEnhancer;
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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: '地点地图'
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
@@ -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: '隐私空间'
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
@@ -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',
|
||||||
|
}
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
@@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -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'),
|
||||||
|
@@ -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'),
|
||||||
|
@@ -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>
|
@@ -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]);
|
||||||
});
|
});
|
||||||
|
@@ -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>
|
||||||
|
24
src/views/Photograph/PrivacySpace/PrivacySpace.vue
Normal file
24
src/views/Photograph/PrivacySpace/PrivacySpace.vue
Normal 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>
|
205
src/views/Preview/PreviewOCR/PreviewOCR.vue
Normal file
205
src/views/Preview/PreviewOCR/PreviewOCR.vue
Normal 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>支持JPG、PNG等常见图片格式,将自动识别图片中的文字内容</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>
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
Reference in New Issue
Block a user