修复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.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
-8
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user