2026-01-05 17:42:50 +08:00

779 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="invoice-page">
<!-- 头部区域 -->
<div class="page-header">
<h1 class="page-title">
我的发票抬头
</h1>
<div class="header-actions">
<a-button type="primary" @click="showAddModal" size="large">
<template #icon><PlusOutlined /></template>
新增抬头
</a-button>
</div>
</div>
<!-- 发票抬头列表 -->
<div class="invoice-list-container">
<a-alert
v-if="invoiceHeaders.length === 0"
message="暂无发票抬头"
description="点击上方按钮添加您的第一个发票抬头"
type="info"
show-icon
style="margin-bottom: 20px;"
/>
<a-row :gutter="[16, 16]">
<a-col
v-for="header in sortedHeaders"
:key="header.id"
:xs="24"
:sm="24"
:md="12"
:lg="8"
>
<a-card
:class="['invoice-card', { 'default-card': header.isDefault }]"
:bordered="true"
>
<template #actions>
<span @click="editHeader(header)">
<EditOutlined />
编辑
</span>
<span
@click="setDefaultHeader(header.id)"
:class="{ disabled: header.isDefault }"
>
<StarOutlined />
{{ header.isDefault ? '已是默认' : '设为默认' }}
</span>
<span @click="showDeleteConfirm(header)">
<DeleteOutlined />
删除
</span>
</template>
<a-card-meta>
<template #title>
<div class="card-title">
<span class="header-name">{{ header.name }}</span>
<a-tag
:color="getTypeColor(header.type)"
size="small"
>
{{ getTypeText(header.type) }}
</a-tag>
<a-tag
:color="getInvoiceTypeColor(header.invoiceType)"
size="small"
>
{{ getInvoiceTypeText(header.invoiceType) }}
</a-tag>
</div>
</template>
<template #description>
<div class="card-content">
<!-- 默认标识 -->
<div v-if="header.isDefault" class="default-badge">
<CheckCircleFilled style="color: #52c41a; margin-right: 4px;" />
<span>默认抬头</span>
</div>
<!-- 详细信息 -->
<div class="info-section">
<div v-if="header.taxNumber" class="info-row">
<span class="info-label">税号</span>
<span class="info-value">{{ header.taxNumber }}</span>
</div>
<div v-if="header.bankName" class="info-row">
<span class="info-label">开户银行</span>
<span class="info-value">{{ header.bankName }}</span>
</div>
<div v-if="header.bankAccount" class="info-row">
<span class="info-label">银行账号</span>
<span class="info-value">{{ header.bankAccount }}</span>
</div>
<div v-if="header.companyAddress" class="info-row">
<span class="info-label">企业地址</span>
<span class="info-value">{{ header.companyAddress }}</span>
</div>
<div v-if="header.companyPhone" class="info-row">
<span class="info-label">企业电话</span>
<span class="info-value">{{ header.companyPhone }}</span>
</div>
<div class="info-row create-time">
<span class="info-label">创建时间</span>
<span class="info-value">{{ header.createTime }}</span>
</div>
</div>
</div>
</template>
</a-card-meta>
</a-card>
</a-col>
</a-row>
</div>
<!-- 新增/编辑弹窗 -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
width="600px"
:maskClosable="false"
:keyboard="false"
@ok="handleSubmit"
@cancel="handleCancel"
:confirm-loading="confirmLoading"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 发票抬头名称 -->
<a-form-item label="发票抬头" name="name" required>
<a-input
v-model:value="formData.name"
placeholder="请输入发票抬头名称"
:maxlength="100"
show-count
/>
</a-form-item>
<!-- 抬头类型 -->
<a-form-item label="抬头类型" name="type" required>
<a-radio-group
v-model:value="formData.type"
button-style="solid"
@change="handleTypeChange"
>
<a-radio-button value="personal">个人</a-radio-button>
<a-radio-button value="enterprise">企业</a-radio-button>
<a-radio-button value="institution">事业单位</a-radio-button>
</a-radio-group>
</a-form-item>
<!-- 发票类型 -->
<a-form-item label="发票类型" name="invoiceType" required>
<a-select
v-model:value="formData.invoiceType"
placeholder="请选择发票类型"
>
<a-select-option value="vat">增值税专用发票</a-select-option>
<a-select-option value="normal">增值税普通发票</a-select-option>
<a-select-option value="electronic">电子普通发票</a-select-option>
<a-select-option value="special">专用发票</a-select-option>
<a-select-option value="general">通用发票</a-select-option>
</a-select>
</a-form-item>
<!-- 税号非个人时显示 -->
<a-form-item
v-if="formData.type !== 'personal'"
label="税号"
name="taxNumber"
required
>
<a-input
v-model:value="formData.taxNumber"
placeholder="请输入纳税人识别号/统一社会信用代码"
:maxlength="30"
/>
</a-form-item>
<!-- 开户银行非个人时显示 -->
<a-form-item
v-if="formData.type !== 'personal'"
label="开户银行"
name="bankName"
>
<a-input
v-model:value="formData.bankName"
placeholder="请输入开户银行名称"
:maxlength="100"
/>
</a-form-item>
<!-- 银行账号非个人时显示 -->
<a-form-item
v-if="formData.type !== 'personal'"
label="银行账号"
name="bankAccount"
>
<a-input
v-model:value="formData.bankAccount"
placeholder="请输入银行账号"
:maxlength="30"
/>
</a-form-item>
<!-- 企业地址非个人时显示 -->
<a-form-item
v-if="formData.type !== 'personal'"
label="企业地址"
name="companyAddress"
>
<a-input
v-model:value="formData.companyAddress"
placeholder="请输入企业注册地址"
:maxlength="200"
/>
</a-form-item>
<!-- 企业电话非个人时显示 -->
<a-form-item
v-if="formData.type !== 'personal'"
label="企业电话"
name="companyPhone"
>
<a-input
v-model:value="formData.companyPhone"
placeholder="请输入企业联系电话"
:maxlength="20"
/>
</a-form-item>
<!-- 设为默认 -->
<a-form-item name="isDefault" :wrapper-col="{ offset: 6 }">
<a-checkbox v-model:checked="formData.isDefault">
设为默认抬头
</a-checkbox>
<div class="form-tip">设为默认后下单时将自动选择此抬头</div>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
StarOutlined,
FileTextOutlined,
CheckCircleFilled,
ExclamationCircleOutlined
} from '@ant-design/icons-vue'
// 初始数据
const initialData = [
{
id: 1,
name: '张三科技有限公司',
type: 'enterprise',
invoiceType: 'vat',
taxNumber: '91370100MA3C4BQ123',
bankName: '中国工商银行北京分行',
bankAccount: '6222021234567890123',
companyAddress: '北京市海淀区中关村软件园一期1号楼',
companyPhone: '010-12345678',
isDefault: true,
createTime: '2024-01-15 10:30:45'
},
{
id: 2,
name: '李四个人',
type: 'personal',
invoiceType: 'normal',
taxNumber: '',
bankName: '',
bankAccount: '',
companyAddress: '',
companyPhone: '',
isDefault: false,
createTime: '2024-01-10 14:20:15'
},
{
id: 3,
name: '某某研究所',
type: 'institution',
invoiceType: 'electronic',
taxNumber: '123456789012345',
bankName: '中国建设银行上海分行',
bankAccount: '6217001234567890123',
companyAddress: '上海市浦东新区张江高科技园区',
companyPhone: '021-87654321',
isDefault: false,
createTime: '2024-01-05 09:15:30'
},
{
id: 4,
name: '王五网络有限公司',
type: 'enterprise',
invoiceType: 'vat',
taxNumber: '91370200MA3D4CQ456',
bankName: '招商银行深圳分行',
bankAccount: '6225881234567890',
companyAddress: '深圳市南山区科技园',
companyPhone: '0755-66889900',
isDefault: false,
createTime: '2024-01-20 16:45:22'
}
]
// 响应式数据
const invoiceHeaders = ref([...initialData])
const modalVisible = ref(false)
const modalMode = ref('add') // 'add' | 'edit'
const confirmLoading = ref(false)
const formRef = ref()
// 表单数据
const formData = reactive({
id: null,
name: '',
type: 'personal',
invoiceType: 'normal',
taxNumber: '',
bankName: '',
bankAccount: '',
companyAddress: '',
companyPhone: '',
isDefault: false
})
// 表单验证规则
const formRules = {
name: [
{ required: true, message: '请输入发票抬头名称', trigger: 'blur' },
{ min: 2, max: 100, message: '抬头名称长度为2-100个字符', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择抬头类型', trigger: 'change' }
],
invoiceType: [
{ required: true, message: '请选择发票类型', trigger: 'change' }
],
taxNumber: [
{
required: formData.type !== 'personal',
message: '请输入税号',
trigger: 'blur'
},
{
pattern: /^[A-Z0-9]{15,20}$/,
message: '税号格式不正确15-20位字母或数字',
trigger: 'blur'
}
],
bankAccount: [
{
pattern: /^[0-9]{12,20}$/,
message: '银行账号格式不正确12-20位数字',
trigger: 'blur'
}
],
companyPhone: [
{
pattern: /^(\d{3,4}-)?\d{7,8}$/,
message: '电话号码格式不正确010-12345678',
trigger: 'blur'
}
]
}
// 计算属性
const modalTitle = computed(() => {
return modalMode.value === 'add' ? '新增发票抬头' : '编辑发票抬头'
})
const sortedHeaders = computed(() => {
return [...invoiceHeaders.value].sort((a, b) => {
// 默认的排在最前面
if (a.isDefault && !b.isDefault) return -1
if (!a.isDefault && b.isDefault) return 1
// 其次按创建时间倒序
return new Date(b.createTime) - new Date(a.createTime)
})
})
// 方法
const showAddModal = () => {
modalMode.value = 'add'
resetForm()
modalVisible.value = true
}
const editHeader = (header) => {
modalMode.value = 'edit'
Object.assign(formData, { ...header })
modalVisible.value = true
}
const resetForm = () => {
Object.assign(formData, {
id: null,
name: '',
type: 'personal',
invoiceType: 'normal',
taxNumber: '',
bankName: '',
bankAccount: '',
companyAddress: '',
companyPhone: '',
isDefault: false
})
if (formRef.value) {
formRef.value.clearValidate()
}
}
const handleTypeChange = () => {
// 类型切换时,如果是个人类型,清除非必填字段
if (formData.type === 'personal') {
formData.taxNumber = ''
formData.bankName = ''
formData.bankAccount = ''
formData.companyAddress = ''
formData.companyPhone = ''
}
}
const handleSubmit = () => {
formRef.value.validate().then(() => {
confirmLoading.value = true
// 模拟API调用延迟
setTimeout(() => {
if (modalMode.value === 'add') {
addNewHeader()
} else {
updateHeader()
}
confirmLoading.value = false
modalVisible.value = false
}, 500)
}).catch(error => {
console.log('表单验证失败:', error)
})
}
const addNewHeader = () => {
const newHeader = {
...formData,
id: Date.now(), // 生成唯一ID
createTime: new Date().toLocaleString('zh-CN')
}
// 如果设置为默认,清除其他默认设置
if (newHeader.isDefault) {
invoiceHeaders.value.forEach(header => {
header.isDefault = false
})
}
invoiceHeaders.value.unshift(newHeader)
message.success('新增发票抬头成功')
}
const updateHeader = () => {
const index = invoiceHeaders.value.findIndex(item => item.id === formData.id)
if (index !== -1) {
const oldHeader = invoiceHeaders.value[index]
// 如果设置为默认,清除其他默认设置
if (formData.isDefault && !oldHeader.isDefault) {
invoiceHeaders.value.forEach(header => {
header.isDefault = false
})
}
// 更新数据
Object.assign(invoiceHeaders.value[index], formData, {
createTime: oldHeader.createTime // 保留创建时间
})
message.success('修改发票抬头成功')
}
}
const setDefaultHeader = (id) => {
invoiceHeaders.value.forEach(header => {
header.isDefault = header.id === id
})
message.success('设置默认抬头成功')
}
const showDeleteConfirm = (header) => {
Modal.confirm({
title: '确认删除',
icon: ExclamationCircleOutlined, // 这里直接传递图标组件
content: `确定要删除发票抬头"${header.name}"吗?此操作不可恢复。`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk() {
deleteHeader(header.id)
}
})
}
const deleteHeader = (id) => {
const index = invoiceHeaders.value.findIndex(item => item.id === id)
if (index !== -1) {
const isDefault = invoiceHeaders.value[index].isDefault
invoiceHeaders.value.splice(index, 1)
// 如果删除的是默认抬头,且还有其他抬头,设置第一个为默认
if (isDefault && invoiceHeaders.value.length > 0) {
invoiceHeaders.value[0].isDefault = true
message.success('已自动设置第一个抬头为默认')
}
message.success('删除发票抬头成功')
}
}
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
// 辅助函数
const getTypeText = (type) => {
const typeMap = {
personal: '个人',
enterprise: '企业',
institution: '事业单位'
}
return typeMap[type] || '未知'
}
const getTypeColor = (type) => {
const colorMap = {
personal: 'blue',
enterprise: 'green',
institution: 'orange'
}
return colorMap[type] || 'default'
}
const getInvoiceTypeText = (type) => {
const typeMap = {
vat: '增值税专用',
normal: '增值税普通',
electronic: '电子普通',
special: '专用发票',
general: '通用发票'
}
return typeMap[type] || '未知'
}
const getInvoiceTypeColor = (type) => {
const colorMap = {
vat: 'red',
normal: 'purple',
electronic: 'cyan',
special: 'magenta',
general: 'gray'
}
return colorMap[type] || 'default'
}
// 初始化
onMounted(() => {
console.log('发票抬头管理页面已加载')
})
</script>
<style scoped>
.invoice-page {
/* padding: 10px; */
/* background-color: #f5f5f5; */
min-height: 100vh;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.page-title {
margin: 0;
/* color: #1890ff; */
font-size: 24px;
display: flex;
align-items: center;
gap: 12px;
}
.page-title .anticon {
font-size: 28px;
}
.header-actions {
display: flex;
gap: 12px;
}
.invoice-list-container {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.invoice-card {
height: 100%;
transition: all 0.3s ease;
border: 1px solid #e8e8e8;
border-radius: 8px;
}
.invoice-card:hover {
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
border-color: #1890ff;
transform: translateY(-2px);
}
.invoice-card.default-card {
border: 2px solid #52c41a;
background: linear-gradient(135deg, #f6ffed 0%, #e6ffd1 100%);
}
.card-title {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.header-name {
font-size: 16px;
font-weight: 600;
color: #333;
line-height: 1.4;
}
.card-content {
margin-top: 12px;
}
.default-badge {
display: inline-flex;
align-items: center;
padding: 4px 8px;
background: #f6ffed;
border: 1px solid #b7eb8f;
border-radius: 4px;
font-size: 12px;
color: #52c41a;
margin-bottom: 12px;
}
.info-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-row {
display: flex;
font-size: 13px;
line-height: 1.5;
word-break: break-all;
}
.info-label {
color: #666;
min-width: 70px;
flex-shrink: 0;
}
.info-value {
color: #333;
flex: 1;
}
.create-time {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #f0f0f0;
color: #999;
font-size: 12px;
}
:deep(.ant-card-actions) {
border-top: 1px solid #f0f0f0;
background: #fafafa;
}
:deep(.ant-card-actions > li) {
margin: 12px 0;
color: #666;
cursor: pointer;
transition: color 0.3s;
}
:deep(.ant-card-actions > li:hover) {
color: #1890ff;
}
:deep(.ant-card-actions > li.disabled) {
color: #ccc;
cursor: not-allowed;
}
:deep(.ant-card-actions > li span) {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
:deep(.ant-modal-body) {
padding: 24px;
}
:deep(.ant-form-item) {
margin-bottom: 16px;
}
.form-tip {
font-size: 12px;
color: #999;
margin-top: 4px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.header-actions {
justify-content: flex-end;
}
:deep(.ant-form .ant-form-item .ant-form-item-label) {
text-align: left;
}
:deep(.ant-form .ant-form-item .ant-form-item-control) {
width: 100%;
}
}
</style>