修复apiresult的问题;增加用户详情查看的逻辑
This commit is contained in:
@@ -12,6 +12,7 @@ import com.bbit.ticket.entity.common.BizException
|
||||
import com.bbit.ticket.entity.common.ErrorCode
|
||||
import com.bbit.ticket.entity.common.PageResult
|
||||
import com.bbit.ticket.entity.common.statusLabel
|
||||
import com.bbit.ticket.entity.common.system.UserRoleBrief
|
||||
import com.bbit.ticket.utils.ApiKeyUtil
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
@@ -90,10 +91,26 @@ object UserDao {
|
||||
|
||||
fun detail(id: Uuid): UserDetailResponse {
|
||||
val user = requireActive(id)
|
||||
val roleIds = SysUserRoleTable.selectAll()
|
||||
.where { SysUserRoleTable.userId eq id }
|
||||
.map { it[SysUserRoleTable.roleId].toString() }
|
||||
return user.toUserDetail(roleIds)
|
||||
val roles = (SysUserRoleTable innerJoin SysRoleTable)
|
||||
.selectAll()
|
||||
.where {
|
||||
(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?) {
|
||||
@@ -226,7 +243,11 @@ object UserDao {
|
||||
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(),
|
||||
username = this[SysUserTable.username],
|
||||
nickname = this[SysUserTable.nickname],
|
||||
@@ -235,13 +256,38 @@ object UserDao {
|
||||
email = this[SysUserTable.email],
|
||||
avatar = this[SysUserTable.avatar],
|
||||
orgId = this[SysUserTable.orgId]?.toString(),
|
||||
orgName = org?.get(SysOrgTable.name),
|
||||
orgCode = org?.get(SysOrgTable.code),
|
||||
status = this[SysUserTable.status],
|
||||
statusLabel = statusLabel(this[SysUserTable.status]),
|
||||
roleIds = roleIds,
|
||||
roles = roles,
|
||||
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],
|
||||
account = this[SysUserTable.taxAccount],
|
||||
taxPassword = this[SysUserTable.taxPassword],
|
||||
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 avatar: String? = null,
|
||||
val orgId: String? = null,
|
||||
val orgName: String? = null,
|
||||
val orgCode: String? = null,
|
||||
val status: String,
|
||||
val statusLabel: String,
|
||||
val roleIds: List<String>,
|
||||
val roles: List<UserRoleBrief> = emptyList(),
|
||||
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 account: String? = null,
|
||||
val taxPassword: 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
|
||||
|
||||
+12
-8
@@ -12,9 +12,10 @@ import com.bbit.ticket.service.openapi.OpenBlueInvoiceService
|
||||
import com.bbit.ticket.service.system.ApiAccessLogService
|
||||
import com.bbit.ticket.utils.requireOpenApiPrincipal
|
||||
import com.bbit.ticket.utils.plugins.myJson
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.server.application.ApplicationCall
|
||||
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.get
|
||||
import io.ktor.server.routing.post
|
||||
@@ -92,20 +93,23 @@ private suspend inline fun <reified T> ApplicationCall.respondOpenApi(
|
||||
try {
|
||||
val response = ok(block())
|
||||
val responseBody = myJson.encodeToString(response)
|
||||
respond(response)
|
||||
respondText(responseBody, ContentType.Application.Json)
|
||||
saveOpenApiLog(appKey, appName, requestBody, response.code, responseBody, "SUCCESS", null, start)
|
||||
} catch (e: PTException) {
|
||||
val response = fail(code = e.code, message = e.message, traceId = e.serialNo)
|
||||
respond(response)
|
||||
saveOpenApiLog(appKey, appName, requestBody, e.code, myJson.encodeToString(response), "FAILED", e.message, start)
|
||||
val responseBody = myJson.encodeToString(response)
|
||||
respondText(responseBody, ContentType.Application.Json)
|
||||
saveOpenApiLog(appKey, appName, requestBody, e.code, responseBody, "FAILED", e.message, start)
|
||||
} catch (e: BizException) {
|
||||
val response = fail(code = e.errorCode, message = e.message)
|
||||
respond(e.status, response)
|
||||
saveOpenApiLog(appKey, appName, requestBody, e.errorCode, myJson.encodeToString(response), "FAILED", e.message, start)
|
||||
val responseBody = myJson.encodeToString(response)
|
||||
respondText(responseBody, ContentType.Application.Json, e.status)
|
||||
saveOpenApiLog(appKey, appName, requestBody, e.errorCode, responseBody, "FAILED", e.message, start)
|
||||
} catch (e: Exception) {
|
||||
val response = fail(code = "-1", message = e.message ?: "开放接口调用失败")
|
||||
respond(response)
|
||||
saveOpenApiLog(appKey, appName, requestBody, "-1", myJson.encodeToString(response), "FAILED", e.message, start)
|
||||
val responseBody = myJson.encodeToString(response)
|
||||
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.fail
|
||||
import com.bbit.ticket.entity.common.ok
|
||||
import com.bbit.ticket.utils.plugins.myJson
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.ApplicationCall
|
||||
import io.ktor.server.response.header
|
||||
import io.ktor.server.response.respond
|
||||
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,
|
||||
) {
|
||||
try {
|
||||
respond(ok(block()))
|
||||
respondJson(ok(block()))
|
||||
} 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) {
|
||||
respond(e.status, fail(code = e.errorCode, message = e.message))
|
||||
respondJson(fail(code = e.errorCode, message = e.message), e.status)
|
||||
} 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? {
|
||||
val value = request.queryParameters[name]?.trim()?.takeIf { it.isNotEmpty() }
|
||||
if (value == null) {
|
||||
respond(fail(code = "-1", message = message))
|
||||
respondJson(fail(code = "-1", message = message))
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -62,10 +91,23 @@ suspend fun ApplicationCall.respondPtPdf(
|
||||
response.header(HttpHeaders.ContentDisposition, "inline; filename=\"$filename\"")
|
||||
respondBytes(block(), ContentType.Application.Pdf)
|
||||
} 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) {
|
||||
respond(e.status, fail(code = e.errorCode, message = e.message))
|
||||
respondJson(fail(code = e.errorCode, message = e.message), e.status)
|
||||
} 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") {
|
||||
call.respondPt("查询企业信息失败") {
|
||||
PTConfigService.getEnterpriseInfo(call.requireCurrentUser().id) ?: emptyMap<String, String>()
|
||||
call.respondPtOrEmptyObject("查询企业信息失败") {
|
||||
PTConfigService.getEnterpriseInfo(call.requireCurrentUser().id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,12 +72,12 @@ fun Route.registerPTAuthRoutes() {
|
||||
}
|
||||
|
||||
get("/digital-account") {
|
||||
call.respondPt("查询数电账号失败") {
|
||||
call.respondPtOrEmptyObject("查询数电账号失败") {
|
||||
val currentUser = call.requireCurrentUser()
|
||||
if (currentUser.taxPayerNum == null) {
|
||||
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") {
|
||||
call.respondPt("查询预设数据失败") {
|
||||
PTConfigService.getPresetData(call.requireCurrentUser().id) ?: emptyMap<String, String>()
|
||||
call.respondPtOrEmptyObject("查询预设数据失败") {
|
||||
PTConfigService.getPresetData(call.requireCurrentUser().id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -21,14 +21,45 @@ export interface UserDetail {
|
||||
email?: string | null
|
||||
avatar?: string | null
|
||||
orgId?: string | null
|
||||
orgName?: string | null
|
||||
orgCode?: string | null
|
||||
status: string
|
||||
statusLabel?: string
|
||||
roleIds: string[]
|
||||
roles: UserRoleBrief[]
|
||||
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
|
||||
account?: string | null
|
||||
taxPassword?: 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 {
|
||||
|
||||
Reference in New Issue
Block a user