# 🚗 机动车销售统一发票组件

> 一个专业的机动车销售统一发票 Vue 组件，支持自定义校验、主题配置、响应式布局

**问题反馈：** 微信 `zkhh6666`（请备注好来意）

---

## 📦 安装 (Installation)

```bash
npm install motorvehicles --save
```

> ⚠️ **重要提示：** 建议为组件设置唯一的 `key` 属性，避免数据复用导致的渲染问题

---

## 🚀 快速开始 (Quick Start)

### 1. 引入组件

```javascript
import MotorVehiclesIvoice from 'motorvehicles'
import 'motorvehicles/motorvehicles.css'  // 可以通过 class 名来覆盖其中属性

export default {
  components: { 
    MotorVehiclesIvoice 
  }
}
```

### 2. 使用组件

```vue
<template>
  <div>
    <!-- 基础使用 -->
    <MotorVehiclesIvoice 
      :key="invoice.id"
      :targetLocation="locationConfig"
      :targetContent="invoiceData"
    />

    <!-- 自定义主题 -->
    <MotorVehiclesIvoice 
      :key="invoice.id"
      :targetLocation="locationConfig"
      :targetContent="invoiceData"
      themeColor="#1890ff"
      fontSize="16px"
      taxRateList="customRateList"
    />
  </div>
</template>
```

---

## 📖 配置详解

### 📌 参数配置 (Props)

| 参数名称 | 说明 | 类型 | 默认值 | 必填 |
|:---:|:---|:---:|:---:|:---:|
| `targetLocation` | 字段位置和校验规则配置 | `Array<Object>` | `[]` | ✅ |
| `targetContent` | 发票数据内容 | `Object` | `{}` | ✅ |
| `mode` | 显示模式 | `String` | `'normal'` | ❌ |
| `themeColor` | 主题颜色（边框和文字颜色） | `String` | `'#964300'` | ❌ |
| `fontSize` | 字体大小 | `String` | `'14px'` | ❌ |
| `borderWidth` | 边框宽度 | `String` | `'1px'` | ❌ |
| `taxRateList` | 税率下拉选项列表 | `Array<Object>` | 默认税率 | ❌ |
| `key` | 组件唯一标识 | `String/Number` | - | 推荐 |

#### mode 模式说明

| 模式值 | 说明 |
|:---:|:---|
| `'normal'` | 正常模式，字段可编辑（根据 disabled 配置） |
| `'look'` | 查看模式，所有字段只读 |

---

### 📋 targetLocation 配置说明

定义发票各字段的位置、状态和校验规则

#### 字段说明

| 字段 | 说明 | 类型 | 示例 | 必填 |
|:---:|:---|:---:|:---|:---:|
| `index` | 字段位置索引（从 0 开始） | `Number` | `0` | ✅ |
| `key` | 字段显示名称（用户可见） | `String` | `'开票日期'` | ✅ |
| `name` | 字段数据名称（对应 targetContent） | `String` | `'kaipiaoriqi'` | ✅ |
| `disabled` | 是否禁用编辑 | `Boolean` | `false` | ❌ |
| `required` | 是否必填 | `Boolean` | `false` | ❌ |
| `type` | 字段类型 | `String` | `'input'` / `'select'` | ❌ |
| `validateFields` | 校验规则 | `Object/Function` | 见下方说明 | ❌ |

#### 校验规则配置

**1. 正则表达式校验：**

```javascript
{
  index: 7,
  key: '购买方名称',
  name: 'purchaserName',
  disabled: false,
  required: true,
  validateFields: {
    rule: /^[\u4e00-\u9fa5a-zA-Z0-9]{2,50}$/,
    message: '购买方名称格式不正确，需2-50个字符'
  }
}
```

**2. 自定义函数校验：**

```javascript
{
  index: 8,
  key: '纳税人识别号',
  name: 'taxNo',
  disabled: false,
  required: true,
  validateFields: (value) => {
    if (!value) return '纳税人识别号不能为空'
    if (!/^[A-Z0-9]{15,20}$/.test(value)) {
      return '纳税人识别号格式不正确'
    }
    return true  // 返回 true 表示校验通过
  }
}
```

#### 完整配置示例

