多数电账号管理
This commit is contained in:
+5
-1
@@ -1,10 +1,14 @@
|
||||
import http from '@/api/http'
|
||||
import type { LoginRequest, LoginResponse, MeResponse } from '@/types/auth'
|
||||
import type { EnterpriseRegisterRequest, LoginRequest, LoginResponse, MeResponse } from '@/types/auth'
|
||||
|
||||
export function loginApi(payload: LoginRequest) {
|
||||
return http.post<never, LoginResponse>('/auth/login', payload)
|
||||
}
|
||||
|
||||
export function registerEnterpriseApi(payload: EnterpriseRegisterRequest) {
|
||||
return http.post<never, LoginResponse>('/auth/register-enterprise', payload)
|
||||
}
|
||||
|
||||
export function logoutApi() {
|
||||
return http.post<never, void>('/auth/logout')
|
||||
}
|
||||
|
||||
@@ -95,83 +95,83 @@ export function getPTInfoApi(): Promise<TaxBureauAccountAuth> {
|
||||
return http.get('/pt/info')
|
||||
}
|
||||
|
||||
export interface TaxEnterpriseRegisterRequest {
|
||||
taxpayerNum: string
|
||||
enterpriseName: string
|
||||
legalPersonName: string
|
||||
contactsName: string
|
||||
contactsEmail: string
|
||||
contactsPhone: string
|
||||
regionCode: string
|
||||
cityName: string
|
||||
enterpriseAddress: string
|
||||
taxRegistrationCertificate: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册企业(纳税人)
|
||||
*/
|
||||
export function registerEnterpriseApi(payload: TaxEnterpriseRegisterRequest): Promise<string> {
|
||||
return http.post('/pt/register', payload)
|
||||
}
|
||||
|
||||
export interface TaxRegisterUserRequest {
|
||||
taxpayerNum: string
|
||||
taxAccount: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 登记账号
|
||||
*/
|
||||
export function registerUserApi(payload: TaxRegisterUserRequest): Promise<string> {
|
||||
return http.post('/pt/registerUser', payload)
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 基础信息配置(本地 CRUD)
|
||||
// =============================================
|
||||
|
||||
/** 企业信息 */
|
||||
export interface EnterpriseInfo {
|
||||
id: string
|
||||
taxpayerNum: string
|
||||
enterpriseName: string
|
||||
legalPersonName: string
|
||||
contactsName: string
|
||||
contactsEmail: string
|
||||
contactsPhone: string
|
||||
regionCode: string
|
||||
cityName: string
|
||||
enterpriseAddress: string
|
||||
taxRegistrationCertificate: string
|
||||
legalPersonName?: string | null
|
||||
contactsName?: string | null
|
||||
contactsEmail?: string | null
|
||||
contactsPhone?: string | null
|
||||
regionCode?: string | null
|
||||
cityName?: string | null
|
||||
enterpriseAddress?: string | null
|
||||
taxRegistrationCertificate?: string | null
|
||||
invitationCode?: string | null
|
||||
reviewStatus?: string | null
|
||||
reviewOpinion?: string | null
|
||||
invoiceKind?: string | null
|
||||
invoiceLayoutFileType?: string | null
|
||||
serviceStatus?: string | null
|
||||
bankName?: string | null
|
||||
bankAccount?: string | null
|
||||
presetAddress?: string | null
|
||||
presetPhone?: string | null
|
||||
}
|
||||
|
||||
/** 获取企业信息 */
|
||||
export function getEnterpriseInfoApi(): Promise<EnterpriseInfo> {
|
||||
return http.get('/pt/enterprise')
|
||||
}
|
||||
|
||||
/** 更新企业信息 */
|
||||
export function updateEnterpriseInfoApi(payload: Partial<EnterpriseInfo>): Promise<string> {
|
||||
return http.put('/pt/enterprise', payload)
|
||||
export function refreshEnterpriseInfoApi(): Promise<EnterpriseInfo> {
|
||||
return http.post('/pt/enterprise/refresh')
|
||||
}
|
||||
|
||||
/** 数电账号信息 */
|
||||
export interface DigitalAccountInfo {
|
||||
export interface DigitalAccountItem {
|
||||
id: string
|
||||
enterpriseId: string
|
||||
taxpayerNum: string
|
||||
taxAccount: string
|
||||
account: string
|
||||
name?: string | null
|
||||
identityType?: string | null
|
||||
operationProposed?: string | null
|
||||
authStatus?: string | null
|
||||
switchable?: string | null
|
||||
wechatUserBindStatus?: string | null
|
||||
lastAuthSuccTime?: string | null
|
||||
loginAuthStatus?: string | null
|
||||
lastLoginAuthTime?: string | null
|
||||
riskAuthStatus?: string | null
|
||||
lastRiskAuthTime?: string | null
|
||||
platformUserId?: string | null
|
||||
platformUsername?: string | null
|
||||
apiKey?: string | null
|
||||
status: string
|
||||
}
|
||||
|
||||
/** 获取数电账号信息 */
|
||||
export function getDigitalAccountApi(): Promise<DigitalAccountInfo> {
|
||||
return http.get('/pt/digital-account')
|
||||
export interface CreateDigitalAccountRequest {
|
||||
account: string
|
||||
taxPassword: string
|
||||
identityType: string
|
||||
phoneNum: string
|
||||
name: string
|
||||
platformPassword: string
|
||||
}
|
||||
|
||||
/** 更新数电账号信息 */
|
||||
export function updateDigitalAccountApi(payload: DigitalAccountInfo): Promise<string> {
|
||||
return http.put('/pt/digital-account', payload)
|
||||
export function listDigitalAccountsApi(): Promise<DigitalAccountItem[]> {
|
||||
return http.get('/pt/digital-accounts')
|
||||
}
|
||||
|
||||
export function refreshDigitalAccountsApi(): Promise<DigitalAccountItem[]> {
|
||||
return http.post('/pt/digital-accounts/refresh')
|
||||
}
|
||||
|
||||
export function createDigitalAccountApi(
|
||||
payload: CreateDigitalAccountRequest
|
||||
): Promise<DigitalAccountItem> {
|
||||
return http.post('/pt/digital-accounts', payload)
|
||||
}
|
||||
|
||||
/** 开票预设数据 */
|
||||
export interface PresetData {
|
||||
bankName: string
|
||||
bankAccount: string
|
||||
@@ -179,16 +179,44 @@ export interface PresetData {
|
||||
phone: string
|
||||
}
|
||||
|
||||
/** 获取开票预设数据 */
|
||||
export function getPresetDataApi(): Promise<PresetData> {
|
||||
return http.get('/pt/preset')
|
||||
export interface EnterpriseBankAccount {
|
||||
bankName: string
|
||||
bankAccount?: string | null
|
||||
source: string
|
||||
}
|
||||
|
||||
export function getPresetDataApi(): Promise<PresetData> {
|
||||
return getEnterpriseInfoApi().then((info) => ({
|
||||
bankName: info.bankName ?? '',
|
||||
bankAccount: info.bankAccount ?? '',
|
||||
address: info.presetAddress ?? '',
|
||||
phone: info.presetPhone ?? ''
|
||||
}))
|
||||
}
|
||||
|
||||
/** 更新开票预设数据 */
|
||||
export function updatePresetDataApi(payload: PresetData): Promise<string> {
|
||||
return http.put('/pt/preset', payload)
|
||||
}
|
||||
|
||||
export function queryEnterpriseBankAccountsApi(): Promise<EnterpriseBankAccount[]> {
|
||||
return http.get('/pt/enterprise/bank-accounts')
|
||||
}
|
||||
|
||||
export interface OpenApiStatisticsItem {
|
||||
digitalAccountId?: string | null
|
||||
account?: string | null
|
||||
interfaceCode?: string | null
|
||||
total: number
|
||||
success: number
|
||||
failed: number
|
||||
avgCostMs: number
|
||||
lastCalledAt?: string | null
|
||||
}
|
||||
|
||||
export function openApiStatisticsApi(): Promise<OpenApiStatisticsItem[]> {
|
||||
return http.get('/pt/openapi/statistics')
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 开票相关
|
||||
// =============================================
|
||||
@@ -269,6 +297,7 @@ export interface OrderInfo {
|
||||
* 数电发票开票请求
|
||||
*/
|
||||
export interface InvoiceRequest {
|
||||
digitalAccountId?: string | null
|
||||
/** 销方纳税人识别号 */
|
||||
taxpayerNum: string
|
||||
/** 发票请求流水号 */
|
||||
@@ -850,8 +879,11 @@ export interface AuthQrcodeResponse {
|
||||
* 获取实名认证二维码
|
||||
* @param qrcodeType 1: 电子税务局 APP, 2: 国家网络身份认证 APP
|
||||
*/
|
||||
export function getAuthQrcodeApi(qrcodeType: string): Promise<AuthQrcodeResponse> {
|
||||
return http.get('/pt/authentication', { params: { qrcodeType } })
|
||||
export function getAuthQrcodeApi(
|
||||
qrcodeType: string,
|
||||
digitalAccountId?: string
|
||||
): Promise<AuthQrcodeResponse> {
|
||||
return http.get('/pt/authentication', { params: { qrcodeType, digitalAccountId } })
|
||||
}
|
||||
|
||||
/** 查询认证二维码扫码状态响应 */
|
||||
|
||||
@@ -60,6 +60,7 @@ function handleSelect(key: string) {
|
||||
<style scoped>
|
||||
:deep(.n-menu) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
remote
|
||||
size="small"
|
||||
:columns="columns"
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
remote
|
||||
size="small"
|
||||
:columns="columns"
|
||||
|
||||
@@ -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>
|
||||
+69
-2063
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,7 @@
|
||||
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
remote
|
||||
:columns="columns"
|
||||
:data="dataSource"
|
||||
|
||||
@@ -190,6 +190,17 @@
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<n-grid item-responsive responsive="screen" cols="2 s:2 m:3 l:4 xl:5 2xl:6" :x-gap="12" :y-gap="4">
|
||||
<n-gi>
|
||||
<n-form-item label="数电账号" path="digitalAccountId">
|
||||
<n-select
|
||||
v-model:value="form.digitalAccountId"
|
||||
:options="digitalAccountOptions"
|
||||
:disabled="isOperator"
|
||||
placeholder="请选择开票员"
|
||||
@update:value="applySelectedDigitalAccount"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="销方纳税人识别号" path="taxpayerNum">
|
||||
<n-input
|
||||
@@ -914,8 +925,13 @@ import {
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { invoiceIssueApi, getEnterpriseInfoApi, getPresetDataApi } from '@/api/piaotong'
|
||||
import type { InvoiceRequest, InvoiceItem, VariableLevyProof } from '@/api/piaotong'
|
||||
import {
|
||||
invoiceIssueApi,
|
||||
getEnterpriseInfoApi,
|
||||
getPresetDataApi,
|
||||
listDigitalAccountsApi
|
||||
} from '@/api/piaotong'
|
||||
import type { DigitalAccountItem, InvoiceRequest, InvoiceItem, VariableLevyProof } from '@/api/piaotong'
|
||||
import type { FormInst, FormRules } from 'naive-ui'
|
||||
|
||||
const message = useMessage()
|
||||
@@ -931,6 +947,14 @@ const orderInputValue = ref('')
|
||||
const activeSidebar = ref<string | number>('basic')
|
||||
|
||||
const currentUser = computed(() => authStore.user)
|
||||
const digitalAccounts = ref<DigitalAccountItem[]>([])
|
||||
const isOperator = computed(() => currentUser.value?.userType === 'DIGITAL_OPERATOR')
|
||||
const digitalAccountOptions = computed(() =>
|
||||
digitalAccounts.value.map((item) => ({
|
||||
label: `${item.name || item.account}(${item.account})`,
|
||||
value: item.id
|
||||
}))
|
||||
)
|
||||
const currentItem = computed({
|
||||
get: () => form.itemList[activeSidebar.value as number] ?? form.itemList[0],
|
||||
set: (val) => {
|
||||
@@ -1144,6 +1168,7 @@ function createEmptyVariableLevyProof(): VariableLevyProof {
|
||||
}
|
||||
|
||||
const form = reactive<InvoiceRequest>({
|
||||
digitalAccountId: null,
|
||||
taxpayerNum: '',
|
||||
invoiceReqSerialNo: '',
|
||||
invoiceIssueKindCode: '82',
|
||||
@@ -1231,6 +1256,13 @@ function generateSerialNo() {
|
||||
autoSerial.value = true
|
||||
}
|
||||
|
||||
function applySelectedDigitalAccount() {
|
||||
const selected = digitalAccounts.value.find((item) => item.id === form.digitalAccountId)
|
||||
if (!selected) return
|
||||
form.taxpayerNum = selected.taxpayerNum
|
||||
form.account = selected.account
|
||||
}
|
||||
|
||||
function confirmRemove(index: number) {
|
||||
const item = form.itemList[index]
|
||||
const itemName = item?.goodsName || `商品 #${item?.lineNo || index + 1}`
|
||||
@@ -1533,20 +1565,24 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (currentUser.value?.taxpayerNum) {
|
||||
form.taxpayerNum = currentUser.value.taxpayerNum
|
||||
}
|
||||
if (currentUser.value?.account) {
|
||||
form.account = currentUser.value.account
|
||||
}
|
||||
generateSerialNo()
|
||||
|
||||
// 加载企业信息和预设数据,自动填入销方信息
|
||||
try {
|
||||
const [enterpriseInfo, presetData] = await Promise.all([
|
||||
const [enterpriseInfo, presetData, accounts] = await Promise.all([
|
||||
getEnterpriseInfoApi(),
|
||||
getPresetDataApi()
|
||||
getPresetDataApi(),
|
||||
listDigitalAccountsApi()
|
||||
])
|
||||
digitalAccounts.value = accounts
|
||||
const defaultAccount =
|
||||
accounts.find((item) => item.id === currentUser.value?.digitalAccountId) || accounts[0]
|
||||
if (defaultAccount) {
|
||||
form.digitalAccountId = defaultAccount.id
|
||||
applySelectedDigitalAccount()
|
||||
} else if (enterpriseInfo?.taxpayerNum) {
|
||||
form.taxpayerNum = enterpriseInfo.taxpayerNum
|
||||
}
|
||||
if (enterpriseInfo?.enterpriseAddress) {
|
||||
form.sellerAddress = enterpriseInfo.enterpriseAddress
|
||||
}
|
||||
@@ -1650,7 +1686,7 @@ watch(
|
||||
|
||||
/** 重置表单到初始状态 */
|
||||
function resetForm() {
|
||||
form.taxpayerNum = currentUser.value?.taxpayerNum || ''
|
||||
form.taxpayerNum = ''
|
||||
form.invoiceReqSerialNo = ''
|
||||
form.buyerName = ''
|
||||
form.buyerTaxpayerNum = ''
|
||||
@@ -1670,7 +1706,7 @@ function resetForm() {
|
||||
form.showSellerBank = '0'
|
||||
form.showBuyerAddrTel = '0'
|
||||
form.showSellerAddrTel = '0'
|
||||
form.account = currentUser.value?.account || ''
|
||||
form.account = ''
|
||||
form.variableLevyFlag = ''
|
||||
form.casherName = ''
|
||||
form.reviewerName = ''
|
||||
@@ -1687,6 +1723,7 @@ function resetForm() {
|
||||
form.variableLevyProofList = []
|
||||
form.orderList = []
|
||||
activeSidebar.value = 'basic'
|
||||
applySelectedDigitalAccount()
|
||||
generateSerialNo()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<section class="page-card">
|
||||
<div class="page-toolbar">
|
||||
<h2 class="page-toolbar-title">开票设置</h2>
|
||||
<div class="page-toolbar-actions">
|
||||
<n-button :loading="bankLoading" @click="openBankPicker">快捷选择银行</n-button>
|
||||
<n-button type="primary" :loading="saving" @click="save">保存</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<n-form :model="form" label-placement="top" class="form">
|
||||
<n-grid responsive="screen" :cols="2" :x-gap="16">
|
||||
<n-gi>
|
||||
<n-form-item label="开户行">
|
||||
<n-input v-model:value="form.bankName" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="银行账号">
|
||||
<n-input v-model:value="form.bankAccount" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="销售方地址">
|
||||
<n-input v-model:value="form.address" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="销售方电话">
|
||||
<n-input v-model:value="form.phone" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<n-modal
|
||||
v-model:show="bankModalVisible"
|
||||
preset="card"
|
||||
title="快捷选择银行"
|
||||
:style="{ width: '520px', maxWidth: '92vw' }"
|
||||
:mask-closable="false"
|
||||
>
|
||||
<n-space vertical>
|
||||
<n-select
|
||||
v-model:value="selectedBankKey"
|
||||
:options="bankOptions"
|
||||
:loading="bankLoading"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择开户行及账号"
|
||||
/>
|
||||
<div class="modal-actions">
|
||||
<n-button @click="bankModalVisible = false">取消</n-button>
|
||||
<n-button type="primary" :disabled="!selectedBankKey" :loading="saving" @click="confirmBank">
|
||||
确定
|
||||
</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { NButton, NForm, NFormItem, NGi, NGrid, NInput, NModal, NSelect, NSpace, useMessage } from 'naive-ui'
|
||||
import {
|
||||
getPresetDataApi,
|
||||
queryEnterpriseBankAccountsApi,
|
||||
updatePresetDataApi
|
||||
} from '@/api/piaotong'
|
||||
import type { EnterpriseBankAccount } from '@/api/piaotong'
|
||||
|
||||
const message = useMessage()
|
||||
const saving = ref(false)
|
||||
const bankLoading = ref(false)
|
||||
const bankModalVisible = ref(false)
|
||||
const selectedBankKey = ref<string | null>(null)
|
||||
const bankAccounts = ref<EnterpriseBankAccount[]>([])
|
||||
const form = reactive({ bankName: '', bankAccount: '', address: '', phone: '' })
|
||||
|
||||
const bankOptions = computed(() =>
|
||||
bankAccounts.value.map((item, index) => ({
|
||||
label: `${item.bankName}${item.bankAccount ? ` / ${item.bankAccount}` : ''}`,
|
||||
value: String(index)
|
||||
}))
|
||||
)
|
||||
|
||||
async function load() {
|
||||
Object.assign(form, await getPresetDataApi())
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
await updatePresetDataApi({ ...form })
|
||||
message.success('开票设置已保存')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openBankPicker() {
|
||||
bankModalVisible.value = true
|
||||
bankLoading.value = true
|
||||
try {
|
||||
bankAccounts.value = await queryEnterpriseBankAccountsApi()
|
||||
selectedBankKey.value = bankAccounts.value.length === 1 ? '0' : null
|
||||
if (bankAccounts.value.length === 0) {
|
||||
message.info('未查询到可选择的开户行及账号')
|
||||
}
|
||||
} finally {
|
||||
bankLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmBank() {
|
||||
if (selectedBankKey.value === null) return
|
||||
const selected = bankAccounts.value[Number(selectedBankKey.value)]
|
||||
if (!selected) return
|
||||
form.bankName = selected.bankName
|
||||
form.bankAccount = selected.bankAccount ?? ''
|
||||
await save()
|
||||
bankModalVisible.value = false
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form {
|
||||
max-width: 880px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<section class="page-card table-fill">
|
||||
<div class="page-toolbar">
|
||||
<h2 class="page-toolbar-title">OpenAPI</h2>
|
||||
<div class="page-toolbar-actions">
|
||||
<n-button :loading="loading" @click="load">
|
||||
<template #icon><n-icon :component="RefreshCw" /></template>
|
||||
刷新
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data="rows"
|
||||
:loading="loading"
|
||||
:pagination="{ pageSize: 12 }"
|
||||
:row-key="(row: OpenApiStatisticsItem) => `${row.digitalAccountId}-${row.interfaceCode}`"
|
||||
:scroll-x="1040"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, ref } from 'vue'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NButton, NDataTable, NIcon, NTag } from 'naive-ui'
|
||||
import { RefreshCw } from 'lucide-vue-next'
|
||||
import { openApiStatisticsApi } from '@/api/piaotong'
|
||||
import type { OpenApiStatisticsItem } from '@/api/piaotong'
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref<OpenApiStatisticsItem[]>([])
|
||||
|
||||
const columns: DataTableColumns<OpenApiStatisticsItem> = [
|
||||
{ title: '数电账号ID', key: 'digitalAccountId', minWidth: 220 },
|
||||
{ title: '接口', key: 'interfaceCode', minWidth: 180 },
|
||||
{ title: '调用次数', key: 'total', width: 100 },
|
||||
{ title: '成功', key: 'success', width: 100, render: (row) => h(NTag, { type: 'success' }, { default: () => row.success }) },
|
||||
{ title: '失败', key: 'failed', width: 100, render: (row) => h(NTag, { type: row.failed > 0 ? 'error' : 'default' }, { default: () => row.failed }) },
|
||||
{ title: '平均耗时(ms)', key: 'avgCostMs', width: 140 },
|
||||
{ title: '最近调用', key: 'lastCalledAt', minWidth: 180 }
|
||||
]
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
rows.value = await openApiStatisticsApi()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
@@ -8,6 +8,7 @@
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
:columns="typeColumns"
|
||||
:data="typeRows"
|
||||
:pagination="typePagination"
|
||||
@@ -26,6 +27,7 @@
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
:columns="itemColumns"
|
||||
:data="itemRows"
|
||||
:pagination="itemPagination"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
:columns="columns"
|
||||
:data="rows"
|
||||
:pagination="pagination"
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
remote
|
||||
size="small"
|
||||
:columns="columns"
|
||||
@@ -166,9 +167,6 @@
|
||||
<n-form-item label="邮箱" path="email">
|
||||
<n-input v-model:value="editForm.email" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="editModal.mode === 'edit'" label="API Key">
|
||||
<n-input v-model:value="editForm.apiKey" readonly />
|
||||
</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>
|
||||
@@ -340,8 +338,7 @@ const baseDetailItems = computed(() => {
|
||||
{ label: '邮箱', value: displayValue(user.email) },
|
||||
{ label: '组织', value: formatOrg(user) },
|
||||
{ label: '状态', value: user.statusLabel || statusLabel(user.status) },
|
||||
{ label: '头像', value: displayValue(user.avatar) },
|
||||
{ label: 'API Key', value: displayValue(user.apiKey) }
|
||||
{ label: '头像', value: displayValue(user.avatar) }
|
||||
]
|
||||
})
|
||||
|
||||
@@ -405,7 +402,6 @@ const editForm = reactive({
|
||||
realName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
apiKey: '',
|
||||
status: 'ENABLED'
|
||||
})
|
||||
|
||||
@@ -526,7 +522,6 @@ function resetEditForm() {
|
||||
editForm.realName = ''
|
||||
editForm.phone = ''
|
||||
editForm.email = ''
|
||||
editForm.apiKey = ''
|
||||
editForm.status = 'ENABLED'
|
||||
}
|
||||
|
||||
@@ -572,7 +567,6 @@ async function openEdit(row: UserListItem) {
|
||||
editForm.realName = detail.realName ?? ''
|
||||
editForm.phone = detail.phone ?? ''
|
||||
editForm.email = detail.email ?? ''
|
||||
editForm.apiKey = detail.apiKey ?? ''
|
||||
editForm.status = detail.status
|
||||
editModal.visible = true
|
||||
}
|
||||
@@ -639,13 +633,6 @@ const columns = computed<DataTableColumns<UserListItem>>(() => [
|
||||
minWidth: 180,
|
||||
render: (row) => (row.roleCodes.length > 0 ? row.roleCodes.join(', ') : '-')
|
||||
},
|
||||
{
|
||||
title: 'API Key',
|
||||
key: 'apiKey',
|
||||
minWidth: 220,
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => row.apiKey || '-'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
|
||||
@@ -275,11 +275,20 @@ async function handleLogout() {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.layout-sider :deep(.n-layout-sider-scroll-container) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-sider-hidden {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -321,9 +330,11 @@ async function handleLogout() {
|
||||
}
|
||||
|
||||
.menu-wrap {
|
||||
height: calc(100% - 64px);
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 10px 8px 14px;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
+15
-2
@@ -1,8 +1,14 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { loginApi, logoutApi, meApi } from '@/api/auth'
|
||||
import { loginApi, logoutApi, meApi, registerEnterpriseApi } from '@/api/auth'
|
||||
import { listDictItemsApi } from '@/api/system/dict'
|
||||
import type { CurrentUserProfile, LoginRequest, MeResponse, MenuNode } from '@/types/auth'
|
||||
import type {
|
||||
CurrentUserProfile,
|
||||
EnterpriseRegisterRequest,
|
||||
LoginRequest,
|
||||
MeResponse,
|
||||
MenuNode
|
||||
} from '@/types/auth'
|
||||
import type { DictItem } from '@/types/system/dict'
|
||||
|
||||
const TOKEN_KEY = 'platform.token'
|
||||
@@ -59,6 +65,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
await loadProfile()
|
||||
}
|
||||
|
||||
async function registerEnterprise(payload: EnterpriseRegisterRequest) {
|
||||
const result = await registerEnterpriseApi(payload)
|
||||
setToken(result.accessToken)
|
||||
await loadProfile()
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
if (token.value) {
|
||||
@@ -168,6 +180,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
setToken,
|
||||
clearAuth,
|
||||
login,
|
||||
registerEnterprise,
|
||||
logout,
|
||||
loadProfile,
|
||||
hasPermission,
|
||||
|
||||
@@ -100,6 +100,22 @@ select {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.page-toolbar-title {
|
||||
margin: 0;
|
||||
color: #111827;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.page-toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-form {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -220,4 +236,9 @@ select {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.page-toolbar-actions {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
+18
-4
@@ -9,21 +9,35 @@ export interface LoginResponse {
|
||||
expiresIn: number
|
||||
}
|
||||
|
||||
export interface EnterpriseRegisterRequest {
|
||||
taxpayerNum: string
|
||||
enterpriseName: string
|
||||
legalPersonName?: string | null
|
||||
contactsName?: string | null
|
||||
contactsEmail?: string | null
|
||||
contactsPhone: string
|
||||
regionCode: string
|
||||
cityName?: string
|
||||
enterpriseAddress?: string | null
|
||||
taxRegistrationCertificate?: string | null
|
||||
password: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
export interface CurrentUserProfile {
|
||||
id: string
|
||||
username: string
|
||||
nickname?: string | null
|
||||
realName?: string | null
|
||||
orgId?: string | null
|
||||
enterpriseId?: string | null
|
||||
digitalAccountId?: string | null
|
||||
userType: 'SYSTEM' | 'ENTERPRISE_ADMIN' | 'DIGITAL_OPERATOR' | string
|
||||
status: string
|
||||
avatar?: string | null
|
||||
email?: string | null
|
||||
phone?: string | null
|
||||
createdAt?: string | null
|
||||
taxpayerNum?: string | null
|
||||
account?: string | null
|
||||
taxPassword?: string | null
|
||||
taxIdentityType?: string | null
|
||||
}
|
||||
|
||||
export interface MenuNode {
|
||||
|
||||
@@ -9,7 +9,6 @@ export interface UserListItem {
|
||||
status: string
|
||||
statusLabel?: string
|
||||
roleCodes: string[]
|
||||
apiKey?: string | null
|
||||
}
|
||||
|
||||
export interface UserDetail {
|
||||
@@ -27,7 +26,6 @@ export interface UserDetail {
|
||||
statusLabel?: string
|
||||
roleIds: string[]
|
||||
roles: UserRoleBrief[]
|
||||
apiKey?: string | null
|
||||
tokenVersion: number
|
||||
lastLoginAt?: string | null
|
||||
lastLoginIp?: string | null
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<template #header>
|
||||
<div class="login-card-header">
|
||||
<div class="login-card-title">{{ appTitle }}</div>
|
||||
<div class="login-card-subtitle">登录后继续使用</div>
|
||||
<div class="login-card-subtitle">
|
||||
{{ isRegister ? '注册企业后直接进入系统' : '登录后继续使用' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,13 +14,20 @@
|
||||
未登录或登录已失效,请重新登录。
|
||||
</n-alert>
|
||||
|
||||
<n-form ref="formRef" :model="form" :rules="rules" size="large" @submit.prevent="onSubmit">
|
||||
<n-form
|
||||
v-if="!isRegister"
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
size="large"
|
||||
@submit.prevent="onLogin"
|
||||
>
|
||||
<n-form-item path="username" label="用户名">
|
||||
<n-input v-model:value="form.username" placeholder="请输入用户名" />
|
||||
<n-input v-model:value="loginForm.username" placeholder="请输入用户名" />
|
||||
</n-form-item>
|
||||
<n-form-item path="password" label="密码">
|
||||
<n-input
|
||||
v-model:value="form.password"
|
||||
v-model:value="loginForm.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
placeholder="请输入密码"
|
||||
@@ -28,6 +37,113 @@
|
||||
登录
|
||||
</n-button>
|
||||
</n-form>
|
||||
|
||||
<n-form
|
||||
v-else
|
||||
ref="registerFormRef"
|
||||
:model="registerForm"
|
||||
:rules="registerRules"
|
||||
size="medium"
|
||||
label-placement="top"
|
||||
@submit.prevent="onRegister"
|
||||
>
|
||||
<n-grid responsive="screen" :cols="2" :x-gap="12">
|
||||
<n-gi>
|
||||
<n-form-item path="taxpayerNum" label="纳税人识别号">
|
||||
<n-input v-model:value="registerForm.taxpayerNum" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="enterpriseName" label="企业名称">
|
||||
<n-input v-model:value="registerForm.enterpriseName" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="legalPersonName" label="法人姓名">
|
||||
<n-input v-model:value="registerForm.legalPersonName" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="contactsName" label="联系人">
|
||||
<n-input v-model:value="registerForm.contactsName" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="contactsPhone" label="联系人手机">
|
||||
<n-input v-model:value="registerForm.contactsPhone" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="contactsEmail" label="联系邮箱">
|
||||
<n-input v-model:value="registerForm.contactsEmail" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="regionCode" label="省份">
|
||||
<n-select
|
||||
v-model:value="registerForm.regionCode"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择省份"
|
||||
:options="regionCodeOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="cityName" label="城市">
|
||||
<n-input v-model:value="registerForm.cityName" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-form-item path="enterpriseAddress" label="企业地址">
|
||||
<n-input v-model:value="registerForm.enterpriseAddress" clearable />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="taxRegistrationCertificate" label="企业注册证书">
|
||||
<div class="cert-upload">
|
||||
<div class="cert-actions">
|
||||
<n-button secondary @click="triggerCertInput">选择图片</n-button>
|
||||
<span class="cert-state">
|
||||
{{ registerForm.taxRegistrationCertificate ? '已选择图片' : '未选择,可不上传' }}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
ref="certInputRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden-input"
|
||||
@change="onCertFileChange"
|
||||
/>
|
||||
<img v-if="certPreview" class="cert-preview" :src="certPreview" alt="企业注册证书预览" />
|
||||
</div>
|
||||
</n-form-item>
|
||||
|
||||
<n-grid responsive="screen" :cols="2" :x-gap="12">
|
||||
<n-gi>
|
||||
<n-form-item path="password" label="登录密码">
|
||||
<n-input v-model:value="registerForm.password" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="confirmPassword" label="确认密码">
|
||||
<n-input
|
||||
v-model:value="registerForm.confirmPassword"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-button type="primary" block size="large" :loading="loading" attr-type="submit">
|
||||
注册并登录
|
||||
</n-button>
|
||||
</n-form>
|
||||
|
||||
<n-button text block class="switch-btn" @click="toggleMode">
|
||||
{{ isRegister ? '已有账号,返回登录' : '注册企业' }}
|
||||
</n-button>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -35,7 +151,18 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { FormInst, FormRules } from 'naive-ui'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
NCard,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NGi,
|
||||
NGrid,
|
||||
NInput,
|
||||
NSelect,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { appEnv } from '@/config/env'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -47,25 +174,142 @@ const authStore = useAuthStore()
|
||||
const appTitle = appEnv.appTitle
|
||||
|
||||
const loading = ref(false)
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
const isRegister = ref(false)
|
||||
const loginFormRef = ref<FormInst | null>(null)
|
||||
const registerFormRef = ref<FormInst | null>(null)
|
||||
const certInputRef = ref<HTMLInputElement | null>(null)
|
||||
const certPreview = ref('')
|
||||
|
||||
const loginForm = reactive({ username: '', password: '' })
|
||||
const registerForm = reactive({
|
||||
taxpayerNum: '',
|
||||
enterpriseName: '',
|
||||
legalPersonName: '',
|
||||
contactsName: '',
|
||||
contactsEmail: '',
|
||||
contactsPhone: '',
|
||||
regionCode: '',
|
||||
cityName: '',
|
||||
enterpriseAddress: '',
|
||||
taxRegistrationCertificate: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
const loginRules: FormRules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: ['blur', 'input'] }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: ['blur', 'input'] }]
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
await formRef.value?.validate()
|
||||
const registerRules: FormRules = {
|
||||
taxpayerNum: [{ required: true, message: '请输入纳税人识别号', trigger: ['blur', 'input'] }],
|
||||
enterpriseName: [{ required: true, message: '请输入企业名称', trigger: ['blur', 'input'] }],
|
||||
contactsPhone: [{ required: true, message: '请输入联系人手机', trigger: ['blur', 'input'] }],
|
||||
regionCode: [{ required: true, message: '请选择省份', trigger: ['blur', 'change'] }],
|
||||
password: [{ required: true, message: '请输入登录密码', trigger: ['blur', 'input'] }],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认登录密码', trigger: ['blur', 'input'] },
|
||||
{
|
||||
validator: () => registerForm.password === registerForm.confirmPassword,
|
||||
message: '两次输入的密码不一致',
|
||||
trigger: ['blur', 'input']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const regionCodeOptions = [
|
||||
{ label: '北京市', value: '11' },
|
||||
{ label: '上海市', value: '31' },
|
||||
{ label: '天津市', value: '12' },
|
||||
{ label: '河北省', value: '13' },
|
||||
{ label: '山西省', value: '14' },
|
||||
{ label: '内蒙古自治区', value: '15' },
|
||||
{ label: '辽宁省', value: '21' },
|
||||
{ label: '吉林省', value: '22' },
|
||||
{ label: '黑龙江省', value: '23' },
|
||||
{ label: '江苏省', value: '32' },
|
||||
{ label: '浙江省', value: '33' },
|
||||
{ label: '安徽省', value: '34' },
|
||||
{ label: '福建省', value: '35' },
|
||||
{ label: '江西省', value: '36' },
|
||||
{ label: '山东省', value: '37' },
|
||||
{ label: '河南省', value: '41' },
|
||||
{ label: '湖北省', value: '42' },
|
||||
{ label: '湖南省', value: '43' },
|
||||
{ label: '广东省', value: '44' },
|
||||
{ label: '广西壮族自治区', value: '45' },
|
||||
{ label: '海南省', value: '46' },
|
||||
{ label: '重庆市', value: '50' },
|
||||
{ label: '四川省', value: '51' },
|
||||
{ label: '贵州省', value: '52' },
|
||||
{ label: '云南省', value: '53' },
|
||||
{ label: '西藏自治区', value: '54' },
|
||||
{ label: '陕西省', value: '61' },
|
||||
{ label: '甘肃省', value: '62' },
|
||||
{ label: '青海省', value: '63' },
|
||||
{ label: '宁夏回族自治区', value: '64' },
|
||||
{ label: '新疆维吾尔自治区', value: '65' }
|
||||
]
|
||||
|
||||
function toggleMode() {
|
||||
isRegister.value = !isRegister.value
|
||||
}
|
||||
|
||||
function triggerCertInput() {
|
||||
certInputRef.value?.click()
|
||||
}
|
||||
|
||||
function onCertFileChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const base64 = String(reader.result || '')
|
||||
registerForm.taxRegistrationCertificate = base64
|
||||
certPreview.value = base64
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
target.value = ''
|
||||
}
|
||||
|
||||
async function enterSystem() {
|
||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/dashboard'
|
||||
await router.replace(redirect)
|
||||
}
|
||||
|
||||
async function onLogin() {
|
||||
await loginFormRef.value?.validate()
|
||||
loading.value = true
|
||||
try {
|
||||
await authStore.login(form)
|
||||
await authStore.login(loginForm)
|
||||
message.success('登录成功')
|
||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/dashboard'
|
||||
await router.replace(redirect)
|
||||
await enterSystem()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onRegister() {
|
||||
await registerFormRef.value?.validate()
|
||||
loading.value = true
|
||||
try {
|
||||
await authStore.registerEnterprise({
|
||||
taxpayerNum: registerForm.taxpayerNum.trim(),
|
||||
enterpriseName: registerForm.enterpriseName.trim(),
|
||||
legalPersonName: registerForm.legalPersonName.trim() || null,
|
||||
contactsName: registerForm.contactsName.trim() || null,
|
||||
contactsEmail: registerForm.contactsEmail.trim() || null,
|
||||
contactsPhone: registerForm.contactsPhone.trim(),
|
||||
regionCode: registerForm.regionCode,
|
||||
cityName: registerForm.cityName.trim(),
|
||||
enterpriseAddress: registerForm.enterpriseAddress.trim() || null,
|
||||
taxRegistrationCertificate: registerForm.taxRegistrationCertificate || null,
|
||||
password: registerForm.password,
|
||||
confirmPassword: registerForm.confirmPassword
|
||||
})
|
||||
message.success('注册成功')
|
||||
await enterSystem()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -74,21 +318,25 @@ async function onSubmit() {
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
padding: 32px 24px;
|
||||
overflow-y: auto;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
border-radius: 16px;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.login-card :deep(.n-card__content) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.login-card-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -109,4 +357,52 @@ async function onSubmit() {
|
||||
.login-alert {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.cert-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cert-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cert-state {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.hidden-input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cert-preview {
|
||||
display: block;
|
||||
max-width: 220px;
|
||||
max-height: 132px;
|
||||
margin-top: 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.login-page {
|
||||
padding: 18px 14px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user