551 lines
15 KiB
Vue
551 lines
15 KiB
Vue
<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="短信登录"
|
|
: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>
|
|
</div>
|
|
</n-space>
|
|
</n-modal>
|
|
|
|
<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>
|
|
|
|
<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,
|
|
NInputGroup,
|
|
NModal,
|
|
NPopconfirm,
|
|
NRadioButton,
|
|
NRadioGroup,
|
|
NSelect,
|
|
NSpace,
|
|
NTag,
|
|
NTooltip,
|
|
useMessage
|
|
} from 'naive-ui'
|
|
import { KeyRound, Plus, RefreshCw, ShieldCheck } from 'lucide-vue-next'
|
|
import {
|
|
createDigitalAccountApi,
|
|
getAuthQrcodeApi,
|
|
listDigitalAccountsApi,
|
|
refreshDigitalAccountsApi,
|
|
sendLoginSmsCodeApi,
|
|
smsLoginApi,
|
|
updateDigitalAccountStatusApi
|
|
} 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 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: '',
|
|
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',
|
|
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) },
|
|
{
|
|
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: '平台状态',
|
|
key: 'status',
|
|
width: 110,
|
|
render: (row) =>
|
|
h(
|
|
NTag,
|
|
{ type: row.status === 'ENABLED' ? 'success' : 'warning' },
|
|
{ default: () => (row.status === 'ENABLED' ? '可用' : '禁用') }
|
|
)
|
|
},
|
|
{
|
|
title: 'API Key',
|
|
key: 'apiKey',
|
|
width: 180,
|
|
ellipsis: true,
|
|
render: (row) => renderEllipsisText(row.apiKey)
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'actions',
|
|
fixed: 'right',
|
|
width: 300,
|
|
render: (row) =>
|
|
h('div', { class: 'row-actions' }, [
|
|
h(
|
|
NButton,
|
|
{
|
|
size: 'small',
|
|
secondary: true,
|
|
disabled: row.status !== 'ENABLED',
|
|
onClick: () => openSms(row)
|
|
},
|
|
{ icon: () => h(NIcon, { component: KeyRound }), default: () => '短信登录' }
|
|
),
|
|
h(
|
|
NButton,
|
|
{
|
|
size: 'small',
|
|
tertiary: true,
|
|
disabled: row.status !== 'ENABLED',
|
|
onClick: () => openQrcode(row)
|
|
},
|
|
{ icon: () => h(NIcon, { component: ShieldCheck }), default: () => '认证' }
|
|
),
|
|
canUpdateStatus.value
|
|
? h(
|
|
NPopconfirm,
|
|
{
|
|
onPositiveClick: () => updateAccountStatus(row)
|
|
},
|
|
{
|
|
trigger: () =>
|
|
h(
|
|
NButton,
|
|
{ size: 'small', tertiary: true, type: row.status === 'ENABLED' ? 'warning' : 'success' },
|
|
{ default: () => (row.status === 'ENABLED' ? '禁用' : '恢复') }
|
|
),
|
|
default: () =>
|
|
row.status === 'ENABLED'
|
|
? `确认禁用数电账号 ${row.account} 吗?`
|
|
: `确认恢复数电账号 ${row.account} 吗?`
|
|
}
|
|
)
|
|
: null
|
|
])
|
|
}
|
|
]
|
|
|
|
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 : '-'
|
|
}
|
|
|
|
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 {
|
|
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 updateAccountStatus(row: DigitalAccountItem) {
|
|
const nextStatus = row.status === 'ENABLED' ? 'DISABLED' : 'ENABLED'
|
|
await updateDigitalAccountStatusApi(row.id, nextStatus)
|
|
message.success(nextStatus === 'ENABLED' ? '数电账号已恢复' : '数电账号已禁用')
|
|
await load()
|
|
}
|
|
|
|
async function openSms(row: DigitalAccountItem) {
|
|
selected.value = row
|
|
smsCode.value = ''
|
|
smsTip.value = ''
|
|
showSms.value = true
|
|
}
|
|
|
|
async function sendSmsCode() {
|
|
if (!selected.value) return
|
|
smsCodeLoading.value = true
|
|
try {
|
|
const res = await sendLoginSmsCodeApi({
|
|
taxpayerNum: selected.value.taxpayerNum,
|
|
account: selected.value.account
|
|
})
|
|
smsTip.value = res.phoneNum ? `验证码已发送至 ${res.phoneNum}` : res.resultMsg
|
|
} finally {
|
|
smsCodeLoading.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
|
|
qrcodeType.value = '1'
|
|
qrcodeImg.value = ''
|
|
qrcodeGenTime.value = ''
|
|
qrcodeTip.value = ''
|
|
showQrcode.value = true
|
|
await loadQrcode()
|
|
}
|
|
|
|
async function loadQrcode() {
|
|
if (!selected.value) return
|
|
qrLoading.value = true
|
|
try {
|
|
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
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
.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;
|
|
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>
|