```javascript
const targetLocation = [
  // 顶部信息区（只读）
  { 
    index: 0, 
    key: '发票名称', 
    name: 'invoiceTitle', 
    disabled: true, 
    type: 'input' 
  },
  { 
    index: 1, 
    key: '发票类型', 
    name: 'invoiceType', 
    disabled: true, 
    type: 'input' 
  },
  { 
    index: 2, 
    key: '发票联次', 
    name: 'invoiceNum', 
    disabled: true, 
    type: 'input' 
  },
  
  // 机打信息（只读）
  { 
    index: 3, 
    key: '机打代码', 
    name: 'machineCode', 
    disabled: true, 
    type: 'input' 
  },
  { 
    index: 4, 
    key: '机打号码', 
    name: 'machineNum', 
    disabled: true, 
    type: 'input' 
  },
  { 
    index: 5, 
    key: '机器编号', 
    name: 'machineSerialNum', 
    disabled: true, 
    type: 'input' 
  },
  { 
    index: 6, 
    key: '税控码', 
    name: 'taxControlCode', 
    disabled: true, 
    type: 'input' 
  },
  
  // 购买方信息（可编辑+必填+校验）
  { 
    index: 7, 
    key: '购买方名称', 
    name: 'purchaserName', 
    disabled: false,
    required: true,
    type: 'input',
    validateFields: {
      rule: /^[\u4e00-\u9fa5a-zA-Z0-9]{2,50}$/,
      message: '购买方名称格式不正确'
    }
  },
  { 
    index: 8, 
    key: '纳税人识别号', 
    name: 'taxNo', 
    disabled: false,
    required: true,
    type: 'select',
    validateFields: (value) => {
      if (!value) return '纳税人识别号不能为空'
      if (!/^[A-Z0-9]{15,20}$/.test(value)) {
        return '纳税人识别号格式不正确'
      }
      return true
    }
  },
  
  // 车辆信息
  { 
    index: 9, 
    key: '车辆类型', 
    name: 'vehicleType', 
    disabled: false,
    required: true,
    type: 'input' 
  },
  { 
    index: 10, 
    key: '厂牌型号', 
    name: 'brandModel', 
    disabled: false,
    required: true,
    type: 'input' 
  },
  { 
    index: 11, 
    key: '产地', 
    name: 'productionPlace', 
    disabled: false,
    type: 'input' 
  },
  { 
    index: 12, 
    key: '合格证号', 
    name: 'certificateNo', 
    disabled: false,
    type: 'input' 
  },
  { 
    index: 13, 
    key: '进口证明书号', 
    name: 'importCertNo', 
    disabled: false,
    type: 'input' 
  },
  { 
    index: 14, 
    key: '商检单号', 
    name: 'inspectionNo', 
    disabled: false,
    type: 'input' 
  },
  { 
    index: 15, 
    key: '发动机号码', 
    name: 'engineNo', 
    disabled: false,
    type: 'input' 
  },
  { 
    index: 16, 
    key: '车辆识别号/车架号码', 
    name: 'vin', 
    disabled: false,
    required: true,
    type: 'input',
    validateFields: {
      rule: /^[A-Z0-9]{17}$/,
      message: '车辆识别号必须为17位'
    }
  },
  
  // 价格信息
  { 
    index: 17, 
    key: '价税合计(大写)', 
    name: 'totalAmount', 
    disabled: true,
    type: 'input' 
  },
  { 
    index: 18, 
    key: '价税合计(小写)', 
    name: 'totalAmountSmall', 
    disabled: false,
    required: true,
    type: 'input',
    validateFields: {
      rule: /^\d+(\.\d{1,2})?$/,
      message: '请输入正确的金额格式'
    }
  },
  
  // 销售方信息
  { 
    index: 19, 
    key: '销货单位名称', 
    name: 'sellerName', 
    disabled: false,
    type: 'input' 
  },
  { 
    index: 20, 
    key: '电话', 
    name: 'sellerPhone', 
    disabled: false,
    type: 'input' 
  },
  { 
    index: 21, 
    key: '纳税人识别号', 
    name: 'sellerTaxNo', 
    disabled: false,
    type: 'input' 
  },
  { 
    index: 22, 
    key: '账号', 
    name: 'sellerAccount', 
    disabled: false,
    type: 'input' 
  },
  { 
    index: 23, 
    key: '地址', 
    name: 'sellerAddress', 
    disabled: false,
    type: 'input' 
  },
  { 
    index: 24, 
    key: '开户银行', 
    name: 'sellerBank', 
    disabled: false,
    type: 'input' 
  },
  
  // 税务信息
  { 
    index: 25, 
    key: '增值税税率或征收率', 
    name: 'taxRate', 
    disabled: false,
    type: 'select' 
  },
  { 
    index: 26, 
    key: '增值税税额', 
    name: 'taxAmount', 
    disabled: true,
    type: 'input' 
  },
  { 
    index: 27, 
    key: '主管税务机关及代码', 
    name: 'taxAuthority', 
    disabled: true,
    type: 'input' 
  },
  { 
    index: 28, 
    key: '不含税价', 
    name: 'amountExcludingTax', 
    disabled: true,
    type: 'input' 
  },
  { 
    index: 29, 
    key: '完税凭证号码', 
    name: 'taxReceiptNo', 
    disabled: true,
    type: 'input' 
  },
  
  // 车辆参数
  { 
    index: 30, 
    key: '吨位', 
    name: 'tonnage', 
    disabled: false,
    type: 'input' 
  },
  { 
    index: 31, 
    key: '限乘人数', 
    name: 'passengerCapacity', 
    disabled: false,
    type: 'input' 
  }
]
```

