增加更多可配置的部分
This commit is contained in:
@@ -70,15 +70,7 @@
|
|||||||
|
|
||||||
<!-- Desktop Actions -->
|
<!-- Desktop Actions -->
|
||||||
<div class="hidden md:flex items-center gap-4">
|
<div class="hidden md:flex items-center gap-4">
|
||||||
<button
|
<ThemeToggle variant="icon" />
|
||||||
type="button"
|
|
||||||
@click="cycleTheme"
|
|
||||||
class="text-art-muted hover:text-art-text transition-colors p-1"
|
|
||||||
:title="themeLabel"
|
|
||||||
:aria-label="themeLabel"
|
|
||||||
>
|
|
||||||
<Icon :name="themeIcon" :size="20" />
|
|
||||||
</button>
|
|
||||||
<a href="#" class="text-art-muted hover:text-art-text transition-colors"><Icon name="github" :size="20" /></a>
|
<a href="#" class="text-art-muted hover:text-art-text transition-colors"><Icon name="github" :size="20" /></a>
|
||||||
<a href="/admin" target="_blank" class="text-art-muted hover:text-art-text transition-colors" title="后台管理">
|
<a href="/admin" target="_blank" class="text-art-muted hover:text-art-text transition-colors" title="后台管理">
|
||||||
<Icon name="layout-dashboard" :size="20" />
|
<Icon name="layout-dashboard" :size="20" />
|
||||||
@@ -182,14 +174,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="pt-6 border-t border-art-border space-y-4">
|
<div class="pt-6 border-t border-art-border space-y-4">
|
||||||
<button
|
<ThemeToggle variant="label" />
|
||||||
type="button"
|
|
||||||
@click="cycleTheme"
|
|
||||||
class="flex items-center gap-2 text-art-muted hover:text-art-text transition-colors text-sm w-full"
|
|
||||||
>
|
|
||||||
<Icon :name="themeIcon" :size="18" />
|
|
||||||
<span>{{ themeLabel }}</span>
|
|
||||||
</button>
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<a href="#" class="flex items-center gap-2 text-art-muted hover:text-art-text transition-colors text-sm">
|
<a href="#" class="flex items-center gap-2 text-art-muted hover:text-art-text transition-colors text-sm">
|
||||||
<Icon name="github" :size="18" />
|
<Icon name="github" :size="18" />
|
||||||
@@ -219,13 +204,12 @@
|
|||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { usePublicSettings } from '../composables/usePublicSettings'
|
import { usePublicSettings } from '../composables/usePublicSettings'
|
||||||
import { useTheme } from '../composables/useTheme'
|
|
||||||
import Icon from './Icon.vue'
|
import Icon from './Icon.vue'
|
||||||
|
import ThemeToggle from './ThemeToggle.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { cache, getStringSetting } = usePublicSettings()
|
const { cache, getStringSetting } = usePublicSettings()
|
||||||
const { themeIcon, themeLabel, cycleTheme } = useTheme()
|
|
||||||
const activeNav = ref('home')
|
const activeNav = ref('home')
|
||||||
const siteTitle = ref<string>('')
|
const siteTitle = ref<string>('')
|
||||||
const visibleMenus = ref<string[]>(['home', 'blog', 'columns', 'works', 'videos', 'snippets', 'about', 'services'])
|
const visibleMenus = ref<string[]>(['home', 'blog', 'columns', 'works', 'videos', 'snippets', 'about', 'services'])
|
||||||
|
|||||||
@@ -9,10 +9,7 @@
|
|||||||
<a v-if="visibleMenus.includes('snippets')" @click="navigate('/snippets')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">代码</a>
|
<a v-if="visibleMenus.includes('snippets')" @click="navigate('/snippets')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">代码</a>
|
||||||
<a v-if="visibleMenus.includes('about')" @click="navigate('/about')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">关于</a>
|
<a v-if="visibleMenus.includes('about')" @click="navigate('/about')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">关于</a>
|
||||||
<a v-if="visibleMenus.includes('services')" @click="navigate('/services')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">合作</a>
|
<a v-if="visibleMenus.includes('services')" @click="navigate('/services')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">合作</a>
|
||||||
<button type="button" @click="cycleTheme" class="font-mono text-sm text-art-muted hover:text-art-accent flex items-center gap-2">
|
<ThemeToggle variant="label" class="font-mono" />
|
||||||
<Icon :name="themeIcon" :size="18" />
|
|
||||||
{{ themeLabel }}
|
|
||||||
</button>
|
|
||||||
<a @click="openAdmin" class="font-mono text-sm text-art-muted mt-4">管理入口</a>
|
<a @click="openAdmin" class="font-mono text-sm text-art-muted mt-4">管理入口</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -21,12 +18,10 @@
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { onMounted, onUpdated, ref, watch } from 'vue'
|
import { onMounted, onUpdated, ref, watch } from 'vue'
|
||||||
import { usePublicSettings } from '../composables/usePublicSettings'
|
import { usePublicSettings } from '../composables/usePublicSettings'
|
||||||
import { useTheme } from '../composables/useTheme'
|
import ThemeToggle from './ThemeToggle.vue'
|
||||||
import Icon from './Icon.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { cache, getStringSetting } = usePublicSettings()
|
const { cache, getStringSetting } = usePublicSettings()
|
||||||
const { themeIcon, themeLabel, cycleTheme } = useTheme()
|
|
||||||
const visibleMenus = ref<string[]>(['home', 'blog', 'columns', 'works', 'videos', 'snippets', 'about', 'services'])
|
const visibleMenus = ref<string[]>(['home', 'blog', 'columns', 'works', 'videos', 'snippets', 'about', 'services'])
|
||||||
|
|
||||||
const applyMenuSettings = () => {
|
const applyMenuSettings = () => {
|
||||||
|
|||||||
81
client/src/components/ThemeToggle.vue
Normal file
81
client/src/components/ThemeToggle.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="variant === 'icon' ? 'relative group' : 'w-full space-y-3'">
|
||||||
|
<!-- Trigger: click cycles -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="
|
||||||
|
variant === 'icon'
|
||||||
|
? 'text-art-muted hover:text-art-text transition-colors p-1'
|
||||||
|
: 'flex items-center gap-2 text-art-muted hover:text-art-text transition-colors text-sm'
|
||||||
|
"
|
||||||
|
:title="themeLabel"
|
||||||
|
:aria-label="themeLabel"
|
||||||
|
:aria-haspopup="variant === 'icon'"
|
||||||
|
@click="cycleTheme"
|
||||||
|
>
|
||||||
|
<Icon :name="themeIcon" :size="variant === 'icon' ? 20 : 18" />
|
||||||
|
<span v-if="variant === 'label'">{{ themeLabel }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Desktop: hover / focus-within dropdown -->
|
||||||
|
<div
|
||||||
|
v-if="variant === 'icon'"
|
||||||
|
class="absolute right-0 top-full pt-2 opacity-0 invisible pointer-events-none group-hover:opacity-100 group-hover:visible group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:visible group-focus-within:pointer-events-auto transition-opacity duration-150 z-[60]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="min-w-[9.5rem] py-1 rounded-lg border border-art-border bg-art-surface/95 backdrop-blur-md shadow-xl"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="opt in options"
|
||||||
|
:key="opt.value"
|
||||||
|
type="button"
|
||||||
|
role="menuitemradio"
|
||||||
|
:aria-checked="themePref === opt.value"
|
||||||
|
class="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-left transition-colors hover:bg-art-text/5"
|
||||||
|
:class="themePref === opt.value ? 'text-art-accent' : 'text-art-muted hover:text-art-text'"
|
||||||
|
@click.stop="setThemePref(opt.value)"
|
||||||
|
>
|
||||||
|
<Icon :name="opt.icon" :size="16" />
|
||||||
|
<span>{{ opt.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile label: inline three options -->
|
||||||
|
<div v-else class="flex flex-col gap-0.5" role="menu">
|
||||||
|
<button
|
||||||
|
v-for="opt in options"
|
||||||
|
:key="opt.value"
|
||||||
|
type="button"
|
||||||
|
role="menuitemradio"
|
||||||
|
:aria-checked="themePref === opt.value"
|
||||||
|
class="flex items-center gap-2.5 px-2 py-2 text-sm text-left rounded-md transition-colors"
|
||||||
|
:class="themePref === opt.value ? 'text-art-accent bg-art-text/5' : 'text-art-muted hover:text-art-text hover:bg-art-text/5'"
|
||||||
|
@click="setThemePref(opt.value)"
|
||||||
|
>
|
||||||
|
<Icon :name="opt.icon" :size="16" />
|
||||||
|
<span>{{ opt.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useTheme, type ThemePref } from '../composables/useTheme'
|
||||||
|
import Icon from './Icon.vue'
|
||||||
|
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
variant?: 'icon' | 'label'
|
||||||
|
}>(), {
|
||||||
|
variant: 'icon',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { themePref, themeIcon, themeLabel, setThemePref, cycleTheme } = useTheme()
|
||||||
|
|
||||||
|
const options: Array<{ value: ThemePref; label: string; icon: string }> = [
|
||||||
|
{ value: 'system', label: '跟随系统', icon: 'monitor' },
|
||||||
|
{ value: 'light', label: '亮色', icon: 'sun' },
|
||||||
|
{ value: 'dark', label: '暗色', icon: 'moon' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
@@ -115,6 +115,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<SessionExpiredModal />
|
<SessionExpiredModal />
|
||||||
|
<ChangePasswordModal v-model:show="showChangePassword" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -124,6 +125,7 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
import { useToast } from '../../composables/useToast'
|
import { useToast } from '../../composables/useToast'
|
||||||
import { useAuth } from '../../composables/useAuth'
|
import { useAuth } from '../../composables/useAuth'
|
||||||
import SessionExpiredModal from './SessionExpiredModal.vue'
|
import SessionExpiredModal from './SessionExpiredModal.vue'
|
||||||
|
import ChangePasswordModal from './ChangePasswordModal.vue'
|
||||||
import AdminMenuItem from './AdminMenuItem.vue'
|
import AdminMenuItem from './AdminMenuItem.vue'
|
||||||
import { getCurrentUser } from '../../services/api'
|
import { getCurrentUser } from '../../services/api'
|
||||||
import { openActiveMenuAncestors, findRouteTitle, type MenuItem } from './adminMenuTypes'
|
import { openActiveMenuAncestors, findRouteTitle, type MenuItem } from './adminMenuTypes'
|
||||||
@@ -137,6 +139,7 @@ const currentUser = ref(getUser())
|
|||||||
|
|
||||||
// Dropdown logic
|
// Dropdown logic
|
||||||
const isUserDropdownOpen = ref(false)
|
const isUserDropdownOpen = ref(false)
|
||||||
|
const showChangePassword = ref(false)
|
||||||
const dropdownRef = ref<HTMLElement | null>(null)
|
const dropdownRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const toggleUserDropdown = () => {
|
const toggleUserDropdown = () => {
|
||||||
@@ -155,7 +158,7 @@ const goToProfile = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const goToProfilePassword = () => {
|
const goToProfilePassword = () => {
|
||||||
router.push({ path: '/admin/profile', hash: '#password' })
|
showChangePassword.value = true
|
||||||
isUserDropdownOpen.value = false
|
isUserDropdownOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
146
client/src/components/admin/ChangePasswordModal.vue
Normal file
146
client/src/components/admin/ChangePasswordModal.vue
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="modal">
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||||
|
@click.self="close"
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-sm mx-4 bg-[#121214] border border-white/10 rounded-xl p-6 shadow-2xl">
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<h3 class="text-lg font-medium text-white">修改密码</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-white/40 hover:text-white transition-colors"
|
||||||
|
aria-label="关闭"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit" class="space-y-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="change-pwd-old">原密码</label>
|
||||||
|
<input
|
||||||
|
id="change-pwd-old"
|
||||||
|
v-model="form.oldPassword"
|
||||||
|
type="password"
|
||||||
|
class="admin-input"
|
||||||
|
required
|
||||||
|
autocomplete="current-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="change-pwd-new">新密码</label>
|
||||||
|
<input
|
||||||
|
id="change-pwd-new"
|
||||||
|
v-model="form.newPassword"
|
||||||
|
type="password"
|
||||||
|
class="admin-input"
|
||||||
|
required
|
||||||
|
minlength="6"
|
||||||
|
autocomplete="new-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 pt-2">
|
||||||
|
<button type="button" class="admin-btn-secondary flex-1" :disabled="submitting" @click="close">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="admin-btn-primary flex-1" :disabled="submitting">
|
||||||
|
{{ submitting ? '提交中...' : '更新密码' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, watch, onUnmounted } from 'vue'
|
||||||
|
import { useToast } from '../../composables/useToast'
|
||||||
|
import { updateCurrentUserPassword } from '../../services/api'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:show': [value: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const submitting = ref(false)
|
||||||
|
const form = reactive({
|
||||||
|
oldPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.oldPassword = ''
|
||||||
|
form.newPassword = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (submitting.value) return
|
||||||
|
emit('update:show', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') close()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(open) => {
|
||||||
|
if (open) {
|
||||||
|
resetForm()
|
||||||
|
document.addEventListener('keydown', onKeydown)
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('keydown', onKeydown)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('keydown', onKeydown)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await updateCurrentUserPassword(form.oldPassword, form.newPassword)
|
||||||
|
toast.success('密码已更新')
|
||||||
|
resetForm()
|
||||||
|
emit('update:show', false)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
toast.error(e instanceof Error ? e.message : '密码更新失败')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-active,
|
||||||
|
.modal-leave-active {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-from,
|
||||||
|
.modal-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -65,24 +65,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Password -->
|
|
||||||
<div class="admin-card p-6 md:p-8 mt-6 space-y-4">
|
|
||||||
<h2 class="text-lg font-medium text-white">修改密码</h2>
|
|
||||||
<form @submit.prevent="handleChangePassword" class="space-y-4 max-w-md">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>原密码</label>
|
|
||||||
<input v-model="passwordForm.oldPassword" type="password" class="admin-input" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>新密码</label>
|
|
||||||
<input v-model="passwordForm.newPassword" type="password" class="admin-input" required minlength="6" />
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="admin-btn-secondary" :disabled="changingPassword">
|
|
||||||
{{ changingPassword ? '提交中...' : '更新密码' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -91,7 +73,7 @@ import { reactive, ref, onMounted } from 'vue'
|
|||||||
import { useToast } from '../../composables/useToast'
|
import { useToast } from '../../composables/useToast'
|
||||||
import { useAuth } from '../../composables/useAuth'
|
import { useAuth } from '../../composables/useAuth'
|
||||||
import { useUserProfileModal } from '../../composables/useUserProfileModal'
|
import { useUserProfileModal } from '../../composables/useUserProfileModal'
|
||||||
import { getCurrentUser, updateCurrentUser, updateCurrentUserPassword } from '../../services/api'
|
import { getCurrentUser, updateCurrentUser } from '../../services/api'
|
||||||
import ImagePicker from '../../components/admin/ImagePicker.vue'
|
import ImagePicker from '../../components/admin/ImagePicker.vue'
|
||||||
import EmailAutocomplete from '../../components/ui/EmailAutocomplete.vue'
|
import EmailAutocomplete from '../../components/ui/EmailAutocomplete.vue'
|
||||||
|
|
||||||
@@ -100,7 +82,6 @@ const { setUser, getUser } = useAuth()
|
|||||||
const { openUserProfile } = useUserProfileModal()
|
const { openUserProfile } = useUserProfileModal()
|
||||||
|
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const changingPassword = ref(false)
|
|
||||||
const userId = ref<number | null>(null)
|
const userId = ref<number | null>(null)
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
@@ -113,11 +94,6 @@ const form = reactive({
|
|||||||
wechatQrcode: '',
|
wechatQrcode: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const passwordForm = reactive({
|
|
||||||
oldPassword: '',
|
|
||||||
newPassword: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadProfile = async () => {
|
const loadProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const user = await getCurrentUser()
|
const user = await getCurrentUser()
|
||||||
@@ -156,20 +132,6 @@ const handleSaveProfile = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChangePassword = async () => {
|
|
||||||
changingPassword.value = true
|
|
||||||
try {
|
|
||||||
await updateCurrentUserPassword(passwordForm.oldPassword, passwordForm.newPassword)
|
|
||||||
passwordForm.oldPassword = ''
|
|
||||||
passwordForm.newPassword = ''
|
|
||||||
toast.success('密码已更新')
|
|
||||||
} catch (e: unknown) {
|
|
||||||
toast.error(e instanceof Error ? e.message : '密码更新失败')
|
|
||||||
} finally {
|
|
||||||
changingPassword.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openPreview = () => {
|
const openPreview = () => {
|
||||||
const id = userId.value || getUser()?.id
|
const id = userId.value || getUser()?.id
|
||||||
if (id) openUserProfile(id)
|
if (id) openUserProfile(id)
|
||||||
|
|||||||
Reference in New Issue
Block a user