- 添加 .dockerignore 和 .env.example 配置文件 - 添加 .gitignore 忽略规则配置 - 创建服务端代理API路由(_file、_modules、_server) - 集成 Ant Design Vue 组件库并配置SSR样式提取 - 定义API响应类型封装 - 创建基础布局组件(blank、console) - 实现应用中心页面和组件(AppsCenter) - 添加文章列表测试页面 - 配置控制台导航菜单结构 - 实现控制台头部组件 - 创建联系页面表单
348 lines
12 KiB
Vue
348 lines
12 KiB
Vue
<template>
|
||
<div class="space-y-4">
|
||
<a-page-header title="账号信息" sub-title="基本资料与企业信息" />
|
||
|
||
<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">
|
||
<a-avatar :size="56" :src="avatarUrl">
|
||
<template v-if="!avatarUrl" #icon>
|
||
<UserOutlined />
|
||
</template>
|
||
</a-avatar>
|
||
<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">
|
||
<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="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-item label="头像 URL" name="avatarUrl">
|
||
<a-input v-model:value="userForm.avatarUrl" placeholder="https://..." />
|
||
</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 } from '@ant-design/icons-vue'
|
||
import { getTenantInfo, getUserInfo, updateLoginUser } from '@/api/layout'
|
||
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' })
|
||
|
||
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 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;
|
||
}
|
||
</style>
|