---

### 📝 targetContent 配置说明

发票的实际数据内容，字段名与 `targetLocation` 中的 `name` 字段对应

```javascript
const targetContent = {
  // 顶部信息
  invoiceTitle: '机动车销售统一发票',
  invoiceType: '增值税专用发票',
  invoiceNum: '第一联：发票联',
  
  // 机打信息
  machineCode: '1100204130',
  machineNum: '01245896',
  machineSerialNum: 'M12345678',
  taxControlCode: '12345678901234567890123456789012',
  
  // 购买方信息
  purchaserName: '张三',
  taxNo: '91110000MA01XXXXX',
  
  // 车辆信息
  vehicleType: '小型轿车',
  brandModel: '特斯拉 Model 3 标准续航后驱升级版',
  productionPlace: '中国上海',
  certificateNo: 'CERT2024123456',
  importCertNo: '',
  inspectionNo: '',
  engineNo: 'ENG20240001',
  vin: 'LRWXXXXXXXXXXX123',
  
  // 价格信息
  totalAmount: '叁拾伍万元整',
  totalAmountSmall: '350,000.00',
  
  // 销售方信息
  sellerName: '某某汽车销售有限公司',
  sellerPhone: '010-12345678',
  sellerTaxNo: '91110000MA01YYYYY',
  sellerAccount: '1234567890123456789',
  sellerAddress: '北京市朝阳区某某街道100号',
  sellerBank: '中国工商银行北京某某支行',
  
  // 税务信息
  taxRate: '0.13',
  taxAmount: '40,265.49',
  taxAuthority: '国家税务总局北京市税务局 11000000',
  amountExcludingTax: '309,734.51',
  taxReceiptNo: '',
  
  // 车辆参数
  tonnage: '',
  passengerCapacity: '5'
}
```

---

### 📊 taxRateList 配置说明

自定义税率下拉选项

```javascript
const taxRateList = [
  { label: '13%', value: '0.13' },
  { label: '9%', value: '0.09' },
  { label: '6%', value: '0.06' },
  { label: '3%', value: '0.03' },
  { label: '0%', value: '0' }
]
```

传入组件：

```vue
<MotorVehiclesIvoice
  :targetLocation="locationConfig"
  :targetContent="invoiceData"
  :taxRateList="taxRateList"
/>
```

---

### 🎨 主题自定义

通过 props 自定义组件样式：

```vue
<!-- 默认主题（棕色） -->
<MotorVehiclesIvoice
  :targetLocation="locationConfig"
  :targetContent="invoiceData"
/>

<!-- 蓝色主题 -->
<MotorVehiclesIvoice
  :targetLocation="locationConfig"
  :targetContent="invoiceData"
  themeColor="#1890ff"
  fontSize="16px"
  borderWidth="2px"
/>

<!-- 红色主题 -->
<MotorVehiclesIvoice
  :targetLocation="locationConfig"
  :targetContent="invoiceData"
  themeColor="#ff4d4f"
  fontSize="12px"
  borderWidth="1px"
/>

<!-- 自定义 CSS 覆盖 -->
<MotorVehiclesIvoice
  class="custom-invoice"
  :targetLocation="locationConfig"
  :targetContent="invoiceData"
/>
```

