多数电账号管理

This commit is contained in:
BBIT-Kai
2026-05-22 15:37:45 +08:00
parent f718ff46da
commit d57ea3960c
63 changed files with 2420 additions and 2885 deletions
@@ -0,0 +1,363 @@
<template>
<div class="page-shell">
<section class="page-card table-fill">
<div class="page-toolbar">
<h2 class="page-toolbar-title">数电账号管理</h2>
<div class="page-toolbar-actions">
<n-button :loading="loading" @click="refreshAccounts">
<template #icon><n-icon :component="RefreshCw" /></template>
刷新
</n-button>
<n-button v-if="canManage" type="primary" @click="showCreate = true">
<template #icon><n-icon :component="Plus" /></template>
新增
</n-button>
</div>
</div>
<div class="card-body card-body-fill table-fill">
<n-data-table
flex-height
size="small"
:columns="columns"
:data="accounts"
:loading="loading"
:pagination="{ pageSize: 10 }"
:row-key="(row: DigitalAccountItem) => row.id"
:scroll-x="1460"
/>
</div>
</section>
<n-modal v-model:show="showCreate" preset="card" title="新增数电账号" class="modal-card">
<n-form ref="createFormRef" :model="createForm" :rules="createRules" label-placement="top">
<n-grid responsive="screen" :cols="2" :x-gap="12">
<n-gi>
<n-form-item label="税局账号" path="account">
<n-input v-model:value="createForm.account" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="税局密码" path="taxPassword">
<n-input v-model:value="createForm.taxPassword" type="password" show-password-on="click" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="姓名" path="name">
<n-input v-model:value="createForm.name" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="手机号" path="phoneNum">
<n-input v-model:value="createForm.phoneNum" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="身份类型" path="identityType">
<n-select v-model:value="createForm.identityType" :options="identityOptions" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="平台密码" path="platformPassword">
<n-input
v-model:value="createForm.platformPassword"
type="password"
show-password-on="click"
/>
</n-form-item>
</n-gi>
</n-grid>
<div class="modal-actions">
<n-button @click="showCreate = false">取消</n-button>
<n-button type="primary" :loading="saving" @click="createAccount">保存</n-button>
</div>
</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="请输入短信验证码" />
<div class="modal-actions">
<n-button @click="showSms = false">取消</n-button>
<n-button type="primary" :loading="smsLoading" @click="submitSms">确认登录</n-button>
</div>
</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>
</div>
</template>
<script setup lang="ts">
import { computed, h, onMounted, reactive, ref } from 'vue'
import type { DataTableColumns, FormInst, FormRules } from 'naive-ui'
import {
NAlert,
NButton,
NDataTable,
NEmpty,
NForm,
NFormItem,
NGi,
NGrid,
NIcon,
NInput,
NModal,
NSelect,
NSpace,
NTag,
useMessage
} from 'naive-ui'
import { KeyRound, Plus, RefreshCw, ShieldCheck } from 'lucide-vue-next'
import {
createDigitalAccountApi,
getAuthQrcodeApi,
listDigitalAccountsApi,
refreshDigitalAccountsApi,
sendLoginSmsCodeApi,
smsLoginApi
} from '@/api/piaotong'
import type { DigitalAccountItem } from '@/api/piaotong'
import { useAuthStore } from '@/stores/auth'
const message = useMessage()
const authStore = useAuthStore()
const loading = ref(false)
const saving = ref(false)
const accounts = ref<DigitalAccountItem[]>([])
const showCreate = ref(false)
const showSms = ref(false)
const showQrcode = ref(false)
const smsLoading = ref(false)
const qrLoading = ref(false)
const smsCode = ref('')
const smsTip = ref('')
const qrcodeImg = ref('')
const selected = ref<DigitalAccountItem | null>(null)
const createFormRef = ref<FormInst | null>(null)
const canManage = computed(() => authStore.user?.userType !== 'DIGITAL_OPERATOR')
const createForm = reactive({
account: '',
taxPassword: '',
identityType: '09',
phoneNum: '',
name: '',
platformPassword: ''
})
const createRules: FormRules = {
account: [{ required: true, message: '请输入税局账号', trigger: ['blur', 'input'] }],
taxPassword: [{ required: true, message: '请输入税局密码', trigger: ['blur', 'input'] }],
name: [{ required: true, message: '请输入姓名', trigger: ['blur', 'input'] }],
phoneNum: [{ required: true, message: '请输入手机号', trigger: ['blur', 'input'] }],
platformPassword: [{ required: true, message: '请输入平台密码', trigger: ['blur', 'input'] }]
}
const identityOptions = [
{ label: '法定代表人', value: '01' },
{ label: '财务负责人', value: '02' },
{ label: '办税员', value: '03' },
{ label: '管理员', value: '05' },
{ label: '开票员', value: '09' }
]
const columns: DataTableColumns<DigitalAccountItem> = [
{ title: '税局账号', key: 'account', minWidth: 140 },
{ title: '平台登录账号', key: 'platformUsername', minWidth: 180, render: (row) => row.platformUsername || '-' },
{ title: '姓名', key: 'name', minWidth: 100 },
{ title: '登录身份', key: 'identityType', render: (row) => identityLabel(row.identityType) },
{ title: '账号状态', key: 'authStatus', render: (row) => authStatusLabel(row.authStatus) },
{
title: '登录认证',
key: 'loginAuthStatus',
render: (row) =>
h(
NTag,
{ type: row.loginAuthStatus === '1' ? 'success' : 'warning' },
{ default: () => (row.loginAuthStatus === '1' ? '已登录' : '未登录') }
)
},
{
title: '风险认证',
key: 'riskAuthStatus',
render: (row) =>
h(
NTag,
{ type: row.riskAuthStatus === '1' ? 'success' : 'warning' },
{ default: () => (row.riskAuthStatus === '1' ? '已认证' : '未认证') }
)
},
{ title: '最近认证', key: 'lastAuthSuccTime', minWidth: 160 },
{
title: 'API Key',
key: 'apiKey',
minWidth: 180,
render: (row) => row.apiKey || '-'
},
{
title: '操作',
key: 'actions',
fixed: 'right',
width: 220,
render: (row) =>
h('div', { class: 'row-actions' }, [
h(
NButton,
{ size: 'small', secondary: true, onClick: () => openSms(row) },
{ icon: () => h(NIcon, { component: KeyRound }), default: () => '短信登录' }
),
h(
NButton,
{ size: 'small', tertiary: true, onClick: () => openQrcode(row) },
{ icon: () => h(NIcon, { component: ShieldCheck }), default: () => '认证' }
)
])
}
]
function identityLabel(value?: string | null) {
return identityOptions.find((item) => item.value === value)?.label || value || '-'
}
function authStatusLabel(value?: string | null) {
const map: Record<string, string> = {
'0': '无需认证',
'1': '风险认证',
'2': '登录认证',
'3': '风险+登录认证'
}
return value ? map[value] || value : '-'
}
async function load() {
loading.value = true
try {
accounts.value = await listDigitalAccountsApi()
} finally {
loading.value = false
}
}
async function refreshAccounts() {
loading.value = true
try {
accounts.value = await refreshDigitalAccountsApi()
message.success('数电账号已刷新')
} finally {
loading.value = false
}
}
async function createAccount() {
await createFormRef.value?.validate()
saving.value = true
try {
await createDigitalAccountApi({ ...createForm })
message.success('数电账号已创建')
showCreate.value = false
await load()
} finally {
saving.value = false
}
}
async function openSms(row: DigitalAccountItem) {
selected.value = row
smsCode.value = ''
smsLoading.value = true
showSms.value = true
try {
const res = await sendLoginSmsCodeApi({ taxpayerNum: row.taxpayerNum, account: row.account })
smsTip.value = res.phoneNum ? `验证码已发送至 ${res.phoneNum}` : res.resultMsg
} finally {
smsLoading.value = false
}
}
async function submitSms() {
if (!selected.value || !smsCode.value.trim()) return
smsLoading.value = true
try {
await smsLoginApi({
taxpayerNum: selected.value.taxpayerNum,
account: selected.value.account,
smsCode: smsCode.value.trim()
})
message.success('短信登录成功')
showSms.value = false
await load()
} finally {
smsLoading.value = false
}
}
async function openQrcode(row: DigitalAccountItem) {
selected.value = row
showQrcode.value = true
await loadQrcode()
}
async function loadQrcode() {
if (!selected.value) return
qrLoading.value = true
try {
const res = await getAuthQrcodeApi('1', selected.value.id)
qrcodeImg.value = res.qrcodeImg ? `data:image/png;base64,${res.qrcodeImg}` : ''
} finally {
qrLoading.value = false
}
}
onMounted(load)
</script>
<style scoped>
.modal-actions,
.row-actions {
display: flex;
align-items: center;
gap: 8px;
}
.row-actions {
flex-wrap: nowrap;
white-space: nowrap;
}
.row-actions :deep(.n-button) {
flex-shrink: 0;
}
.modal-card {
width: min(720px, 92vw);
}
.sms-card {
width: min(420px, 92vw);
}
.modal-actions {
justify-content: flex-end;
}
.qrcode-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.qrcode-wrap img {
width: 220px;
height: 220px;
}
</style>