Merge pull request #199 from Aqr-K/dev-user

This commit is contained in:
jxxghp
2024-10-17 06:55:52 +08:00
committed by GitHub
3 changed files with 135 additions and 45 deletions
+2 -2
View File
@@ -115,7 +115,7 @@ onMounted(() => {
</VAvatar> </VAvatar>
<div> <div>
<div class="text-h6">{{ movieSubscriptions }}</div> <div class="text-h6">{{ movieSubscriptions }}</div>
<div class="text-sm">电影订阅</div> <div class="text-sm text-no-wrap">电影订阅</div>
</div> </div>
</div> </div>
<div class="d-flex align-center"> <div class="d-flex align-center">
@@ -124,7 +124,7 @@ onMounted(() => {
</VAvatar> </VAvatar>
<div> <div>
<div class="text-h6">{{ tvShowSubscriptions }}</div> <div class="text-h6">{{ tvShowSubscriptions }}</div>
<div class="text-sm">电视剧订阅</div> <div class="text-sm text-no-wrap">电视剧订阅</div>
</div> </div>
</div> </div>
</VCardText> </VCardText>
+81 -21
View File
@@ -22,9 +22,15 @@ const props = defineProps({
oper: String, oper: String,
}) })
// 当前用户名称 // 当前登录用户名称
const currentUser = store.state.auth.userName const currentUser = store.state.auth.userName
// 用户名
const userName = ref('')
// 当前头像缓存
const nowAvatar = ref(avatar1)
// 注册事件 // 注册事件
const emit = defineEmits(['save', 'close']) const emit = defineEmits(['save', 'close'])
@@ -48,7 +54,7 @@ const userForm = ref<User>({
}, },
}) })
// changeAvatar function // 更新头像
function changeAvatar(file: Event) { function changeAvatar(file: Event) {
const fileReader = new FileReader() const fileReader = new FileReader()
const { files } = file.target as HTMLInputElement const { files } = file.target as HTMLInputElement
@@ -56,15 +62,22 @@ function changeAvatar(file: Event) {
fileReader.readAsDataURL(files[0]) fileReader.readAsDataURL(files[0])
fileReader.onload = () => { fileReader.onload = () => {
if (typeof fileReader.result === 'string') { if (typeof fileReader.result === 'string') {
userForm.value.avatar = fileReader.result nowAvatar.value = fileReader.result
} }
} }
} }
} }
// reset avatar image // 重置默认头像
function resetAvatar() { function resetDefaultAvatar() {
userForm.value.avatar = avatar1 nowAvatar.value = avatar1
$toast.success('已重置为默认头像,待保存后生效!')
}
// 还原当前头像
function restoreNowAvatar() {
nowAvatar.value = userForm.value.avatar
$toast.success('已还原当前使用头像!')
} }
// 提示框 // 提示框
@@ -82,6 +95,8 @@ async function fetchUserInfo() {
userForm.value = await api.get(`user/${props.username}`) userForm.value = await api.get(`user/${props.username}`)
if (userForm.value) { if (userForm.value) {
userForm.value.avatar = userForm.value.avatar || avatar1 userForm.value.avatar = userForm.value.avatar || avatar1
nowAvatar.value = userForm.value.avatar
userName.value = userForm.value.name
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -122,11 +137,17 @@ async function updateUser() {
} }
userForm.value.password = newPassword.value userForm.value.password = newPassword.value
} }
const oldAvatar = userForm.value.avatar
userForm.value.avatar = nowAvatar.value
startNProgress() startNProgress()
try { try {
const result: { [key: string]: any } = await api.put('user/', userForm.value) const result: { [key: string]: any } = await api.put('user/', userForm.value)
if (result.success) { if (result.success) {
$toast.success(`${userForm.value?.name} 更新成功!`) $toast.success(`${userForm.value?.name} 更新成功!`)
// 通知 localStorage 立刻更新头像
if (oldAvatar !== nowAvatar.value && isCurrentUser.value) {
store.commit('auth/setAvatar', nowAvatar.value)
}
emit('save') emit('save')
} else { } else {
$toast.error(`${userForm.value?.name} 更新失败:${result.message}`) $toast.error(`${userForm.value?.name} 更新失败:${result.message}`)
@@ -152,49 +173,64 @@ const canControl = computed(() => {
if (props.oper === 'add') { if (props.oper === 'add') {
return true return true
} else { } else {
// 编辑显示的用户与当前用户不一致时,有权限 // 调用isCurrentUser函数判断是否为当前用户
if (props.username !== currentUser) { return !(isCurrentUser.value)
return true
}
} }
}) })
// 检查是否为当前用户
const isCurrentUser = computed(() => {
return props.username === currentUser
})
onMounted(() => { onMounted(() => {
if (props.oper !== 'add') { if (props.oper !== 'add') {
fetchUserInfo() fetchUserInfo()
} }
}) })
// 监听 localStorage 中的头像变化
watch(() => store.state.auth.avatar, () => {
nowAvatar.value = store.state.auth.avatar
})
</script> </script>
<template> <template>
<VDialog scrollable :close-on-back="false" persistent eager max-width="50rem" :fullscreen="!display.mdAndUp.value"> <VDialog scrollable :close-on-back="false" persistent eager max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard <VCard
:title="`${props.oper === 'add' ? '新增' : '编辑'}用户${props.oper !== 'add' ? ` - ${userForm.name}` : ''}`" :title="`${props.oper === 'add' ? '新增' : '编辑'}用户${props.oper !== 'add' ? ` - ${userName}` : ''}`"
class="rounded-t" class="rounded-t"
> >
<DialogCloseBtn @click="emit('close')" /> <DialogCloseBtn @click="emit('close')" />
<VDivider /> <VDivider />
<VCardText class="d-flex"> <VCardText class="d-flex">
<!-- 👉 Avatar --> <!-- 👉 Avatar -->
<VAvatar rounded="lg" size="100" class="me-6" :image="userForm.avatar" /> <VAvatar rounded="lg" size="100" class="me-6" :image="nowAvatar" />
<!-- 👉 Upload Photo --> <!-- 👉 Upload Photo -->
<form class="d-flex flex-column justify-center gap-5"> <form class="d-flex flex-column justify-center gap-5">
<div class="d-flex flex-wrap gap-2"> <div class="d-flex flex-wrap gap-2">
<VBtn color="primary" @click="refInputEl?.click()"> <VBtn color="primary" @click="refInputEl?.click()">
<VIcon icon="mdi-cloud-upload-outline" /> <VIcon icon="mdi-cloud-upload-outline" />
<span v-if="display.mdAndUp.value" class="ms-2">上传头像</span> <span v-if="display.mdAndUp.value" class="ms-2">上传头像</span>
</VBtn> </VBtn>
<input ref="refInputEl" type="file" name="file" accept=".jpeg,.png,.jpg,GIF" hidden @input="changeAvatar" /> <input ref="refInputEl" type="file" name="file" accept=".jpeg,.png,.jpg,GIF" hidden @input="changeAvatar" />
<VBtn type="reset" color="error" variant="tonal" @click="resetAvatar">
<VBtn type="reset" color="error" variant="tonal" @click="restoreNowAvatar">
<VIcon icon="mdi-refresh" /> <VIcon icon="mdi-refresh" />
<span v-if="display.mdAndUp.value" class="ms-2">重置</span> <span v-if="display.mdAndUp.value" class="ms-2">还原当前头像</span>
</VBtn> </VBtn>
<VBtn type="reset" color="error" variant="tonal" @click="resetDefaultAvatar">
<VIcon icon="mdi-refresh" />
<span v-if="display.mdAndUp.value" class="ms-2">重置默认头像</span>
</VBtn>
</div> </div>
<p class="text-body-1 mb-0">允许 JPGGIF PNG 格式 最大尺寸 800K</p> <p class="text-body-1 mb-0">允许 JPGPNGGIF 格式 最大尺寸 800K</p>
</form> </form>
</VCardText> </VCardText>
<VCardText> <VCardText>
@@ -207,7 +243,12 @@ onMounted(() => {
<VTextField v-model="userForm.name" density="comfortable" label="用户名" /> <VTextField v-model="userForm.name" density="comfortable" label="用户名" />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField v-model="userForm.email" density="comfortable" label="邮箱" type="email" /> <VTextField
v-model="userForm.email"
density="comfortable"
clearable
label="邮箱"
type="email" />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
@@ -215,6 +256,7 @@ onMounted(() => {
density="comfortable" density="comfortable"
:type="isNewPasswordVisible ? 'text' : 'password'" :type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'" :append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
clearable
label="密码" label="密码"
autocomplete="" autocomplete=""
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible" @click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
@@ -227,6 +269,7 @@ onMounted(() => {
density="comfortable" density="comfortable"
:type="isConfirmPasswordVisible ? 'text' : 'password'" :type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'" :append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
clearable
label="确认密码" label="确认密码"
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible" @click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
/> />
@@ -247,21 +290,38 @@ onMounted(() => {
</VDivider> </VDivider>
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField v-model="userForm.settings.wechat_userid" density="comfortable" label="微信用户" /> <VTextField
v-model="userForm.settings.wechat_userid"
density="comfortable"
clearable
label="微信用户" />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField v-model="userForm.settings.telegram_userid" density="comfortable" label="Telegram用户" /> <VTextField
v-model="userForm.settings.telegram_userid"
density="comfortable"
clearable
label="Telegram用户" />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField v-model="userForm.settings.slack_userid" density="comfortable" label="Slack用户" /> <VTextField
v-model="userForm.settings.slack_userid"
density="comfortable"
clearable
label="Slack用户" />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField v-model="userForm.settings.vocechat_userid" density="comfortable" label="VoceChat用户" /> <VTextField
v-model="userForm.settings.vocechat_userid"
density="comfortable"
clearable
label="VoceChat用户" />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="userForm.settings.synologychat_userid" v-model="userForm.settings.synologychat_userid"
density="comfortable" density="comfortable"
clearable
label="SynologyChat用户" label="SynologyChat用户"
/> />
</VCol> </VCol>
+51 -21
View File
@@ -1,11 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toast-notification' import {useToast} from 'vue-toast-notification'
import QrcodeVue from 'qrcode.vue' import QrcodeVue from 'qrcode.vue'
import { VForm } from 'vuetify/lib/components/index.mjs' import {VForm} from 'vuetify/lib/components/index.mjs'
import api from '@/api' import api from '@/api'
import type { User } from '@/api/types' import type {User} from '@/api/types'
import avatar1 from '@images/avatars/avatar-1.png' import avatar1 from '@images/avatars/avatar-1.png'
import { useDisplay } from 'vuetify' import {useDisplay} from 'vuetify'
import store from "@/store";
// 显示器宽度 // 显示器宽度
const display = useDisplay() const display = useDisplay()
@@ -32,6 +33,9 @@ const secret = ref('')
// 确认双重验证密码 // 确认双重验证密码
const otpPassword = ref('') const otpPassword = ref('')
// 当前头像缓存
const nowAvatar = ref(avatar1)
// 当前用户信息 // 当前用户信息
const accountInfo = ref<User>({ const accountInfo = ref<User>({
id: 0, id: 0,
@@ -58,7 +62,7 @@ const allUsers = ref<User[]>([])
// 二维码信息 // 二维码信息
const qrCode = ref('') const qrCode = ref('')
// changeAvatar function // 更新头像
function changeAvatar(file: Event) { function changeAvatar(file: Event) {
const fileReader = new FileReader() const fileReader = new FileReader()
const { files } = file.target as HTMLInputElement const { files } = file.target as HTMLInputElement
@@ -67,16 +71,23 @@ function changeAvatar(file: Event) {
fileReader.readAsDataURL(files[0]) fileReader.readAsDataURL(files[0])
fileReader.onload = () => { fileReader.onload = () => {
if (typeof fileReader.result === 'string') { if (typeof fileReader.result === 'string') {
accountInfo.value.avatar = fileReader.result nowAvatar.value = fileReader.result
saveAccountInfo() $toast.success('新头像上传成功,待保存后生效!')
} }
} }
} }
} }
// reset avatar image // 重置默认头像
function resetAvatar() { function resetDefaultAvatar() {
accountInfo.value.avatar = avatar1 nowAvatar.value = avatar1
$toast.success('已重置为默认头像,待保存后生效!')
}
// 还原当前头像
function restoreNowAvatar() {
nowAvatar.value = accountInfo.value.avatar
$toast.success('已还原当前使用头像!')
} }
// 调用API,加载当前用户数据 // 调用API,加载当前用户数据
@@ -85,7 +96,10 @@ async function loadAccountInfo() {
const user: User = await api.get('user/current') const user: User = await api.get('user/current')
console.log(user) console.log(user)
accountInfo.value = user accountInfo.value = user
if (!accountInfo.value.avatar) accountInfo.value.avatar = avatar1 if (!accountInfo.value.avatar) {
accountInfo.value.avatar = avatar1
}
nowAvatar.value = accountInfo.value.avatar
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
@@ -101,9 +115,17 @@ async function saveAccountInfo() {
} }
accountInfo.value.password = newPassword.value accountInfo.value.password = newPassword.value
} }
const oldAvatar = accountInfo.value.avatar
accountInfo.value.avatar = nowAvatar.value
try { try {
const result: { [key: string]: any } = await api.put('user/', accountInfo.value) const result: { [key: string]: any } = await api.put('user/', accountInfo.value)
if (result.success) $toast.success('用户信息保存成功!') if (result.success) {
$toast.success('用户信息保存成功!')
if (oldAvatar !== nowAvatar.value) {
// 通知 localStorage 中的用户头像发生变化
store.commit('auth/setAvatar', nowAvatar.value)
}
}
else $toast.error(`用户信息保存失败:${result.message}`) else $toast.error(`用户信息保存失败:${result.message}`)
} catch (error) { } catch (error) {
console.log(error) console.log(error)
@@ -113,9 +135,7 @@ async function saveAccountInfo() {
// 调用API,查询所有用户 // 调用API,查询所有用户
async function loadAllUsers() { async function loadAllUsers() {
try { try {
const result: User[] = await api.get('/user/') allUsers.value = await api.get('/user/')
allUsers.value = result
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
@@ -182,6 +202,11 @@ onMounted(() => {
loadAccountInfo() loadAccountInfo()
loadAllUsers() loadAllUsers()
}) })
// 监听 localStorage 中的用户头像变化
watch(() => store.state.auth.avatar, () => {
nowAvatar.value = store.state.auth.avatar
})
</script> </script>
<template> <template>
@@ -191,14 +216,14 @@ onMounted(() => {
<VCard title="个人信息"> <VCard title="个人信息">
<VCardText class="d-flex"> <VCardText class="d-flex">
<!-- 👉 Avatar --> <!-- 👉 Avatar -->
<VAvatar rounded="lg" size="100" class="me-6" :image="accountInfo.avatar" /> <VAvatar rounded="lg" size="100" class="me-6" :image="nowAvatar" />
<!-- 👉 Upload Photo --> <!-- 👉 Upload Photo -->
<form class="d-flex flex-column justify-center gap-5"> <form class="d-flex flex-column justify-center gap-5">
<div class="d-flex flex-wrap gap-2"> <div class="d-flex flex-wrap gap-2">
<VBtn color="primary" @click="refInputEl?.click()"> <VBtn color="primary" @click="refInputEl?.click()">
<VIcon icon="mdi-cloud-upload-outline" /> <VIcon icon="mdi-cloud-upload-outline" />
<span v-if="display.mdAndUp.value" class="ms-2">上传头像</span> <span v-if="display.mdAndUp.value" class="ms-2">上传头像</span>
</VBtn> </VBtn>
<input <input
@@ -210,9 +235,14 @@ onMounted(() => {
@input="changeAvatar" @input="changeAvatar"
/> />
<VBtn type="reset" color="error" variant="tonal" @click="resetAvatar"> <VBtn type="reset" color="error" variant="tonal" @click="restoreNowAvatar">
<VIcon icon="mdi-refresh" /> <VIcon icon="mdi-refresh" />
<span v-if="display.mdAndUp.value" class="ms-2">重置</span> <span v-if="display.mdAndUp.value" class="ms-2">还原当前头像</span>
</VBtn>
<VBtn type="reset" color="error" variant="tonal" @click="resetDefaultAvatar">
<VIcon icon="mdi-refresh" />
<span v-if="display.mdAndUp.value" class="ms-2">重置默认头像</span>
</VBtn> </VBtn>
<VBtn <VBtn
@@ -222,12 +252,12 @@ onMounted(() => {
> >
<VIcon icon="mdi-account-key" /> <VIcon icon="mdi-account-key" />
<span v-if="display.mdAndUp.value" class="ms-2">{{ <span v-if="display.mdAndUp.value" class="ms-2">{{
accountInfo.is_otp ? '关闭验证' : '双重验证' accountInfo.is_otp ? '关闭双重验证' : '开启双重验证'
}}</span> }}</span>
</VBtn> </VBtn>
</div> </div>
<p class="text-body-1 mb-0">允许 JPGGIF PNG 格式 最大尺寸 800K</p> <p class="text-body-1 mb-0">允许 JPGPNGGIF 格式 最大尺寸 800K</p>
</form> </form>
</VCardText> </VCardText>