也可以通过 CSS 覆盖样式：

```css
/* 覆盖默认样式 */
.custom-invoice .MotorVehiclesIvoice_AllBox_Font {
  font-size: 16px !important;
  color: #1890ff !important;
}

.custom-invoice .MotorVehiclesIvoice_AllBox_border {
  border-color: #1890ff !important;
}
```

---

## 🔧 方法 (Methods)

### getFieldsValue()

获取组件当前的所有字段值

**返回值：** `Object` - 包含所有字段的数据对象

```javascript
// 通过 ref 调用
const invoiceData = this.$refs.motorInvoice.getFieldsValue()

console.log(invoiceData)
// 返回:
// {
//   purchaserName: '张三',
//   taxNo: '91110000MA01XXXXX',
//   vehicleType: '小型轿车',
//   brandModel: '特斯拉 Model 3',
//   totalAmountSmall: '350,000.00',
//   ...
// }
```

**使用示例：**

```vue
<template>
  <div>
    <MotorVehiclesIvoice
      ref="motorInvoice"
      :targetLocation="locationConfig"
      :targetContent="invoiceData"
    />
    <button @click="handleSave">保存</button>
  </div>
</template>

<script>
export default {
  methods: {
    handleSave() {
      const data = this.$refs.motorInvoice.getFieldsValue()
      console.log('当前发票数据:', data)
      // 调用接口保存...
    }
  }
}
</script>
```

---

### validateFields(callback)

触发表单校验，校验 `targetLocation` 中配置的所有规则

**参数：**
- `callback: Function(error, values)` - 校验完成后的回调函数
  - `error`: 校验失败时的错误对象，格式：`{ code: 500, message: '错误信息' }`
  - `values`: 校验成功时的所有字段值

**校验规则优先级：**
1. **必填校验：** 检查 `required: true` 的字段
2. **正则校验：** 检查配置了 `validateFields.rule` 的字段
3. **自定义校验：** 执行配置的 `validateFields` 函数

```javascript
this.$refs.motorInvoice.validateFields((error, values) => {
  if (error) {
    // 校验失败
    console.error('校验失败:', error.message)
    this.$message.error(error.message)
  } else {
    // 校验通过
    console.log('校验通过，数据:', values)
    this.submitInvoice(values)
  }
})
```

**完整使用示例：**

```vue
<template>
  <div>
    <MotorVehiclesIvoice
      ref="motorInvoice"
      :targetLocation="locationConfig"
      :targetContent="formData"
    />
    <div class="button-group">
      <button @click="handleValidate">校验</button>
      <button @click="handleSubmit">提交</button>
      <button @click="handleReset">重置</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      locationConfig: [...],  // targetLocation 配置
      formData: {...}         // targetContent 数据
    }
  },
  methods: {
    // 仅校验
    handleValidate() {
      this.$refs.motorInvoice.validateFields((error, values) => {
        if (error) {
          this.$message.error(error.message)
        } else {
          this.$message.success('校验通过')
        }
      })
    },
    
    // 校验并提交
    handleSubmit() {
      this.$refs.motorInvoice.validateFields(async (error, values) => {
        if (error) {
          this.$message.error(error.message)
          return
        }
        
        try {
          const res = await this.$api.saveInvoice(values)
          this.$message.success('保存成功')
        } catch (err) {
          this.$message.error('保存失败：' + err.message)
        }
      })
    },
    
    // 重置表单
    handleReset() {
      this.formData = {
        purchaserName: '',
        taxNo: '',
        vehicleType: '',
        // ... 重置所有字段
      }
    }
  }
}
</script>
```

---

## 💡 使用技巧

### 1. 避免数据复用问题

为组件设置唯一的 `key` 避免 Vue 复用导致的数据错乱：

```vue
<MotorVehiclesIvoice 
  v-for="invoice in invoiceList"
  :key="invoice.id"
  :targetContent="invoice.data"
/>
```

### 2. 动态禁用字段

根据业务逻辑动态控制字段是否可编辑：

