supports local file system object storage and use it as the default avatar provider

This commit is contained in:
MaysWind
2024-07-27 23:29:18 +08:00
parent 731b6e8bad
commit 2e04affb00
26 changed files with 858 additions and 29 deletions
+5
View File
@@ -0,0 +1,5 @@
const supportedImageExtensions = '.jpg,.jpeg,.png,.gif,.webp';
export default {
supportedImageExtensions: supportedImageExtensions
}
+32
View File
@@ -2,12 +2,14 @@ import axios from 'axios';
import apiConstants from '@/consts/api.js';
import userState from './userstate.js';
import { isBoolean } from './common.js';
import {
getGoogleMapAPIKey,
getBaiduMapAK,
getAmapApplicationKey
} from './server_settings.js';
import { getTimezoneOffsetMinutes } from './datetime.js';
import { generateRandomUUID } from './misc.js';
let needBlockRequest = false;
let blockedRequests = [];
@@ -192,6 +194,14 @@ export default {
incomeAmountColor
});
},
updateAvatar: ({ avatarFile }) => {
return axios.postForm('v1/users/avatar/update.json', {
avatar: avatarFile
});
},
removeAvatar: () => {
return axios.post('v1/users/avatar/remove.json');
},
resendVerifyEmailByLoginedUser: () => {
return axios.post('v1/users/verify_email/resend.json');
},
@@ -552,5 +562,27 @@ export default {
},
generateAmapApiInternalProxyUrl: () => {
return `${window.location.origin}${apiConstants.baseAmapApiProxyUrlPath}`;
},
getInternalAvatarUrlWithToken(avatarUrl, disableBrowserCache) {
if (!avatarUrl) {
return avatarUrl;
}
const params = [];
params.push('token=' + userState.getToken());
if (disableBrowserCache) {
if (isBoolean(disableBrowserCache)) {
params.push('_nocache=' + generateRandomUUID());
} else {
params.push('_nocache=' + disableBrowserCache);
}
}
if (avatarUrl.indexOf('?') >= 0) {
return avatarUrl + '&' + params.join('&');
} else {
return avatarUrl + '?' + params.join('&');
}
}
};
+11
View File
@@ -592,6 +592,7 @@ export default {
'not implemented': 'Not implemented',
'system is busy': 'System is busy',
'not supported': 'Not supported',
'image type not supported': 'Image type is not supported',
'database operation failed': 'Database operation failed',
'SMTP server is not enabled': 'SMTP server is not enabled',
'incomplete or incorrect submission': 'Incomplete or incorrect submission',
@@ -624,6 +625,9 @@ export default {
'email validation not allowed': 'Email validation is not allowed',
'decimal separator and digit grouping symbol cannot be equal': 'Decimal separator and digit grouping symbol cannot be equal',
'user default account is hidden': 'Cannot set hidden account as default account',
'no user avatar': 'There is no user avatar file',
'user avatar is empty': 'User avatar file is empty',
'user avatar not exists': 'User avatar does not exist',
'unauthorized access': 'Unauthorized access',
'current token is invalid': 'Current token is invalid',
'current token is expired': 'Current token is expired',
@@ -1159,6 +1163,8 @@ export default {
'Basic Settings': 'Basic Settings',
'Security Settings': 'Security Settings',
'Two-Factor Authentication Settings': 'Two-Factor Authentication Settings',
'Update Avatar': 'Update Avatar',
'Remove Avatar': 'Remove Avatar',
'(Verified)': '(Verified)',
'(Not Verified)': '(Not Verified)',
'Email address is verified': 'Email address is verified',
@@ -1170,6 +1176,11 @@ export default {
'Please enter your current password when modifying your password': 'Please enter your current password when modifying your password',
'Nothing has been modified': 'Nothing has been modified',
'Your profile has been successfully updated': 'Your profile has been successfully updated',
'Unable to update user avatar': 'Unable to update user avatar',
'Your avatar has been successfully updated': 'Your avatar has been successfully updated',
'Are you sure you want to remove avatar?': 'Are you sure you want to remove avatar?',
'Unable to remove user avatar': 'Unable to remove user avatar',
'Your avatar has been successfully removed': 'Your avatar has been successfully removed',
'Unable to update user profile': 'Unable to update user profile',
'After changing the password, other devices will be logged out. Please use the new password to log in on other devices.': 'After changing the password, other devices will be logged out. Please use the new password to log in on other devices.',
'Data Management': 'Data Management',
+11
View File
@@ -592,6 +592,7 @@ export default {
'not implemented': '未实现',
'system is busy': '系统繁忙',
'not supported': '不支持',
'image type not supported': '图片类型不支持',
'database operation failed': '数据库操作失败',
'SMTP server is not enabled': 'SMTP 服务器没有启用',
'incomplete or incorrect submission': '提交不完整或不正确',
@@ -624,6 +625,9 @@ export default {
'email validation not allowed': '不允许邮箱验证',
'decimal separator and digit grouping symbol cannot be equal': '小数点和数字分组符号不能相同',
'user default account is hidden': '不能把隐藏账户设置为默认账户',
'no user avatar': '没有用户头像文件',
'user avatar is empty': '用户头像文件为空',
'user avatar not exists': '用户头像不存在',
'unauthorized access': '未授权的登录',
'current token is invalid': '当前认证令牌无效',
'current token is expired': '当前认证令牌已过期',
@@ -1159,6 +1163,8 @@ export default {
'Basic Settings': '基本设置',
'Security Settings': '安全设置',
'Two-Factor Authentication Settings': '两步验证设置',
'Update Avatar': '更新头像',
'Remove Avatar': '删除头像',
'(Verified)': '(已验证)',
'(Not Verified)': '(未验证)',
'Email address is verified': '邮箱地址已验证',
@@ -1170,6 +1176,11 @@ export default {
'Please enter your current password when modifying your password': '修改密码时请输入您的当前密码',
'Nothing has been modified': '没有修改的项目',
'Your profile has been successfully updated': '您的用户信息更新成功',
'Unable to update user avatar': '无法更新用户头像',
'Your avatar has been successfully updated': '您的头像更新成功',
'Are you sure you want to remove avatar?': '您确定要删除头像?',
'Unable to remove user avatar': '无法删除用户头像',
'Your avatar has been successfully removed': '您的用户头像删除成功',
'Unable to update user profile': '无法更新用户信息',
'After changing the password, other devices will be logged out. Please use the new password to log in on other devices.': '密码修改后,其他设备将会退出登录,请使用新密码在其他设备上重新登录。',
'Data Management': '数据管理',
+56 -1
View File
@@ -18,7 +18,7 @@ export const useUserStore = defineStore('user', {
},
currentUserAvatar(state) {
const userInfo = state.currentUserBasicInfo || {};
return userInfo.avatar || null;
return state.getUserAvatarUrl(userInfo, false);
},
currentUserDefaultAccountId(state) {
const userInfo = state.currentUserBasicInfo || {};
@@ -126,6 +126,54 @@ export const useUserStore = defineStore('user', {
});
});
},
updateUserAvatar({ avatarFile }) {
return new Promise((resolve, reject) => {
services.updateAvatar({ avatarFile }).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to update user avatar' });
return;
}
resolve(data.result);
}).catch(error => {
logger.error('failed to update user avatar', error);
if (error.response && error.response.data && error.response.data.errorMessage) {
reject({ error: error.response.data });
} else if (!error.processed) {
reject({ message: 'Unable to update user avatar' });
} else {
reject(error);
}
});
});
},
removeUserAvatar() {
return new Promise((resolve, reject) => {
services.removeAvatar().then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to remove user avatar' });
return;
}
resolve(data.result);
}).catch(error => {
logger.error('failed to remove user avatar', error);
if (error.response && error.response.data && error.response.data.errorMessage) {
reject({ error: error.response.data });
} else if (!error.processed) {
reject({ message: 'Unable to remove user avatar' });
} else {
reject(error);
}
});
});
},
getUserDataStatistics() {
return new Promise((resolve, reject) => {
services.getUserDataStatistics().then(response => {
@@ -178,5 +226,12 @@ export const useUserStore = defineStore('user', {
});
});
},
getUserAvatarUrl(userInfo, disableBrowserCache) {
if (!userInfo || !userInfo.avatar) {
return null;
}
return services.getInternalAvatarUrlWithToken(userInfo.avatar, disableBrowserCache);
}
}
});
@@ -8,15 +8,21 @@
</template>
<v-card-text class="d-flex">
<v-avatar rounded="lg" color="primary" variant="tonal" size="100" class="me-4">
<v-img :src="oldProfile.avatar" v-if="oldProfile.avatar">
<v-avatar rounded="lg" color="primary" variant="tonal" size="100" class="me-4" :class="{ 'cursor-pointer': oldProfile.avatarProvider === 'internal' }">
<v-img :src="currentUserAvatar" v-if="currentUserAvatar">
<template #placeholder>
<div class="d-flex align-center justify-center fill-height">
<v-icon size="48" :icon="icons.user"/>
</div>
</template>
</v-img>
<v-icon size="48" :icon="icons.user" v-else-if="!oldProfile.avatar"/>
<v-icon size="48" :icon="icons.user" v-else-if="!currentUserAvatar"/>
<v-menu activator="parent" width="200" location="bottom" offset="14px" v-if="oldProfile.avatarProvider === 'internal'">
<v-list>
<v-list-item :disabled="saving" :title="$t('Update Avatar')" @click="showOpenAvatarDialog"></v-list-item>
<v-list-item :disabled="!currentUserAvatar || saving" :title="$t('Remove Avatar')" @click="removeAvatar"></v-list-item>
</v-list>
</v-menu>
</v-avatar>
<div class="d-flex flex-column justify-center gap-3">
<div class="d-flex text-body-1">
@@ -301,6 +307,7 @@
<confirm-dialog ref="confirmDialog"/>
<snack-bar ref="snackbar" />
<input ref="avatarInput" type="file" style="display: none" :accept="supportedImageExtensions" @change="updateAvatar($event)" />
</template>
<script>
@@ -312,7 +319,9 @@ import { useAccountsStore } from '@/stores/account.js';
import { useOverviewStore } from '@/stores/overview.js';
import datetimeConstants from '@/consts/datetime.js';
import fileConstants from '@/consts/file.js';
import { getNameByKeyValue } from '@/lib/common.js';
import { generateRandomUUID } from '@/lib/misc.js';
import { getCategorizedAccounts } from '@/lib/account.js';
import { isUserVerifyEmailEnabled } from '@/lib/server_settings.js';
import { setExpenseAndIncomeAmountColor } from '@/lib/ui.js';
@@ -366,6 +375,7 @@ export default {
expenseAmountColor: 0,
incomeAmountColor: 0
},
avatarNoCacheId: '',
emailVerified: false,
loading: true,
resending: false,
@@ -428,6 +438,12 @@ export default {
allTransactionEditScopeTypes() {
return this.$locale.getAllTransactionEditScopeTypes();
},
supportedImageExtensions() {
return fileConstants.supportedImageExtensions;
},
currentUserAvatar() {
return this.userStore.getUserAvatarUrl(this.oldProfile, this.avatarNoCacheId);
},
isUserVerifyEmailEnabled() {
return isUserVerifyEmailEnabled();
},
@@ -558,6 +574,61 @@ export default {
}
});
},
showOpenAvatarDialog() {
this.$refs.avatarInput.click();
},
updateAvatar(event) {
if (!event || !event.target || !event.target.files || !event.target.files.length) {
return;
}
const self = this;
const avatarFile = event.target.files[0];
event.target.value = null;
self.saving = true;
self.userStore.updateUserAvatar({ avatarFile }).then(response => {
self.saving = false;
if (response) {
self.avatarNoCacheId = generateRandomUUID();
self.setCurrentUserProfile(response);
}
self.$refs.snackbar.showMessage('Your avatar has been successfully updated');
}).catch(error => {
self.saving = false;
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
});
},
removeAvatar() {
const self = this;
self.$refs.confirmDialog.open('Are you sure you want to remove avatar?').then(() => {
self.saving = true;
self.userStore.removeUserAvatar().then(response => {
self.saving = false;
if (response) {
self.setCurrentUserProfile(response);
}
self.$refs.snackbar.showMessage('Your profile has been successfully updated');
}).catch(error => {
self.saving = false;
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
});
});
},
reset() {
this.setCurrentUserProfile(this.oldProfile);
},