src/views/financialManagement/voucher/index.vue
@@ -69,87 +69,142 @@
      </PIMTable>
    </div>
    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="900px" append-to-body>
      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="凭证字号" prop="voucherNo">
              <el-input v-model="form.voucherNo" placeholder="系统自动生成" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="凭证日期" prop="voucherDate">
              <el-date-picker v-model="form.voucherDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%;" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="附件张数" prop="attachmentCount">
              <el-input-number v-model="form.attachmentCount" :min="0" style="width: 100%;" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="凭证分录" prop="entries">
          <el-table :data="form.entries" border style="width: 100%">
            <el-table-column type="index" label="序号" width="60" />
            <el-table-column prop="subjectCode" label="科目编码" width="120">
              <template #default="{ $index }">
                <el-select v-model="form.entries[$index].subjectCode" placeholder="选择科目" filterable style="width: 100%;" @change="(val) => handleSubjectChange(val, $index)">
                  <el-option v-for="item in subjectList" :key="item.code" :label="item.code" :value="item.code" />
                </el-select>
              </template>
            </el-table-column>
            <el-table-column prop="subjectName" label="科目名称" width="150">
              <template #default="{ $index }">
                <el-input v-model="form.entries[$index].subjectName" disabled />
              </template>
            </el-table-column>
            <el-table-column prop="summary" label="摘要">
              <template #default="{ $index }">
                <el-input v-model="form.entries[$index].summary" placeholder="请输入摘要" />
              </template>
            </el-table-column>
            <el-table-column prop="debit" label="借方金额" width="130">
              <template #default="{ $index }">
                <el-input-number v-model="form.entries[$index].debit" :min="0" :precision="2" style="width: 100%;" @change="calculateTotal" />
              </template>
            </el-table-column>
            <el-table-column prop="credit" label="贷方金额" width="130">
              <template #default="{ $index }">
                <el-input-number v-model="form.entries[$index].credit" :min="0" :precision="2" style="width: 100%;" @change="calculateTotal" />
              </template>
            </el-table-column>
            <el-table-column label="操作" width="80">
              <template #default="{ $index }">
                <el-button type="danger" link @click="removeEntry($index)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
          <div style="display: flex; justify-content: space-between; margin-top: 10px;">
            <el-button type="primary" link @click="addEntry">+ 添加分录</el-button>
            <div>
              <span style="margin-right: 20px;">合计: 借方 <span :class="totalDebitEntry === totalCreditEntry ? 'text-success' : 'text-danger'">¥{{ formatMoney(totalDebitEntry) }}</span></span>
              <span>贷方 <span :class="totalDebitEntry === totalCreditEntry ? 'text-success' : 'text-danger'">¥{{ formatMoney(totalCreditEntry) }}</span></span>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="1200px" @confirm="submitForm" @cancel="dialogVisible = false">
      <div class="voucher-container">
        <div class="voucher-header">
          <h2 class="voucher-title">记账凭证</h2>
          <div class="voucher-period">{{ form.voucherDate ? form.voucherDate.substring(0, 7) + '期' : '' }}</div>
        </div>
        <el-form :model="form" :rules="rules" ref="formRef" label-width="0">
          <div class="voucher-info">
            <div class="voucher-no-section">
              <span class="label">凭证字:</span>
              <el-select v-model="form.voucherPrefix" style="width: 70px;">
                <el-option label="记" value="记" />
              </el-select>
              <el-input v-model="form.voucherNum" style="width: 60px;" />
              <span class="label" style="margin-left: 5px;">号</span>
            </div>
            <div class="voucher-date-section">
              <span class="label">日期:</span>
              <el-date-picker v-model="form.voucherDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 140px;" />
            </div>
            <div class="voucher-attachment-section">
              <span class="label">附件:</span>
              <el-input-number v-model="form.attachmentCount" :min="0" :controls="false" style="width: 60px;" />
              <span class="label" style="margin-left: 5px;">张</span>
              <el-button type="primary" link style="margin-left: 10px;">上传文件</el-button>
            </div>
          </div>
        </el-form-item>
        <el-form-item label="制单人" prop="creator">
          <el-input v-model="form.creator" disabled />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
          <div class="voucher-table">
            <table class="accounting-voucher">
              <thead>
                <tr>
                  <th class="col-summary" rowspan="2">摘要</th>
                  <th class="col-subject" rowspan="2">会计科目</th>
                  <th class="col-debit-header" colspan="11">借方</th>
                  <th class="col-credit-header" colspan="11">贷方</th>
                  <th class="col-action" rowspan="2">操作</th>
                </tr>
                <tr class="amount-header">
                  <th>亿</th>
                  <th>千</th>
                  <th>百</th>
                  <th>十</th>
                  <th>万</th>
                  <th>千</th>
                  <th>百</th>
                  <th>十</th>
                  <th>元</th>
                  <th>角</th>
                  <th>分</th>
                  <th>亿</th>
                  <th>千</th>
                  <th>百</th>
                  <th>十</th>
                  <th>万</th>
                  <th>千</th>
                  <th>百</th>
                  <th>十</th>
                  <th>元</th>
                  <th>角</th>
                  <th>分</th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="(entry, rowIndex) in form.entries" :key="rowIndex" @click="selectRow(rowIndex)" :class="{ 'selected-row': selectedRowIndex === rowIndex }">
                  <td class="col-summary">
                    <el-input v-model="entry.summary" placeholder="请输入摘要" @focus="selectRow(rowIndex)" />
                  </td>
                  <td class="col-subject">
                    <el-select v-model="entry.subjectCode" placeholder="选择科目" filterable @change="(val) => handleSubjectChange(val, rowIndex)" @focus="selectRow(rowIndex)">
                      <el-option v-for="item in subjectList" :key="item.code" :label="item.code + item.name" :value="item.code" />
                    </el-select>
                    <div class="subject-name">{{ entry.subjectName }}</div>
                  </td>
                  <!-- 借方11列 -->
                  <template v-if="editingCell.row === rowIndex && editingCell.type === 'debit'">
                    <td colspan="11" class="debit-input-cell">
                      <el-input-number ref="amountInputRef" v-model="entry.debit" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" />
                    </td>
                  </template>
                  <template v-else>
                    <td v-for="(digit, dIndex) in getAmountDigits(entry.debit, 11)" :key="'debit-'+dIndex" class="amount-cell debit-cell" @click="openAmountInput(rowIndex, 'debit')">
                      <span :class="{ 'text-primary': digit !== '', 'zero': digit === '' }">{{ digit || '' }}</span>
                    </td>
                  </template>
                  <!-- 贷方11列 -->
                  <template v-if="editingCell.row === rowIndex && editingCell.type === 'credit'">
                    <td colspan="11" class="credit-input-cell">
                      <el-input-number ref="amountInputRef" v-model="entry.credit" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" />
                    </td>
                  </template>
                  <template v-else>
                    <td v-for="(digit, dIndex) in getAmountDigits(entry.credit, 11)" :key="'credit-'+dIndex" class="amount-cell credit-cell" @click="openAmountInput(rowIndex, 'credit')">
                      <span :class="{ 'text-danger': digit !== '', 'zero': digit === '' }">{{ digit || '' }}</span>
                    </td>
                  </template>
                  <td class="col-action">
                    <el-button type="danger" link size="small" @click="removeEntry(rowIndex)" icon="Delete" :disabled="form.entries.length <= 2">删除</el-button>
                  </td>
                </tr>
                <tr class="total-row">
                  <td class="col-summary" colspan="2" style="text-align: center; font-weight: bold;">合计:</td>
                  <td v-for="(digit, index) in getAmountDigits(totalDebitEntry, 11)" :key="'total-debit-'+index" class="amount-cell total-debit-cell">
                    <span :class="{ 'text-primary': digit !== '' }">{{ digit }}</span>
                  </td>
                  <td v-for="(digit, index) in getAmountDigits(totalCreditEntry, 11)" :key="'total-credit-'+index" class="amount-cell total-credit-cell">
                    <span :class="{ 'text-danger': digit !== '' }">{{ digit }}</span>
                  </td>
                  <td class="col-action"></td>
                </tr>
              </tbody>
            </table>
          </div>
          <div class="voucher-toolbar">
            <el-button type="primary" link @click="addEntry" icon="Plus">新增行</el-button>
          </div>
          <div class="voucher-footer">
            <div class="creator-section">
              <span class="label">制单人:{{ form.creator }}</span>
            </div>
          </div>
        </el-form>
      </div>
      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="submitForm" :disabled="totalDebitEntry !== totalCreditEntry">确定</el-button>
        <div>
          <el-button type="primary" @click="submitForm" :disabled="!isBalanced">保存</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </div>
      </template>
    </el-dialog>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from "vue";
