修复apiresult的问题;增加用户详情查看的逻辑

This commit is contained in:
BBIT-Kai
2026-05-21 14:55:08 +08:00
parent 40f1c27e71
commit 3506efe894
7 changed files with 503 additions and 33 deletions
@@ -12,6 +12,7 @@ import com.bbit.ticket.entity.common.BizException
import com.bbit.ticket.entity.common.ErrorCode import com.bbit.ticket.entity.common.ErrorCode
import com.bbit.ticket.entity.common.PageResult import com.bbit.ticket.entity.common.PageResult
import com.bbit.ticket.entity.common.statusLabel import com.bbit.ticket.entity.common.statusLabel
import com.bbit.ticket.entity.common.system.UserRoleBrief
import com.bbit.ticket.utils.ApiKeyUtil import com.bbit.ticket.utils.ApiKeyUtil
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import org.jetbrains.exposed.v1.core.Op import org.jetbrains.exposed.v1.core.Op
@@ -90,10 +91,26 @@ object UserDao {
fun detail(id: Uuid): UserDetailResponse { fun detail(id: Uuid): UserDetailResponse {
val user = requireActive(id) val user = requireActive(id)
val roleIds = SysUserRoleTable.selectAll() val roles = (SysUserRoleTable innerJoin SysRoleTable)
.where { SysUserRoleTable.userId eq id } .selectAll()
.map { it[SysUserRoleTable.roleId].toString() } .where {
return user.toUserDetail(roleIds) (SysUserRoleTable.userId eq id) and
SysRoleTable.deletedAt.isNull()
}
.map {
UserRoleBrief(
id = it[SysRoleTable.id].toString(),
name = it[SysRoleTable.name],
code = it[SysRoleTable.code],
)
}
val roleIds = roles.map { it.id }
val org = user[SysUserTable.orgId]?.let { orgId ->
SysOrgTable.selectAll()
.where { (SysOrgTable.id eq orgId) and SysOrgTable.deletedAt.isNull() }
.singleOrNull()
}
return user.toUserDetail(roleIds, roles, org)
} }
fun updateProfile(id: Uuid, request: UpdateUserRequest, orgId: Uuid?) { fun updateProfile(id: Uuid, request: UpdateUserRequest, orgId: Uuid?) {
@@ -226,7 +243,11 @@ object UserDao {
apiKey = this[SysUserTable.apiKey], apiKey = this[SysUserTable.apiKey],
) )
private fun ResultRow.toUserDetail(roleIds: List<String>) = UserDetailResponse( private fun ResultRow.toUserDetail(
roleIds: List<String>,
roles: List<UserRoleBrief>,
org: ResultRow?,
) = UserDetailResponse(
id = this[SysUserTable.id].toString(), id = this[SysUserTable.id].toString(),
username = this[SysUserTable.username], username = this[SysUserTable.username],
nickname = this[SysUserTable.nickname], nickname = this[SysUserTable.nickname],
@@ -235,13 +256,38 @@ object UserDao {
email = this[SysUserTable.email], email = this[SysUserTable.email],
avatar = this[SysUserTable.avatar], avatar = this[SysUserTable.avatar],
orgId = this[SysUserTable.orgId]?.toString(), orgId = this[SysUserTable.orgId]?.toString(),
orgName = org?.get(SysOrgTable.name),
orgCode = org?.get(SysOrgTable.code),
status = this[SysUserTable.status], status = this[SysUserTable.status],
statusLabel = statusLabel(this[SysUserTable.status]), statusLabel = statusLabel(this[SysUserTable.status]),
roleIds = roleIds, roleIds = roleIds,
roles = roles,
apiKey = this[SysUserTable.apiKey], apiKey = this[SysUserTable.apiKey],
tokenVersion = this[SysUserTable.tokenVersion],
lastLoginAt = this[SysUserTable.lastLoginAt]?.toString(),
lastLoginIp = this[SysUserTable.lastLoginIp],
createdAt = this[SysUserTable.createdAt].toString(),
createdBy = this[SysUserTable.createdBy]?.toString(),
updatedAt = this[SysUserTable.updatedAt]?.toString(),
updatedBy = this[SysUserTable.updatedBy]?.toString(),
deletedAt = this[SysUserTable.deletedAt]?.toString(),
deletedBy = this[SysUserTable.deletedBy]?.toString(),
version = this[SysUserTable.version],
taxpayerNum = this[SysUserTable.taxpayerNum], taxpayerNum = this[SysUserTable.taxpayerNum],
account = this[SysUserTable.taxAccount], account = this[SysUserTable.taxAccount],
taxPassword = this[SysUserTable.taxPassword],
taxIdentityType = this[SysUserTable.taxIdentityType], taxIdentityType = this[SysUserTable.taxIdentityType],
taxContactName = this[SysUserTable.taxContactName],
taxContactPhone = this[SysUserTable.taxContactPhone],
taxContactEmail = this[SysUserTable.taxContactEmail],
taxLegalPersonName = this[SysUserTable.taxLegalPersonName],
taxEnterpriseName = this[SysUserTable.taxEnterpriseName],
taxRegionCode = this[SysUserTable.taxRegionCode],
taxCityName = this[SysUserTable.taxCityName],
taxEnterpriseAddress = this[SysUserTable.taxEnterpriseAddress],
taxRegistrationCertificate = this[SysUserTable.taxRegistrationCertificate],
bankName = this[SysUserTable.bankName],
bankAccount = this[SysUserTable.bankAccount],
presetAddress = this[SysUserTable.presetAddress],
presetPhone = this[SysUserTable.presetPhone],
) )
} }
@@ -24,14 +24,46 @@ data class UserDetailResponse(
val email: String? = null, val email: String? = null,
val avatar: String? = null, val avatar: String? = null,
val orgId: String? = null, val orgId: String? = null,
val orgName: String? = null,
val orgCode: String? = null,
val status: String, val status: String,
val statusLabel: String, val statusLabel: String,
val roleIds: List<String>, val roleIds: List<String>,
val roles: List<UserRoleBrief> = emptyList(),
val apiKey: String? = null, val apiKey: String? = null,
val tokenVersion: Int,
val lastLoginAt: String? = null,
val lastLoginIp: String? = null,
val createdAt: String? = null,
val createdBy: String? = null,
val updatedAt: String? = null,
val updatedBy: String? = null,
val deletedAt: String? = null,
val deletedBy: String? = null,
val version: Int,
val taxpayerNum: String? = null, val taxpayerNum: String? = null,
val account: String? = null, val account: String? = null,
val taxPassword: String? = null,
val taxIdentityType: String? = null, val taxIdentityType: String? = null,
val taxContactName: String? = null,
val taxContactPhone: String? = null,
val taxContactEmail: String? = null,
val taxLegalPersonName: String? = null,
val taxEnterpriseName: String? = null,
val taxRegionCode: String? = null,
val taxCityName: String? = null,
val taxEnterpriseAddress: String? = null,
val taxRegistrationCertificate: String? = null,
val bankName: String? = null,
val bankAccount: String? = null,
val presetAddress: String? = null,
val presetPhone: String? = null,
)
@Serializable
data class UserRoleBrief(
val id: String,
val name: String,
val code: String,
) )
@Serializable @Serializable
@@ -12,9 +12,10 @@ import com.bbit.ticket.service.openapi.OpenBlueInvoiceService
import com.bbit.ticket.service.system.ApiAccessLogService import com.bbit.ticket.service.system.ApiAccessLogService
import com.bbit.ticket.utils.requireOpenApiPrincipal import com.bbit.ticket.utils.requireOpenApiPrincipal
import com.bbit.ticket.utils.plugins.myJson import com.bbit.ticket.utils.plugins.myJson
import io.ktor.http.ContentType
import io.ktor.server.application.ApplicationCall import io.ktor.server.application.ApplicationCall
import io.ktor.server.request.receive import io.ktor.server.request.receive
import io.ktor.server.response.respond import io.ktor.server.response.respondText
import io.ktor.server.routing.Route import io.ktor.server.routing.Route
import io.ktor.server.routing.get import io.ktor.server.routing.get
import io.ktor.server.routing.post import io.ktor.server.routing.post
@@ -92,20 +93,23 @@ private suspend inline fun <reified T> ApplicationCall.respondOpenApi(
try { try {
val response = ok(block()) val response = ok(block())
val responseBody = myJson.encodeToString(response) val responseBody = myJson.encodeToString(response)
respond(response) respondText(responseBody, ContentType.Application.Json)
saveOpenApiLog(appKey, appName, requestBody, response.code, responseBody, "SUCCESS", null, start) saveOpenApiLog(appKey, appName, requestBody, response.code, responseBody, "SUCCESS", null, start)
} catch (e: PTException) { } catch (e: PTException) {
val response = fail(code = e.code, message = e.message, traceId = e.serialNo) val response = fail(code = e.code, message = e.message, traceId = e.serialNo)
respond(response) val responseBody = myJson.encodeToString(response)
saveOpenApiLog(appKey, appName, requestBody, e.code, myJson.encodeToString(response), "FAILED", e.message, start) respondText(responseBody, ContentType.Application.Json)
saveOpenApiLog(appKey, appName, requestBody, e.code, responseBody, "FAILED", e.message, start)
} catch (e: BizException) { } catch (e: BizException) {
val response = fail(code = e.errorCode, message = e.message) val response = fail(code = e.errorCode, message = e.message)
respond(e.status, response) val responseBody = myJson.encodeToString(response)
saveOpenApiLog(appKey, appName, requestBody, e.errorCode, myJson.encodeToString(response), "FAILED", e.message, start) respondText(responseBody, ContentType.Application.Json, e.status)
saveOpenApiLog(appKey, appName, requestBody, e.errorCode, responseBody, "FAILED", e.message, start)
} catch (e: Exception) { } catch (e: Exception) {
val response = fail(code = "-1", message = e.message ?: "开放接口调用失败") val response = fail(code = "-1", message = e.message ?: "开放接口调用失败")
respond(response) val responseBody = myJson.encodeToString(response)
saveOpenApiLog(appKey, appName, requestBody, "-1", myJson.encodeToString(response), "FAILED", e.message, start) respondText(responseBody, ContentType.Application.Json)
saveOpenApiLog(appKey, appName, requestBody, "-1", responseBody, "FAILED", e.message, start)
} }
} }
@@ -4,12 +4,15 @@ import com.bbit.ticket.entity.common.BizException
import com.bbit.ticket.entity.common.PTException import com.bbit.ticket.entity.common.PTException
import com.bbit.ticket.entity.common.fail import com.bbit.ticket.entity.common.fail
import com.bbit.ticket.entity.common.ok import com.bbit.ticket.entity.common.ok
import com.bbit.ticket.utils.plugins.myJson
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall import io.ktor.server.application.ApplicationCall
import io.ktor.server.response.header import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes import io.ktor.server.response.respondBytes
import io.ktor.server.response.respondText
import kotlinx.serialization.encodeToString
/** /**
* 使用统一票通响应格式执行接口逻辑。 * 使用统一票通响应格式执行接口逻辑。
@@ -22,13 +25,39 @@ suspend inline fun <reified T> ApplicationCall.respondPt(
crossinline block: suspend () -> T, crossinline block: suspend () -> T,
) { ) {
try { try {
respond(ok(block())) respondJson(ok(block()))
} catch (e: PTException) { } catch (e: PTException) {
respond(fail(code = e.code, message = e.message, traceId = e.serialNo)) respondJson(fail(code = e.code, message = e.message, traceId = e.serialNo))
} catch (e: BizException) { } catch (e: BizException) {
respond(e.status, fail(code = e.errorCode, message = e.message)) respondJson(fail(code = e.errorCode, message = e.message), e.status)
} catch (e: Exception) { } catch (e: Exception) {
respond(fail(code = "-1", message = e.message ?: fallbackMessage)) respondJson(fail(code = "-1", message = e.message ?: fallbackMessage))
}
}
/**
* 使用统一票通响应格式执行可空查询,空结果按空对象返回。
*
* @param fallbackMessage 未知异常时返回给前端的兜底提示。
* @param block 当前接口要执行的查询逻辑。
*/
suspend inline fun <reified T : Any> ApplicationCall.respondPtOrEmptyObject(
fallbackMessage: String,
crossinline block: suspend () -> T?,
) {
try {
val data = block()
if (data == null) {
respondJson(ok(emptyMap<String, String>()))
} else {
respondJson(ok(data))
}
} catch (e: PTException) {
respondJson(fail(code = e.code, message = e.message, traceId = e.serialNo))
} catch (e: BizException) {
respondJson(fail(code = e.errorCode, message = e.message), e.status)
} catch (e: Exception) {
respondJson(fail(code = "-1", message = e.message ?: fallbackMessage))
} }
} }
@@ -41,7 +70,7 @@ suspend inline fun <reified T> ApplicationCall.respondPt(
suspend fun ApplicationCall.requiredQueryParameter(name: String, message: String): String? { suspend fun ApplicationCall.requiredQueryParameter(name: String, message: String): String? {
val value = request.queryParameters[name]?.trim()?.takeIf { it.isNotEmpty() } val value = request.queryParameters[name]?.trim()?.takeIf { it.isNotEmpty() }
if (value == null) { if (value == null) {
respond(fail(code = "-1", message = message)) respondJson(fail(code = "-1", message = message))
} }
return value return value
} }
@@ -62,10 +91,23 @@ suspend fun ApplicationCall.respondPtPdf(
response.header(HttpHeaders.ContentDisposition, "inline; filename=\"$filename\"") response.header(HttpHeaders.ContentDisposition, "inline; filename=\"$filename\"")
respondBytes(block(), ContentType.Application.Pdf) respondBytes(block(), ContentType.Application.Pdf)
} catch (e: PTException) { } catch (e: PTException) {
respond(fail(code = e.code, message = e.message, traceId = e.serialNo)) respondJson(fail(code = e.code, message = e.message, traceId = e.serialNo))
} catch (e: BizException) { } catch (e: BizException) {
respond(e.status, fail(code = e.errorCode, message = e.message)) respondJson(fail(code = e.errorCode, message = e.message), e.status)
} catch (e: Exception) { } catch (e: Exception) {
respond(fail(code = "-1", message = e.message ?: fallbackMessage)) respondJson(fail(code = "-1", message = e.message ?: fallbackMessage))
} }
} }
/**
* 显式序列化 JSON 响应,避免 Ktor 在泛型 helper 中丢失 ApiResult<T> 类型信息。
*
* @param data 要返回的响应对象。
* @param status HTTP 状态码。
*/
suspend inline fun <reified T> ApplicationCall.respondJson(
data: T,
status: HttpStatusCode = HttpStatusCode.OK,
) {
respondText(myJson.encodeToString(data), ContentType.Application.Json, status)
}
@@ -57,8 +57,8 @@ fun Route.registerPTAuthRoutes() {
} }
get("/enterprise") { get("/enterprise") {
call.respondPt("查询企业信息失败") { call.respondPtOrEmptyObject("查询企业信息失败") {
PTConfigService.getEnterpriseInfo(call.requireCurrentUser().id) ?: emptyMap<String, String>() PTConfigService.getEnterpriseInfo(call.requireCurrentUser().id)
} }
} }
@@ -72,12 +72,12 @@ fun Route.registerPTAuthRoutes() {
} }
get("/digital-account") { get("/digital-account") {
call.respondPt("查询数电账号失败") { call.respondPtOrEmptyObject("查询数电账号失败") {
val currentUser = call.requireCurrentUser() val currentUser = call.requireCurrentUser()
if (currentUser.taxPayerNum == null) { if (currentUser.taxPayerNum == null) {
throw BizException("-1", "请先完善用户信息", HttpStatusCode.OK) throw BizException("-1", "请先完善用户信息", HttpStatusCode.OK)
} }
PTConfigService.getDigitalAccount(currentUser.id) ?: emptyMap<String, String>() PTConfigService.getDigitalAccount(currentUser.id)
} }
} }
@@ -91,8 +91,8 @@ fun Route.registerPTAuthRoutes() {
} }
get("/preset") { get("/preset") {
call.respondPt("查询预设数据失败") { call.respondPtOrEmptyObject("查询预设数据失败") {
PTConfigService.getPresetData(call.requireCurrentUser().id) ?: emptyMap<String, String>() PTConfigService.getPresetData(call.requireCurrentUser().id)
} }
} }
+317 -2
View File
@@ -45,6 +45,96 @@
</div> </div>
</section> </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 <n-modal
v-model:show="editModal.visible" v-model:show="editModal.visible"
preset="card" preset="card"
@@ -169,7 +259,12 @@ import {
updateUserStatusApi updateUserStatusApi
} from '@/api/system/user' } from '@/api/system/user'
import { listRolesApi } from '@/api/system/role' 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 message = useMessage()
const authStore = useAuthStore() const authStore = useAuthStore()
@@ -227,6 +322,76 @@ const pagination = computed(() => ({
})) }))
const editFormRef = ref<FormInst | null>(null) 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({ const editModal = reactive({
visible: false, visible: false,
mode: 'create' as 'create' | 'edit', mode: 'create' as 'create' | 'edit',
@@ -373,6 +538,29 @@ function openCreate() {
editModal.visible = true 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) { async function openEdit(row: UserListItem) {
editModal.mode = 'edit' editModal.mode = 'edit'
editModal.title = `编辑用户 - ${row.username}` editModal.title = `编辑用户 - ${row.username}`
@@ -485,13 +673,24 @@ const columns = computed<DataTableColumns<UserListItem>>(() => [
{ {
title: '操作', title: '操作',
key: 'actions', key: 'actions',
width: 360, width: 430,
render: (row) => render: (row) =>
h( h(
NSpace, NSpace,
{ size: 6 }, { size: 6 },
{ {
default: () => [ default: () => [
h(
NButton,
{
size: 'small',
tertiary: true,
type: 'info',
class: 'action-btn',
onClick: () => openDetail(row)
},
{ default: () => '详情' }
),
authStore.hasPermission('system:user:update') authStore.hasPermission('system:user:update')
? h( ? h(
NButton, NButton,
@@ -560,3 +759,119 @@ const columns = computed<DataTableColumns<UserListItem>>(() => [
} }
]) ])
</script> </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>
+32 -1
View File
@@ -21,14 +21,45 @@ export interface UserDetail {
email?: string | null email?: string | null
avatar?: string | null avatar?: string | null
orgId?: string | null orgId?: string | null
orgName?: string | null
orgCode?: string | null
status: string status: string
statusLabel?: string statusLabel?: string
roleIds: string[] roleIds: string[]
roles: UserRoleBrief[]
apiKey?: string | null apiKey?: string | null
tokenVersion: number
lastLoginAt?: string | null
lastLoginIp?: string | null
createdAt?: string | null
createdBy?: string | null
updatedAt?: string | null
updatedBy?: string | null
deletedAt?: string | null
deletedBy?: string | null
version: number
taxpayerNum?: string | null taxpayerNum?: string | null
account?: string | null account?: string | null
taxPassword?: string | null
taxIdentityType?: string | null taxIdentityType?: string | null
taxContactName?: string | null
taxContactPhone?: string | null
taxContactEmail?: string | null
taxLegalPersonName?: string | null
taxEnterpriseName?: string | null
taxRegionCode?: string | null
taxCityName?: string | null
taxEnterpriseAddress?: string | null
taxRegistrationCertificate?: string | null
bankName?: string | null
bankAccount?: string | null
presetAddress?: string | null
presetPhone?: string | null
}
export interface UserRoleBrief {
id: string
name: string
code: string
} }
export interface UserQuery { export interface UserQuery {