初始版本

This commit is contained in:
2026-04-23 16:30:57 +08:00
commit 0d0683a6e6
538 changed files with 113042 additions and 0 deletions

View File

@@ -0,0 +1,516 @@
<template>
<div class="space-y-4">
<a-page-header title="账号信息" :sub-title="pageSubTitle" />
<a-spin :spinning="loading" tip="加载中...">
<a-row :gutter="[16, 16]">
<a-col :xs="24" :lg="12">
<a-card :bordered="false" class="card" title="基本资料">
<div class="flex items-center gap-4">
<!-- 可点击更换头像 -->
<div class="avatar-wrapper" @click="triggerAvatarUpload" :title="'点击更换头像'">
<a-avatar :size="56" :src="avatarPreview || avatarUrl">
<template v-if="!avatarPreview && !avatarUrl" #icon>
<UserOutlined />
</template>
</a-avatar>
<div class="avatar-overlay">
<a-spin v-if="avatarUploading" :size="'small'" />
<CameraOutlined v-else />
</div>
<input
ref="avatarInputRef"
type="file"
accept="image/*"
style="display: none"
@change="onAvatarFileChange"
/>
</div>
<div class="min-w-0">
<div class="text-base font-semibold text-gray-900">
{{ user?.nickname || user?.username || '未命名用户' }}
</div>
<div class="text-gray-500">
{{ user?.phone || (user as any)?.mobile || user?.email || '' }}
</div>
</div>
</div>
<a-divider />
<a-descriptions :column="1" size="small" bordered>
<a-descriptions-item label="用户ID">{{ user?.userId ?? '-' }}</a-descriptions-item>
<a-descriptions-item label="账号">{{ user?.username ?? '-' }}</a-descriptions-item>
<a-descriptions-item label="昵称">{{ user?.nickname ?? '-' }}</a-descriptions-item>
<a-descriptions-item label="手机号">{{ user?.phone || (user as any)?.mobile || '-' }}</a-descriptions-item>
<a-descriptions-item label="邮箱">{{ user?.email ?? '-' }}</a-descriptions-item>
<a-descriptions-item label="租户ID">{{ user?.tenantId ?? '-' }}</a-descriptions-item>
<a-descriptions-item label="租户名称">{{ user?.tenantName ?? '-' }}</a-descriptions-item>
</a-descriptions>
<div class="mt-6 flex justify-end gap-2">
<a-button @click="reload" :loading="loading">刷新</a-button>
<a-button type="primary" :disabled="!user" @click="openEditUser">编辑</a-button>
</div>
</a-card>
</a-col>
<a-col :xs="24" :lg="12" v-if="isCompanyUser">
<a-card :bordered="false" class="card" title="企业信息">
<a-descriptions :column="1" size="small" bordered>
<a-descriptions-item label="企业ID">{{ company?.companyId ?? '-' }}</a-descriptions-item>
<a-descriptions-item label="企业简称">{{ company?.shortName ?? '-' }}</a-descriptions-item>
<a-descriptions-item label="企业全称">{{ company?.companyName ?? company?.tenantName ?? '-' }}</a-descriptions-item>
<a-descriptions-item label="绑定域名">{{ company?.domain ?? company?.freeDomain ?? '-' }}</a-descriptions-item>
<a-descriptions-item label="联系电话">{{ company?.phone ?? '-' }}</a-descriptions-item>
<a-descriptions-item label="邮箱">{{ company?.email ?? '-' }}</a-descriptions-item>
<a-descriptions-item label="地址">
{{ companyAddress || '-' }}
</a-descriptions-item>
<a-descriptions-item label="实名认证">
<a-tag v-if="company?.authentication" color="green">已认证</a-tag>
<a-tag v-else color="default">未认证</a-tag>
</a-descriptions-item>
</a-descriptions>
<div class="mt-6 flex justify-end gap-2">
<a-button @click="reload" :loading="loading">刷新</a-button>
<a-button type="primary" :disabled="!company" @click="openEditCompany">编辑</a-button>
</div>
</a-card>
</a-col>
</a-row>
</a-spin>
<a-modal
v-model:open="editUserOpen"
title="编辑基本资料"
:confirm-loading="savingUser"
ok-text="保存"
cancel-text="取消"
@ok="submitUser"
>
<a-form ref="userFormRef" layout="vertical" :model="userForm" :rules="userRules">
<a-form-item label="头像" name="avatarUrl">
<div class="modal-avatar-wrap">
<div class="modal-avatar-btn" @click="triggerAvatarUpload">
<a-avatar :size="64" :src="avatarPreview || avatarUrl">
<template v-if="!avatarPreview && !avatarUrl" #icon><UserOutlined /></template>
</a-avatar>
<div class="modal-avatar-overlay">
<a-spin v-if="avatarUploading" :size="'small'" />
<CameraOutlined v-else />
</div>
</div>
<span class="modal-avatar-hint">点击头像更换图片</span>
</div>
</a-form-item>
<a-form-item label="昵称" name="nickname">
<a-input v-model:value="userForm.nickname" placeholder="请输入昵称" />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="userForm.email" placeholder="例如name@example.com" />
</a-form-item>
</a-form>
</a-modal>
<a-modal
v-model:open="editCompanyOpen"
title="编辑企业信息"
:confirm-loading="savingCompany"
ok-text="保存"
cancel-text="取消"
@ok="submitCompany"
>
<a-form ref="companyFormRef" layout="vertical" :model="companyForm" :rules="companyRules">
<a-form-item label="企业简称" name="shortName">
<a-input v-model:value="companyForm.shortName" placeholder="例如:某某科技" />
</a-form-item>
<a-form-item label="企业全称" name="companyName">
<a-input v-model:value="companyForm.companyName" placeholder="例如:某某科技有限公司" />
</a-form-item>
<a-form-item label="绑定域名" name="domain">
<a-input v-model:value="companyForm.domain" placeholder="例如example.com" />
</a-form-item>
<a-form-item label="联系电话" name="phone">
<a-input v-model:value="companyForm.phone" placeholder="请输入联系电话" />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="companyForm.email" placeholder="例如service@example.com" />
</a-form-item>
<a-form-item label="地址" name="address">
<a-textarea v-model:value="companyForm.address" :auto-size="{ minRows: 2, maxRows: 4 }" />
</a-form-item>
<a-form-item label="发票抬头" name="invoiceHeader">
<a-input v-model:value="companyForm.invoiceHeader" placeholder="用于开票" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { message, type FormInstance } from 'ant-design-vue'
import { UserOutlined, CameraOutlined } from '@ant-design/icons-vue'
import { getTenantInfo, getUserInfo, updateLoginUser } from '@/api/layout'
import { updateUser } from '@/api/system/user'
import { uploadOssAvatar } from '@/api/system/file'
import { updateCompany } from '@/api/system/company'
import type { Company } from '@/api/system/company/model'
import type { User } from '@/api/system/user/model'
definePageMeta({ layout: 'console', ssr: false })
const loading = ref(false)
const savingUser = ref(false)
const savingCompany = ref(false)
const user = ref<User | null>(null)
const company = ref<Company | null>(null)
// ===== 头像上传 =====
const avatarInputRef = ref<HTMLInputElement | null>(null)
const avatarUploading = ref(false)
const avatarPreview = ref('') // 本地预览 / 上传后的新 URL
function triggerAvatarUpload() {
avatarInputRef.value?.click()
}
async function onAvatarFileChange(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
// 类型校验
if (!file.type.startsWith('image/')) {
message.error('请选择图片文件')
return
}
// 大小校验 5MB
if (file.size > 5 * 1024 * 1024) {
message.error('图片大小不能超过 5MB')
return
}
// 本地预览
const reader = new FileReader()
reader.onload = (ev) => {
avatarPreview.value = ev.target?.result as string
}
reader.readAsDataURL(file)
// 上传
avatarUploading.value = true
try {
const userId = user.value?.userId
const ext = file.name.split('.').pop() || 'jpg'
const fileName = `avatar_${userId}_${Date.now()}.${ext}`
const record = await uploadOssAvatar(file, fileName)
const url = (record?.url || record?.downloadUrl || '').trim()
if (!url) throw new Error('上传成功但未返回文件地址')
// 立即保存到用户信息
await updateUser({ userId, avatarUrl: url } as User)
avatarPreview.value = url
userForm.avatarUrl = url
if (user.value) user.value.avatarUrl = url
message.success('头像更新成功')
} catch (e) {
avatarPreview.value = ''
message.error(e instanceof Error ? e.message : '上传失败,请重试')
} finally {
avatarUploading.value = false
// 清空 input允许重复选同一文件
if (avatarInputRef.value) avatarInputRef.value.value = ''
}
}
// 是否为企业或开发者用户(非普通用户)
const isCompanyUser = computed(() => {
const type = user.value?.type
return type === 1 || type === 2
})
// 动态副标题
const pageSubTitle = computed(() => {
if (isCompanyUser.value) {
return '基本资料与企业信息'
}
return '基本资料'
})
const avatarUrl = computed(() => {
const candidate =
user.value?.avatarUrl ||
user.value?.avatar ||
user.value?.merchantAvatar ||
user.value?.logo ||
''
if (typeof candidate !== 'string') return ''
const normalized = candidate.trim()
if (!normalized || normalized === 'null' || normalized === 'undefined') return ''
return normalized
})
const companyAddress = computed(() => {
const parts = [company.value?.province, company.value?.city, company.value?.region, company.value?.address]
.map((v) => (typeof v === 'string' ? v.trim() : ''))
.filter(Boolean)
return parts.join(' ')
})
async function load() {
loading.value = true
try {
const [uRes, cRes] = await Promise.allSettled([getUserInfo(), getTenantInfo()])
if (uRes.status === 'fulfilled') {
user.value = uRes.value
} else {
console.error(uRes.reason)
message.error(uRes.reason instanceof Error ? uRes.reason.message : '获取用户信息失败')
}
if (cRes.status === 'fulfilled') {
company.value = cRes.value
} else {
console.error(cRes.reason)
message.error(cRes.reason instanceof Error ? cRes.reason.message : '获取企业信息失败')
}
} finally {
loading.value = false
}
}
async function reload() {
await load()
}
const editUserOpen = ref(false)
const userFormRef = ref<FormInstance>()
const userForm = reactive<{ nickname?: string; email?: string; avatarUrl?: string }>({
nickname: '',
email: '',
avatarUrl: ''
})
const userRules = reactive({
nickname: [{ required: true, message: '请输入昵称', type: 'string' }],
email: [{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }]
})
function openEditUser() {
if (!user.value) return
userForm.nickname = user.value.nickname ?? ''
userForm.email = user.value.email ?? ''
userForm.avatarUrl = user.value.avatarUrl ?? avatarUrl.value ?? ''
editUserOpen.value = true
}
async function submitUser() {
if (!user.value) return
try {
await userFormRef.value?.validate()
} catch {
return
}
const nickname = (userForm.nickname ?? '').trim()
if (!nickname) {
message.error('请输入昵称')
return
}
const email = (userForm.email ?? '').trim()
const avatar = (userForm.avatarUrl ?? '').trim()
savingUser.value = true
try {
await updateLoginUser({
userId: user.value.userId,
nickname,
email: email || undefined,
avatarUrl: avatar || undefined
} as User)
message.success('保存成功')
editUserOpen.value = false
await load()
} catch (e: unknown) {
console.error(e)
message.error(e instanceof Error ? e.message : '保存失败')
} finally {
savingUser.value = false
}
}
const editCompanyOpen = ref(false)
const companyFormRef = ref<FormInstance>()
const companyForm = reactive<{
companyId?: number
shortName?: string
companyName?: string
domain?: string
phone?: string
email?: string
address?: string
invoiceHeader?: string
}>({
companyId: undefined,
shortName: '',
companyName: '',
domain: '',
phone: '',
email: '',
address: '',
invoiceHeader: ''
})
const companyRules = reactive({
companyName: [{ required: true, message: '请输入企业全称', type: 'string' }],
email: [{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }],
phone: [
{
validator: (_rule: unknown, value: unknown) => {
const normalized = typeof value === 'string' ? value.trim() : ''
if (!normalized) return Promise.resolve()
const mobileReg = /^1[3-9]\d{9}$/
if (mobileReg.test(normalized)) return Promise.resolve()
return Promise.reject(new Error('手机号格式不正确'))
},
trigger: 'blur'
}
]
})
function openEditCompany() {
if (!company.value) return
companyForm.companyId = company.value.companyId
companyForm.shortName = company.value.shortName ?? ''
companyForm.companyName = company.value.companyName ?? company.value.tenantName ?? ''
companyForm.domain = company.value.domain ?? ''
companyForm.phone = company.value.phone ?? ''
companyForm.email = company.value.email ?? ''
companyForm.address = company.value.address ?? ''
companyForm.invoiceHeader = company.value.invoiceHeader ?? ''
editCompanyOpen.value = true
}
async function submitCompany() {
if (!company.value) return
try {
await companyFormRef.value?.validate()
} catch {
return
}
if (!companyForm.companyId) {
message.error('企业ID缺失无法保存')
return
}
const companyName = (companyForm.companyName ?? '').trim()
if (!companyName) {
message.error('请输入企业全称')
return
}
savingCompany.value = true
try {
const domain = (companyForm.domain ?? '').trim()
const phone = (companyForm.phone ?? '').trim()
const email = (companyForm.email ?? '').trim()
const address = (companyForm.address ?? '').trim()
const invoiceHeader = (companyForm.invoiceHeader ?? '').trim()
await updateCompany({
companyId: companyForm.companyId,
shortName: (companyForm.shortName ?? '').trim() || undefined,
companyName,
domain: domain || undefined,
phone: phone || undefined,
email: email || undefined,
address: address || undefined,
invoiceHeader: invoiceHeader || undefined
} as Company)
message.success('保存成功')
editCompanyOpen.value = false
await load()
} catch (e: unknown) {
console.error(e)
message.error(e instanceof Error ? e.message : '保存失败')
} finally {
savingCompany.value = false
}
}
onMounted(load)
</script>
<style scoped>
.card {
border-radius: 12px;
}
/* ===== 头像上传 ===== */
.avatar-wrapper {
position: relative;
cursor: pointer;
flex-shrink: 0;
width: 56px;
height: 56px;
border-radius: 50%;
overflow: hidden;
}
.avatar-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
opacity: 0;
transition: opacity 0.2s;
border-radius: 50%;
}
.avatar-wrapper:hover .avatar-overlay {
opacity: 1;
}
/* 弹窗内头像 */
.modal-avatar-wrap {
display: flex;
align-items: center;
gap: 12px;
}
.modal-avatar-btn {
position: relative;
cursor: pointer;
width: 64px;
height: 64px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
}
.modal-avatar-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
opacity: 0;
transition: opacity 0.2s;
border-radius: 50%;
}
.modal-avatar-btn:hover .modal-avatar-overlay {
opacity: 1;
}
.modal-avatar-hint {
font-size: 13px;
color: rgba(0, 0, 0, 0.45);
}
</style>