import { ref, reactive, onMounted, computed, nextTick } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
defineOptions({
  name: "凭证管理",
@@ -198,11 +253,24 @@
const form = reactive({
  voucherNo: "",
  voucherPrefix: "记",
  voucherNum: "",
  voucherDate: "",
  attachmentCount: 0,
  entries: [],
  creator: "张三",
  remark: "",
});
const selectedRowIndex = ref(-1);
const editingCell = reactive({
  row: -1,
  type: "",
});
const amountInputRef = ref(null);
const isBalanced = computed(() => {
  return totalDebitEntry.value === totalCreditEntry.value && totalDebitEntry.value > 0;
});
const rules = {
@@ -283,6 +351,56 @@
  form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 });
};
const selectRow = (index) => {
  selectedRowIndex.value = index;
};
const openAmountInput = (index, type) => {
  editingCell.row = index;
  editingCell.type = type;
  nextTick(() => {
    if (amountInputRef.value) {
      amountInputRef.value.focus();
    }
  });
};
const finishEdit = () => {
  editingCell.row = -1;
  editingCell.type = "";
};
const getAmountDigits = (amount, length) => {
  if (!amount || amount === 0) {
    return new Array(length).fill('');
  }
  const amountStr = Number(amount).toFixed(2);
  const [intPart, decPart] = amountStr.split('.');
  const fullAmount = intPart + decPart;
  // 左填充0到指定长度
  const paddedAmount = fullAmount.padStart(length, '0');
  const digits = paddedAmount.split('');
  // 找到第一个非零数字的位置
  let firstNonZeroIndex = 0;
  for (let i = 0; i < digits.length; i++) {
    if (digits[i] !== '0') {
      firstNonZeroIndex = i;
      break;
    }
  }
  // 只隐藏前导零(第一个非零数字之前的零)
  return digits.map((d, index) => {
    if (index < firstNonZeroIndex) {
      return ''; // 前导零显示为空
    }
    return d; // 保留金额中的零
  });
};
const removeEntry = (index) => {
  form.entries.splice(index, 1);
  calculateTotal();
@@ -302,14 +420,23 @@
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "新增凭证";
  const nextNum = String(mockData.length + 1).padStart(2, "0");
  Object.assign(form, {
    voucherNo: "记-" + String(mockData.length + 1).padStart(4, "0"),
    voucherNo: "记-" + nextNum,
    voucherPrefix: "记",
    voucherNum: nextNum,
    voucherDate: new Date().toISOString().split('T')[0],
    attachmentCount: 0,
    entries: [{ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 }],
    entries: [
      { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 },
      { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 },
      { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 },
      { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 },
    ],
    creator: "张三",
    remark: "",
  });
  selectedRowIndex.value = 0;
  dialogVisible.value = true;
};
@@ -317,7 +444,18 @@
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑凭证";
  Object.assign(form, row);
  const parts = row.voucherNo.split('-');
  Object.assign(form, {
    ...row,
    voucherPrefix: parts[0] || '记',
    voucherNum: parts[1] || '',
  });
  if (form.entries.length < 4) {
    while (form.entries.length < 4) {
      form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 });
    }
  }
  selectedRowIndex.value = 0;
  dialogVisible.value = true;
};
@@ -366,20 +504,33 @@
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      if (totalDebitEntry.value !== totalCreditEntry.value) {
      if (!isBalanced.value) {
        ElMessage.error("借贷不平衡,请检查分录");
        return;
      }
      const summary = form.entries.find(e => e.debit > 0)?.summary || "";
      const validEntries = form.entries.filter(e => e.subjectCode && (e.debit > 0 || e.credit > 0));
      const summary = validEntries.find(e => e.debit > 0)?.summary || "";
      const voucherNo = `${form.voucherPrefix}-${form.voucherNum}`;
      const dataToSave = {
        ...form,
        voucherNo,
        summary,
        debit: totalDebitEntry.value,
        credit: totalCreditEntry.value,
        entries: validEntries,
      };
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form, summary, debit: totalDebitEntry.value, credit: totalCreditEntry.value };
          mockData[index] = { ...mockData[index], ...dataToSave };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form, summary, debit: totalDebitEntry.value, credit: totalCreditEntry.value, status: "unposted" });
        mockData.push({ id: newId, ...dataToSave, status: "unposted" });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
