大改三角色逻辑:超级管理员、企业管理员、开票员
This commit is contained in:
+1
-10
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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' },
|
||||
|
||||
@@ -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>
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) : '-'
|
||||
|
||||
Reference in New Issue
Block a user