修复apiresult的问题;增加用户详情查看的逻辑
This commit is contained in:
@@ -45,6 +45,96 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<n-modal
|
||||
v-model:show="detailModal.visible"
|
||||
preset="card"
|
||||
title="用户详情"
|
||||
:style="{ width: '920px', maxWidth: '92vw' }"
|
||||
:mask-closable="false"
|
||||
content-style="padding: 0; max-height: calc(85vh - 130px); overflow-y: auto"
|
||||
>
|
||||
<n-spin :show="detailModal.loading">
|
||||
<template v-if="detailUser">
|
||||
<div class="detail-shell">
|
||||
<div class="detail-header">
|
||||
<div>
|
||||
<div class="detail-name">{{ detailUser.nickname || detailUser.realName || detailUser.username }}</div>
|
||||
<div class="detail-username">{{ detailUser.username }}</div>
|
||||
<div class="detail-tags">
|
||||
<n-tag :type="statusTagType(detailUser.status)" size="small" round>
|
||||
{{ detailUser.statusLabel || statusLabel(detailUser.status) }}
|
||||
</n-tag>
|
||||
<n-tag v-if="detailUser.orgName || detailUser.orgCode" size="small" round>
|
||||
{{ detailUser.orgName || detailUser.orgCode }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-total">
|
||||
<span>角色数</span>
|
||||
<strong>{{ detailUser.roles?.length || 0 }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">基础信息</div>
|
||||
<div class="detail-grid">
|
||||
<div v-for="item in baseDetailItems" :key="item.label" class="detail-item">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">角色信息</div>
|
||||
<div class="role-tags">
|
||||
<n-tag
|
||||
v-for="role in detailUser.roles"
|
||||
:key="role.id"
|
||||
size="small"
|
||||
round
|
||||
type="info"
|
||||
>
|
||||
{{ role.name }}({{ role.code }})
|
||||
</n-tag>
|
||||
<span v-if="detailUser.roles.length === 0" class="empty-text">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">税务信息</div>
|
||||
<div class="detail-grid">
|
||||
<div v-for="item in taxDetailItems" :key="item.label" class="detail-item">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">银行与预设信息</div>
|
||||
<div class="detail-grid">
|
||||
<div v-for="item in presetDetailItems" :key="item.label" class="detail-item">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">系统信息</div>
|
||||
<div class="detail-grid">
|
||||
<div v-for="item in systemDetailItems" :key="item.label" class="detail-item">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-spin>
|
||||
</n-modal>
|
||||
|
||||
<n-modal
|
||||
v-model:show="editModal.visible"
|
||||
preset="card"
|
||||
@@ -169,7 +259,12 @@ import {
|
||||
updateUserStatusApi
|
||||
} from '@/api/system/user'
|
||||
import { listRolesApi } from '@/api/system/role'
|
||||
import type { CreateUserRequest, UpdateUserRequest, UserListItem } from '@/types/system/user'
|
||||
import type {
|
||||
CreateUserRequest,
|
||||
UpdateUserRequest,
|
||||
UserDetail,
|
||||
UserListItem
|
||||
} from '@/types/system/user'
|
||||
|
||||
const message = useMessage()
|
||||
const authStore = useAuthStore()
|
||||
@@ -227,6 +322,76 @@ const pagination = computed(() => ({
|
||||
}))
|
||||
|
||||
const editFormRef = ref<FormInst | null>(null)
|
||||
const detailModal = reactive({
|
||||
visible: false,
|
||||
loading: false
|
||||
})
|
||||
const detailUser = ref<UserDetail | null>(null)
|
||||
|
||||
const baseDetailItems = computed(() => {
|
||||
const user = detailUser.value
|
||||
if (!user) return []
|
||||
return [
|
||||
{ label: '用户ID', value: displayValue(user.id) },
|
||||
{ label: '用户名', value: displayValue(user.username) },
|
||||
{ label: '昵称', value: displayValue(user.nickname) },
|
||||
{ label: '姓名', value: displayValue(user.realName) },
|
||||
{ label: '手机号', value: displayValue(user.phone) },
|
||||
{ label: '邮箱', value: displayValue(user.email) },
|
||||
{ label: '组织', value: formatOrg(user) },
|
||||
{ label: '状态', value: user.statusLabel || statusLabel(user.status) },
|
||||
{ label: '头像', value: displayValue(user.avatar) },
|
||||
{ label: 'API Key', value: displayValue(user.apiKey) }
|
||||
]
|
||||
})
|
||||
|
||||
const taxDetailItems = computed(() => {
|
||||
const user = detailUser.value
|
||||
if (!user) return []
|
||||
return [
|
||||
{ label: '纳税人识别号', value: displayValue(user.taxpayerNum) },
|
||||
{ label: '数电账号', value: displayValue(user.account) },
|
||||
{ label: '身份类型', value: displayValue(user.taxIdentityType) },
|
||||
{ label: '联系人', value: displayValue(user.taxContactName) },
|
||||
{ label: '联系人电话', value: displayValue(user.taxContactPhone) },
|
||||
{ label: '联系人邮箱', value: displayValue(user.taxContactEmail) },
|
||||
{ label: '法定代表人', value: displayValue(user.taxLegalPersonName) },
|
||||
{ label: '企业名称', value: displayValue(user.taxEnterpriseName) },
|
||||
{ label: '地区编码', value: displayValue(user.taxRegionCode) },
|
||||
{ label: '城市', value: displayValue(user.taxCityName) },
|
||||
{ label: '企业地址', value: displayValue(user.taxEnterpriseAddress) },
|
||||
{ label: '税务登记证', value: displayValue(user.taxRegistrationCertificate) }
|
||||
]
|
||||
})
|
||||
|
||||
const presetDetailItems = computed(() => {
|
||||
const user = detailUser.value
|
||||
if (!user) return []
|
||||
return [
|
||||
{ label: '开户行', value: displayValue(user.bankName) },
|
||||
{ label: '银行账号', value: displayValue(user.bankAccount) },
|
||||
{ label: '预设地址', value: displayValue(user.presetAddress) },
|
||||
{ label: '预设电话', value: displayValue(user.presetPhone) }
|
||||
]
|
||||
})
|
||||
|
||||
const systemDetailItems = computed(() => {
|
||||
const user = detailUser.value
|
||||
if (!user) return []
|
||||
return [
|
||||
{ label: 'Token版本', value: displayValue(user.tokenVersion) },
|
||||
{ label: '数据版本', value: displayValue(user.version) },
|
||||
{ label: '最后登录时间', value: displayValue(user.lastLoginAt) },
|
||||
{ label: '最后登录IP', value: displayValue(user.lastLoginIp) },
|
||||
{ label: '创建时间', value: displayValue(user.createdAt) },
|
||||
{ label: '创建人', value: displayValue(user.createdBy) },
|
||||
{ label: '更新时间', value: displayValue(user.updatedAt) },
|
||||
{ label: '更新人', value: displayValue(user.updatedBy) },
|
||||
{ label: '删除时间', value: displayValue(user.deletedAt) },
|
||||
{ label: '删除人', value: displayValue(user.deletedBy) }
|
||||
]
|
||||
})
|
||||
|
||||
const editModal = reactive({
|
||||
visible: false,
|
||||
mode: 'create' as 'create' | 'edit',
|
||||
@@ -373,6 +538,29 @@ function openCreate() {
|
||||
editModal.visible = true
|
||||
}
|
||||
|
||||
function displayValue(value: unknown) {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function formatOrg(user: UserDetail) {
|
||||
if (user.orgName && user.orgCode) return `${user.orgName}(${user.orgCode})`
|
||||
return user.orgName || user.orgCode || user.orgId || '-'
|
||||
}
|
||||
|
||||
async function openDetail(row: UserListItem) {
|
||||
detailModal.visible = true
|
||||
detailModal.loading = true
|
||||
detailUser.value = null
|
||||
try {
|
||||
detailUser.value = await getUserDetailApi(row.id)
|
||||
} catch {
|
||||
message.error('查询用户详情失败')
|
||||
} finally {
|
||||
detailModal.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openEdit(row: UserListItem) {
|
||||
editModal.mode = 'edit'
|
||||
editModal.title = `编辑用户 - ${row.username}`
|
||||
@@ -485,13 +673,24 @@ const columns = computed<DataTableColumns<UserListItem>>(() => [
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 360,
|
||||
width: 430,
|
||||
render: (row) =>
|
||||
h(
|
||||
NSpace,
|
||||
{ size: 6 },
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
tertiary: true,
|
||||
type: 'info',
|
||||
class: 'action-btn',
|
||||
onClick: () => openDetail(row)
|
||||
},
|
||||
{ default: () => '详情' }
|
||||
),
|
||||
authStore.hasPermission('system:user:update')
|
||||
? h(
|
||||
NButton,
|
||||
@@ -560,3 +759,119 @@ const columns = computed<DataTableColumns<UserListItem>>(() => [
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-shell {
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 20px 0 16px;
|
||||
border-bottom: 1px solid #eef1f5;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
color: #111827;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-username {
|
||||
margin-top: 4px;
|
||||
color: #6b7280;
|
||||
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.detail-total {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.detail-total span {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-total strong {
|
||||
color: #111827;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
margin-bottom: 12px;
|
||||
color: #111827;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
padding: 14px;
|
||||
border: 1px solid #eef1f5;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.detail-item span {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-item strong {
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.role-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #9ca3af;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.detail-total {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user