初始化

This commit is contained in:
zg
2026-07-15 16:13:27 +08:00
parent 6d6f559793
commit c4023c1623
276 changed files with 42622 additions and 0 deletions

View File

@@ -0,0 +1,302 @@
<template>
<div class="personal-page">
<div class="profile-grid">
<!-- 头像 + 基本信息 -->
<div class="profile-card">
<div class="card-header">
<el-icon :size="16"><User /></el-icon>
<span>基本信息</span>
</div>
<div class="card-body">
<!-- 头像上传 -->
<div class="avatar-section">
<div class="avatar-wrapper" @click="triggerUpload">
<el-avatar :size="80" :src="form.avatar" class="profile-avatar">
{{ (form.realName || form.username || 'U').charAt(0) }}
</el-avatar>
<div class="avatar-overlay">
<el-icon :size="20"><Camera /></el-icon>
</div>
</div>
<input ref="fileInputRef" type="file" accept="image/*" style="display:none" @change="handleAvatarUpload" />
<div class="avatar-info">
<div class="avatar-name">{{ form.realName || form.username }}</div>
<div class="avatar-role">{{ form.post || '普通用户' }}</div>
<el-button size="small" text type="primary" @click="triggerUpload">
<el-icon><Upload /></el-icon>更换头像
</el-button>
</div>
</div>
<el-form :model="form" label-width="80px" label-position="left" class="info-form">
<el-form-item label="用户名">
<el-input :model-value="form.username" disabled />
</el-form-item>
<el-form-item label="姓名">
<el-input v-model="form.realName" placeholder="请输入真实姓名" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="岗位">
<el-input v-model="form.post" placeholder="请输入岗位" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="saveProfile">
<el-icon><Check /></el-icon>保存信息
</el-button>
</el-form-item>
</el-form>
</div>
</div>
<!-- 修改密码 + 账户信息 -->
<div class="right-cards">
<!-- 修改密码 -->
<div class="profile-card">
<div class="card-header">
<el-icon :size="16"><Lock /></el-icon>
<span>修改密码</span>
</div>
<div class="card-body">
<el-form ref="pwdFormRef" :model="pwdForm" :rules="pwdRules" label-width="80px" label-position="left">
<el-form-item label="原密码" prop="oldPwd">
<el-input v-model="pwdForm.oldPwd" type="password" placeholder="请输入原密码" show-password />
</el-form-item>
<el-form-item label="新密码" prop="newPwd">
<el-input v-model="pwdForm.newPwd" type="password" placeholder="至少6位" show-password />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPwd">
<el-input v-model="pwdForm.confirmPwd" type="password" placeholder="再次输入" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="changingPwd" @click="changePassword">
<el-icon><Key /></el-icon>修改密码
</el-button>
</el-form-item>
</el-form>
</div>
</div>
<!-- 账户信息 -->
<div class="profile-card">
<div class="card-header">
<el-icon :size="16"><InfoFilled /></el-icon>
<span>账户信息</span>
</div>
<div class="card-body">
<div class="meta-list">
<div class="meta-row">
<span class="meta-label">用户ID</span>
<code class="meta-value mono">{{ form.userId?.substring(0, 12) || '-' }}...</code>
</div>
<div class="meta-row">
<span class="meta-label">用户名</span>
<span class="meta-value">{{ form.username || '-' }}</span>
</div>
<div class="meta-row">
<span class="meta-label">注册时间</span>
<span class="meta-value">{{ fmtDate(form.createdAt) }}</span>
</div>
<div class="meta-row">
<span class="meta-label">最后登录</span>
<span class="meta-value">{{ fmtDate(form.lastLoginTime) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { User, Lock, Key, Check, InfoFilled, Camera, Upload } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import request from '@/utils/request'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const fileInputRef = ref<HTMLInputElement>()
const form = reactive({
userId: '' as string | null,
username: '',
realName: '',
email: '',
phone: '',
avatar: '',
post: '',
createdAt: '',
lastLoginTime: '',
})
const saving = ref(false)
function fmtDate(s: string | null | undefined): string {
if (!s) return '-'
try {
const d = new Date(s)
if (isNaN(d.getTime())) return s
return d.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
} catch { return s }
}
onMounted(async () => {
try {
const res = await request.get('/zgapi/v1/auth/user/info')
const d = res.data
Object.assign(form, {
userId: d.userId ?? '',
username: d.username ?? '',
realName: d.realName ?? '',
email: d.email ?? '',
phone: d.phone ?? '',
avatar: d.avatar ?? '',
post: d.post ?? '',
createdAt: d.createdAt ?? '',
lastLoginTime: d.lastLoginTime ?? '',
})
setTimeout(() => pwdFormRef.value?.clearValidate(), 0)
} catch { /* ignore */ }
})
function triggerUpload() {
fileInputRef.value?.click()
}
async function handleAvatarUpload(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
const fd = new FormData()
fd.append('file', file)
fd.append('fileType', 'avatar')
fd.append('userId', form.userId || '')
try {
const res = await request.post('/zgapi/v1/admin/file/upload', fd)
// 文件存入数据库后用文件ID构建访问路径
const fileId = res.data?.id
if (!fileId) { ElMessage.error('上传返回异常'); return }
const gatewayUrl = import.meta.env.VITE_GATEWAY_URL || ''
const avatarUrl = `${gatewayUrl}/zgapi/v1/admin/file/${fileId}/view`
form.avatar = avatarUrl
// 更新用户头像字段
await request.put('/zgapi/v1/admin/user', {
id: form.userId,
avatar: avatarUrl,
})
// 同步刷新 store 中的用户信息(也会更新 Redis TokenInfo
await authStore.fetchUserInfo()
ElMessage.success('头像已更新')
} catch { ElMessage.error('上传失败') }
}
async function saveProfile() {
saving.value = true
try {
await request.put('/zgapi/v1/admin/user', {
id: form.userId,
username: form.username,
realName: form.realName,
email: form.email,
phone: form.phone,
post: form.post,
})
await authStore.fetchUserInfo()
ElMessage.success('个人信息已更新')
} catch { /* ignore */ } finally {
saving.value = false
}
}
// 密码
const pwdFormRef = ref<FormInstance>()
const changingPwd = ref(false)
const pwdForm = reactive({ oldPwd: '', newPwd: '', confirmPwd: '' })
const pwdRules: FormRules = {
oldPwd: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
newPwd: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码至少6位', trigger: 'blur' },
],
confirmPwd: [
{ required: true, message: '请再次输入', trigger: 'blur' },
{ validator: (_r: any, v: string, cb: any) => v !== pwdForm.newPwd ? cb(new Error('两次密码不一致')) : cb(), trigger: 'blur' },
],
}
async function changePassword() {
const valid = await pwdFormRef.value?.validate().catch(() => false)
if (!valid) return
changingPwd.value = true
try {
await request.put('/zgapi/v1/auth/password/update', null, {
params: { oldPwd: pwdForm.oldPwd, newPwd: pwdForm.newPwd },
})
ElMessage.success('密码修改成功,即将重新登录')
pwdForm.oldPwd = ''; pwdForm.newPwd = ''; pwdForm.confirmPwd = ''
setTimeout(() => {
authStore.logout()
window.location.href = '/login'
}, 1500)
} catch { /* ignore */ } finally {
changingPwd.value = false
}
}
</script>
<style scoped>
.personal-page { max-width: 960px; }
.profile-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.right-cards { display: flex; flex-direction: column; gap: 20px; }
.profile-card {
background: var(--bg-surface); border-radius: var(--radius-lg);
border: 1px solid var(--border-color); box-shadow: var(--shadow-xs); overflow: hidden;
}
.card-header {
display: flex; align-items: center; gap: 8px;
padding: 16px 20px; border-bottom: 1px solid var(--border-light);
font-weight: 600; font-size: 14px; color: var(--text-primary);
}
.card-body { padding: 24px 20px; }
/* 头像 */
.avatar-section { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid var(--border-light); }
.avatar-wrapper { position: relative; cursor: pointer; flex-shrink: 0; }
.profile-avatar { box-shadow: 0 4px 12px rgba(99,102,241,0.2); }
.avatar-overlay {
position: absolute; inset: 0; border-radius: 50%;
background: rgba(0,0,0,0.35); display: flex; align-items: center; justify-content: center;
opacity: 0; transition: opacity var(--transition-fast); color: #fff;
}
.avatar-wrapper:hover .avatar-overlay { opacity: 1; }
.avatar-name { font-size: 18px; font-weight: 600; color: var(--text-primary); margin-bottom: 2px; }
.avatar-role { font-size: 13px; color: var(--text-muted); margin-bottom: 8px; }
/* 表单 */
.info-form :deep(.el-form-item) { margin-bottom: 18px; }
.info-form :deep(.el-form-item:last-child) { margin-bottom: 0; padding-top: 4px; }
/* 元信息 */
.meta-list { display: flex; flex-direction: column; }
.meta-row { display: flex; justify-content: space-between; align-items: center; padding: 14px 0; border-bottom: 1px solid var(--border-light); }
.meta-row:last-child { border-bottom: none; }
.meta-label { font-size: 13px; color: var(--text-muted); font-weight: 500; }
.meta-value { font-size: 13px; color: var(--text-secondary); font-weight: 600; }
.meta-value.mono { font-family: 'SF Mono', 'Consolas', monospace; font-size: 12px; }
@media (max-width: 768px) {
.profile-grid { grid-template-columns: 1fr; gap: 12px; }
.personal-page { padding: 0; }
.avatar-section { flex-direction: column; text-align: center; }
.avatar-info { align-items: center; }
.card-body { padding: 16px; }
}
</style>