```javascript
computed: {
  locationConfig() {
    return this.baseLocation.map(item => {
      // 已提交的发票，所有字段禁用
      if (this.invoiceStatus === 'submitted') {
        return { ...item, disabled: true }
      }
      // 审核中的发票，仅部分字段可编辑
      if (this.invoiceStatus === 'reviewing') {
        const editableFields = ['sellerPhone', 'sellerAddress']
        return {
          ...item,
          disabled: !editableFields.includes(item.name)
        }
      }
      return item
    })
  }
}
```

### 3. 自定义复杂校验

实现多字段联动校验：

```javascript
{
  index: 18,
  key: '价税合计(小写)',
  name: 'totalAmountSmall',
  required: true,
  validateFields: (value) => {
    const amount = parseFloat(value.replace(/,/g, ''))
    const taxRate = parseFloat(this.formData.taxRate)
    const taxAmount = parseFloat(this.formData.taxAmount.replace(/,/g, ''))
    
    // 校验：价税合计 ≈ 不含税价 + 税额
    const expectedTotal = amount / (1 + taxRate)
    const actualTotal = amount - taxAmount
    
    if (Math.abs(expectedTotal - actualTotal) > 0.01) {
      return '价税合计与税额不匹配，请检查'
    }
    
    return true
  }
}
```

### 4. 打印功能

添加打印样式：

```vue
<template>
  <div>
    <MotorVehiclesIvoice
      ref="invoice"
      class="print-invoice"
      :targetLocation="locationConfig"
      :targetContent="invoiceData"
    />
    <button @click="handlePrint">打印</button>
  </div>
</template>

<script>
export default {
  methods: {
    handlePrint() {
      window.print()
    }
  }
}
</script>

<style>
@media print {
  /* 打印时隐藏按钮等元素 */
  button {
    display: none;
  }
  
  /* 调整发票样式 */
  .print-invoice {
    width: 210mm;
    height: 297mm;
    page-break-after: always;
  }
}
</style>
```

### 5. 数据初始化

从接口获取数据并初始化组件：

```javascript
async mounted() {
  try {
    // 获取发票配置
    const config = await this.$api.getInvoiceConfig()
    this.locationConfig = config.fields
    
    // 获取发票数据
    const invoiceId = this.$route.params.id
    if (invoiceId) {
      const data = await this.$api.getInvoiceDetail(invoiceId)
      this.invoiceData = data
    }
  } catch (err) {
    this.$message.error('数据加载失败')
  }
}
```

---

## 📸 效果预览

![机动车发票组件效果图](https://img-blog.csdnimg.cn/d91289ff903b4958aa2721f466078aab.png)

---

## 📝 注意事项

1. ⚠️ **key 的重要性：** 在列表渲染时务必设置唯一的 `key`，避免数据复用问题

2. ⚠️ **校验函数返回值：** 自定义校验函数必须返回 `true`（通过）或错误信息字符串（失败）

3. ⚠️ **必填字段标识：** 配置 `required: true` 的字段会在标签后显示红色 `*` 号

4. ⚠️ **字段索引顺序：** `targetLocation` 的 `index` 必须按顺序从 0 开始递增

5. ⚠️ **下拉框配置：** `type: 'select'` 的字段需要配置对应的 `taxRateList`

6. ⚠️ **样式覆盖：** 引入 CSS 后可以通过 class 名覆盖默认样式

7. ⚠️ **mode 模式：** `look` 模式下所有字段只读，`normal` 模式根据 `disabled` 配置决定

---

## 🆚 与增值税发票组件的区别

| 特性 | 机动车发票 | 增值税发票 |
|:---:|:---|:---|
| **数据结构** | 固定字段位置 | 灵活的 table 结构 |
| **适用场景** | 汽车销售 | 通用商品/服务 |
| **校验方式** | 单字段独立校验 | 支持行级校验 |
| **明细行数** | 固定格式 | 动态多行 |

---

## 📅 版本记录

### v1.0.0 (2025-11-25)
- 🎉 插件首次发布
- ✨ 支持机动车销售统一发票渲染
- ✨ 支持字段校验（必填、正则、自定义函数）
- ✨ 支持主题自定义（颜色、字体、边框）
- ✨ 支持只读/编辑模式切换

---

## 📄 License

MIT License

---

## 🤝 贡献与反馈

欢迎提交 Issue 和 Pull Request！

**联系方式：** 微信 `zkhh6666`（请备注好来意）

---

## 🔗 相关链接

- **GitHub 仓库：** [待补充]
- **在线示例：** [待补充]