🎨 update
This commit is contained in:
3
components.d.ts
vendored
3
components.d.ts
vendored
@@ -119,6 +119,7 @@ declare module 'vue' {
|
||||
GradientText: typeof import('./src/components/MyUI/GradientText/GradientText.vue')['default']
|
||||
HeatmapPro: typeof import('./src/components/HeatmapPro/HeatmapPro.vue')['default']
|
||||
ImageEnhancer: typeof import('./src/components/ImageEnhancer/ImageEnhancer.vue')['default']
|
||||
ImageList: typeof import('./src/views/Photograph/PrivacySpace/ImageList.vue')['default']
|
||||
ImageShare: typeof import('./src/views/Share/ImageShare/ImageShare.vue')['default']
|
||||
ImageToolbar: typeof import('./src/components/ImageToolbar/ImageToolbar.vue')['default']
|
||||
ImageUpload: typeof import('./src/components/ImageUpload/ImageUpload.vue')['default']
|
||||
@@ -138,6 +139,7 @@ declare module 'vue' {
|
||||
Login: typeof import('./src/views/Admin/Auth/Login.vue')['default']
|
||||
LoginFooter: typeof import('./src/views/Login/LoginFooter.vue')['default']
|
||||
LoginPage: typeof import('./src/views/Login/LoginPage.vue')['default']
|
||||
LogoutOutlined: typeof import('@ant-design/icons-vue')['LogoutOutlined']
|
||||
MainPage: typeof import('./src/views/Main/MainPage.vue')['default']
|
||||
MessageReport: typeof import('./src/components/CommentReply/src/MessageReport/MessageReport.vue')['default']
|
||||
NotFound: typeof import('./src/views/404/NotFound.vue')['default']
|
||||
@@ -161,6 +163,7 @@ declare module 'vue' {
|
||||
Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default']
|
||||
PreviewBlurDetect: typeof import('./src/views/Preview/PreviewBlurDetect/PreviewBlurDetect.vue')['default']
|
||||
PreviewOCR: typeof import('./src/views/Preview/PreviewOCR/PreviewOCR.vue')['default']
|
||||
PrivacyImageList: typeof import('./src/views/Photograph/PrivacySpace/PrivacyImageList.vue')['default']
|
||||
PrivacySpace: typeof import('./src/views/Photograph/PrivacySpace/PrivacySpace.vue')['default']
|
||||
QrcodeOutlined: typeof import('@ant-design/icons-vue')['QrcodeOutlined']
|
||||
QRLogin: typeof import('./src/views/QRLogin/QRLogin.vue')['default']
|
||||
|
12
package.json
12
package.json
@@ -37,13 +37,13 @@
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@vladmandic/face-api": "^1.7.15",
|
||||
"@vuepic/vue-datepicker": "^11.0.1",
|
||||
"@vuepic/vue-datepicker": "^11.0.2",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"@vueuse/integrations": "^13.0.0",
|
||||
"alova": "^3.2.10",
|
||||
"animejs": "^3.2.2",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.8.3",
|
||||
"axios": "^1.8.4",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
@@ -70,7 +70,7 @@
|
||||
"qr-scanner-wechat": "^0.1.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"swiper": "^11.2.5",
|
||||
"swiper": "^11.2.6",
|
||||
"unplugin-auto-import": "^19.1.1",
|
||||
"upscaler": "^1.0.0-beta.19",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
@@ -86,12 +86,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"sass": "^1.85.1",
|
||||
"sass": "^1.86.0",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.26.1",
|
||||
"typescript-eslint": "^8.27.0",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"vite": "^6.2.2",
|
||||
"vite-plugin-bundle-obfuscator": "1.4.2",
|
||||
|
@@ -4,9 +4,9 @@
|
||||
:theme="app.themeConfig"
|
||||
>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="animation" mode="out-in">
|
||||
<component :is="Component"/>
|
||||
</transition>
|
||||
<!-- <transition name="animation" mode="out-in">-->
|
||||
<component :is="Component"/>
|
||||
<!-- </transition>-->
|
||||
</router-view>
|
||||
</AConfigProvider>
|
||||
</template>
|
||||
|
22
src/api/admin/index.ts
Normal file
22
src/api/admin/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import {service} from "@/utils/alova/service.ts";
|
||||
|
||||
/**
|
||||
* 管理员登录接口
|
||||
* @param param
|
||||
*/
|
||||
export const adminAccountLoginApi = (param: any) => {
|
||||
return service.Post('/api/user/admin/login', {
|
||||
account: param.account,
|
||||
password: param.password,
|
||||
dots: param.dots,
|
||||
key: param.key
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
ignoreToken: true,
|
||||
authRole: 'admin',
|
||||
signature: true
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
@@ -21,3 +21,14 @@ export const getSlideCaptchaDataApi = () => {
|
||||
});
|
||||
|
||||
};
|
||||
/**
|
||||
* 获取文字验证码图片数据
|
||||
*/
|
||||
export const getTextCaptchaDataApi = () => {
|
||||
return service.Get('/api/captcha/text/generate', {
|
||||
meta: {
|
||||
ignoreToken: false
|
||||
},
|
||||
});
|
||||
|
||||
};
|
||||
|
@@ -600,12 +600,16 @@ export const getShareStatisticsInfoApi = () => {
|
||||
* @param provider
|
||||
* @param bucket
|
||||
* @param password
|
||||
* @param key
|
||||
* @param dots
|
||||
*/
|
||||
export const getPrivateImageListApi = (provider: string, bucket: string, password: string) => {
|
||||
export const getPrivateImageListApi = (provider: string, bucket: string, password: string, key: string, dots: any) => {
|
||||
return service.Post('/api/auth/storage/image/private/list', {
|
||||
provider: provider,
|
||||
bucket: bucket,
|
||||
password: password,
|
||||
key: key,
|
||||
dots: dots,
|
||||
}, {
|
||||
cacheFor: {
|
||||
expire: 60 * 5,
|
||||
@@ -625,7 +629,7 @@ export const getPrivateImageListApi = (provider: string, bucket: string, passwor
|
||||
export const getCoordinateListApi = () => {
|
||||
return service.Post('/api/auth/storage/coordinate/list', {}, {
|
||||
cacheFor: {
|
||||
expire:60 * 60 * 24 * 7,
|
||||
expire: 60 * 60 * 24 * 7,
|
||||
mode: "restore",
|
||||
},
|
||||
meta: {
|
||||
@@ -636,3 +640,28 @@ export const getCoordinateListApi = () => {
|
||||
hitSource: ["upload-file", "delete-images"],
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 获取单个隐私照片url
|
||||
* @param id
|
||||
* @param provider
|
||||
* @param bucket
|
||||
* @param password
|
||||
*/
|
||||
export const getPrivateImageSingleUrlApi = (id: number,password: string, provider: string, bucket: string, ) => {
|
||||
return service.Post('/api/auth/storage/image/private/url/single', {
|
||||
id: id,
|
||||
provider: provider,
|
||||
bucket: bucket,
|
||||
password: password,
|
||||
}, {
|
||||
cacheFor: {
|
||||
expire: 60 * 60 * 24,
|
||||
mode: "restore",
|
||||
},
|
||||
meta: {
|
||||
ignoreToken: false,
|
||||
signature: false,
|
||||
},
|
||||
name: "get-private-image-single-url",
|
||||
});
|
||||
};
|
||||
|
BIN
src/assets/images/bg_1.png
Normal file
BIN
src/assets/images/bg_1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
1
src/assets/svgs/cover_image.svg
Normal file
1
src/assets/svgs/cover_image.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1742574097238" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7182" width="200" height="200"><path d="M0 256v708.266667C0 996.266667 27.733333 1024 59.733333 1024h902.4c34.133333 0 59.733333-27.733333 59.733334-59.733333V256H0z" fill="#F3F3F3" p-id="7183"></path><path d="M0 59.733333V256h1024V59.733333C1024 27.733333 996.266667 0 964.266667 0H59.733333C27.733333 0 0 27.733333 0 59.733333z" fill="#8AD7F8" p-id="7184"></path><path d="M484.266667 128m-49.066667 0a49.066667 49.066667 0 1 0 98.133333 0 49.066667 49.066667 0 1 0-98.133333 0Z" fill="#FFFFFF" p-id="7185"></path><path d="M307.2 128m-49.066667 0a49.066667 49.066667 0 1 0 98.133334 0 49.066667 49.066667 0 1 0-98.133334 0Z" fill="#F3705A" p-id="7186"></path><path d="M132.266667 128m-49.066667 0a49.066667 49.066667 0 1 0 98.133333 0 49.066667 49.066667 0 1 0-98.133333 0Z" fill="#415A6B" p-id="7187"></path><path d="M836.266667 601.6H187.733333c-40.533333 0-72.533333-32-72.533333-72.533333s32-72.533333 72.533333-72.533334h646.4c40.533333 0 72.533333 32 72.533334 72.533334s-32 72.533333-70.4 72.533333z" fill="#415A6B" p-id="7188"></path><path d="M642.133333 601.6H187.733333c-40.533333 0-72.533333-32-72.533333-72.533333s32-72.533333 72.533333-72.533334h454.4c40.533333 0 72.533333 32 72.533334 72.533334s-32 72.533333-72.533334 72.533333z" fill="#FFD15C" p-id="7189"></path><path d="M185.6 819.2v-98.133333h21.333333v78.933333h42.666667v19.2h-64zM345.6 804.266667c-10.666667 10.666667-21.333333 14.933333-36.266667 14.933333s-27.733333-4.266667-36.266666-14.933333c-10.666667-10.666667-14.933333-21.333333-14.933334-36.266667s4.266667-25.6 14.933334-36.266667c10.666667-10.666667 21.333333-14.933333 36.266666-14.933333s27.733333 4.266667 36.266667 14.933333c10.666667 10.666667 14.933333 21.333333 14.933333 36.266667s-4.266667 27.733333-14.933333 36.266667z m-6.4-36.266667c0-8.533333-2.133333-17.066667-8.533333-23.466667-6.4-6.4-12.8-8.533333-21.333334-8.533333s-14.933333 2.133333-21.333333 8.533333c-6.4 6.4-8.533333 12.8-8.533333 23.466667 0 8.533333 2.133333 17.066667 8.533333 23.466667 6.4 6.4 12.8 8.533333 21.333333 8.533333s14.933333-2.133333 21.333334-8.533333c4.266667-6.4 8.533333-12.8 8.533333-23.466667zM448 819.2l-8.533333-21.333333h-40.533334l-8.533333 21.333333h-23.466667l42.666667-98.133333h21.333333l42.666667 98.133333H448z m-29.866667-70.4l-12.8 29.866667h25.6l-12.8-29.866667zM556.8 733.866667c8.533333 8.533333 14.933333 21.333333 14.933333 36.266666s-4.266667 27.733333-12.8 36.266667c-8.533333 8.533333-23.466667 12.8-42.666666 12.8h-34.133334v-98.133333h34.133334c19.2 0 32 4.266667 40.533333 12.8z m-14.933333 57.6c6.4-4.266667 8.533333-12.8 8.533333-21.333334 0-10.666667-2.133333-17.066667-8.533333-21.333333-6.4-4.266667-12.8-8.533333-25.6-8.533333h-12.8v59.733333h14.933333c10.666667 0 17.066667-4.266667 23.466667-8.533333zM590.933333 721.066667h21.333334v98.133333h-21.333334v-98.133333zM706.133333 721.066667h21.333334v98.133333h-21.333334l-46.933333-61.866667v61.866667h-21.333333v-98.133333h21.333333l49.066667 64v-64zM817.066667 768h21.333333v34.133333c-10.666667 10.666667-23.466667 17.066667-40.533333 17.066667-14.933333 0-27.733333-4.266667-36.266667-14.933333-10.666667-10.666667-14.933333-21.333333-14.933333-36.266667s4.266667-27.733333 14.933333-36.266667c10.666667-10.666667 21.333333-14.933333 36.266667-14.933333s25.6 4.266667 36.266666 14.933333l-10.666666 17.066667c-4.266667-4.266667-8.533333-6.4-12.8-8.533333-4.266667-2.133333-8.533333-2.133333-12.8-2.133334-8.533333 0-14.933333 2.133333-21.333334 8.533334-6.4 6.4-8.533333 12.8-8.533333 23.466666s2.133333 17.066667 8.533333 21.333334c6.4 6.4 12.8 8.533333 19.2 8.533333 8.533333 0 14.933333-2.133333 19.2-4.266667V768z" fill="#F3705A" p-id="7190"></path></svg>
|
After Width: | Height: | Size: 3.7 KiB |
1
src/assets/svgs/tool-box.svg
Normal file
1
src/assets/svgs/tool-box.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1742575445234" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13779" width="200" height="200"><path d="M648 496a144 144 0 0 0-272 0H144v-112a112 112 0 0 1 112-112h512a112 112 0 0 1 112 112v112z" fill="#25EFE9" p-id="13780"></path><path d="M256 880a112 112 0 0 1-112-112V592h232a144 144 0 0 0 272 0H880v176a112 112 0 0 1-112 112z" fill="#00D1C6" p-id="13781"></path><path d="M864 608v160a96 96 0 0 1-96 96H256a96 96 0 0 1-96-96V608h204.8a161.6 161.6 0 0 0 294.4 0H864m32-32H635.2a126.4 126.4 0 0 1-246.4 0H128v192a128 128 0 0 0 128 128h512a128 128 0 0 0 128-128V576z" fill="#333333" p-id="13782"></path><path d="M512 544m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#00D1C6" p-id="13783"></path><path d="M768 256h-64a128 128 0 0 0-128-128h-128a128 128 0 0 0-128 128h-64a128 128 0 0 0-128 128v128h260.8a126.4 126.4 0 0 1 246.4 0H896v-128a128 128 0 0 0-128-128z m-320-96h128a96 96 0 0 1 96 96H352a96 96 0 0 1 96-96z m416 320H659.2a161.6 161.6 0 0 0-294.4 0H160v-96a96 96 0 0 1 96-96h512a96 96 0 0 1 96 96z" fill="#333333" p-id="13784"></path></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@@ -152,18 +152,19 @@ const {uploading, send: submitFile, abort} = useRequest(uploadFile, {
|
||||
* @param file
|
||||
*/
|
||||
async function customUploadRequest(file: any) {
|
||||
const formData = new FormData();
|
||||
|
||||
const compressedFile = await imageCompression(file.file, upload.options);
|
||||
// 生成缩略图
|
||||
const {binaryData, width, height, size} = await generateThumbnail(compressedFile);
|
||||
upload.predictResult.thumb_w = width;
|
||||
upload.predictResult.thumb_h = height;
|
||||
upload.predictResult.thumb_size = size;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file.file);
|
||||
if (binaryData) {
|
||||
formData.append("thumbnail", binaryData);
|
||||
}
|
||||
|
||||
|
||||
formData.append("file", file.file);
|
||||
formData.append("data", JSON.stringify({
|
||||
provider: upload.storageSelected?.[0],
|
||||
bucket: upload.storageSelected?.[1],
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<Spin size="middle" :spinning="imageStore.imageListLoading" indicator="spin-dot" tip="loading..." :rotate="true">
|
||||
<div style="width:100%;height:100%;" v-if="props.imageList">
|
||||
<div style="width:100%;height:100%;"
|
||||
v-if="props.imageList && upload.storageSelected?.[0] && upload.storageSelected?.[1]">
|
||||
<div v-for="(itemList, indexList) in props.imageList" :key="indexList" class="group-container"
|
||||
:class="{ 'has-selected': hasSelected(itemList) }">
|
||||
<div class="date-header">
|
||||
@@ -27,10 +28,11 @@
|
||||
:alt="item.file_name"
|
||||
:key="index"
|
||||
:height="200"
|
||||
:fallback="cover_image"
|
||||
style="height: 200px;max-width: 800px;object-fit: cover;"
|
||||
:preview="{
|
||||
src: item.url,
|
||||
}"
|
||||
}"
|
||||
loading="lazy">
|
||||
<template #previewMask>
|
||||
</template>
|
||||
@@ -62,7 +64,7 @@ import useStore from "@/store";
|
||||
import complete from '@/assets/svgs/complete.svg';
|
||||
import stop from '@/assets/svgs/stop.svg';
|
||||
import greyComplete from '@/assets/svgs/grey-complete.svg';
|
||||
|
||||
import cover_image from "@/assets/svgs/cover_image.svg";
|
||||
const props = defineProps({
|
||||
imageList: {
|
||||
type: Array as () => any[],
|
||||
@@ -71,7 +73,7 @@ const props = defineProps({
|
||||
});
|
||||
const iconSize = ref(23);
|
||||
const imageStore = useStore().image;
|
||||
|
||||
const upload = useStore().upload;
|
||||
|
||||
const toggleGroup = (group: any) => {
|
||||
const currentIds = group.list.map((item: any) => item.id);
|
||||
|
@@ -25,18 +25,27 @@
|
||||
</div>
|
||||
|
||||
<!-- 社区按钮 -->
|
||||
<!-- <div class="button-wrapper">-->
|
||||
<!-- <AButton type="text" shape="circle" size="large" class="header-menu-item-btn">-->
|
||||
<!-- <template #icon>-->
|
||||
<!-- <AAvatar size="default" shape="circle" :src="community"/>-->
|
||||
<!-- </template>-->
|
||||
<!-- </AButton>-->
|
||||
<!-- </div>-->
|
||||
<div class="button-wrapper">
|
||||
<ATooltip title="工具箱" color="cyan">
|
||||
<APopover placement="bottom" trigger="click">
|
||||
<template #content>
|
||||
<div class="tool-box-content">
|
||||
</div>
|
||||
</template>
|
||||
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn">
|
||||
<template #icon>
|
||||
<AAvatar size="default" shape="circle" :src="toolBox"/>
|
||||
</template>
|
||||
</AButton>
|
||||
</APopover>
|
||||
</ATooltip>
|
||||
</div>
|
||||
<!-- 上传按钮 -->
|
||||
|
||||
<div class="button-wrapper">
|
||||
<ATooltip title="隐私空间" color="geekblue">
|
||||
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn">
|
||||
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn"
|
||||
@click="router.push('/main/photo/privacy/space')">
|
||||
<template #icon>
|
||||
<AAvatar size="default" shape="circle" :src="privacy"/>
|
||||
</template>
|
||||
@@ -176,6 +185,7 @@ import useStore from "@/store";
|
||||
import ImageUpload from "@/components/ImageUpload/ImageUpload.vue";
|
||||
import {getStorageConfigListApi} from "@/api/storage";
|
||||
import {ProviderIcon} from "@/constant/provider_map.ts";
|
||||
import toolBox from "@/assets/svgs/tool-box.svg";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -198,7 +208,6 @@ const menuCSSStyle: any = reactive({
|
||||
});
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getUserConfigList();
|
||||
});
|
||||
@@ -359,6 +368,13 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
}
|
||||
.tool-box-content {
|
||||
width: 150px;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
@@ -34,7 +34,7 @@ export const useSearchStore = defineStore(
|
||||
icon: image
|
||||
}
|
||||
]);
|
||||
const searchOption = ref<string>(options[0].value);
|
||||
const searchOption = ref<string[]>([options[0].value]);
|
||||
const searchValue = ref<string>('');
|
||||
|
||||
const getIconByValue = (value: string) => {
|
||||
|
@@ -1,19 +1,51 @@
|
||||
import localForage from "localforage";
|
||||
|
||||
export const useSystemStore = defineStore(
|
||||
'system',
|
||||
() => {
|
||||
const isCollapsed = ref<boolean>(false);
|
||||
const admin: any = reactive({
|
||||
uid: '',
|
||||
username: '',
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
status: '',
|
||||
});
|
||||
const token: any = reactive({
|
||||
accessToken: '',
|
||||
expireAt: '',
|
||||
});
|
||||
|
||||
const privacyPassword = ref<string>('');
|
||||
|
||||
const privacyImageData = reactive<Record<string, string>>({});
|
||||
|
||||
const getPrivacyImage = (key: string): string | undefined => {
|
||||
return privacyImageData[key];
|
||||
};
|
||||
|
||||
// 添加密码(保持相同API)
|
||||
const addPrivacyImage = (key: string, imageData: string) => {
|
||||
privacyImageData[key] = imageData;
|
||||
};
|
||||
|
||||
return {
|
||||
isCollapsed,
|
||||
admin,
|
||||
token,
|
||||
privacyPassword,
|
||||
privacyImageData,
|
||||
getPrivacyImage,
|
||||
addPrivacyImage,
|
||||
};
|
||||
},
|
||||
{
|
||||
// 开启数据持久化
|
||||
persistedState: {
|
||||
persist: true,
|
||||
storage: localStorage,
|
||||
storage: localForage,
|
||||
key: 'STORE-SYSTEM',
|
||||
includePaths: ['isCollapsed']
|
||||
}
|
||||
includePaths: ['isCollapsed', 'admin', "privacyPassword", "privacyImageData"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@@ -112,7 +112,7 @@ export const useUploadStore = defineStore(
|
||||
const image = new Image();
|
||||
// 压缩图片配置
|
||||
const options = reactive({
|
||||
maxSizeMB: 0.4,
|
||||
maxSizeMB: 0.3,
|
||||
maxWidthOrHeight: 750,
|
||||
maxIteration: 2,
|
||||
useWebWorker: true,
|
||||
|
@@ -11,6 +11,7 @@ import {axiosRequestAdapter} from "@alova/adapter-axios";
|
||||
import {refreshToken} from "@/api/user";
|
||||
import generateKeySecretSignature from "@/utils/signature/signature.ts";
|
||||
import {handleErrorCode} from "@/utils/errorCode/errorCodeHandler.ts";
|
||||
import localForage from "localforage";
|
||||
|
||||
|
||||
const {onAuthRequired, onResponseRefreshToken} = createServerTokenAuthentication<typeof VueHook,
|
||||
@@ -68,14 +69,14 @@ export const service = createAlova({
|
||||
if (response.data instanceof Blob) {
|
||||
return response;
|
||||
}
|
||||
const userStore = useStore().user;
|
||||
const {code} = response.data;
|
||||
if (code === 403) {
|
||||
await userStore.logout();
|
||||
Modal.warning({
|
||||
title: i18n.global.t('error.loginExpired'),
|
||||
content: i18n.global.t('error.authTokenExpired'),
|
||||
onOk() {
|
||||
localStorage.clear();
|
||||
localForage.clear();
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 1000);
|
||||
|
@@ -19,16 +19,16 @@
|
||||
<div class="main-content">
|
||||
<AForm>
|
||||
<div style="padding: 1px; margin: 5px 0">
|
||||
<AFormItem v-bind="formUse.validateInfos.username">
|
||||
<AFormItem v-bind="formUse.validateInfos.account">
|
||||
<AInput
|
||||
v-model:value="formModel.username"
|
||||
v-model:value="formModel.account"
|
||||
placeholder="请输入账号"
|
||||
size="large"
|
||||
type="text"
|
||||
@pressEnter="doLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined :style="formStates.username ? { color: '#c0c0c0' } : {}"/>
|
||||
<UserOutlined :style="formStates.account ? { color: '#c0c0c0' } : {}"/>
|
||||
</template>
|
||||
</AInput>
|
||||
</AFormItem>
|
||||
@@ -50,12 +50,6 @@
|
||||
</AFormItem>
|
||||
</div>
|
||||
|
||||
<div style="padding: 0 5px; overflow: hidden">
|
||||
<ACheckbox v-model:checked="formModel.rememberMe">
|
||||
自动登录
|
||||
</ACheckbox>
|
||||
</div>
|
||||
|
||||
<AFormItem style="margin: 30px 0 0">
|
||||
<AButton
|
||||
size="large"
|
||||
@@ -79,27 +73,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AModal v-model:open="showTextCaptcha" :footer="null" :closable="false" width="375" :centered="true"
|
||||
:maskClosable="false" :bodyStyle="{padding: 0}">
|
||||
<gocaptcha-click
|
||||
:config="{}"
|
||||
:data="{
|
||||
image: captchaData.image,
|
||||
thumb: captchaData.thumb,
|
||||
}"
|
||||
:events="textCaptchaEvent"
|
||||
ref="captcha"
|
||||
/>
|
||||
</AModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {UserOutlined, LockOutlined} from '@ant-design/icons-vue';
|
||||
// import {notification} from 'ant-design-vue';
|
||||
import {notification} from 'ant-design-vue';
|
||||
import Form from 'ant-design-vue/es/form';
|
||||
import {getTextCaptchaDataApi} from "@/api/captcha";
|
||||
import {useThrottleFn} from "@vueuse/core";
|
||||
import {adminAccountLoginApi} from "@/api/admin";
|
||||
import useStore from "@/store";
|
||||
|
||||
|
||||
defineOptions({name: 'Login'});
|
||||
|
||||
const loading = ref(false);
|
||||
// const router = useRouter();
|
||||
|
||||
const captcha = ref(null);
|
||||
const captchaData = ref<any>({});
|
||||
const showTextCaptcha = ref(false);
|
||||
const systemStore = useStore().system;
|
||||
const formModel = reactive({
|
||||
username: '',
|
||||
account: '',
|
||||
password: '',
|
||||
rememberMe: true,
|
||||
});
|
||||
const textCaptchaEvent: any = {
|
||||
confirm: (dots: any, reset: () => void) => {
|
||||
confirmTextCaptcha(dots, reset);
|
||||
},
|
||||
close: () => {
|
||||
showTextCaptcha.value = false;
|
||||
loading.value = false;
|
||||
},
|
||||
refresh: () => {
|
||||
refreshCaptcha();
|
||||
},
|
||||
};
|
||||
|
||||
const formRules = reactive({
|
||||
username: [
|
||||
account: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入用户名',
|
||||
@@ -114,7 +138,7 @@ const formRules = reactive({
|
||||
});
|
||||
|
||||
const formStates = reactive({
|
||||
username: computed(() => formUse.validateInfos.username.validateStatus !== 'error'),
|
||||
account: computed(() => formUse.validateInfos.account.validateStatus !== 'error'),
|
||||
password: computed(() => formUse.validateInfos.password.validateStatus !== 'error'),
|
||||
});
|
||||
|
||||
@@ -126,41 +150,74 @@ const formUse = Form.useForm(
|
||||
const doLogin = async () => {
|
||||
try {
|
||||
await formUse.validate();
|
||||
|
||||
// const success = (_: any) => {
|
||||
// notification.success({
|
||||
// message: '系统提示',
|
||||
// duration: 0.8,
|
||||
// description: `欢迎回来`,
|
||||
// onClose: () => {
|
||||
// loading.value = false;
|
||||
// router.push({path: '/'});
|
||||
// },
|
||||
// });
|
||||
// };
|
||||
//
|
||||
// const failure = (err: any) => {
|
||||
// if (err.message) {
|
||||
// notification.error({
|
||||
// message: '系统提示',
|
||||
// duration: 0.8,
|
||||
// description: err.message,
|
||||
// onClose: () => {
|
||||
// loading.value = false;
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// setTimeout(() => {
|
||||
// loading.value = false;
|
||||
// }, 500);
|
||||
// };
|
||||
|
||||
getTextCaptcha().then(() => {
|
||||
showTextCaptcha.value = true;
|
||||
});
|
||||
loading.value = true;
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showTextCaptcha.value = false;
|
||||
loading.value = false;
|
||||
console.warn(err);
|
||||
}
|
||||
};
|
||||
|
||||
async function getTextCaptcha() {
|
||||
const res: any = await getTextCaptchaDataApi();
|
||||
if (res && res.code === 200) {
|
||||
captchaData.value = {
|
||||
key: res.data.key,
|
||||
image: res.data.image,
|
||||
thumb: res.data.thumb,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const refreshCaptcha = useThrottleFn(getTextCaptcha, 3000);
|
||||
|
||||
async function confirmTextCaptcha(dots: any, reset: () => void) {
|
||||
const dotArr: any[] = [];
|
||||
for (let i = 0; i < dots.length; i++) {
|
||||
const dot: any = dots[i];
|
||||
dotArr.push(dot.x, dot.y);
|
||||
}
|
||||
const params: any = {
|
||||
dots: dotArr.join(','),
|
||||
account: formModel.account,
|
||||
password: formModel.password,
|
||||
key: captchaData.value.key,
|
||||
};
|
||||
const res: any = await adminAccountLoginApi(params);
|
||||
if (res && res.code === 200) {
|
||||
const {uid, access_token, expire_at, username, avatar, nickname, status} = res.data;
|
||||
systemStore.admin.uid = uid;
|
||||
systemStore.admin.username = username;
|
||||
systemStore.admin.avatar = avatar;
|
||||
systemStore.admin.nickname = nickname;
|
||||
systemStore.admin.status = status;
|
||||
systemStore.token.accessToken = access_token;
|
||||
systemStore.token.expire_at = expire_at;
|
||||
notification.success({
|
||||
message: `系统提示`,
|
||||
duration: 2,
|
||||
description: "欢迎回来!",
|
||||
onClose: () => {
|
||||
loading.value = false;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
notification.warning({
|
||||
message: `系统提示`,
|
||||
duration: 2,
|
||||
description: res.msg,
|
||||
onClose: () => {
|
||||
loading.value = false;
|
||||
},
|
||||
});
|
||||
await refreshCaptcha();
|
||||
}
|
||||
reset();
|
||||
showTextCaptcha.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
@@ -41,7 +41,7 @@ async function initMap() {
|
||||
center: [34.3237, 108.5525],
|
||||
}).setView([34.3237, 108.5525], 13);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
L.tileLayer('https://webrd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}', {
|
||||
maxZoom: 8,
|
||||
}).addTo(map);
|
||||
const res: any = await getCoordinateListApi();
|
||||
|
@@ -23,6 +23,7 @@ import useStore from "@/store";
|
||||
import ImageToolbar from "@/components/ImageToolbar/ImageToolbar.vue";
|
||||
|
||||
import ImageWaterfallList from "@/components/ImageWaterfallList/ImageWaterfallList.vue";
|
||||
import {message} from "ant-design-vue";
|
||||
|
||||
const imageStore = useStore().image;
|
||||
const imageList = ref<any[]>([]);
|
||||
@@ -33,6 +34,10 @@ const upload = useStore().upload;
|
||||
|
||||
|
||||
async function getImageList(id: number) {
|
||||
if (!upload.storageSelected?.[0] || !upload.storageSelected?.[1]) {
|
||||
message.error("请选择存储配置");
|
||||
return;
|
||||
}
|
||||
imageStore.imageListLoading = true;
|
||||
const res: any = await queryLocationDetailListApi(id, upload.storageSelected?.[0], upload.storageSelected?.[1]);
|
||||
console.log(res);
|
||||
|
@@ -12,7 +12,8 @@
|
||||
</ATooltip>
|
||||
|
||||
</div>
|
||||
<div class="location-album-content" v-if="locationAlbums && locationAlbums.length>0 ">
|
||||
<div class="location-album-content"
|
||||
v-if="locationAlbums && locationAlbums.length>0 && upload.storageSelected?.[0] && upload.storageSelected?.[1] ">
|
||||
<div class="location-album-content-item" v-for="(item, index) in locationAlbums" :key="index">
|
||||
<span class="location-album-description">{{ item.location }}</span>
|
||||
<div class="location-album-location-list">
|
||||
@@ -44,6 +45,7 @@ import {queryLocationAlbumApi} from "@/api/storage";
|
||||
import useStore from "@/store";
|
||||
import empty from "@/assets/svgs/empty.svg";
|
||||
import map from "@/assets/svgs/map.svg";
|
||||
import {message} from "ant-design-vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -56,6 +58,10 @@ function handleClick(id: number, name: string) {
|
||||
const locationAlbums = ref<any[]>([]);
|
||||
|
||||
async function getLocationAlbums(provider: string, bucket: string) {
|
||||
if (!upload.storageSelected?.[0] || !upload.storageSelected?.[1]) {
|
||||
message.error("请选择存储配置");
|
||||
return;
|
||||
}
|
||||
const res: any = await queryLocationAlbumApi(provider, bucket);
|
||||
if (res && res.code === 200) {
|
||||
locationAlbums.value = res.data.records;
|
||||
|
@@ -18,7 +18,8 @@
|
||||
<PeopleAlbumToolbar :face-list="imageStore.faceList"/>
|
||||
<div class="people-album-container">
|
||||
<Spin :spinning="imageStore.faceListLoading" size="large" indicator="spin-dot">
|
||||
<div class="people-album-content" v-if="imageStore.faceList.length !== 0">
|
||||
<div class="people-album-content"
|
||||
v-if="imageStore.faceList.length !== 0 && upload.storageSelected?.[0] && upload.storageSelected?.[1]">
|
||||
<CheckCard
|
||||
v-for="(item, index) in imageStore.faceList"
|
||||
:key="index"
|
||||
@@ -106,6 +107,7 @@ import PeopleAlbumToolbar from "@/views/Album/PeopleAlbum/PeopleAlbumToolbar.vue
|
||||
const addNameInputValue = ref<string>('');
|
||||
|
||||
const imageStore = useStore().image;
|
||||
const upload =useStore().upload;
|
||||
|
||||
function showAddNameInput(index: number) {
|
||||
if (imageStore.faceList[index]) {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<Spin tip="Loading..." :spinning="imageStore.albumListLoading" size="middle">
|
||||
<div class="phoalbum-item-container" v-if="imageStore.albumList">
|
||||
<div class="phoalbum-item-container"
|
||||
v-if="imageStore.albumList && uploadStore.storageSelected?.[0] && uploadStore.storageSelected?.[1]">
|
||||
<div class="phoalbum-item"
|
||||
v-for="(album, index) in imageStore.albumList"
|
||||
:key="album.id"
|
||||
@@ -125,6 +126,10 @@ async function deleteAlbum(id: number) {
|
||||
* @param id
|
||||
*/
|
||||
async function downloadAlbumImage(id: number) {
|
||||
if (!uploadStore.storageSelected?.[0] || !uploadStore.storageSelected?.[1]) {
|
||||
message.error("请选择存储配置");
|
||||
return;
|
||||
}
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
@@ -62,6 +62,10 @@ const access_password = ref<string>("");
|
||||
* 创建分享
|
||||
*/
|
||||
async function createShare() {
|
||||
if (!upload.storageSelected?.[0] || !upload.storageSelected?.[1]) {
|
||||
message.error("请选择存储配置");
|
||||
return;
|
||||
}
|
||||
const res: any = await albumShareApi(
|
||||
imageStore.albumShareId,
|
||||
expire_date.value,
|
||||
|
@@ -85,6 +85,10 @@ const upload = useStore().upload;
|
||||
|
||||
|
||||
async function getImageList(id: number) {
|
||||
if (!upload.storageSelected?.[0] || !upload.storageSelected?.[1]) {
|
||||
message.error("请选择存储配置");
|
||||
return;
|
||||
}
|
||||
imageStore.imageListLoading = true;
|
||||
const res: any = await queryAlbumDetailListApi(id, upload.storageSelected?.[0], upload.storageSelected?.[1], parseInt(route.query.type as string));
|
||||
if (res && res.code === 200) {
|
||||
|
@@ -5,7 +5,8 @@
|
||||
<AButton type="link" size="large" class="thing-album-button">事物</AButton>
|
||||
|
||||
</div>
|
||||
<div class="thing-album-content" v-if="albumList && albumList.length>0">
|
||||
<div class="thing-album-content"
|
||||
v-if="albumList && albumList.length>0 && upload.storageSelected?.[0] && upload.storageSelected?.[1]">
|
||||
<div class="thing-album-content-item" v-for="(item, index) in albumList" :key="index">
|
||||
<span class="thing-album-title">{{ getZhCategoryNameByEnName(item.category) }}</span>
|
||||
<div class="thing-album-wrapper">
|
||||
@@ -39,12 +40,17 @@ import {queryThingAlbumApi} from "@/api/storage";
|
||||
import {getZhCategoryNameByEnName, getZhLabelNameByEnName} from "@/constant/coco_ssd_label_category.ts";
|
||||
import useStore from "@/store";
|
||||
import empty from "@/assets/svgs/empty.svg";
|
||||
import {message} from "ant-design-vue";
|
||||
|
||||
|
||||
const albumList = ref<any[]>([]);
|
||||
|
||||
async function getAlbumList(provider: string, bucket: string) {
|
||||
const res: any = await queryThingAlbumApi(provider, bucket);
|
||||
async function getAlbumList() {
|
||||
if (!upload.storageSelected?.[0] || !upload.storageSelected?.[1]) {
|
||||
message.error("请选择存储配置");
|
||||
return;
|
||||
}
|
||||
const res: any = await queryThingAlbumApi(upload.storageSelected?.[0], upload.storageSelected?.[1]);
|
||||
if (res && res.code === 200) {
|
||||
albumList.value = res.data.records;
|
||||
}
|
||||
@@ -65,7 +71,7 @@ function handleClick(id: string, category: string, tag: string) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getAlbumList(upload.storageSelected?.[0], upload.storageSelected?.[1]);
|
||||
getAlbumList();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
@@ -84,6 +84,10 @@ const router = useRouter();
|
||||
* 获取所有图片
|
||||
*/
|
||||
async function getAllImages(type: string) {
|
||||
if (!upload.storageSelected?.[0] || !upload.storageSelected?.[1]) {
|
||||
message.error("请选择存储配置");
|
||||
return;
|
||||
}
|
||||
imageList.value = [];
|
||||
imageStore.imageListLoading = true;
|
||||
const res: any = await queryAllImagesApi(type, imageStore.switchValue, upload.storageSelected?.[0], upload.storageSelected?.[1]);
|
||||
|
317
src/views/Photograph/PrivacySpace/PrivacyImageList.vue
Normal file
317
src/views/Photograph/PrivacySpace/PrivacyImageList.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<Spin size="middle" :spinning="imageStore.imageListLoading" indicator="spin-dot" tip="loading..." :rotate="true">
|
||||
<div style="width:100%;height:100%;"
|
||||
v-if="props.imageList && upload.storageSelected?.[0] && upload.storageSelected?.[1]">
|
||||
<div v-for="(itemList, indexList) in props.imageList" :key="indexList" class="group-container"
|
||||
:class="{ 'has-selected': hasSelected(itemList) }">
|
||||
<div class="date-header">
|
||||
<img :src="getGroupIcon(itemList)" alt="Hover" class="custom-checkbox"
|
||||
:style="{ width: iconSize + 'px', height: iconSize + 'px' }"
|
||||
@click.stop="toggleGroup(itemList)"
|
||||
/>
|
||||
<span class="date-text">{{ itemList.date }}</span>
|
||||
</div>
|
||||
<AImagePreviewGroup>
|
||||
<div class="photo-list">
|
||||
<div v-for="(item, index) in itemList.list" :key="index"
|
||||
:class="{'photo-item': true, 'animate': true, [`animate-delay-${index}`]: true}">
|
||||
<CheckCard :key="index"
|
||||
class="photo-item"
|
||||
margin="0"
|
||||
border-radius="0"
|
||||
v-model="imageStore.selected"
|
||||
:showHoverCircle="true"
|
||||
:iconSize="20"
|
||||
:showSelectedEffect="true"
|
||||
:value="item.id">
|
||||
<div @click="getSingleUrl(item.id)">
|
||||
<AImage
|
||||
:alt="item.file_name"
|
||||
:key="index"
|
||||
:height="200"
|
||||
:src="item.thumbnail"
|
||||
:fallback="cover_image"
|
||||
style="height: 200px;max-width: 800px;object-fit: cover;"
|
||||
:preview="{
|
||||
src: url,
|
||||
visible,
|
||||
onVisibleChange: setVisible,
|
||||
}"
|
||||
loading="lazy">
|
||||
<template #previewMask>
|
||||
<span>{{ item.file_name }}</span>
|
||||
</template>
|
||||
</AImage>
|
||||
</div>
|
||||
</CheckCard>
|
||||
</div>
|
||||
</div>
|
||||
</AImagePreviewGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!imageStore.imageListLoading && !props.imageList" class="empty-content">
|
||||
<AEmpty :image="empty"
|
||||
:image-style="{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}">
|
||||
<template #description>
|
||||
<span style="color: #999999;font-size: 16px;font-weight: 500;line-height: 1.5;">
|
||||
还没检测到任何图片,快去上传吧!
|
||||
</span>
|
||||
</template>
|
||||
</AEmpty>
|
||||
</div>
|
||||
</Spin>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import empty from "@/assets/svgs/empty.svg";
|
||||
import useStore from "@/store";
|
||||
import complete from '@/assets/svgs/complete.svg';
|
||||
import stop from '@/assets/svgs/stop.svg';
|
||||
import greyComplete from '@/assets/svgs/grey-complete.svg';
|
||||
import {getPrivateImageSingleUrlApi} from "@/api/storage";
|
||||
import Spin from "@/components/MyUI/Spin/Spin.vue";
|
||||
import cover_image from "@/assets/svgs/cover_image.svg";
|
||||
|
||||
const props = defineProps({
|
||||
imageList: {
|
||||
type: Array as () => any[],
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
const iconSize = ref(23);
|
||||
const imageStore = useStore().image;
|
||||
const upload = useStore().upload;
|
||||
const sysStore = useStore().system;
|
||||
const toggleGroup = (group: any) => {
|
||||
const currentIds = group.list.map((item: any) => item.id);
|
||||
const allSelected = currentIds.every(id =>
|
||||
imageStore.selected.includes(id)
|
||||
);
|
||||
|
||||
// 创建新数组保证响应式更新
|
||||
imageStore.selected = allSelected
|
||||
? imageStore.selected.filter(id => !currentIds.includes(id))
|
||||
: [...new Set([...imageStore.selected, ...currentIds])];
|
||||
};
|
||||
|
||||
// 新增计算分组状态
|
||||
const groupState = computed(() => (group: any) => {
|
||||
const currentIds = group.list.map((item: any) => item.id);
|
||||
if (currentIds.length === 0) return 'empty';
|
||||
|
||||
const selectedCount = currentIds.filter(id =>
|
||||
imageStore.selected.includes(id)
|
||||
).length;
|
||||
|
||||
if (selectedCount === currentIds.length) return 'all';
|
||||
if (selectedCount > 0) return 'partial';
|
||||
return 'none';
|
||||
});
|
||||
|
||||
// 动态获取图标
|
||||
const getGroupIcon = (group: any) => {
|
||||
switch (groupState.value(group)) {
|
||||
case 'all':
|
||||
return complete;
|
||||
case 'partial':
|
||||
return stop;
|
||||
default:
|
||||
return greyComplete;
|
||||
}
|
||||
};
|
||||
|
||||
// 新增计算属性判断是否有选中
|
||||
const hasSelected = computed(() => (group: any) => {
|
||||
return group.list.some((item: any) =>
|
||||
imageStore.selected.includes(item.id)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const visible = ref<boolean>(false);
|
||||
const setVisible = (value: boolean): void => {
|
||||
if (url.value) {
|
||||
visible.value = value;
|
||||
}
|
||||
};
|
||||
const url = ref<string>("");
|
||||
|
||||
async function getSingleUrl(id: number) {
|
||||
const res: any = await getPrivateImageSingleUrlApi(id, sysStore.privacyPassword, upload.storageSelected?.[0], upload.storageSelected?.[1]);
|
||||
if (res && res.code === 200) {
|
||||
url.value = "data:image/jpeg;base64," + res.data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.photo-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
padding: 10px;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.animate {
|
||||
opacity: 0;
|
||||
transform: translateX(-50px);
|
||||
animation: fadeInRight 0.5s forwards;
|
||||
}
|
||||
|
||||
@for $i from 0 through 30 { // 假设最多有20张图片,你可以根据实际情况调整
|
||||
.animate-delay-#{$i} {
|
||||
animation-delay: #{$i * 0.1}s; // 每张图片延迟0.1秒显示
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.date-text {
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
transition: transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: transform;
|
||||
|
||||
/* 新增固定位置规则 */
|
||||
.has-selected & {
|
||||
transform: translateX(28px) !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.group-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
|
||||
// 有选中时的常显逻辑
|
||||
&.has-selected {
|
||||
.custom-checkbox {
|
||||
opacity: 1 !important;
|
||||
transform: translateX(8px) scale(1) !important;
|
||||
}
|
||||
|
||||
// 保持日期位移效果
|
||||
&:hover .date-text {
|
||||
transform: translateX(28px);
|
||||
}
|
||||
}
|
||||
|
||||
// 无选中时的悬停逻辑
|
||||
&:not(.has-selected) {
|
||||
.custom-checkbox {
|
||||
opacity: 0;
|
||||
transform: translateX(8px) scale(0);
|
||||
|
||||
// 悬停显示
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transform: translateX(8px) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 分组悬停时的日期位移
|
||||
&:hover {
|
||||
.date-text {
|
||||
transform: translateX(28px);
|
||||
}
|
||||
|
||||
.custom-checkbox {
|
||||
opacity: 1;
|
||||
transform: translateX(8px) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@keyframes checkScale {
|
||||
0% {
|
||||
transform: translateX(8px) scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(8px) scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(8px) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: translateX(8px) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(8px) scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(8px) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.date-header {
|
||||
position: relative;
|
||||
height: 24px;
|
||||
margin: 8px 0;
|
||||
padding-left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.custom-checkbox {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
opacity: 0;
|
||||
transform: translateX(8px) scale(0);
|
||||
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* 分开定义过渡属性 */
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #1890ff;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
transition: transform 0.2s ease 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
.date-checkbox:checked + .custom-checkbox::after {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
</style>
|
@@ -1,24 +1,167 @@
|
||||
<template>
|
||||
|
||||
|
||||
<div class="privacy-photo">
|
||||
<div class="privacy-photo-header">
|
||||
<AButton type="link" size="large" class="privacy-photo-title">
|
||||
<template #icon>
|
||||
<AAvatar size="small" shape="square" :src="privacy"/>
|
||||
</template>
|
||||
隐私空间
|
||||
</AButton>
|
||||
<AButton type="default" danger shape="round" size="middle" @click="goBack">
|
||||
<template #icon>
|
||||
<LogoutOutlined/>
|
||||
</template>
|
||||
退出
|
||||
</AButton>
|
||||
</div>
|
||||
<div class="photo-list" v-if="imageList && imageList.length > 0">
|
||||
<PrivacyImageList :image-list="imageList"/>
|
||||
</div>
|
||||
<div v-else class="password-verify">
|
||||
<AInputPassword status="warning" size="large" placeholder="请输入密码" style="width: 20%"
|
||||
v-model:value="password"
|
||||
@keyup.enter="getTextCaptcha"
|
||||
/>
|
||||
<p style="font-size: 12px;color: #999;">回车后可查看隐私图片列表</p>
|
||||
<AModal v-model:open="showTextCaptcha" :footer="null" :closable="false" width="375" :centered="true"
|
||||
:maskClosable="false" :bodyStyle="{padding: 0}">
|
||||
<gocaptcha-click
|
||||
:config="{}"
|
||||
:data="{
|
||||
image: captchaData.image,
|
||||
thumb: captchaData.thumb,
|
||||
}"
|
||||
:events="textCaptchaEvent"
|
||||
ref="captcha"
|
||||
/>
|
||||
</AModal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import useStore from "@/store";
|
||||
import {getPrivateImageListApi} from "@/api/storage";
|
||||
import {message} from "ant-design-vue";
|
||||
import privacy from "@/assets/svgs/privacy.svg";
|
||||
import {getTextCaptchaDataApi} from "@/api/captcha";
|
||||
import PrivacyImageList from "@/views/Photograph/PrivacySpace/PrivacyImageList.vue";
|
||||
|
||||
const uploadStore = useStore().upload;
|
||||
// const imageStore = useStore().image;
|
||||
const router = useRouter();
|
||||
|
||||
async function getPrivateImageList() {
|
||||
const res: any = await getPrivateImageListApi(uploadStore.storageSelected?.[0], uploadStore.storageSelected?.[1], "111");
|
||||
console.log(res);
|
||||
if (res && res.code === 200) { /* empty */
|
||||
const imageList = ref<any[]>([]);
|
||||
|
||||
const captcha = ref<any>(null);
|
||||
const captchaData = ref<any>({});
|
||||
const showTextCaptcha = ref(false);
|
||||
const password = ref<string>("");
|
||||
const sysStroe = useStore().system;
|
||||
|
||||
const textCaptchaEvent: any = {
|
||||
confirm: (dots: any, reset: () => void) => {
|
||||
getPrivateImageList(dots, reset);
|
||||
},
|
||||
close: () => {
|
||||
showTextCaptcha.value = false;
|
||||
},
|
||||
refresh: () => {
|
||||
getTextCaptcha();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
async function getTextCaptcha() {
|
||||
if (!password.value) return;
|
||||
const res: any = await getTextCaptchaDataApi();
|
||||
if (res && res.code === 200) {
|
||||
captchaData.value = {
|
||||
key: res.data.key,
|
||||
image: res.data.image,
|
||||
thumb: res.data.thumb,
|
||||
};
|
||||
showTextCaptcha.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPrivateImageList();
|
||||
async function getPrivateImageList(dots: any, reset: () => void) {
|
||||
if (!uploadStore.storageSelected?.[0] || !uploadStore.storageSelected?.[1]) {
|
||||
return;
|
||||
}
|
||||
if (!captchaData.value.key || !password.value) return;
|
||||
const dotArr: any[] = [];
|
||||
for (let i = 0; i < dots.length; i++) {
|
||||
const dot: any = dots[i];
|
||||
dotArr.push(dot.x, dot.y);
|
||||
}
|
||||
const res: any = await getPrivateImageListApi(uploadStore.storageSelected?.[0], uploadStore.storageSelected?.[1], password.value, captchaData.value.key, dotArr.join(','));
|
||||
if (res && res.code === 200) {
|
||||
imageList.value = res.data.records;
|
||||
sysStroe.privacyPassword = password.value;
|
||||
} else {
|
||||
message.warning(res.msg);
|
||||
sysStroe.privacyPassword = "";
|
||||
showTextCaptcha.value = false;
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.go(-1);
|
||||
sysStroe.privacyPassword = "";
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
sysStroe.privacyPassword = "";
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.privacy-photo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.privacy-photo-header {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
|
||||
.privacy-photo-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.photo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: calc(100% - 65px);
|
||||
}
|
||||
|
||||
.password-verify {
|
||||
width: calc(100vw - 230px);
|
||||
height: calc(100vh - 155px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -55,6 +55,10 @@ const imageList = ref<any[]>([]);
|
||||
|
||||
|
||||
const getRecentImages = async () => {
|
||||
if (!upload.storageSelected?.[0] || !upload.storageSelected?.[1]) {
|
||||
message.error("请选择存储配置");
|
||||
return;
|
||||
}
|
||||
imageStore.imageListLoading = true;
|
||||
const res: any = await queryRecentImagesApi(upload.storageSelected?.[0], upload.storageSelected?.[1]);
|
||||
if (res && res.code === 200) {
|
||||
|
@@ -77,7 +77,7 @@ function handleClick({key}) {
|
||||
.personal-center-header {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.2)), url("@/assets/images/bg.webp");
|
||||
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.2)), url("@/assets/images/bg_1.png");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
|
Reference in New Issue
Block a user