Compare commits

..

2 Commits

Author SHA1 Message Date
叶锦涛
4ba4f80946 修改发票管理页面 2025-11-25 15:39:29 +08:00
叶锦涛
ed7cb2dab5 修改发票管理页面 2025-11-25 15:30:33 +08:00
2 changed files with 100 additions and 119 deletions

View File

@@ -1,9 +0,0 @@
-- 为adm_healthcare_service表添加practitionerId字段
-- 执行时间2024年
-- 描述添加出诊医生ID字段以支持挂号费管理中的出诊医生功能
ALTER TABLE "public"."adm_healthcare_service"
ADD COLUMN "practitioner_id" int8;
-- 为新字段添加注释
COMMENT ON COLUMN "public"."adm_healthcare_service"."practitioner_id" IS '出诊医生ID';

View File

@@ -4,12 +4,6 @@
<!-- 页面标题和用户信息 --> <!-- 页面标题和用户信息 -->
<div class="header-section"> <div class="header-section">
<h2>发票管理</h2> <h2>发票管理</h2>
<div class="user-info">
<span class="welcome-text">欢迎, {{ currentUser.name }}</span>
<span class="role-badge" :class="currentUser.role">
{{ currentUser.role === 'admin' ? '管理员' : '操作员' }}
</span>
</div>
</div> </div>
<!-- 操作按钮区域 --> <!-- 操作按钮区域 -->
@@ -58,35 +52,14 @@
<td class="sequence-number">{{ index + 1 }}</td> <td class="sequence-number">{{ index + 1 }}</td>
<td class="employee-info"> <td class="employee-info">
<div class="input-container"> <div class="input-container">
<select <!-- 操作员字段始终不可编辑 -->
v-if="item.isActive" <span class="employee-name">{{ item.operator || '-' }}</span>
v-model="item.operator"
class="form-control"
>
<option value="">请选择操作员</option>
<option
v-for="user in userList"
:key="user.employeeId"
:value="user.name"
:data-employee-id="user.employeeId"
@change="updateEmployeeId(item, user.employeeId)"
>
{{ user.name }}
</option>
</select>
<span v-else class="employee-name">{{ item.operator || '-' }}</span>
</div> </div>
</td> </td>
<td class="employee-id-cell"> <td class="employee-id-cell">
<div class="input-container"> <div class="input-container">
<input <!-- 员工工号字段始终不可编辑 -->
v-if="item.isActive" <span>{{ item.employeeId || '-' }}</span>
v-model="item.employeeId"
class="form-control"
placeholder="请输入员工工号"
maxlength="10"
/>
<span v-else>{{ item.employeeId || '-' }}</span>
</div> </div>
</td> </td>
<td class="date-cell"> <td class="date-cell">
@@ -191,6 +164,7 @@
<script> <script>
import { listUser } from '@/api/system/user'; import { listUser } from '@/api/system/user';
import request from '@/utils/request'; // 导入请求工具 import request from '@/utils/request'; // 导入请求工具
import useUserStore from '@/store/modules/user'; // 导入用户store
export default { export default {
name: 'InvoiceManagement', name: 'InvoiceManagement',
@@ -198,9 +172,10 @@ export default {
return { return {
// 用户信息和权限 // 用户信息和权限
currentUser: { currentUser: {
name: 'admin', name: '',
employeeId: '1702', nickName: '',
role: 'admin' // operator: 普通操作员, admin: 管理员 employeeId: '',
role: '' // operator: 普通操作员, admin: 管理员
}, },
// 用户列表用于操作员下拉选择将在created中从后端获取 // 用户列表用于操作员下拉选择将在created中从后端获取
userList: [], userList: [],
@@ -222,6 +197,17 @@ export default {
}, },
created() { created() {
console.log('组件初始化,开始加载数据'); 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(() => { this.getUserList().then(() => {
console.log('用户列表加载完成,开始加载发票数据'); console.log('用户列表加载完成,开始加载发票数据');
@@ -247,12 +233,17 @@ export default {
methods: { methods: {
// 获取用户列表 // 获取用户列表
getUserList() { getUserList() {
return listUser().then((res) => { // 传递分页参数,获取所有用户数据
const queryParams = {
pageNum: 1,
pageSize: 1000 // 设置较大的pageSize以获取所有用户
};
return listUser(queryParams).then((res) => {
// 从响应中提取用户列表,并转换为需要的格式 // 从响应中提取用户列表,并转换为需要的格式
this.userList = res.data.records.map(user => ({ this.userList = res.data.records.map(user => ({
name: user.nickName, // 使用用户昵称作为显示名称 name: user.nickName || user.username || user.name, // 尝试多种可能的名称字段
employeeId: user.userId, // 使用用户ID作为员工工号 employeeId: user.userId, // 使用用户ID作为员工工号
role: user.practitionerRolesDtoList?.some(role => role.roleKey === 'admin') ? 'admin' : 'operator' role: user.role || 'operator' // 默认为普通操作员
})); }));
console.log('获取到的用户列表:', this.userList); console.log('获取到的用户列表:', this.userList);
return Promise.resolve(); return Promise.resolve();
@@ -451,42 +442,33 @@ export default {
return user ? user.name : ''; 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() { addNewRow() {
// 新增行时自动填充当前用户信息 // 新增行时自动填充当前用户信息
// 使用负数作为临时ID避免与后端数据库ID冲突 // 使用负数作为临时ID避免与后端数据库ID冲突
const newId = -Math.max(...this.invoiceData.map(item => Math.abs(item.id)), 0) - 1; let maxId = 0;
this.invoiceData.push({ 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, id: newId,
segmentId: newSegmentId, // 为新记录设置segmentId
operator: this.currentUser.name, // 自动使用当前用户名称 operator: this.currentUser.name, // 自动使用当前用户名称
employeeId: this.currentUser.employeeId, employeeId: this.currentUser.employeeId,
date: new Date().toISOString().split('T')[0], date: currentDate, // 自动填充当日日期
startNum: '', startNum: '',
endNum: '', endNum: '',
currentNum: '', currentNum: '',
status: '未使用', status: '未使用',
isActive: true, // 新增行默认处于编辑状态 isActive: true, // 新增行默认处于编辑状态
isNewRecord: true // 添加标记表示这是新记录 isNewRecord: true // 添加标记表示这是新记录
}); };
console.log('添加新行,自动填充领用日期为当日:', { newRecord });
this.invoiceData.push(newRecord);
}, },
deleteRow(record) { deleteRow(record) {
@@ -495,6 +477,19 @@ export default {
return; 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) { if (this.currentUser.role !== 'admin' && record.employeeId !== this.currentUser.employeeId) {
alert('您没有权限删除此记录!操作员只能删除自己的记录。'); alert('您没有权限删除此记录!操作员只能删除自己的记录。');
@@ -815,17 +810,15 @@ export default {
}, },
// 从字符串末尾向前提取前缀,直到遇到第一个字母 // 从字符串末尾向前提取前缀,直到遇到第一个字母
// 规则:前缀定义为从号码最末位开始往前推直到出现字母为止前面字符若无字母则无前缀 // 规则:前缀定义为从号码最末位开始往前推直到出现字母为止,其前面字符全部称为前缀;若无字母则无前缀
extractPrefixFromEnd(str) { extractPrefixFromEnd(str) {
if (!str) return ''; if (!str) return '';
// 从末尾向前遍历字符串 // 从末尾向前遍历字符串
for (let i = str.length - 1; i >= 0; i--) { for (let i = str.length - 1; i >= 0; i--) {
// 如果遇到字母,返回从开始到该字母之前的部分 // 如果遇到字母,返回从开始到该字母的所有字符
// 例如:对于"ABC123",从末尾向前找到第一个字母'C',前缀就是'AB'
// 对于"123ABC456",从末尾向前找到第一个字母'C',前缀就是"123AB"
if (/[A-Za-z]/.test(str[i])) { if (/[A-Za-z]/.test(str[i])) {
return str.substring(0, i); return str.substring(0, i + 1);
} }
} }
// 如果没有找到字母,则无前缀 // 如果没有找到字母,则无前缀
@@ -924,6 +917,21 @@ export default {
}, },
// 检查号码范围是否与其他记录重叠 // 检查号码范围是否与其他记录重叠
// 提取数字部分进行比较的辅助方法
extractNumber(str) {
if (!str) return 0;
const match = str.match(/\d+$/);
return match ? parseInt(match[0], 10) : 0;
},
// 提取完整前缀的辅助方法(包括字母和数字)
extractPrefix(str) {
if (!str) return '';
// 匹配开头的所有非数字字符(字母等)和随后的数字部分,直到遇到第一个非数字字符为止
const match = str.match(/^[A-Za-z0-9]+/);
return match ? match[0] : '';
},
checkRangeOverlap(record) { checkRangeOverlap(record) {
// 确保当前记录有必要的数据 // 确保当前记录有必要的数据
if (!record || !record.startNum || !record.endNum) { if (!record || !record.startNum || !record.endNum) {
@@ -934,25 +942,10 @@ export default {
// 使用keyId作为比较依据因为它是前端唯一标识符 // 使用keyId作为比较依据因为它是前端唯一标识符
const otherRecords = this.invoiceData.filter(item => item.keyId !== record.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 currentPrefix = this.extractPrefix(record.startNum);
const currentStartNum = extractNumber(record.startNum); const currentStartNum = this.extractNumber(record.startNum);
const currentEndNum = extractNumber(record.endNum); const currentEndNum = this.extractNumber(record.endNum);
// 确保当前记录的起始号码小于等于终止号码 // 确保当前记录的起始号码小于等于终止号码
if (currentStartNum > currentEndNum) { if (currentStartNum > currentEndNum) {
@@ -963,14 +956,14 @@ export default {
if (!item.startNum || !item.endNum) continue; if (!item.startNum || !item.endNum) continue;
// 提取其他记录的前缀 // 提取其他记录的前缀
const otherPrefix = extractPrefix(item.startNum); const otherPrefix = this.extractPrefix(item.startNum);
// 检查前缀是否匹配(实现数字对应数字,字母对应字母的匹配规则) // 检查前缀是否匹配(实现数字对应数字,字母对应字母的匹配规则)
if (currentPrefix !== otherPrefix) continue; if (currentPrefix !== otherPrefix) continue;
// 提取其他记录的数字范围 // 提取其他记录的数字范围
const otherStartNum = extractNumber(item.startNum); const otherStartNum = this.extractNumber(item.startNum);
const otherEndNum = extractNumber(item.endNum); const otherEndNum = this.extractNumber(item.endNum);
// 全面检查范围重叠 // 全面检查范围重叠
const hasOverlap = const hasOverlap =
@@ -996,12 +989,6 @@ export default {
// 如果当前号码为空,允许保存 // 如果当前号码为空,允许保存
if (!currentNum) return true; if (!currentNum) return true;
// 数字部分比较(纯数字或字母前缀+数字)
const extractNumber = (str) => {
const match = str.match(/\d+$/);
return match ? parseInt(match[0], 10) : 0;
};
// 检查两个字符串的每一位是否类型匹配(数字对应数字,字母对应字母) // 检查两个字符串的每一位是否类型匹配(数字对应数字,字母对应字母)
const checkCharacterTypesMatch = (str1, str2) => { const checkCharacterTypesMatch = (str1, str2) => {
const maxLength = Math.max(str1.length, str2.length); const maxLength = Math.max(str1.length, str2.length);
@@ -1023,18 +1010,13 @@ export default {
return false; return false;
} }
const currentNumValue = extractNumber(currentNum); const currentNumValue = this.extractNumber(currentNum);
const startNumValue = extractNumber(startNum); const startNumValue = this.extractNumber(startNum);
const endNumValue = extractNumber(endNum); const endNumValue = this.extractNumber(endNum);
return currentNumValue >= startNumValue && currentNumValue <= endNumValue; return currentNumValue >= startNumValue && currentNumValue <= endNumValue;
}, },
// 验证单个记录
// 测试用例说明:
// 1. 测试起始和终止号码前缀一致性:例如"AB123"和"AB456"通过,"AB123"和"AC456"失败
// 2. 测试当前号码与起始号码前缀一致性:例如"AB123"作为起始,"AB125"作为当前号码通过,"AC125"失败
// 3. 测试无字母情况:纯数字"123456"作为起始、终止和当前号码都通过验证
validateRecord(record, rowIndex) { validateRecord(record, rowIndex) {
const errors = []; const errors = [];
const rowInfo = rowIndex ? `${rowIndex}` : ''; const rowInfo = rowIndex ? `${rowIndex}` : '';
@@ -1145,24 +1127,23 @@ export default {
const savePromises = recordsToSave.map((record, index) => { const savePromises = recordsToSave.map((record, index) => {
// 准备发送到后端的数据格式适配adm_invoice_segment表结构 // 准备发送到后端的数据格式适配adm_invoice_segment表结构
const dataToSend = { const dataToSend = {
// 对于更新操作同时传递id和segmentId确保后端能正确识别 // 员工和开票员信息确保字段名称与后端API期望一致
...(!record.isNewRecord && { id: record.keyId }),
// 确保segmentId不为null优先使用keyId然后是segmentId新记录时使用生成的临时ID
segmentId: record.keyId || record.segmentId || Date.now(),
// 员工和开票员信息
employeeId: record.employeeId,
employeeName: record.operator || this.getUserNameById(record.employeeId),
invoicingStaffId: record.employeeId, invoicingStaffId: record.employeeId,
invoicingStaffName: record.operator || this.getUserNameById(record.employeeId), invoicingStaffName: record.operator || this.getUserNameById(record.employeeId),
// 日期信息 employeeId: record.employeeId, // 同时提供employeeId字段以保持一致性
// 日期信息确保同时设置billBusDate和createDate字段
billBusDate: record.date ? new Date(record.date) : null, billBusDate: record.date ? new Date(record.date) : null,
createDate: record.date ? new Date(record.date) : null, createDate: record.date ? new Date(record.date) : null,
// 号码信息,使用数据库表对应的字段名 // 号码信息,使用后端API期望的字段名
beginNumber: record.startNum, beginNumber: record.startNum,
startNum: record.startNum, // 同时提供startNum字段以保持一致性
endNumber: record.endNum, endNumber: record.endNum,
endNum: record.endNum, // 同时提供endNum字段以保持一致性
currentNumber: record.currentNum, currentNumber: record.currentNum,
currentNum: record.currentNum, // 同时提供currentNum字段以保持一致性
// 状态信息 // 状态信息
status: record.status || '未使用', status: record.status || '未使用',
statusEnum: { value: record.status || '未使用' }, // 同时提供statusEnum字段
// 备注信息 // 备注信息
remark: record.currentNum ? remark: record.currentNum ?
(record.remark && !record.remark.includes('当前使用号码:') ? (record.remark && !record.remark.includes('当前使用号码:') ?
@@ -1174,6 +1155,13 @@ export default {
deleteFlag: '0' 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 ? '新增' : '更新'}的原始记录对象:`, { console.log(`准备${record.isNewRecord ? '新增' : '更新'}的原始记录对象:`, {
keyId: record.keyId, keyId: record.keyId,
@@ -1200,6 +1188,8 @@ export default {
return response; return response;
}).catch(err => { }).catch(err => {
console.error(`${operation}记录 ${index + 1} 失败:`, 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捕获 throw err; // 重新抛出错误以便上层catch捕获
}); });
}); });