Files
his/openhis-ui-vue3/src/views/basicmanage/InvoiceManagement/index.vue
2025-11-25 15:30:33 +08:00

1642 lines
54 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-management">
<div class="container">
<!-- 页面标题和用户信息 -->
<div class="header-section">
<h2>发票管理</h2>
</div>
<!-- 操作按钮区域 -->
<div class="button-group">
<button @click="addNewRow" class="btn btn-primary">
<i class="icon-plus"></i> 添加新行
</button>
<button @click="saveData" class="btn btn-success" :disabled="saveButtonText === '保存中...'">
{{ saveButtonText }}
</button>
</div>
<!-- 错误提示区域 -->
<div v-if="validationErrors.length > 0" class="alert alert-danger error-messages">
<h4 class="error-title">验证失败</h4>
<ul class="error-list">
<li v-for="(error, index) in validationErrors" :key="index" class="error-item">
{{ error }}
</li>
</ul>
</div>
<!-- 数据表格 -->
<div class="table-container">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>序号</th>
<th>操作员</th>
<th>员工工号</th>
<th>领用日期</th>
<th>起始号码</th>
<th>终止号码</th>
<th>当前使用号码</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in filteredData"
:key="item.keyId"
:class="{'editing-row': item.isActive }"
>
<td class="sequence-number">{{ index + 1 }}</td>
<td class="employee-info">
<div class="input-container">
<!-- 操作员字段始终不可编辑 -->
<span class="employee-name">{{ item.operator || '-' }}</span>
</div>
</td>
<td class="employee-id-cell">
<div class="input-container">
<!-- 员工工号字段始终不可编辑 -->
<span>{{ item.employeeId || '-' }}</span>
</div>
</td>
<td class="date-cell">
<div class="input-container">
<input
v-if="item.isActive"
v-model="item.date"
type="date"
class="form-control"
placeholder="领用日期"
/>
<span v-else>{{ item.date || '-' }}</span>
</div>
</td>
<td>
<div class="input-container">
<input
v-if="item.isActive"
v-model="item.startNum"
class="form-control"
placeholder="请输入起始号码"
maxlength="12"
/>
<span v-else class="invoice-number">{{ item.startNum || '-' }}</span>
</div>
</td>
<td>
<div class="input-container">
<input
v-if="item.isActive"
v-model="item.endNum"
class="form-control"
placeholder="请输入终止号码"
maxlength="12"
/>
<span v-else class="invoice-number">{{ item.endNum || '-' }}</span>
</div>
</td>
<td>
<div class="input-container">
<input
v-if="item.isActive"
v-model="item.currentNum"
class="form-control"
placeholder="当前使用号码"
maxlength="12"
/>
<span v-else class="invoice-number current">{{ item.currentNum || '-' }}</span>
</div>
</td>
<td class="action-buttons">
<button
v-if="!item.isActive"
@click="toggleEdit(item.keyId)"
class="btn btn-primary btn-sm mr-1"
title="编辑"
>
<i class="icon-edit"></i> 编辑
</button>
<button
v-if="item.isActive"
@click="toggleEdit(item.keyId)"
class="btn btn-secondary btn-sm mr-1"
title="取消"
>
<i class="icon-cancel"></i> 取消
</button>
<button
@click="deleteRow(item)"
class="btn btn-danger btn-sm"
title="删除"
>
<i class="icon-delete"></i> 删除
</button>
</td>
</tr>
<tr v-if="filteredData.length === 0">
<td colspan="8" class="no-data-row">
暂无数据请添加新记录
</td>
</tr>
</tbody>
</table>
</div>
<!-- 提示信息 -->
<div class="info-section">
<div class="info-card">
<h5>操作提示</h5>
<ul class="info-list">
<li>发票号码支持纯数字或字母前缀+数字组合</li>
<li>发票号码最大长度为12位</li>
<li>起始和终止号码长度必须一致</li>
<li>修改记录后请点击保存按钮</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import { listUser } from '@/api/system/user';
import request from '@/utils/request'; // 导入请求工具
import useUserStore from '@/store/modules/user'; // 导入用户store
export default {
name: 'InvoiceManagement',
data() {
return {
// 用户信息和权限
currentUser: {
name: '',
nickName: '',
employeeId: '',
role: '' // operator: 普通操作员, admin: 管理员
},
// 用户列表用于操作员下拉选择将在created中从后端获取
userList: [],
saveButtonText: '保存',
// 发票数据模型,增加更多必要字段
invoiceData: [], // 初始为空数组,无默认数据
// 表单验证相关状态
validationErrors: [],
// 过滤后显示的数据
filteredData: []
}
},
computed: {
// 计算属性:判断是否为管理员
isAdmin() {
return this.currentUser.role === 'admin';
}
},
created() {
console.log('组件初始化,开始加载数据');
// 获取当前登录用户信息
const userStore = useUserStore();
this.currentUser = {
name: userStore.nickName || userStore.name,
nickName: userStore.nickName,
employeeId: userStore.id || userStore.practitionerId,
role: userStore.roles && userStore.roles.length > 0 ? userStore.roles[0] : 'operator'
};
console.log('当前登录用户信息:', this.currentUser);
// 从后端获取用户列表后再加载发票数据,确保用户信息可用
this.getUserList().then(() => {
console.log('用户列表加载完成,开始加载发票数据');
this.loadInvoiceData();
}).catch(() => {
console.warn('用户列表加载失败,仍尝试加载发票数据');
this.loadInvoiceData();
});
// 初始化过滤数据,确保页面首次渲染时有数据显示
this.filterDataByPermission();
},
watch: {
// 监听invoiceData变化自动更新过滤后的数据
invoiceData: {
handler() {
this.filterDataByPermission();
},
deep: true
}
},
methods: {
// 获取用户列表
getUserList() {
// 传递分页参数,获取所有用户数据
const queryParams = {
pageNum: 1,
pageSize: 1000 // 设置较大的pageSize以获取所有用户
};
return listUser(queryParams).then((res) => {
// 从响应中提取用户列表,并转换为需要的格式
this.userList = res.data.records.map(user => ({
name: user.nickName || user.username || user.name, // 尝试多种可能的名称字段
employeeId: user.userId, // 使用用户ID作为员工工号
role: user.role || 'operator' // 默认为普通操作员
}));
console.log('获取到的用户列表:', this.userList);
return Promise.resolve();
}).catch(error => {
console.error('获取用户列表失败:', error);
return Promise.reject(error);
});
},
// 从后端加载发票数据
loadInvoiceData() {
// 使用request工具从后端API获取发票段数据
request({
url: '/basicmanage/invoice-segment/page', // 更新为发票段的API路径
method: 'get',
params: {
pageNo: 1,
pageSize: 10000 // 进一步增大分页大小,确保能获取数据库中的所有记录
}
}).then(res => {
console.log('获取到的发票段数据响应:', res);
// 添加更多调试信息
console.log('响应总记录数:', res.data.total || '未知');
console.log('实际获取记录数:', res.data.records ? res.data.records.length : 0);
// 检查响应数据格式
if (!res || !res.data) {
console.error('响应数据格式不正确:', res);
if (this.$message) {
this.$message.error('获取发票数据失败:响应格式不正确');
} else {
alert('获取发票数据失败:响应格式不正确');
}
return;
}
// 处理响应数据将后端adm_invoice_segment数据映射到前端所需的数据结构
if (res.data.records && res.data.records.length > 0) {
console.log(`成功获取到 ${res.data.records.length} 条发票段记录`);
this.invoiceData = res.data.records.map((record, index) => {
const mappedRecord = {
id: record.id, // 保留原始id
segmentId: record.segmentId, // 保留原始segmentId
// 为前端操作使用的标识符
keyId: record.id || record.segmentId, // 用于前端组件的key
// 更健壮的操作员名称获取逻辑
// 更健壮的操作员名称获取逻辑,确保类型转换一致
operator: (record.invoicingStaffName || record.employeeName) ?
record.invoicingStaffName || record.employeeName :
((record.invoicingStaffId || record.employeeId) ?
this.getUserNameById(record.invoicingStaffId || record.employeeId) : '-'),
employeeId: record.invoicingStaffId || record.employeeId || '',
date: record.billBusDate || record.createDate ?
new Date(record.billBusDate || record.createDate).toISOString().split('T')[0] : '',
startNum: record.startNum || record.beginNumber || '',
endNum: record.endNum || record.endNumber || '',
// 从remark字段中提取currentNum值支持多种格式
currentNum: record.currentNum || record.currentNumber ||
(record.remark && record.remark.includes('当前使用号码:') ?
record.remark.split('当前使用号码:')[1].split(';')[0].trim() : ''),
status: record.statusEnum ? record.statusEnum.value : record.status || '未使用',
remark: record.remark || '', // 保留原始remark字段
isActive: false // 确保初始状态不是编辑状态
};
console.log(`记录 ${index + 1} 映射后的数据:`, mappedRecord);
return mappedRecord;
});
console.log('最终映射后的发票数据:', this.invoiceData);
// 确保数据加载后立即过滤
this.filterDataByPermission();
} else {
console.log('未获取到发票段记录或记录为空');
this.invoiceData = [];
this.filterDataByPermission(); // 即使没有数据也调用过滤函数
}
}).catch(error => {
console.error('获取发票数据失败:', error);
console.error('错误详情:', error.response || error);
// 显示更详细的错误信息
let errorMessage = '加载发票数据失败';
if (error.response) {
errorMessage += `: ${error.response.status} ${error.response.statusText}`;
} else if (error.message) {
errorMessage += `: ${error.message}`;
}
if (this.$message) {
this.$message.error(errorMessage);
} else {
alert(errorMessage);
}
});
},
// 更新发票数据到后端
updateInvoiceData(record) {
// 准备发送到后端的数据格式适配adm_invoice_segment表结构
const dataToSend = {
id: record.id, // 保持一致的ID字段
segmentId: record.segmentId || record.id, // 优先使用record.segmentId确保segment_id不为空
invoicingStaffId: record.employeeId,
employeeId: record.employeeId, // 同时提供employeeId字段
billBusDate: record.date ? new Date(record.date) : null,
createDate: record.date ? new Date(record.date) : null,
startNum: record.startNum,
beginNumber: record.startNum, // 同时提供beginNumber字段
endNum: record.endNum,
endNumber: record.endNum, // 同时提供endNumber字段
currentNum: record.currentNum,
currentNumber: record.currentNum, // 同时提供currentNumber字段
statusEnum: { value: record.status },
status: record.status, // 同时提供status字段
tenantId: 0, // 添加tenantId字段
deleteFlag: '0' // 添加deleteFlag字段
};
// 确保remark字段包含当前使用号码信息
if (record.currentNum) {
dataToSend.remark = `当前使用号码:${record.currentNum}`;
}
console.log('准备更新发票段数据:', {
url: '/basicmanage/invoice-segment/update',
data: dataToSend
});
// 调用后端API保存数据
request({
url: '/basicmanage/invoice-segment/update',
method: 'post',
data: dataToSend
}).then(response => {
console.log('发票段数据更新成功:', response);
return response;
}).catch(error => {
console.error('发票段数据更新失败:', error);
console.error('错误详情:', error.response || error);
throw error;
}).then(res => {
console.log('更新发票数据成功:', res);
}).catch(error => {
console.error('更新发票数据失败:', error);
if (this.$message) {
this.$message.error('更新数据失败,请稍后重试');
}
});
},
// 根据用户权限过滤数据
filterDataByPermission() {
console.log('开始过滤数据,当前用户角色:', this.currentUser.role);
console.log('过滤前数据总量:', this.invoiceData.length);
if (this.isAdmin) {
// 管理员可以看到所有数据
console.log('管理员模式,显示所有数据');
this.filteredData = [...this.invoiceData];
} else {
// 普通操作员只能看到自己的数据,确保类型一致
console.log('操作员模式,过滤条件:', this.currentUser.employeeId);
const currentEmployeeId = String(this.currentUser.employeeId);
this.filteredData = this.invoiceData.filter(item =>
String(item.employeeId) === currentEmployeeId
);
}
console.log('过滤后显示的数据量:', this.filteredData.length);
},
// 检查是否有权限修改指定记录
canModifyRecord(record) {
// 管理员可以修改所有记录,普通用户只能修改自己的
return this.isAdmin || record.employeeId === this.currentUser.employeeId;
},
// 检查权限并执行操作
checkPermissionAndExecute(record, operation) {
if (this.canModifyRecord(record)) {
operation();
} else {
alert('您没有权限修改此记录!');
}
},
// 更新员工ID
updateEmployeeId(item, employeeId) {
item.employeeId = employeeId;
},
// 根据员工ID获取用户名称
getUserNameById(employeeId) {
if (!employeeId) return '';
const user = this.userList.find(u => String(u.employeeId) === String(employeeId));
return user ? user.name : '';
},
// 检查是否有权限修改指定记录
canModifyRecord(record) {
// 管理员可以修改所有记录,普通用户只能修改自己的
return this.isAdmin || record.employeeId === this.currentUser.employeeId;
},
// 检查权限并执行操作
checkPermissionAndExecute(record, operation) {
if (this.canModifyRecord(record)) {
operation();
} else {
alert('您没有权限修改此记录!');
}
},
// 更新员工ID
updateEmployeeId(item, employeeId) {
item.employeeId = employeeId;
},
addNewRow() {
// 新增行时自动填充当前用户信息
// 使用负数作为临时ID避免与后端数据库ID冲突
let maxId = 0;
if (this.invoiceData.length > 0) {
maxId = Math.max(...this.invoiceData.map(item => Math.abs(item.id)));
}
const newId = -(maxId + 1);
const newSegmentId = Date.now(); // 生成唯一的segmentId
const currentDate = new Date().toISOString().split('T')[0];
const newRecord = {
id: newId,
segmentId: newSegmentId, // 为新记录设置segmentId
operator: this.currentUser.name, // 自动使用当前用户名称
employeeId: this.currentUser.employeeId,
date: currentDate, // 自动填充当日日期
startNum: '',
endNum: '',
currentNum: '',
status: '未使用',
isActive: true, // 新增行默认处于编辑状态
isNewRecord: true // 添加标记表示这是新记录
};
console.log('添加新行,自动填充领用日期为当日:', { newRecord });
this.invoiceData.push(newRecord);
},
deleteRow(record) {
if (!record) {
alert('未找到要删除的记录');
return;
}
// 对于新添加的记录直接从前端移除不需要调用API
if (record.isNewRecord) {
if (confirm('确定要删除这条未保存的记录吗?')) {
const index = this.invoiceData.findIndex(item => item.keyId === record.keyId);
if (index > -1) {
this.invoiceData.splice(index, 1);
this.filterDataByPermission();
alert('删除成功');
}
}
return;
}
// 严格检查权限:只有管理员或记录所有者可以删除
if (this.currentUser.role !== 'admin' && record.employeeId !== this.currentUser.employeeId) {
alert('您没有权限删除此记录!操作员只能删除自己的记录。');
return;
}
// 检查发票号码段使用状态
// 检查1: 如果当前号码已使用且不等于起始号码,说明已使用
// 检查2: 如果状态明确为已使用,也不允许删除
if ((record.currentNum && record.currentNum !== record.startNum) || record.status === '已使用') {
alert('该发票号码段已被使用,无法删除!请确认发票使用情况。');
return;
}
if (confirm('确定要删除这条记录吗?删除后无法恢复。')) {
// 添加详细的请求数据日志
console.log('删除记录详情:', record);
// 构建删除数据优先使用id如果没有则使用segmentId
const idsToDelete = [];
if (record.id) idsToDelete.push(record.id);
if (record.segmentId && record.segmentId !== record.id) idsToDelete.push(record.segmentId);
// 如果没有有效的ID尝试使用前端的keyId
const deleteData = { ids: idsToDelete.length > 0 ? idsToDelete : [record.keyId] };
console.log('准备发送删除请求:', { url: '/basicmanage/invoice-segment/delete', method: 'post', data: deleteData });
// 更新日志以反映新的ID处理方式
console.log('删除数据:', deleteData);
console.log('删除ID类型检查:', deleteData.ids.map(id => ({value: id, type: typeof id})));
// 调用删除API
console.log('开始删除操作使用的ID:', deleteData.ids);
console.log('请求数据:', deleteData);
request({
url: '/basicmanage/invoice-segment/delete',
method: 'post',
data: deleteData
}).then(res => {
console.log('删除请求响应:', res);
console.log('响应类型:', typeof res);
console.log('响应结构:', JSON.stringify(res, null, 2));
// 处理响应 - 支持多种响应格式
let isSuccess = false;
let message = '';
// 检查是否为成功响应
if (res.code === 200 || res.data?.code === 200) {
isSuccess = true;
message = res.msg || res.data?.msg || '删除成功';
}
// 检查是否有特定的成功标志
else if (res.data?.success || res.success) {
isSuccess = true;
message = res.msg || res.data?.msg || '删除成功';
}
// 检查是否直接包含成功消息
else if (res.msg?.includes('成功') || res.data?.msg?.includes('成功')) {
isSuccess = true;
message = res.msg || res.data?.msg || '删除成功';
}
// 其他情况视为失败
else {
message = res.msg || res.data?.msg || '删除失败';
console.error('响应中未包含成功标志:', { code: res.code || res.data?.code, msg: message });
}
// 检查是否在响应中包含影响行数信息
let affectedRows = null;
if (res.data?.affectedRows !== undefined) {
affectedRows = res.data.affectedRows;
} else if (res.affectedRows !== undefined) {
affectedRows = res.affectedRows;
}
console.log('删除操作影响行数:', affectedRows);
// 根据结果处理
if (isSuccess) {
// 特别处理影响行数为0的情况
if (affectedRows === 0) {
console.warn('删除操作成功但未影响任何记录,可能记录不存在或已被删除');
// 从前端数据中移除该记录(即使后端没有实际删除)
const index = this.invoiceData.findIndex(item => item.keyId === record.keyId);
if (index > -1) {
this.invoiceData.splice(index, 1);
this.filterDataByPermission();
}
// 显示友好提示
const notFoundMessage = '记录已不存在或已被删除,已从当前列表中移除';
if (this.$message) {
this.$message.warning(notFoundMessage);
} else {
alert(notFoundMessage);
}
} else {
// 正常删除成功的情况
const index = this.invoiceData.findIndex(item => item.keyId === record.keyId);
if (index > -1) {
this.invoiceData.splice(index, 1);
// 删除后刷新过滤数据
this.filterDataByPermission();
}
if (this.$message) {
this.$message.success(message);
} else {
alert(message);
}
}
} else {
// 处理失败响应
console.error('删除失败:', message, res);
if (this.$message) {
this.$message.error(message);
} else {
alert(message);
}
}
}).catch(error => {
// 详细的错误处理
console.error('删除请求异常捕获:', error);
console.error('错误类型:', typeof error);
console.error('完整错误对象:', error);
// 尝试获取更多错误信息
if (error.response) {
console.error('错误响应数据:', JSON.stringify(error.response, null, 2));
} else if (error.config) {
console.error('请求配置:', JSON.stringify(error.config, null, 2));
}
// 智能错误消息提取
let errorMessage = '删除失败';
// 1. 首先尝试从error对象的message属性获取来自request.js的详细错误
if (error.message) {
errorMessage = error.message;
console.log('从error.message获取到错误:', errorMessage);
}
// 2. 然后检查是否有response对象从中提取更详细的错误信息
else if (error.response) {
errorMessage = `${error.response.status} ${error.response.statusText}`;
console.log('从error.response获取到状态:', errorMessage);
if (error.response.data) {
if (error.response.data.msg) {
errorMessage = error.response.data.msg;
} else if (error.response.data.message) {
errorMessage = error.response.data.message;
} else if (typeof error.response.data === 'string') {
errorMessage = error.response.data;
}
console.log('从error.response.data获取到消息:', errorMessage);
}
}
// 3. 最后如果是字符串错误,直接使用
else if (typeof error === 'string') {
errorMessage = error;
console.log('直接使用字符串错误:', errorMessage);
}
// 显示错误信息
console.error('最终确定的错误消息:', errorMessage);
if (this.$message) {
this.$message.error(errorMessage);
} else {
alert(errorMessage);
}
});
}
},
// 切换编辑状态
toggleEdit(keyId) {
const record = this.invoiceData.find(item => item.keyId === keyId);
if (!record) return;
this.checkPermissionAndExecute(record, () => {
// 判断是否是取消编辑操作
if (record.isActive) {
// 检查是否是未保存的新增行通过isNewRecord标记判断
const isUnsavedNewRow = record.isNewRecord;
if (isUnsavedNewRow) {
// 对于未保存的新增行,直接删除
const index = this.invoiceData.findIndex(item => item.keyId === keyId);
if (index > -1) {
this.invoiceData.splice(index, 1);
// 删除后刷新过滤数据
this.filterDataByPermission();
}
} else {
// 对于已保存的行,正常切换编辑状态
record.isActive = false;
// 清除错误信息
this.validationErrors = [];
}
} else {
// 进入编辑状态
record.isActive = true;
}
});
},
/* 测试用例:
1. 点击新增按钮 -> 添加一行新记录带有isNewRecord标记
2. 直接点击取消按钮 -> 应该删除该行,不保留任何痕迹
3. 点击新增按钮 -> 添加一行新记录
4. 输入一些数据后再点击取消按钮 -> 应该删除该行,不保留数据
5. 点击新增按钮 -> 添加一行新记录
6. 点击保存按钮 -> 保存成功后移除isNewRecord标记
7. 再次点击编辑,然后点击取消 -> 应该只是退出编辑状态,不删除记录
*/
// 发票号码使用后自动加1功能
incrementInvoiceNumber(employeeId) {
// 查找当前操作员的发票记录
const record = this.invoiceData.find(item => item.employeeId === employeeId);
if (!record) {
return { success: false, message: '未找到当前操作员的发票号码记录' };
}
// 确保当前号码有效
if (!record.currentNum) {
// 如果当前号码未设置,初始化为起始号码
record.currentNum = record.startNum;
} else {
try {
// 处理发票号码自动加1支持包含字母的复杂格式
const currentNumStr = record.currentNum.toString();
let incremented = false;
let newNum = '';
// 从右向左处理每个字符
for (let i = currentNumStr.length - 1; i >= 0; i--) {
const char = currentNumStr[i];
if (/\d/.test(char)) {
// 数字字符加1
const num = parseInt(char) + 1;
if (num < 10) {
newNum = currentNumStr.substring(0, i) + num + newNum.substring(0);
incremented = true;
break;
} else {
// 进位处理
newNum = '0' + newNum;
}
} else {
// 非数字字符保持不变
newNum = char + newNum;
}
}
// 如果全部是9需要在前面补1
if (!incremented) {
newNum = '1' + newNum;
}
// 验证新号码长度与原号码一致
if (newNum.length !== record.startNum.toString().length) {
return { success: false, message: '发票号码位数不足,无法继续使用' };
}
// 验证新号码是否在范围内
if (!this.validateCurrentNumberInRange(newNum, record.startNum, record.endNum)) {
return { success: false, message: '发票号码已超出设定范围,请更换发票号码段' };
}
// 更新当前号码
record.currentNum = newNum;
} catch (error) {
return { success: false, message: '发票号码自动加1时发生错误' };
}
}
// 标记为已使用
if (record.status !== '已使用') {
record.status = '已使用';
}
// 保存更新到后端
this.updateInvoiceData(record);
return { success: true, currentNum: record.currentNum };
},
// 使用当前发票号码(业务调用入口)
useCurrentInvoiceNumber() {
// 获取当前登录用户ID
const currentEmployeeId = this.currentUser.employeeId;
if (!currentEmployeeId) {
alert('未找到当前操作员信息');
return null;
}
// 查找当前操作员的发票记录
const record = this.invoiceData.find(item => item.employeeId === currentEmployeeId);
if (!record) {
alert('未找到当前操作员的发票号码段设置');
return null;
}
// 获取当前可用号码
const usableNumber = record.currentNum || record.startNum;
// 执行号码加1操作为下次使用做准备
const result = this.incrementInvoiceNumber(currentEmployeeId);
if (!result.success) {
alert(result.message);
return null;
}
// 触发保存操作以持久化更新
this.runAllValidations();
return usableNumber;
},
// 从字符串末尾向前提取前缀,直到遇到第一个字母
// 规则:前缀定义为从号码最末位开始往前推直到出现字母为止,其前面的字符全部称为前缀;若无字母则无前缀
extractPrefixFromEnd(str) {
if (!str) return '';
// 从末尾向前遍历字符串
for (let i = str.length - 1; i >= 0; i--) {
// 如果遇到字母,返回从开始到该字母的所有字符
if (/[A-Za-z]/.test(str[i])) {
return str.substring(0, i + 1);
}
}
// 如果没有找到字母,则无前缀
return '';
},
// 验证发票号码格式
validateInvoiceNumber(num) {
// 检查是否为空
if (!num) return { valid: false, message: '发票号码不能为空' };
// 检查长度最大12位
if (num.length > 12) return { valid: false, message: '发票号码长度不能超过12位' };
// 检查格式(纯数字或字母前缀+数字)
const pattern = /^([A-Za-z]+)?\d+$/;
if (!pattern.test(num)) {
return { valid: false, message: '发票号码格式不正确(只能是纯数字或字母前缀+数字组合)' };
}
return { valid: true };
},
// 验证起始和终止发票号码长度一致性
validateNumberLengths(startNum, endNum) {
// 即使一个为空,也需要验证非空字段的最小长度要求
if (!startNum && !endNum) {
return false; // 两个都为空
}
// 验证起始号码格式和长度
if (startNum) {
const startStr = startNum.toString();
if (startStr.length < 1) {
return false; // 起始发票号码不能为空
}
}
// 验证终止号码格式和长度
if (endNum) {
const endStr = endNum.toString();
if (endStr.length < 1) {
return false; // 终止发票号码不能为空
}
}
// 两个都有值时,必须长度一致
if (startNum && endNum) {
const startLength = startNum.toString().length;
const endLength = endNum.toString().length;
if (startLength !== endLength) {
return false; // 起始和终止发票号码长度必须完全一致
}
}
return true;
},
// 检查号码不重复(针对所有操作员)
checkNumberUniqueness(record) {
// 查找所有其他记录(不限操作员)
// 使用keyId作为比较依据因为它是前端唯一标识符
const otherRecords = this.invoiceData.filter(
item => item.keyId !== record.keyId
);
for (const item of otherRecords) {
// 检查起始号码是否重复
if (item.startNum === record.startNum) {
return { valid: false, message: `起始号码 ${record.startNum} 已存在于其他操作员的记录中` };
}
// 检查终止号码是否重复
if (item.endNum === record.endNum) {
return { valid: false, message: `终止号码 ${record.endNum} 已存在于其他操作员的记录中` };
}
// 检查号码范围是否有包含关系
// 检查当前记录的起始号码是否在其他记录的范围内
if (record.startNum && item.startNum && item.endNum) {
const isInRange = this.validateCurrentNumberInRange(record.startNum, item.startNum, item.endNum);
if (isInRange) {
return { valid: false, message: `起始号码 ${record.startNum} 已包含在 ${item.operator} 的票据范围 ${item.startNum}-${item.endNum}` };
}
}
// 检查当前记录的终止号码是否在其他记录的范围内
if (record.endNum && item.startNum && item.endNum) {
const isInRange = this.validateCurrentNumberInRange(record.endNum, item.startNum, item.endNum);
if (isInRange) {
return { valid: false, message: `终止号码 ${record.endNum} 已包含在 ${item.operator} 的票据范围 ${item.startNum}-${item.endNum}` };
}
}
}
return { valid: true };
},
// 检查号码范围是否与其他记录重叠
checkRangeOverlap(record) {
// 确保当前记录有必要的数据
if (!record || !record.startNum || !record.endNum) {
return { valid: false, message: '记录信息不完整' };
}
// 查找所有其他记录
// 使用keyId作为比较依据因为它是前端唯一标识符
const otherRecords = this.invoiceData.filter(item => item.keyId !== record.keyId);
// 提取数字部分进行比较的辅助函数
const extractNumber = function(str) {
if (!str) return 0;
const match = str.match(/\d+$/);
return match ? parseInt(match[0], 10) : 0;
};
// 提取完整前缀的辅助函数(包括字母和数字)
const extractPrefix = function(str) {
if (!str) return '';
// 匹配开头的所有非数字字符(字母等)和随后的数字部分,直到遇到第一个非数字字符为止
const match = str.match(/^[A-Za-z0-9]+/);
return match ? match[0] : '';
};
// 提取当前记录的前缀和数字范围
const currentPrefix = extractPrefix(record.startNum);
const currentStartNum = extractNumber(record.startNum);
const currentEndNum = extractNumber(record.endNum);
// 确保当前记录的起始号码小于等于终止号码
if (currentStartNum > currentEndNum) {
return { valid: false, message: '起始号码不能大于终止号码' };
}
for (const item of otherRecords) {
if (!item.startNum || !item.endNum) continue;
// 提取其他记录的前缀
const otherPrefix = extractPrefix(item.startNum);
// 检查前缀是否匹配(实现数字对应数字,字母对应字母的匹配规则)
if (currentPrefix !== otherPrefix) continue;
// 提取其他记录的数字范围
const otherStartNum = extractNumber(item.startNum);
const otherEndNum = extractNumber(item.endNum);
// 全面检查范围重叠
const hasOverlap =
(currentStartNum >= otherStartNum && currentStartNum <= otherEndNum) ||
(currentEndNum >= otherStartNum && currentEndNum <= otherEndNum) ||
(currentStartNum <= otherStartNum && currentEndNum >= otherEndNum) ||
(otherStartNum <= currentStartNum && otherEndNum >= currentEndNum);
if (hasOverlap) {
// 提供更详细的错误信息,指明与哪条记录的范围重叠
return {
valid: false,
message: '票据设置重复,与' + item.operator + '的票据范围' + item.startNum + '-' + item.endNum + '重叠'
};
}
}
return { valid: true };
},
// 检查当前号码是否在起始和终止范围内
validateCurrentNumberInRange(currentNum, startNum, endNum) {
// 如果当前号码为空,允许保存
if (!currentNum) return true;
// 数字部分比较(纯数字或字母前缀+数字)
const extractNumber = (str) => {
const match = str.match(/\d+$/);
return match ? parseInt(match[0], 10) : 0;
};
// 检查两个字符串的每一位是否类型匹配(数字对应数字,字母对应字母)
const checkCharacterTypesMatch = (str1, str2) => {
const maxLength = Math.max(str1.length, str2.length);
for (let i = 0; i < maxLength; i++) {
const char1 = str1[i] || '';
const char2 = str2[i] || '';
const isDigit1 = /\d/.test(char1);
const isDigit2 = /\d/.test(char2);
// 如果一个是数字而另一个不是,则类型不匹配
if (isDigit1 !== isDigit2) {
return false;
}
}
return true;
};
// 验证当前号码与起始号码、终止号码的字符类型匹配
if (!checkCharacterTypesMatch(currentNum, startNum) || !checkCharacterTypesMatch(currentNum, endNum)) {
return false;
}
const currentNumValue = extractNumber(currentNum);
const startNumValue = extractNumber(startNum);
const endNumValue = extractNumber(endNum);
return currentNumValue >= startNumValue && currentNumValue <= endNumValue;
},
validateRecord(record, rowIndex) {
const errors = [];
const rowInfo = rowIndex ? `${rowIndex}` : '';
// 验证操作员不为空
if (!record.operator || !record.employeeId) {
errors.push(`${rowInfo}: 未设置票据号码的员工`);
}
// 验证起始号码
const startNumValidation = this.validateInvoiceNumber(record.startNum);
if (!startNumValidation.valid) {
errors.push(`${rowInfo}: 起始号码 ${startNumValidation.message}`);
}
// 验证终止号码
const endNumValidation = this.validateInvoiceNumber(record.endNum);
if (!endNumValidation.valid) {
errors.push(`${rowInfo}: 终止号码 ${endNumValidation.message}`);
}
// 验证起始和终止号码长度一致
if (record.startNum && record.endNum && !this.validateNumberLengths(record.startNum, record.endNum)) {
errors.push(`${rowInfo}: 起始发票号码与终止发票号码长度必须完全一致`);
}
// 验证起始和终止号码前缀一致性
if (record.startNum && record.endNum) {
const startPrefix = this.extractPrefixFromEnd(record.startNum);
const endPrefix = this.extractPrefixFromEnd(record.endNum);
if (startPrefix !== endPrefix) {
errors.push(`${rowInfo}: 起始和终止号码前缀必须一致(前缀定义:从号码最末位开始往前推直到出现字母为止的前面字符)`);
}
}
// 验证当前号码前缀与起始号码前缀一致性
if (record.currentNum && record.startNum) {
const currentPrefix = this.extractPrefixFromEnd(record.currentNum);
const startPrefix = this.extractPrefixFromEnd(record.startNum);
if (currentPrefix !== startPrefix) {
errors.push(`${rowInfo}: 当前使用号码前缀与起始号码前缀必须一致(前缀定义:从号码最末位开始往前推直到出现字母为止的前面字符)`);
}
}
// 验证当前号码在范围内
if (record.currentNum && record.startNum && record.endNum &&
!this.validateCurrentNumberInRange(record.currentNum, record.startNum, record.endNum)) {
errors.push(`${rowInfo}: 当前使用号码不在有效范围内`);
}
// 验证号码唯一性
const uniquenessCheck = this.checkNumberUniqueness(record);
if (!uniquenessCheck.valid) {
errors.push(`${rowInfo}: ${uniquenessCheck.message}`);
}
// 检查号码范围是否与其他记录重叠
if (record.startNum && record.endNum) {
const overlapCheck = this.checkRangeOverlap(record);
if (!overlapCheck.valid) {
errors.push(`${rowInfo}: ${overlapCheck.message}`);
}
}
return errors;
},
// 执行所有验证
runAllValidations() {
this.validationErrors = [];
// 验证每个记录,添加行号信息
this.invoiceData.forEach((record, index) => {
const recordErrors = this.validateRecord(record, index + 1); // 行号从1开始
if (recordErrors.length > 0) {
this.validationErrors.push(...recordErrors);
}
});
return this.validationErrors.length === 0;
},
// 显示验证错误
showValidationErrors() {
if (this.validationErrors.length > 0) {
alert('验证失败:\n' + this.validationErrors.join('\n'));
}
},
// 保存数据
saveData() {
// 执行验证
if (!this.runAllValidations()) {
this.showValidationErrors();
return;
}
this.saveButtonText = '保存中...';
// 准备要保存的记录列表
const recordsToSave = this.invoiceData.filter(item => item.isActive || item.isNewRecord);
console.log(`准备保存 ${recordsToSave.length} 条记录`);
// 创建保存Promise数组
const savePromises = recordsToSave.map((record, index) => {
// 准备发送到后端的数据格式适配adm_invoice_segment表结构
const dataToSend = {
// 员工和开票员信息确保字段名称与后端API期望一致
invoicingStaffId: record.employeeId,
invoicingStaffName: record.operator || this.getUserNameById(record.employeeId),
employeeId: record.employeeId, // 同时提供employeeId字段以保持一致性
// 日期信息确保同时设置billBusDate和createDate字段
billBusDate: record.date ? new Date(record.date) : null,
createDate: record.date ? new Date(record.date) : null,
// 号码信息使用后端API期望的字段名
beginNumber: record.startNum,
startNum: record.startNum, // 同时提供startNum字段以保持一致性
endNumber: record.endNum,
endNum: record.endNum, // 同时提供endNum字段以保持一致性
currentNumber: record.currentNum,
currentNum: record.currentNum, // 同时提供currentNum字段以保持一致性
// 状态信息
status: record.status || '未使用',
statusEnum: { value: record.status || '未使用' }, // 同时提供statusEnum字段
// 备注信息
remark: record.currentNum ?
(record.remark && !record.remark.includes('当前使用号码:') ?
`${record.remark}; 当前使用号码:${record.currentNum}` :
`当前使用号码:${record.currentNum}`
) : (record.remark || ''),
// 添加租户ID和删除标志
tenantId: 1, // 假设默认租户ID为1
deleteFlag: '0'
};
// 对于更新记录设置id字段对于新记录不设置id让后端自动生成
if (!record.isNewRecord) {
dataToSend.id = record.id || record.keyId;
}
// 对于所有记录确保设置segmentId字段因为数据库表中segment_id是必填的
dataToSend.segmentId = record.segmentId || Date.now(); // 确保segmentId有值
// 添加调试信息,打印完整的记录对象
console.log(`准备${record.isNewRecord ? '新增' : '更新'}的原始记录对象:`, {
keyId: record.keyId,
id: record.id,
segmentId: record.segmentId,
...record
});
// 根据是否是新记录选择不同的API
const url = record.isNewRecord ? '/basicmanage/invoice-segment/add' : '/basicmanage/invoice-segment/update';
const operation = record.isNewRecord ? '新增' : '更新';
console.log(`${operation}记录 ${index + 1}:`, {
url: url,
data: dataToSend
});
return request({
url: url,
method: 'post',
data: dataToSend
}).then(response => {
console.log(`${operation}记录 ${index + 1} 成功:`, response);
return response;
}).catch(err => {
console.error(`${operation}记录 ${index + 1} 失败:`, err);
console.error(`${operation}记录 ${index + 1} 失败详情:`, JSON.stringify(err, null, 2));
console.error(`${operation}记录 ${index + 1} 请求数据:`, JSON.stringify(dataToSend, null, 2));
throw err; // 重新抛出错误以便上层catch捕获
});
});
// 批量保存数据
Promise.all(savePromises)
.then(results => {
console.log('批量保存发票数据成功:', results);
// 保存成功后将所有记录设为非编辑状态并移除isNewRecord标记
this.invoiceData.forEach(item => {
item.isActive = false;
// 移除新增记录标记,确保后续编辑再取消时不会被删除
if (item.isNewRecord) {
delete item.isNewRecord;
}
});
this.saveButtonText = '保存';
// 显示成功消息
const successMessage = `成功保存 ${results.length} 条发票段记录!`;
console.log(successMessage);
if (this.$message) {
this.$message.success(successMessage);
} else {
alert(successMessage);
}
// 重新加载数据以确保数据一致性
console.log('保存成功后重新加载数据...');
this.loadInvoiceData();
})
.catch(error => {
console.error('保存发票数据失败:', error);
console.error('错误详情:', error.response || error);
this.saveButtonText = '保存';
// 显示错误消息
let errorMessage = '保存数据失败';
if (error.response) {
errorMessage += `: ${error.response.status} ${error.response.statusText}`;
} else if (error.message) {
errorMessage += `: ${error.message}`;
}
console.error(errorMessage);
if (this.$message) {
this.$message.error(errorMessage);
} else {
alert(errorMessage);
}
});
},
},
mounted() {
// 页面加载后初始化数据
this.filterDataByPermission();
}
};
</script>
<style scoped>
.invoice-management {
padding: 20px;
min-height: 100vh;
background-color: #f0f2f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
padding: 20px;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #e6e8eb;
}
.header-section h2 {
margin: 0;
color: #1f2937;
font-size: 20px;
font-weight: 600;
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
}
.welcome-text {
font-size: 14px;
color: #606266;
}
.role-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.role-badge.admin {
background-color: #f0f9ff;
color: #2d8cf0;
border: 1px solid #d9ecff;
}
.role-badge.operator {
background-color: #f6ffed;
color: #67c23a;
border: 1px solid #d9f7be;
}
.button-group {
margin-bottom: 16px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.button-group .btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
font-size: 14px;
font-weight: 500;
border-radius: 4px;
transition: all 0.2s ease;
}
.button-group .btn:hover {
transform: translateY(-1px);
}
.error-messages {
margin-bottom: 16px;
background-color: #fef0f0;
border: 1px solid #fde2e2;
border-radius: 4px;
padding: 12px 16px;
}
.error-title {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: #f56c6c;
}
.error-list {
margin: 0;
padding-left: 20px;
}
.error-item {
margin-bottom: 4px;
font-size: 13px;
color: #e64340;
}
.table-container {
margin-bottom: 20px;
border-radius: 4px;
overflow: hidden;
border: 1px solid #ebeef5;
}
.table {
width: 100%;
margin: 0;
border-collapse: collapse;
}
.table thead {
background-color: #f5f7fa;
color: #606266;
}
.table th {
padding: 10px 12px;
font-weight: 600;
text-align: left;
border-bottom: 1px solid #ebeef5;
font-size: 13px;
}
.table td {
padding: 10px 12px;
vertical-align: middle;
border-bottom: 1px solid #ebeef5;
font-size: 13px;
color: #303133;
}
/* 序号单元格样式 */
.table td.sequence-number {
font-weight: 500;
color: #606266;
text-align: center;
width: 60px;
}
.table tbody tr:hover {
background-color: #fafafa;
}
.table tbody tr.editing-row {
background-color: #f0f9ff !important;
transition: background-color 0.2s ease;
}
.employee-info {
display: flex;
align-items: center;
gap: 8px;
}
.employee-name {
font-weight: 500;
color: #303133;
}
.employee-id {
font-size: 12px;
color: #909399;
background-color: #f4f4f5;
padding: 2px 6px;
border-radius: 8px;
}
.invoice-number {
display: inline-block;
padding: 4px 8px;
border-radius: 3px;
background-color: #f4f4f5;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
color: #303133;
}
.invoice-number.current {
background-color: #ecfdf5;
color: #0369a1;
border: 1px solid #d1fae5;
}
.form-control {
transition: all 0.2s ease;
font-size: 13px;
padding: 4px 8px;
}
.form-control:focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
.action-buttons {
white-space: nowrap;
}
.action-buttons .btn {
padding: 4px 8px;
font-size: 12px;
margin-right: 4px;
}
.no-data-row {
text-align: center;
padding: 30px 0 !important;
color: #909399;
font-style: normal;
background-color: #fafafa;
}
.info-section {
margin-top: 20px;
}
.info-card {
background-color: #f5f7fa;
border-left: 3px solid #409eff;
padding: 12px 16px;
border-radius: 4px;
}
.info-card h5 {
margin: 0 0 8px 0;
color: #303133;
font-size: 13px;
font-weight: 600;
}
.info-list {
margin: 0;
padding-left: 16px;
}
.info-list li {
margin-bottom: 4px;
font-size: 12px;
color: #606266;
line-height: 1.5;
}
/* 响应式设计 */
@media (max-width: 768px) {
.invoice-management {
padding: 12px;
}
.container {
padding: 16px;
border-radius: 4px;
}
.header-section {
flex-direction: column;
align-items: flex-start;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 8px;
}
.header-section h2 {
font-size: 18px;
}
.user-info {
flex-direction: column;
align-items: flex-start;
gap: 6px;
}
.button-group {
width: 100%;
gap: 6px;
}
.button-group .btn {
flex: 1;
justify-content: center;
min-width: 80px;
padding: 8px 12px;
}
.table-container {
border-radius: 4px;
overflow-x: auto;
}
.table {
font-size: 12px;
}
.table th,
.table td {
padding: 6px 8px;
white-space: nowrap;
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 4px;
}
.action-buttons .btn {
margin-right: 0;
padding: 4px 8px;
font-size: 11px;
}
.no-data-row {
padding: 20px 0 !important;
font-size: 12px;
}
.info-card {
padding: 10px 12px;
}
.info-list {
padding-left: 12px;
}
.info-list li {
font-size: 11px;
}
}
/* 平板设备优化 */
@media (min-width: 769px) and (max-width: 1024px) {
.container {
max-width: 95%;
}
.header-section h2 {
font-size: 19px;
}
.table th,
.table td {
padding: 8px 10px;
}
}
</style>