🎨 update

This commit is contained in:
2025-03-22 01:44:22 +08:00
parent 0068d36ac2
commit 86053b6bd8
30 changed files with 774 additions and 98 deletions

3
components.d.ts vendored
View File

@@ -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']

View File

@@ -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",

View File

@@ -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
View 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
}
}
);
};

View File

@@ -21,3 +21,14 @@ export const getSlideCaptchaDataApi = () => {
});
};
/**
* 获取文字验证码图片数据
*/
export const getTextCaptchaDataApi = () => {
return service.Get('/api/captcha/text/generate', {
meta: {
ignoreToken: false
},
});
};

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View 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

View 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

View File

@@ -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],

View File

@@ -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);

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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"],
},
}
);

View File

@@ -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,

View File

@@ -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);

View File

@@ -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>

View File

@@ -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();

View File

@@ -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);

View File

@@ -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;

View File

@@ -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]) {

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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]);

View 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>

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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;