1642 lines
54 KiB
Vue
1642 lines
54 KiB
Vue
<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> |