大改三角色逻辑:超级管理员、企业管理员、开票员

This commit is contained in:
BBIT-Kai
2026-05-27 09:10:54 +08:00
parent 59fa2beb03
commit 9fd80980e7
67 changed files with 1141 additions and 2230 deletions
+1 -10
View File
@@ -1,5 +1,5 @@
import http from '@/api/http'
import type { ApiAccessLogPage, OperationLogPage } from '@/types/logs'
import type { OperationLogPage } from '@/types/logs'
export function listOperationLogsApi(params: {
page: number
@@ -9,12 +9,3 @@ export function listOperationLogsApi(params: {
}) {
return http.get<never, OperationLogPage>('/logs/operation', { params })
}
export function listApiAccessLogsApi(params: {
page: number
pageSize: number
keyword?: string
status?: string
}) {
return http.get<never, ApiAccessLogPage>('/logs/api-access', { params })
}
-61
View File
@@ -1,61 +0,0 @@
import http from '@/api/http'
import type { DictItemPage, DictTypePage } from '@/types/system/dict'
export function listDictTypesApi(params: { page: number; pageSize: number; keyword?: string }) {
return http.get<never, DictTypePage>('/system/dict-types', { params })
}
export function createDictTypeApi(payload: {
code: string
name: string
status: string
remark?: string
}) {
return http.post<never, { id: string }>('/system/dict-types', payload)
}
export function updateDictTypeApi(
id: string,
payload: { name: string; status: string; remark?: string }
) {
return http.put<never, void>(`/system/dict-types/${id}`, payload)
}
export function deleteDictTypeApi(id: string) {
return http.delete<never, void>(`/system/dict-types/${id}`)
}
export function listDictItemsApi(params: { page: number; pageSize: number; typeId?: string }) {
return http.get<never, DictItemPage>('/system/dict-items', { params })
}
export function createDictItemApi(payload: {
typeId: string
label: string
value: string
color?: string
sort: number
status: string
remark?: string
}) {
return http.post<never, { id: string }>('/system/dict-items', payload)
}
export function updateDictItemApi(
id: string,
payload: {
typeId: string
label: string
value: string
color?: string
sort: number
status: string
remark?: string
}
) {
return http.put<never, void>(`/system/dict-items/${id}`, payload)
}
export function deleteDictItemApi(id: string) {
return http.delete<never, void>(`/system/dict-items/${id}`)
}
+6
View File
@@ -0,0 +1,6 @@
import http from '@/api/http'
import type { EnterpriseManagePage, EnterpriseQuery } from '@/types/system/enterprise'
export function listEnterprisesApi(params: EnterpriseQuery) {
return http.get<never, EnterpriseManagePage>('/system/enterprises', { params })
}
-18
View File
@@ -1,18 +0,0 @@
import http from '@/api/http'
import type { CreateOrgRequest, OrgTreeNode, UpdateOrgRequest } from '@/types/system/org'
export function listOrgsApi() {
return http.get<never, OrgTreeNode[]>('/system/orgs')
}
export function createOrgApi(payload: CreateOrgRequest) {
return http.post<never, { id: string }>('/system/orgs', payload)
}
export function updateOrgApi(id: string, payload: UpdateOrgRequest) {
return http.put<never, void>(`/system/orgs/${id}`, payload)
}
export function deleteOrgApi(id: string) {
return http.delete<never, void>(`/system/orgs/${id}`)
}
+6 -1
View File
@@ -108,12 +108,17 @@ function handleSelect(key: string) {
}
:deep(.n-menu-item-content--selected) {
color: var(--app-primary);
font-weight: 600;
box-shadow: none;
}
:deep(.n-menu-item-content--selected::before) {
background: transparent;
background: var(--app-primary-soft);
}
:deep(.n-menu-item-content:hover::before) {
background: var(--app-primary-subtle);
}
:deep(.n-menu--collapsed .n-menu-item-content),
+9 -7
View File
@@ -41,7 +41,7 @@ function onClose(path: string) {
.tabs-shell {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #eef1f5;
border-top: 1px solid var(--app-border);
}
.tabs-track {
@@ -64,9 +64,9 @@ function onClose(path: string) {
height: 32px;
padding: 0 12px;
border: 1px solid transparent;
border-radius: 9px;
border-radius: 8px;
background: transparent;
color: #6b7280;
color: var(--app-text-muted);
cursor: pointer;
transition:
background 0.16s ease,
@@ -75,13 +75,15 @@ function onClose(path: string) {
}
.tab-chip:hover {
background: #f3f4f6;
color: #111827;
background: var(--app-primary-soft);
color: var(--app-primary);
}
.tab-chip-active {
background: #111827;
color: #ffffff;
border-color: transparent;
background: var(--app-primary-soft);
color: var(--app-primary);
font-weight: 600;
}
.tab-title {
+103 -2
View File
@@ -1,5 +1,5 @@
<template>
<div class="page-shell">
<div class="page-shell dashboard-page">
<div class="dashboard-grid">
<div class="soft-stat">
<n-statistic label="可见菜单" :value="visibleMenuTotal" />
@@ -17,33 +17,134 @@
<n-statistic label="已打开页签" :value="authStore.tabs.length" />
</div>
</div>
<div class="menu-shortcut-grid">
<button
v-for="menu in shortcutMenus"
:key="menu.id"
class="soft-stat menu-shortcut"
type="button"
@click="openMenu(menu)"
>
<span class="shortcut-icon">
<n-icon :size="22">
<component :is="resolveIcon(menu.icon)" />
</n-icon>
</span>
<span class="shortcut-content">
<span class="shortcut-title">{{ menu.title }}</span>
</span>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, type Component } from 'vue'
import { NIcon } from 'naive-ui'
import * as LucideIcons from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { statusLabel } from '@/utils/display'
import type { MenuNode } from '@/types/auth'
const authStore = useAuthStore()
const router = useRouter()
function flattenMenus(nodes: MenuNode[]): MenuNode[] {
return nodes.flatMap((item) => [item, ...flattenMenus(item.children ?? [])])
}
function resolveIcon(icon?: string | null) {
return (
(icon ? (LucideIcons[icon as keyof typeof LucideIcons] as unknown as Component | undefined) : undefined) ??
LucideIcons.PanelRightOpen
)
}
function openMenu(menu: MenuNode) {
if (menu.path) {
router.push(menu.path)
}
}
const visibleMenus = computed(() => flattenMenus(authStore.menus).filter((item) => item.visible))
const visibleMenuTotal = computed(() => visibleMenus.value.length)
const visiblePageTotal = computed(
() => visibleMenus.value.filter((item) => item.type === 'MENU' && item.path).length
)
const shortcutMenus = computed(() =>
visibleMenus.value.filter((item) => item.type === 'MENU' && Boolean(item.path))
)
const currentStatus = computed(() => statusLabel(authStore.user?.status))
</script>
<style scoped>
.dashboard-page {
overflow: auto;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.menu-shortcut-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.menu-shortcut {
width: 100%;
min-height: 92px;
display: flex;
align-items: center;
gap: 14px;
color: #111827;
text-align: left;
cursor: pointer;
transition:
border-color 0.16s ease,
box-shadow 0.16s ease,
transform 0.16s ease;
}
.menu-shortcut:hover {
border-color: #94a3b8;
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.1);
transform: translateY(-1px);
}
.menu-shortcut:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
.shortcut-icon {
width: 42px;
height: 42px;
flex: 0 0 42px;
display: grid;
place-items: center;
border-radius: 10px;
color: #2563eb;
background: var(--app-primary-soft);
}
.shortcut-content {
min-width: 0;
display: flex;
flex-direction: column;
}
.shortcut-title {
overflow: hidden;
color: #111827;
font-size: 15px;
font-weight: 600;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
-142
View File
@@ -1,142 +0,0 @@
<template>
<div class="page-shell">
<section class="page-card table-fill">
<div class="page-toolbar">
<n-form inline :show-label="false" class="toolbar-form">
<n-form-item>
<n-input v-model:value="query.keyword" clearable placeholder="应用/路径" />
</n-form-item>
<n-form-item>
<n-select
v-model:value="query.status"
clearable
placeholder="状态"
:options="statusOptions"
style="width: 140px"
/>
</n-form-item>
<n-form-item>
<n-space>
<n-button type="primary" @click="refetch">查询</n-button>
<n-button @click="reset">重置</n-button>
</n-space>
</n-form-item>
</n-form>
</div>
<div class="card-body card-body-fill table-fill">
<n-data-table
flex-height
remote
size="small"
:columns="columns"
:data="rows"
:loading="apiAccessLogQuery.isLoading.value"
:pagination="pagination"
:row-key="(row: ApiAccessLogItem) => row.id"
@update:page="onPage"
@update:page-size="onPageSize"
/>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { computed, h, reactive } from 'vue'
import { NTag, type DataTableColumns } from 'naive-ui'
import { useQuery } from '@tanstack/vue-query'
import { listApiAccessLogsApi } from '@/api/logs'
import type { ApiAccessLogItem } from '@/types/logs'
import { statusLabel, statusTagType } from '@/utils/display'
import { renderPagePrefix } from '@/utils/pagination'
const query = reactive({
page: 1,
pageSize: 20,
keyword: '',
status: null as string | null
})
const statusOptions = [
{ label: '成功', value: 'SUCCESS' },
{ label: '失败', value: 'FAIL' }
]
const apiAccessLogQuery = useQuery({
queryKey: computed(() => ['logs', 'api-access', { ...query }]),
queryFn: () =>
listApiAccessLogsApi({
page: query.page,
pageSize: query.pageSize,
keyword: query.keyword || undefined,
status: query.status || undefined
})
})
const rows = computed(() => apiAccessLogQuery.data.value?.items ?? [])
const pagination = computed(() => ({
page: query.page,
pageSize: query.pageSize,
itemCount: apiAccessLogQuery.data.value?.total ?? 0,
showSizePicker: true,
pageSizes: [10, 20, 50],
prefix: renderPagePrefix
}))
function refetch() {
query.page = 1
apiAccessLogQuery.refetch()
}
function reset() {
query.keyword = ''
query.status = null
query.page = 1
apiAccessLogQuery.refetch()
}
function onPage(page: number) {
query.page = page
}
function onPageSize(pageSize: number) {
query.pageSize = pageSize
query.page = 1
}
const columns = computed<DataTableColumns<ApiAccessLogItem>>(() => [
{ title: '时间', key: 'createdAt', width: 170 },
{
title: '应用',
key: 'appName',
minWidth: 160,
render: (row) =>
h('div', { class: 'leading-5' }, [
h('div', { class: 'font-medium text-slate-900' }, row.appName || '-'),
h('div', { class: 'text-xs text-slate-500' }, row.appKey || '未提供 AppKey')
])
},
{ title: '方法', key: 'httpMethod', width: 90 },
{ title: '路径', key: 'requestPath', minWidth: 240 },
{ title: '响应码', key: 'responseCode', width: 100, render: (row) => row.responseCode || '-' },
{
title: '状态',
key: 'status',
width: 90,
render: (row) =>
h(
NTag,
{ type: statusTagType(row.status), size: 'small' },
{ default: () => statusLabel(row.status) }
)
},
{ title: '耗时', key: 'costMs', width: 90, render: (row) => `${row.costMs} ms` },
{
title: '错误信息',
key: 'errorMessage',
minWidth: 180,
render: (row) => row.errorMessage || '-'
}
])
</script>
@@ -74,10 +74,23 @@
</n-form>
</n-modal>
<n-modal v-model:show="showSms" preset="card" title="短信登录" class="sms-card">
<n-space vertical>
<n-alert v-if="smsTip" type="info">{{ smsTip }}</n-alert>
<n-input v-model:value="smsCode" placeholder="请输入短信验证码" />
<n-modal
v-model:show="showSms"
preset="card"
title="短信登录"
:style="{ width: '360px', maxWidth: '88vw' }"
content-style="padding: 14px 18px 18px"
>
<n-space vertical size="small" class="auth-panel">
<div class="auth-account">
<span>税局账号</span>
<strong>{{ selected?.account || '-' }}</strong>
</div>
<n-alert v-if="smsTip" type="info" :show-icon="false">{{ smsTip }}</n-alert>
<n-input-group>
<n-input v-model:value="smsCode" placeholder="请输入短信验证码" />
<n-button :loading="smsCodeLoading" @click="sendSmsCode">发送验证码</n-button>
</n-input-group>
<div class="modal-actions">
<n-button @click="showSms = false">取消</n-button>
<n-button type="primary" :loading="smsLoading" @click="submitSms">确认登录</n-button>
@@ -85,12 +98,36 @@
</n-space>
</n-modal>
<n-modal v-model:show="showQrcode" preset="card" title="风险认证" class="sms-card">
<div class="qrcode-wrap">
<img v-if="qrcodeImg" :src="qrcodeImg" alt="认证二维码" />
<n-empty v-else description="暂无二维码" />
<n-button size="small" :loading="qrLoading" @click="loadQrcode">刷新二维码</n-button>
</div>
<n-modal
v-model:show="showQrcode"
preset="card"
title="风险认证"
:style="{ width: '400px', maxWidth: '88vw' }"
content-style="padding: 14px 18px 18px"
>
<n-space vertical size="small" class="auth-panel">
<div class="auth-account">
<span>税局账号</span>
<strong>{{ selected?.account || '-' }}</strong>
</div>
<n-radio-group v-model:value="qrcodeType" size="small" @update:value="loadQrcode">
<n-radio-button value="1">电子税务局 APP</n-radio-button>
<n-radio-button value="2">国家网络身份认证 APP</n-radio-button>
</n-radio-group>
<div class="qrcode-wrap">
<img v-if="qrcodeImg" :src="qrcodeImg" :alt="qrcodeTypeLabel" />
<n-empty v-else description="暂无二维码" />
</div>
<div class="qrcode-meta">
<span>{{ qrcodeTypeLabel }}</span>
<small v-if="qrcodeGenTime">生成时间:{{ qrcodeGenTime }}</small>
<small v-if="qrcodeTip">{{ qrcodeTip }}</small>
</div>
<div class="modal-actions">
<n-button @click="showQrcode = false">关闭</n-button>
<n-button type="primary" :loading="qrLoading" @click="loadQrcode">刷新二维码</n-button>
</div>
</n-space>
</n-modal>
</div>
</template>
@@ -109,11 +146,15 @@ import {
NGrid,
NIcon,
NInput,
NInputGroup,
NModal,
NPopconfirm,
NRadioButton,
NRadioGroup,
NSelect,
NSpace,
NTag,
NTooltip,
useMessage
} from 'naive-ui'
import { KeyRound, Plus, RefreshCw, ShieldCheck } from 'lucide-vue-next'
@@ -138,15 +179,22 @@ const showCreate = ref(false)
const showSms = ref(false)
const showQrcode = ref(false)
const smsLoading = ref(false)
const smsCodeLoading = ref(false)
const qrLoading = ref(false)
const smsCode = ref('')
const smsTip = ref('')
const qrcodeImg = ref('')
const qrcodeType = ref('1')
const qrcodeGenTime = ref('')
const qrcodeTip = ref('')
const selected = ref<DigitalAccountItem | null>(null)
const createFormRef = ref<FormInst | null>(null)
const canManage = computed(() => authStore.user?.userType !== 'DIGITAL_OPERATOR')
const canUpdateStatus = computed(() => authStore.hasPermission('digital-account:status'))
const qrcodeTypeLabel = computed(() =>
qrcodeType.value === '2' ? '国家网络身份认证 APP' : '电子税务局 APP'
)
const createForm = reactive({
account: '',
@@ -175,7 +223,13 @@ const identityOptions = [
const columns: DataTableColumns<DigitalAccountItem> = [
{ title: '税局账号', key: 'account', minWidth: 140 },
{ title: '平台登录账号', key: 'platformUsername', minWidth: 180, render: (row) => row.platformUsername || '-' },
{
title: '平台登录账号',
key: 'platformUsername',
width: 180,
ellipsis: true,
render: (row) => renderEllipsisText(row.platformUsername)
},
{ title: '姓名', key: 'name', minWidth: 100 },
{ title: '登录身份', key: 'identityType', render: (row) => identityLabel(row.identityType) },
{ title: '账号状态', key: 'authStatus', render: (row) => authStatusLabel(row.authStatus) },
@@ -214,8 +268,9 @@ const columns: DataTableColumns<DigitalAccountItem> = [
{
title: 'API Key',
key: 'apiKey',
minWidth: 180,
render: (row) => row.apiKey || '-'
width: 180,
ellipsis: true,
render: (row) => renderEllipsisText(row.apiKey)
},
{
title: '操作',
@@ -282,6 +337,18 @@ function authStatusLabel(value?: string | null) {
return value ? map[value] || value : '-'
}
function renderEllipsisText(value?: string | null) {
if (!value) return '-'
return h(
NTooltip,
{ trigger: 'hover' },
{
trigger: () => h('span', { class: 'cell-ellipsis' }, value),
default: () => value
}
)
}
async function load() {
loading.value = true
try {
@@ -324,13 +391,21 @@ async function updateAccountStatus(row: DigitalAccountItem) {
async function openSms(row: DigitalAccountItem) {
selected.value = row
smsCode.value = ''
smsLoading.value = true
smsTip.value = ''
showSms.value = true
}
async function sendSmsCode() {
if (!selected.value) return
smsCodeLoading.value = true
try {
const res = await sendLoginSmsCodeApi({ taxpayerNum: row.taxpayerNum, account: row.account })
const res = await sendLoginSmsCodeApi({
taxpayerNum: selected.value.taxpayerNum,
account: selected.value.account
})
smsTip.value = res.phoneNum ? `验证码已发送至 ${res.phoneNum}` : res.resultMsg
} finally {
smsLoading.value = false
smsCodeLoading.value = false
}
}
@@ -353,6 +428,10 @@ async function submitSms() {
async function openQrcode(row: DigitalAccountItem) {
selected.value = row
qrcodeType.value = '1'
qrcodeImg.value = ''
qrcodeGenTime.value = ''
qrcodeTip.value = ''
showQrcode.value = true
await loadQrcode()
}
@@ -361,8 +440,11 @@ async function loadQrcode() {
if (!selected.value) return
qrLoading.value = true
try {
const res = await getAuthQrcodeApi('1', selected.value.id)
const res = await getAuthQrcodeApi(qrcodeType.value, selected.value.id)
qrcodeImg.value = res.qrcodeImg ? `data:image/png;base64,${res.qrcodeImg}` : ''
qrcodeType.value = res.qrcodeType || qrcodeType.value
qrcodeGenTime.value = res.genTime || ''
qrcodeTip.value = res.resultMsg || ''
} finally {
qrLoading.value = false
}
@@ -388,27 +470,81 @@ onMounted(load)
flex-shrink: 0;
}
.cell-ellipsis {
display: inline-block;
width: 148px;
max-width: 148px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
.modal-card {
width: min(720px, 92vw);
}
.sms-card {
width: min(420px, 92vw);
}
.modal-actions {
justify-content: flex-end;
margin-top: 10px;
padding-top: 12px;
border-top: 1px solid #eef1f5;
}
.auth-panel {
width: 100%;
}
.auth-account {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 10px;
border: 1px solid #eef1f5;
border-radius: 8px;
background: #fafafa;
}
.auth-account span {
color: #6b7280;
font-size: 12px;
}
.auth-account strong {
color: #111827;
font-size: 13px;
font-weight: 600;
word-break: break-all;
}
.qrcode-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
justify-content: center;
min-height: 236px;
border: 1px solid #eef1f5;
border-radius: 8px;
background: #fff;
}
.qrcode-wrap img {
width: 220px;
height: 220px;
}
.qrcode-meta {
display: flex;
flex-direction: column;
gap: 2px;
color: #6b7280;
font-size: 12px;
text-align: center;
}
.qrcode-meta span {
color: #111827;
font-weight: 600;
}
</style>
@@ -1,15 +1,17 @@
<template>
<div class="page">
<!-- ===== 顶部工具栏操作按钮 ===== -->
<div class="toolbar">
<div class="toolbar-actions">
<div class="page-shell invoice-issue-page">
<section class="page-card invoice-issue-card">
<!-- ===== 顶部工具栏操作按钮 ===== -->
<div class="page-toolbar invoice-toolbar">
<h2 class="page-toolbar-title">开具蓝票</h2>
<div class="page-toolbar-actions toolbar-actions">
<n-button type="primary" @click="addItem">新增商品</n-button>
<n-button type="primary" @click="addVariableLevyProof">新增差额征税凭证明细</n-button>
<n-button type="primary" @click="showOrderNoDialog">新增单据号</n-button>
</div>
</div>
</div>
<!-- ===== 主体左栏 + 右栏 ===== -->
<main class="main">
<!-- ===== 主体左栏 + 右栏 ===== -->
<main class="main card-body card-body-fill">
<!-- 左栏导航列表 -->
<aside class="sidebar">
<div class="sidebar-head">
@@ -879,7 +881,8 @@
</section>
</n-form>
</section>
</main>
</main>
</section>
<!-- ===== 新增单据号弹窗 ===== -->
<n-modal
@@ -1728,41 +1731,38 @@ function resetForm() {
</script>
<style scoped>
/* ===== 全屏布局 ===== */
.page {
height: 100%;
.invoice-issue-page {
overflow: hidden;
}
.invoice-issue-card {
display: flex;
flex-direction: column;
background: #f7f8fa;
overflow: hidden;
}
/* ===== 主体 ===== */
.main {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
background: var(--app-surface);
}
/* ===== 左栏 ===== */
.sidebar {
width: 204px;
flex-shrink: 0;
background: #fff;
border-right: 1px solid #e8e8e8;
background: var(--app-surface);
border-right: 1px solid var(--app-border);
display: flex;
flex-direction: column;
}
/* ===== 顶部工具栏 ===== */
.toolbar {
flex-shrink: 0;
background: #fff;
border-bottom: 1px solid #e8e8e8;
padding: 5px 14px;
display: flex;
justify-content: flex-end;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
.invoice-toolbar {
flex-wrap: wrap;
}
.toolbar-actions {
@@ -1775,17 +1775,17 @@ function resetForm() {
.sidebar-head {
display: flex;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #eee;
min-height: 44px;
padding: 10px 14px;
border-bottom: 1px solid var(--app-border);
flex-shrink: 0;
}
.sidebar-title {
font-size: 12px;
font-weight: 600;
color: #999;
color: var(--app-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.sidebar-body {
@@ -1796,15 +1796,15 @@ function resetForm() {
.sidebar-foot {
flex-shrink: 0;
padding: 8px 12px;
border-top: 1px solid #eee;
padding: 10px 12px;
border-top: 1px solid var(--app-border);
}
.nav-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 12px;
padding: 8px 12px;
cursor: pointer;
transition: background 0.12s;
border-left: 3px solid transparent;
@@ -1812,12 +1812,12 @@ function resetForm() {
}
.nav-item:hover {
background: #f5f5f5;
background: var(--app-primary-subtle);
}
.nav-item.active {
background: #f0f5ff;
border-left-color: #409eff;
background: var(--app-primary-soft);
border-left-color: var(--app-primary);
}
.nav-item-body {
@@ -1830,25 +1830,25 @@ function resetForm() {
.nav-icon {
flex-shrink: 0;
color: #999;
color: var(--app-text-muted);
display: flex;
align-items: center;
}
.nav-item.active .nav-icon {
color: #409eff;
color: var(--app-primary);
}
.nav-label {
font-size: 13px;
color: #333;
color: var(--app-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nav-item.active .nav-label {
color: #409eff;
color: var(--app-primary);
font-weight: 600;
}
@@ -1877,7 +1877,7 @@ function resetForm() {
.nav-divider {
height: 1px;
background: #eee;
background: var(--app-border);
margin: 4px 12px;
}
@@ -1885,25 +1885,27 @@ function resetForm() {
.content {
flex: 1;
overflow-y: auto;
padding: 10px 12px;
padding: 12px;
background: #f8fafc;
}
/* ===== Section 卡片 ===== */
.section {
background: #fff;
border: 1px solid #eee;
border-radius: 6px;
margin-bottom: 8px;
background: #ffffff;
border: 1px solid var(--app-border);
border-radius: 10px;
margin-bottom: 10px;
overflow: hidden;
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
}
.section-head {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 12px;
border-bottom: 1px solid #f2f2f2;
background: #fbfcfe;
padding: 9px 12px;
border-bottom: 1px solid var(--app-border);
background: #ffffff;
}
.section-badge {
@@ -1924,20 +1926,20 @@ function resetForm() {
}
.section-badge.optional {
color: #096dd9;
background: #e6f7ff;
border: 1px solid #bae7ff;
color: var(--app-primary);
background: var(--app-primary-soft);
border: 1px solid var(--app-border-strong);
}
.section-title {
font-size: 13px;
font-weight: 620;
color: #222;
color: var(--app-text);
}
.section-hint {
font-size: 11px;
color: #bbb;
color: var(--app-text-muted);
margin-left: auto;
}
@@ -1989,7 +1991,7 @@ function resetForm() {
width: 100%;
max-height: 190px;
border-right: 0;
border-bottom: 1px solid #e8e8e8;
border-bottom: 1px solid var(--app-border);
}
.sidebar-body {
-237
View File
@@ -1,237 +0,0 @@
<template>
<div class="page-shell">
<div class="grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-2">
<section class="page-card table-fill">
<div class="page-toolbar">
<span class="text-sm font-semibold text-slate-700">字典类型</span>
<n-button size="small" type="primary" @click="openTypeCreate">新增类型</n-button>
</div>
<div class="card-body card-body-fill table-fill">
<n-data-table
flex-height
:columns="typeColumns"
:data="typeRows"
:pagination="typePagination"
remote
@update:page="onTypePage"
@update:page-size="onTypePageSize"
/>
</div>
</section>
<section class="page-card table-fill">
<div class="page-toolbar">
<span class="text-sm font-semibold text-slate-700">字典项</span>
<n-button size="small" type="primary" :disabled="!selectedTypeId" @click="openItemCreate"
>新增字典项</n-button
>
</div>
<div class="card-body card-body-fill table-fill">
<n-data-table
flex-height
:columns="itemColumns"
:data="itemRows"
:pagination="itemPagination"
remote
@update:page="onItemPage"
@update:page-size="onItemPageSize"
/>
</div>
</section>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, h, reactive, ref, watch } from 'vue'
import { NButton, NPopconfirm, NTag, NSpace, useMessage, type DataTableColumns } from 'naive-ui'
import { useMutation, useQuery } from '@tanstack/vue-query'
import {
createDictItemApi,
createDictTypeApi,
deleteDictItemApi,
deleteDictTypeApi,
listDictItemsApi,
listDictTypesApi
} from '@/api/system/dict'
import type { DictItem, DictTypeItem } from '@/types/system/dict'
import { statusLabel, statusTagType } from '@/utils/display'
import { renderPagePrefix } from '@/utils/pagination'
const message = useMessage()
const selectedTypeId = ref<string>('')
const typeQuery = reactive({ page: 1, pageSize: 10, keyword: '' })
const itemQuery = reactive({ page: 1, pageSize: 10 })
const typesReq = useQuery({
queryKey: computed(() => ['dict-types', { ...typeQuery }]),
queryFn: () => listDictTypesApi({ ...typeQuery, keyword: typeQuery.keyword || undefined })
})
const itemsReq = useQuery({
queryKey: computed(() => ['dict-items', selectedTypeId.value, { ...itemQuery }]),
queryFn: () => listDictItemsApi({ ...itemQuery, typeId: selectedTypeId.value || undefined })
})
const typeRows = computed(() => typesReq.data.value?.items ?? [])
const itemRows = computed(() => itemsReq.data.value?.items ?? [])
const typePagination = computed(() => ({
page: typeQuery.page,
pageSize: typeQuery.pageSize,
itemCount: typesReq.data.value?.total ?? 0,
showSizePicker: true,
pageSizes: [10, 20],
prefix: renderPagePrefix
}))
const itemPagination = computed(() => ({
page: itemQuery.page,
pageSize: itemQuery.pageSize,
itemCount: itemsReq.data.value?.total ?? 0,
showSizePicker: true,
pageSizes: [10, 20],
prefix: renderPagePrefix
}))
watch(
typeRows,
(v) => {
if (!selectedTypeId.value && v.length > 0) selectedTypeId.value = v[0].id
},
{ immediate: true }
)
const createTypeMutation = useMutation({
mutationFn: () =>
createDictTypeApi({
code: `TYPE_${Date.now()}`,
name: `新类型${Date.now().toString().slice(-4)}`,
status: 'ENABLED'
}),
onSuccess: async () => {
message.success('新增类型成功')
await typesReq.refetch()
}
})
const createItemMutation = useMutation({
mutationFn: () =>
createDictItemApi({
typeId: selectedTypeId.value,
label: `新项${Date.now().toString().slice(-4)}`,
value: `${Date.now()}`,
sort: 0,
status: 'ENABLED'
}),
onSuccess: async () => {
message.success('新增字典项成功')
await itemsReq.refetch()
}
})
const deleteTypeMutation = useMutation({
mutationFn: deleteDictTypeApi,
onSuccess: async () => {
message.success('删除成功')
selectedTypeId.value = ''
await typesReq.refetch()
await itemsReq.refetch()
}
})
const deleteItemMutation = useMutation({
mutationFn: deleteDictItemApi,
onSuccess: async () => {
message.success('删除成功')
await itemsReq.refetch()
}
})
function openTypeCreate() {
createTypeMutation.mutate()
}
function openItemCreate() {
if (!selectedTypeId.value) return
createItemMutation.mutate()
}
function onTypePage(p: number) {
typeQuery.page = p
}
function onTypePageSize(s: number) {
typeQuery.pageSize = s
typeQuery.page = 1
}
function onItemPage(p: number) {
itemQuery.page = p
}
function onItemPageSize(s: number) {
itemQuery.pageSize = s
itemQuery.page = 1
}
const typeColumns = computed<DataTableColumns<DictTypeItem>>(() => [
{ title: '编码', key: 'code' },
{
title: '名称',
key: 'name',
render: (r) => h('a', { onClick: () => (selectedTypeId.value = r.id) }, r.name)
},
{
title: '状态',
key: 'status',
render: (r) =>
h(
NTag,
{ size: 'small', type: statusTagType(r.status) },
{ default: () => statusLabel(r.status) }
)
},
{
title: '操作',
key: 'actions',
render: (r) =>
h(
NPopconfirm,
{ onPositiveClick: () => deleteTypeMutation.mutate(r.id) },
{
trigger: () =>
h(NButton, { size: 'tiny', tertiary: true, type: 'error' }, { default: () => '删除' }),
default: () => '确认删除?'
}
)
}
])
const itemColumns = computed<DataTableColumns<DictItem>>(() => [
{ title: '标签', key: 'label' },
{ title: '值', key: 'value' },
{
title: '颜色',
key: 'color',
render: (r) =>
r.color
? h(NTag, { size: 'small', color: { color: r.color } }, { default: () => r.color })
: '-'
},
{
title: '状态',
key: 'status',
render: (r) =>
h(
NTag,
{ size: 'small', type: statusTagType(r.status) },
{ default: () => statusLabel(r.status) }
)
},
{
title: '操作',
key: 'actions',
render: (r) =>
h(NSpace, { size: 6 }, () => [
h(
NPopconfirm,
{ onPositiveClick: () => deleteItemMutation.mutate(r.id) },
{
trigger: () =>
h(
NButton,
{ size: 'tiny', tertiary: true, type: 'error' },
{ default: () => '删除' }
),
default: () => '确认删除?'
}
)
])
}
])
</script>
@@ -0,0 +1,141 @@
<template>
<div class="page-shell">
<section class="page-card table-fill">
<div class="page-toolbar">
<n-form inline :show-label="false" class="toolbar-form">
<n-form-item>
<n-input v-model:value="queryForm.keyword" clearable placeholder="企业名称 / 税号" />
</n-form-item>
<n-form-item>
<n-space>
<n-button type="primary" @click="handleSearch">查询</n-button>
<n-button @click="handleReset">重置</n-button>
</n-space>
</n-form-item>
</n-form>
</div>
<div class="card-body card-body-fill table-fill">
<n-data-table
flex-height
remote
size="small"
:columns="columns"
:data="tableRows"
:loading="enterpriseQuery.isLoading.value"
:pagination="pagination"
:row-key="(row: EnterpriseManageItem) => row.id"
@update:page="onPageChange"
@update:page-size="onPageSizeChange"
/>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { computed, h, reactive } from 'vue'
import type { DataTableColumns } from 'naive-ui'
import { NSpace, NTag } from 'naive-ui'
import { useQuery } from '@tanstack/vue-query'
import { listEnterprisesApi } from '@/api/system/enterprise'
import type { EnterpriseManageItem } from '@/types/system/enterprise'
import { renderPagePrefix } from '@/utils/pagination'
const queryForm = reactive({
keyword: ''
})
const pager = reactive({
page: 1,
pageSize: 10
})
const enterpriseQuery = useQuery({
queryKey: computed(() => ['system', 'enterprises', { ...queryForm, ...pager }]),
queryFn: () =>
listEnterprisesApi({
page: pager.page,
pageSize: pager.pageSize,
keyword: queryForm.keyword || undefined
})
})
const tableRows = computed(() => enterpriseQuery.data.value?.items ?? [])
const pagination = computed(() => ({
page: pager.page,
pageSize: pager.pageSize,
itemCount: enterpriseQuery.data.value?.total ?? 0,
pageSizes: [10, 20, 50],
showSizePicker: true,
prefix: renderPagePrefix
}))
const columns: DataTableColumns<EnterpriseManageItem> = [
{
title: '企业信息',
key: 'enterpriseName',
minWidth: 260,
render: (row) =>
h('div', { class: 'leading-5' }, [
h('div', { class: 'font-medium text-slate-900' }, row.enterpriseName),
h('div', { class: 'text-xs text-slate-500' }, row.taxpayerNum)
])
},
{
title: '联系人',
key: 'contactsName',
minWidth: 160,
render: (row) => row.contactsName || row.legalPersonName || '-'
},
{
title: '联系电话',
key: 'contactsPhone',
minWidth: 140,
render: (row) => row.contactsPhone || '-'
},
{
title: '地区',
key: 'cityName',
minWidth: 160,
render: (row) => [row.regionCode, row.cityName].filter(Boolean).join(' / ') || '-'
},
{
title: '票通状态',
key: 'serviceStatus',
width: 140,
render: (row) =>
h(NSpace, { size: 6 }, () => [
h(NTag, { size: 'small' }, { default: () => row.reviewStatus || '-' }),
h(NTag, { size: 'small' }, { default: () => row.serviceStatus || '-' })
])
},
{
title: '注册时间',
key: 'createdAt',
minWidth: 190,
render: (row) => row.createdAt
}
]
function onPageChange(page: number) {
pager.page = page
}
function onPageSizeChange(pageSize: number) {
pager.pageSize = pageSize
pager.page = 1
}
function handleSearch() {
pager.page = 1
enterpriseQuery.refetch()
}
function handleReset() {
queryForm.keyword = ''
pager.page = 1
enterpriseQuery.refetch()
}
</script>
-1
View File
@@ -126,7 +126,6 @@ const iconOptions = [
{ label: '组织', value: 'Building2' },
{ label: '盾牌', value: 'Shield' },
{ label: '侧栏', value: 'PanelLeft' },
{ label: '字典', value: 'BookType' },
{ label: '日志', value: 'Logs' },
{ label: '文本日志', value: 'ScrollText' },
{ label: '接口链路', value: 'Waypoints' },
-238
View File
@@ -1,238 +0,0 @@
<template>
<div class="page-shell">
<section class="page-card table-fill">
<div class="page-toolbar">
<span class="text-sm font-semibold text-slate-700">组织架构</span>
<n-button type="primary" @click="openCreate()">新增组织</n-button>
</div>
<div class="card-body card-body-fill tree-card-body">
<n-tree
block-line
expand-on-click
:data="treeData"
key-field="id"
label-field="name"
children-field="children"
:render-switcher-icon="renderSwitcherIcon"
:render-label="renderLabel"
:expanded-keys="expandedKeys"
@update:expanded-keys="onExpanded"
/>
</div>
</section>
<n-modal
v-model:show="modal.visible"
preset="card"
:title="modal.mode === 'create' ? '新增组织' : '编辑组织'"
class="w-[520px]"
>
<n-form ref="formRef" :model="form" :rules="rules" label-placement="left" label-width="90">
<n-form-item label="上级组织">
<n-tree-select
v-model:value="form.parentId"
clearable
:options="treeSelectOptions"
placeholder="可选"
key-field="id"
label-field="name"
children-field="children"
/>
</n-form-item>
<n-form-item label="组织名称" path="name"
><n-input v-model:value="form.name"
/></n-form-item>
<n-form-item label="组织编码" path="code"
><n-input v-model:value="form.code" :disabled="modal.mode === 'edit'"
/></n-form-item>
<n-form-item label="排序"
><n-input-number v-model:value="form.sort" :min="0"
/></n-form-item>
<n-form-item label="状态">
<n-radio-group v-model:value="form.status">
<n-radio-button value="ENABLED">启用</n-radio-button>
<n-radio-button value="DISABLED">禁用</n-radio-button>
</n-radio-group>
</n-form-item>
</n-form>
<template #footer>
<div class="flex justify-end gap-2">
<n-button @click="modal.visible = false">取消</n-button>
<n-button type="primary" :loading="saveMutation.isPending.value" @click="save"
>保存</n-button
>
</div>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { computed, h, reactive, ref } from 'vue'
import type { FormInst, FormRules, TreeOption } from 'naive-ui'
import { NButton, NPopconfirm, NSpace, NTag, useMessage } from 'naive-ui'
import { ChevronRight } from 'lucide-vue-next'
import { useMutation, useQuery } from '@tanstack/vue-query'
import { createOrgApi, deleteOrgApi, listOrgsApi, updateOrgApi } from '@/api/system/org'
import type { OrgTreeNode } from '@/types/system/org'
import { statusLabel, statusTagType } from '@/utils/display'
const message = useMessage()
const formRef = ref<FormInst | null>(null)
const expandedKeys = ref<string[]>([])
const modal = reactive({ visible: false, mode: 'create' as 'create' | 'edit', id: '' })
const form = reactive({
parentId: null as string | null,
name: '',
code: '',
sort: 0,
status: 'ENABLED'
})
const rules: FormRules = {
name: [{ required: true, message: '请输入组织名称', trigger: ['blur', 'input'] }],
code: [{ required: true, message: '请输入组织编码', trigger: ['blur', 'input'] }]
}
const orgQuery = useQuery({
queryKey: ['system', 'orgs'],
queryFn: listOrgsApi
})
const treeData = computed(() => orgQuery.data.value ?? [])
const treeSelectOptions = computed(() => treeData.value as unknown as TreeOption[])
const saveMutation = useMutation({
mutationFn: async () => {
const payload = {
parentId: form.parentId ?? undefined,
name: form.name.trim(),
code: form.code.trim(),
sort: form.sort,
status: form.status
}
if (modal.mode === 'create') return createOrgApi(payload)
return updateOrgApi(modal.id, payload)
},
onSuccess: async () => {
message.success('保存成功')
modal.visible = false
await orgQuery.refetch()
expandedKeys.value = collectAllKeys(treeData.value)
}
})
const deleteMutation = useMutation({
mutationFn: deleteOrgApi,
onSuccess: async () => {
message.success('删除成功')
await orgQuery.refetch()
}
})
function openCreate(parentId?: string) {
modal.mode = 'create'
modal.id = ''
form.parentId = parentId ?? null
form.name = ''
form.code = ''
form.sort = 0
form.status = 'ENABLED'
modal.visible = true
}
function openEdit(node: OrgTreeNode) {
modal.mode = 'edit'
modal.id = node.id
form.parentId = node.parentId ?? null
form.name = node.name
form.code = node.code
form.sort = node.sort
form.status = node.status
modal.visible = true
}
async function save() {
await formRef.value?.validate()
await saveMutation.mutateAsync()
}
function renderLabel(payload: { option: unknown }) {
const node = payload.option as OrgTreeNode
return h('div', { class: 'flex w-full items-center justify-between py-2' }, [
h('div', { class: 'flex items-center gap-2' }, [
h('span', node.name),
h(
NTag,
{ size: 'small', type: statusTagType(node.status) },
{ default: () => statusLabel(node.status) }
)
]),
h(NSpace, { size: 6 }, () => [
h(
NButton,
{ size: 'small', tertiary: true, onClick: () => openCreate(node.id) },
{ default: () => '新增子级' }
),
h(
NButton,
{ size: 'small', tertiary: true, type: 'primary', onClick: () => openEdit(node) },
{ default: () => '编辑' }
),
h(
NPopconfirm,
{ onPositiveClick: () => deleteMutation.mutate(node.id) },
{
trigger: () =>
h(NButton, { size: 'small', tertiary: true, type: 'error' }, { default: () => '删除' }),
default: () => '确认删除该组织吗?'
}
)
])
])
}
function onExpanded(keys: string[]) {
expandedKeys.value = keys
}
function collectAllKeys(nodes: OrgTreeNode[]): string[] {
return nodes.flatMap((node) => [node.id, ...collectAllKeys(node.children ?? [])])
}
function renderSwitcherIcon() {
return h(ChevronRight, { size: 16, strokeWidth: 2.25, class: 'tree-switcher-icon' })
}
</script>
<style scoped>
.tree-card-body {
padding: 8px 10px 12px;
}
:deep(.n-tree-node-switcher) {
width: 28px;
height: 42px;
margin-top: 0;
display: flex;
align-items: center;
justify-content: center;
align-self: stretch;
}
:deep(.tree-switcher-icon) {
display: block;
color: #0f8f86;
}
:deep(.n-tree-node-content) {
min-height: 42px;
align-items: center;
border-radius: 14px;
}
:deep(.n-tree-node-content__text) {
flex: 1;
min-width: 0;
padding: 0;
}
</style>
+2 -2
View File
@@ -119,8 +119,8 @@ const statusOptions = [
]
const dataScopeOptions = [
{ label: '全部数据', value: 'ALL' },
{ label: '本组织及下级', value: 'DEPT' },
{ label: '本组织', value: 'DEPT_ONLY' },
{ label: '本企业', value: 'DEPT' },
{ label: '本企业', value: 'DEPT_ONLY' },
{ label: '仅本人', value: 'SELF' }
]
const roleQuery = useQuery({
+55 -9
View File
@@ -18,6 +18,16 @@
style="width: 140px"
/>
</n-form-item>
<n-form-item>
<n-select
v-model:value="queryForm.enterpriseId"
clearable
filterable
placeholder="企业"
:options="enterpriseOptions"
style="width: 260px"
/>
</n-form-item>
<n-form-item>
<n-space>
<n-button type="primary" @click="handleSearch">查询</n-button>
@@ -65,8 +75,8 @@
<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 v-if="detailUser.enterpriseName || detailUser.taxpayerNum" size="small" round>
{{ detailUser.enterpriseName || detailUser.taxpayerNum }}
</n-tag>
</div>
</div>
@@ -147,6 +157,15 @@
<n-form-item label="邮箱" path="email">
<n-input v-model:value="editForm.email" />
</n-form-item>
<n-form-item label="企业" path="enterpriseId">
<n-select
v-model:value="editForm.enterpriseId"
clearable
filterable
:options="enterpriseOptions"
placeholder="可选"
/>
</n-form-item>
<n-form-item v-if="editModal.mode === 'create'" label="状态" path="status">
<n-radio-group v-model:value="editForm.status">
<n-radio-button value="ENABLED">启用</n-radio-button>
@@ -237,6 +256,7 @@ import {
updateUserStatusApi
} from '@/api/system/user'
import { listRolesApi } from '@/api/system/role'
import { listEnterprisesApi } from '@/api/system/enterprise'
import type {
CreateUserRequest,
UpdateUserRequest,
@@ -251,7 +271,8 @@ const queryClient = useQueryClient()
const queryForm = reactive({
username: '',
nickname: '',
status: null as string | null
status: null as string | null,
enterpriseId: null as string | null
})
const pager = reactive({
@@ -272,7 +293,8 @@ const usersQuery = useQuery({
pageSize: pager.pageSize,
username: queryForm.username || undefined,
nickname: queryForm.nickname || undefined,
status: queryForm.status || undefined
status: queryForm.status || undefined,
enterpriseId: queryForm.enterpriseId || undefined
})
})
@@ -281,6 +303,11 @@ const rolesQuery = useQuery({
queryFn: () => listRolesApi({ page: 1, pageSize: 200, status: 'ENABLED' })
})
const enterprisesQuery = useQuery({
queryKey: ['system', 'enterprises', 'options'],
queryFn: () => listEnterprisesApi({ page: 1, pageSize: 200 })
})
const roleOptions = computed(() =>
(rolesQuery.data.value?.items ?? []).map((role) => ({
label: `${role.name} (${role.code})`,
@@ -288,6 +315,13 @@ const roleOptions = computed(() =>
}))
)
const enterpriseOptions = computed(() =>
(enterprisesQuery.data.value?.items ?? []).map((enterprise) => ({
label: `${enterprise.enterpriseName}${enterprise.taxpayerNum}`,
value: enterprise.id
}))
)
const tableRows = computed(() => usersQuery.data.value?.items ?? [])
const pagination = computed(() => ({
@@ -316,7 +350,7 @@ const baseDetailItems = computed(() => {
{ label: '姓名', value: displayValue(user.realName) },
{ label: '手机号', value: displayValue(user.phone) },
{ label: '邮箱', value: displayValue(user.email) },
{ label: '组织', value: formatOrg(user) },
{ label: '企业', value: formatEnterprise(user) },
{ label: '状态', value: user.statusLabel || statusLabel(user.status) },
{ label: '头像', value: displayValue(user.avatar) }
]
@@ -352,6 +386,7 @@ const editForm = reactive({
realName: '',
phone: '',
email: '',
enterpriseId: null as string | null,
status: 'ENABLED'
})
@@ -370,6 +405,7 @@ const saveMutation = useMutation({
realName: editForm.realName.trim() || undefined,
phone: editForm.phone.trim() || undefined,
email: editForm.email.trim() || undefined,
enterpriseId: editForm.enterpriseId || undefined,
status: editForm.status
}
return createUserApi(payload)
@@ -378,7 +414,8 @@ const saveMutation = useMutation({
nickname: editForm.nickname.trim() || undefined,
realName: editForm.realName.trim() || undefined,
phone: editForm.phone.trim() || undefined,
email: editForm.email.trim() || undefined
email: editForm.email.trim() || undefined,
enterpriseId: editForm.enterpriseId || undefined
}
return updateUserApi(editModal.id, payload)
},
@@ -461,6 +498,7 @@ function handleReset() {
queryForm.username = ''
queryForm.nickname = ''
queryForm.status = null
queryForm.enterpriseId = null
pager.page = 1
usersQuery.refetch()
}
@@ -472,6 +510,7 @@ function resetEditForm() {
editForm.realName = ''
editForm.phone = ''
editForm.email = ''
editForm.enterpriseId = null
editForm.status = 'ENABLED'
}
@@ -488,9 +527,9 @@ function displayValue(value: unknown) {
return String(value)
}
function formatOrg(user: UserDetail) {
if (user.orgName && user.orgCode) return `${user.orgName}${user.orgCode}`
return user.orgName || user.orgCode || user.orgId || '-'
function formatEnterprise(user: UserDetail) {
if (user.enterpriseName && user.taxpayerNum) return `${user.enterpriseName}${user.taxpayerNum}`
return user.enterpriseName || user.taxpayerNum || user.enterpriseId || '-'
}
async function openDetail(row: UserListItem) {
@@ -517,6 +556,7 @@ async function openEdit(row: UserListItem) {
editForm.realName = detail.realName ?? ''
editForm.phone = detail.phone ?? ''
editForm.email = detail.email ?? ''
editForm.enterpriseId = detail.enterpriseId ?? null
editForm.status = detail.status
editModal.visible = true
}
@@ -583,6 +623,12 @@ const columns = computed<DataTableColumns<UserListItem>>(() => [
minWidth: 180,
render: (row) => (row.roleCodes.length > 0 ? row.roleCodes.join(', ') : '-')
},
{
title: '企业',
key: 'enterpriseName',
minWidth: 220,
render: (row) => row.enterpriseName || row.taxpayerNum || '-'
},
{
title: '状态',
key: 'status',
+16 -14
View File
@@ -249,13 +249,13 @@ async function handleLogout() {
.app-shell {
height: 100vh;
overflow: hidden;
background: #f5f6f8;
background: var(--app-bg);
}
.layout-sider {
position: relative;
background: #ffffff;
border-right: 1px solid #e7eaf0;
background: var(--app-surface);
border-right: 1px solid var(--app-border);
}
.layout-sider :deep(.n-layout-sider-scroll-container),
@@ -294,7 +294,7 @@ async function handleLogout() {
gap: 12px;
min-height: 64px;
padding: 16px;
border-bottom: 1px solid #eef1f5;
border-bottom: 1px solid var(--app-border);
}
.brand-seal {
@@ -303,9 +303,10 @@ async function handleLogout() {
justify-content: center;
width: 36px;
height: 36px;
border-radius: 10px;
background: #111827;
border-radius: 11px;
background: var(--app-primary);
color: #ffffff;
box-shadow: 0 12px 24px rgba(37, 99, 235, 0.18);
}
.brand-glyph {
@@ -322,7 +323,7 @@ async function handleLogout() {
.brand-name {
min-width: 0;
overflow: hidden;
color: #111827;
color: var(--app-text);
font-size: 15px;
font-weight: 600;
text-overflow: ellipsis;
@@ -367,7 +368,7 @@ async function handleLogout() {
.sider-resizer:hover::after,
:global(.sider-resizing) .sider-resizer::after {
background: #d1d5db;
background: var(--app-border-strong);
}
:global(.sider-resizing) {
@@ -397,8 +398,9 @@ async function handleLogout() {
.app-header {
flex: 0 0 auto;
padding: 16px 20px 10px;
border-bottom: 1px solid #e7eaf0;
background: #ffffff;
border-bottom: 1px solid var(--app-border);
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(12px);
}
.header-main {
@@ -416,7 +418,7 @@ async function handleLogout() {
}
.header-icon-btn {
color: #4b5563;
color: #475569;
}
.header-meta {
@@ -425,7 +427,7 @@ async function handleLogout() {
.header-title {
margin: 0 0 4px;
color: #111827;
color: var(--app-text);
font-size: 22px;
font-weight: 600;
}
@@ -437,12 +439,12 @@ async function handleLogout() {
}
.identity-name {
color: #374151;
color: var(--app-text-muted);
font-size: 13px;
}
.logout-btn {
color: #374151;
color: #475569;
}
.layout-content {
+1 -27
View File
@@ -1,7 +1,6 @@
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import { loginApi, logoutApi, meApi, registerEnterpriseApi } from '@/api/auth'
import { listDictItemsApi } from '@/api/system/dict'
import type {
CurrentUserProfile,
EnterpriseRegisterRequest,
@@ -9,7 +8,6 @@ import type {
MeResponse,
MenuNode
} from '@/types/auth'
import type { DictItem } from '@/types/system/dict'
const TOKEN_KEY = 'platform.token'
@@ -35,7 +33,6 @@ export const useAuthStore = defineStore('auth', () => {
const menus = ref<MenuNode[]>([])
const permissions = ref<string[]>([])
const layoutCollapsed = ref(false)
const dictCache = ref<Record<string, DictItem[]>>({})
const tabs = ref<TabItem[]>(createDefaultTabs())
const tabRefreshMarks = ref<Record<string, number>>({})
const loaded = ref(false)
@@ -52,7 +49,6 @@ export const useAuthStore = defineStore('auth', () => {
user.value = null
menus.value = []
permissions.value = []
dictCache.value = {}
loaded.value = false
tabs.value = createDefaultTabs()
tabRefreshMarks.value = {}
@@ -103,24 +99,6 @@ export const useAuthStore = defineStore('auth', () => {
return permissions.value.includes(permission)
}
async function loadDictByTypeId(typeId: string, force = false) {
const cached = dictCache.value[typeId]
if (cached && !force) return cached
const result = await listDictItemsApi({ typeId, page: 1, pageSize: 200 })
dictCache.value[typeId] = result.items.filter((item) => item.status === 'ENABLED')
return dictCache.value[typeId]
}
function getDictLabel(typeId: string, value: string) {
const item = dictCache.value[typeId]?.find((entry) => entry.value === value)
return item?.label ?? value
}
function getDictColor(typeId: string, value: string) {
const item = dictCache.value[typeId]?.find((entry) => entry.value === value)
return item?.color ?? undefined
}
function addTab(tab: TabItem) {
const normalizedPath = normalizeTabPath(tab.path)
if (tabs.value.some((item) => item.path === normalizedPath)) return
@@ -172,7 +150,6 @@ export const useAuthStore = defineStore('auth', () => {
menus,
permissions,
layoutCollapsed,
dictCache,
tabs,
tabRefreshMarks,
loaded,
@@ -188,9 +165,6 @@ export const useAuthStore = defineStore('auth', () => {
closeTab,
closeOtherTabs,
closeAllTabs,
refreshTab,
loadDictByTypeId,
getDictLabel,
getDictColor
refreshTab
}
})
+29 -17
View File
@@ -4,17 +4,20 @@
:root {
color: #111827;
background: #f5f6f8;
background: #f6f7fb;
font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
--app-bg: #f5f6f8;
--app-bg: #f6f7fb;
--app-surface: #ffffff;
--app-border: #e7eaf0;
--app-border-strong: #d7dce5;
--app-primary: #111827;
--app-primary-soft: #f3f4f6;
--app-surface-tint: #fafbff;
--app-border: #e5e7eb;
--app-border-strong: #cbd5e1;
--app-primary: #2563eb;
--app-primary-deep: #1d4ed8;
--app-primary-soft: #edf4ff;
--app-primary-subtle: #f7faff;
--app-text: #111827;
--app-text-muted: #6b7280;
--app-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
--app-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
}
* {
@@ -46,13 +49,13 @@ select {
.app-panel {
border: 1px solid var(--app-border);
border-radius: 14px;
border-radius: 12px;
background: var(--app-surface);
box-shadow: var(--app-shadow);
}
.page-title {
color: #111827;
color: var(--app-text);
font-size: 18px;
font-weight: 600;
}
@@ -78,7 +81,7 @@ select {
.page-title-xl {
margin: 0;
color: #111827;
color: var(--app-text);
font-size: 20px;
font-weight: 600;
}
@@ -97,12 +100,12 @@ select {
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--app-border);
background: #ffffff;
background: var(--app-surface);
}
.page-toolbar-title {
margin: 0;
color: #111827;
color: var(--app-text);
font-size: 16px;
font-weight: 600;
line-height: 1.4;
@@ -195,15 +198,15 @@ select {
.page-card .n-data-table-th {
font-weight: 500;
color: #6b7280;
color: var(--app-text-muted);
}
.page-card .n-data-table-td {
color: #111827;
color: var(--app-text);
}
.page-card .n-data-table-tr:hover .n-data-table-td {
background: #fafafa;
background: var(--app-primary-subtle);
}
.action-btn {
@@ -212,8 +215,8 @@ select {
.soft-stat {
border: 1px solid var(--app-border);
border-radius: 14px;
background: #ffffff;
border-radius: 12px;
background: var(--app-surface);
box-shadow: var(--app-shadow);
padding: 18px;
}
@@ -222,6 +225,15 @@ select {
font-weight: 500;
}
.n-button.n-button--primary-type {
--n-color: var(--app-primary) !important;
--n-color-hover: var(--app-primary-deep) !important;
--n-color-pressed: #1e40af !important;
--n-border: 1px solid var(--app-primary) !important;
--n-border-hover: 1px solid var(--app-primary-deep) !important;
--n-border-pressed: 1px solid #1e40af !important;
}
.n-input,
.n-base-selection {
font-size: 13px;
-1
View File
@@ -29,7 +29,6 @@ export interface CurrentUserProfile {
username: string
nickname?: string | null
realName?: string | null
orgId?: string | null
enterpriseId?: string | null
digitalAccountId?: string | null
userType: 'SYSTEM' | 'ENTERPRISE_ADMIN' | 'DIGITAL_OPERATOR' | string
-15
View File
@@ -14,19 +14,4 @@ export interface OperationLogItem {
createdAt: string
}
export interface ApiAccessLogItem {
id: string
traceId?: string | null
appKey?: string | null
appName?: string | null
httpMethod: string
requestPath: string
responseCode?: string | null
status: string
errorMessage?: string | null
costMs: number
createdAt: string
}
export type OperationLogPage = PageResult<OperationLogItem>
export type ApiAccessLogPage = PageResult<ApiAccessLogItem>
-25
View File
@@ -1,25 +0,0 @@
import type { PageResult } from '@/types/http'
export interface DictTypeItem {
id: string
code: string
name: string
status: string
statusLabel?: string
remark?: string | null
}
export interface DictItem {
id: string
typeId: string
label: string
value: string
color?: string | null
sort: number
status: string
statusLabel?: string
remark?: string | null
}
export type DictTypePage = PageResult<DictTypeItem>
export type DictItemPage = PageResult<DictItem>
+29
View File
@@ -0,0 +1,29 @@
import type { PageResult } from '@/types/http'
export interface EnterpriseManageItem {
id: string
taxpayerNum: string
enterpriseName: string
legalPersonName?: string | null
contactsName?: string | null
contactsEmail?: string | null
contactsPhone?: string | null
regionCode?: string | null
cityName?: string | null
enterpriseAddress?: string | null
reviewStatus?: string | null
reviewOpinion?: string | null
invoiceKind?: string | null
invoiceLayoutFileType?: string | null
serviceStatus?: string | null
createdAt: string
updatedAt?: string | null
}
export interface EnterpriseQuery {
keyword?: string
page: number
pageSize: number
}
export type EnterpriseManagePage = PageResult<EnterpriseManageItem>
-25
View File
@@ -1,25 +0,0 @@
export interface OrgTreeNode {
id: string
parentId?: string | null
name: string
code: string
sort: number
status: string
statusLabel?: string
children: OrgTreeNode[]
}
export interface CreateOrgRequest {
parentId?: string
name: string
code: string
sort: number
status: string
}
export interface UpdateOrgRequest {
parentId?: string
name: string
sort: number
status: string
}
+9 -7
View File
@@ -5,7 +5,9 @@ export interface UserListItem {
username: string
nickname?: string | null
realName?: string | null
orgId?: string | null
enterpriseId?: string | null
enterpriseName?: string | null
taxpayerNum?: string | null
status: string
statusLabel?: string
roleCodes: string[]
@@ -19,9 +21,9 @@ export interface UserDetail {
phone?: string | null
email?: string | null
avatar?: string | null
orgId?: string | null
orgName?: string | null
orgCode?: string | null
enterpriseId?: string | null
enterpriseName?: string | null
taxpayerNum?: string | null
status: string
statusLabel?: string
roleIds: string[]
@@ -48,7 +50,7 @@ export interface UserQuery {
username?: string
nickname?: string
status?: string
orgId?: string
enterpriseId?: string
page: number
pageSize: number
}
@@ -63,7 +65,7 @@ export interface CreateUserRequest {
phone?: string
email?: string
avatar?: string
orgId?: string
enterpriseId?: string
status: string
}
@@ -73,7 +75,7 @@ export interface UpdateUserRequest {
phone?: string
email?: string
avatar?: string
orgId?: string
enterpriseId?: string
}
export interface UpdateUserStatusRequest {
+2 -2
View File
@@ -29,8 +29,8 @@ export function menuTypeLabel(type?: string | null) {
export function dataScopeLabel(scope?: string | null) {
const map: Record<string, string> = {
ALL: '全部数据',
DEPT: '本组织及下级',
DEPT_ONLY: '本组织',
DEPT: '本企业',
DEPT_ONLY: '本企业',
SELF: '仅本人'
}
return scope ? (map[scope] ?? scope) : '-'