@@ -415,4 +566,271 @@
  color: #f56c6c;
  font-weight: bold;
}
.text-primary {
  color: #409eff;
}
.voucher-container {
  background: #fff;
  padding: 20px;
}
.voucher-header {
  text-align: center;
  margin-bottom: 15px;
  .voucher-title {
    font-size: 22px;
    font-weight: bold;
    margin: 0 0 5px 0;
    color: #303133;
  }
  .voucher-period {
    font-size: 14px;
    color: #909399;
  }
}
.voucher-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
  padding: 0 10px;
  .label {
    font-size: 14px;
    color: #606266;
  }
  .voucher-no-section,
  .voucher-date-section,
  .voucher-attachment-section {
    display: flex;
    align-items: center;
  }
}
.voucher-table {
  border: 1px solid #dcdfe6;
  border-right: none;
  margin-bottom: 15px;
}
.accounting-voucher {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  th,
  td {
    border: 1px solid #dcdfe6;
    text-align: center;
    padding: 0;
    height: 36px;
  }
  & th:last-child,
  & td:last-child {
    border-right: none !important;
  }
  thead {
    background-color: #f5f7fa;
    th {
      font-weight: normal;
      color: #606266;
      font-size: 12px;
    }
    .col-summary,
    .col-subject {
      font-weight: bold;
      font-size: 13px;
    }
    .col-debit-header,
    .col-credit-header {
      background-color: #ecf5ff;
      color: #409eff;
      font-weight: bold;
    }
  }
  .amount-header {
    th {
      font-size: 11px;
      padding: 2px 0;
      background-color: #f5f7fa;
    }
  }
  .col-summary {
    width: 160px;
    min-width: 160px;
  }
  .col-subject {
    width: 180px;
    min-width: 180px;
  }
  .col-action {
    width: 60px;
    min-width: 60px;
    text-align: center;
  }
  .amount-cell {
    width: 24px;
    min-width: 24px;
    max-width: 24px;
    padding: 0;
    font-size: 13px;
    font-family: 'Courier New', monospace;
    cursor: pointer;
    text-align: center;
    &:hover {
      background-color: #f5f7fa;
    }
    span {
      display: block;
      width: 100%;
      height: 100%;
      line-height: 36px;
      &.zero {
        color: #c0c4cc;
      }
    }
  }
  .debit-input-cell,
  .credit-input-cell {
    padding: 0;
    background-color: #ecf5ff;
    .full-width-input {
      width: 100%;
      :deep(.el-input__wrapper) {
        padding: 0 10px;
        box-shadow: none;
        background-color: transparent;
      }
      input {
        text-align: right;
        font-size: 14px;
        height: 34px;
      }
    }
  }
  tbody {
    tr {
      &:hover {
        background-color: #f5f7fa;
      }
      &.selected-row {
        background-color: #ecf5ff;
      }
    }
    td {
      .el-input {
        .el-input__wrapper {
          box-shadow: none;
          padding: 0 5px;
        }
        input {
          text-align: center;
          height: 34px;
        }
      }
      .el-select {
        width: 100%;
        .el-input__wrapper {
          box-shadow: none;
        }
        input {
          text-align: center;
          height: 34px;
        }
      }
    }
    .col-summary {
      .el-input input {
        text-align: left;
        padding-left: 10px;
      }
    }
    .col-subject {
      position: relative;
      .el-select {
        .el-input input {
          font-size: 12px;
        }
      }
      .subject-name {
        font-size: 11px;
        color: #909399;
        margin-top: 2px;
        line-height: 1.2;
      }
    }
  }
  .total-row {
    background-color: #fdf6ec;
    td {
      font-weight: bold;
    }
    .total-cell {
      background-color: #fdf6ec;
      font-weight: bold;
    }
  }
}
.voucher-toolbar {
  display: flex;
  justify-content: flex-start;
  padding: 10px 0;
  margin-top: 5px;
}
.voucher-footer {
  display: flex;
  justify-content: flex-end;
  padding: 0 10px;
  margin-top: 10px;
  .creator-section {
    .label {
      font-size: 14px;
      color: #606266;
    }
  }
}
:deep(.el-dialog__body) {
  padding: 10px 20px;
}
</style>