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

515 lines
16 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="app-container">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="onAdd">新设(A)</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="onDelete">删除(D)</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Check" :disabled="multiple" @click="() => onSave()">保存(S)</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Close" @click="onClose">关闭(X)</el-button>
</el-col>
<el-col v-if="canToggleViewAll" :span="4">
<el-switch
v-model="viewAll"
active-text="查看全部"
inactive-text="仅本人"
@change="getList"
/>
</el-col>
</el-row>
<el-table
v-loading="loading"
:data="tableData"
@selection-change="handleSelectionChange"
:row-class-name="tableRowClassName"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="操作员" prop="operatorName" min-width="120" />
<el-table-column label="员工工号" prop="staffNo" min-width="120" />
<el-table-column label="领用日期" prop="receiveDate" min-width="140">
<template #default="{ row }">
<el-date-picker
v-if="row._editing"
v-model="row.receiveDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
style="width: 100%"
/>
<span v-else>{{ row.receiveDate }}</span>
</template>
</el-table-column>
<el-table-column label="起始号码" prop="startNo" min-width="140">
<template #default="{ row }">
<el-input
v-if="row._editing"
v-model.trim="row.startNo"
@blur="() => validateNumField(row, 'startNo')"
/>
<span v-else>{{ row.startNo }}</span>
</template>
</el-table-column>
<el-table-column label="终止号码" prop="endNo" min-width="140">
<template #default="{ row }">
<el-input
v-if="row._editing"
v-model.trim="row.endNo"
@blur="() => validateNumField(row, 'endNo')"
/>
<span v-else>{{ row.endNo }}</span>
</template>
</el-table-column>
<el-table-column label="使用号码" prop="usedNo" min-width="140">
<template #default="{ row }">
<el-input
v-if="row._editing"
v-model.trim="row.usedNo"
@blur="() => validateNumField(row, 'usedNo')"
/>
<span v-else>{{ row.usedNo }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
<el-button type="primary" link icon="Edit" @click="() => openEdit(scope.row, scope.$index)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
<!-- 编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" append-to-body>
<el-form label-width="100px">
<el-form-item label="操作员">
<el-input v-model="editForm.operatorName" disabled />
</el-form-item>
<el-form-item label="员工工号">
<el-input v-model="editForm.staffNo" disabled />
</el-form-item>
<el-form-item label="领用日期">
<el-date-picker v-model="editForm.receiveDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
<el-form-item label="起始号码">
<el-input v-model.trim="editForm.startNo" @blur="() => validateNumField(editForm, 'startNo')" />
</el-form-item>
<el-form-item label="终止号码">
<el-input v-model.trim="editForm.endNo" @blur="() => validateNumField(editForm, 'endNo')" />
</el-form-item>
<el-form-item label="使用号码">
<el-input v-model.trim="editForm.usedNo" @blur="() => validateNumField(editForm, 'usedNo')" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="confirmEdit"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup name="InvoiceManagement">
import useUserStore from '@/store/modules/user'
import { getConfigKey } from '@/api/system/config'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const ids = ref([])
const multiple = ref(true)
const viewAll = ref(false)
const canToggleViewAll = ref(false)
const dialogVisible = ref(false)
const dialogTitle = ref('编辑门诊号码段')
const editIndex = ref(-1)
const editForm = reactive({
receiveDate: '',
startNo: '',
endNo: '',
usedNo: '',
operatorName: '',
staffNo: '',
})
const data = reactive({
queryParams: {
pageNo: 1,
pageSize: 10,
onlySelf: true,
},
})
const { queryParams } = toRefs(data)
initConfig()
getList()
async function initConfig() {
try {
const res = await getConfigKey('outpatient_no_view_all')
canToggleViewAll.value = (res?.msg === 'Y' || res?.data === 'Y')
} catch (e) {
canToggleViewAll.value = false
}
}
function handleSelectionChange(selection) {
ids.value = selection.map((item) => item.id)
multiple.value = !selection.length
}
function onAdd() {
const now = new Date()
const yyyy = now.getFullYear()
const mm = String(now.getMonth() + 1).padStart(2, '0')
const dd = String(now.getDate()).padStart(2, '0')
tableData.value.push({
id: undefined,
operatorId: userStore.id,
operatorName: userStore.name || userStore.nickName,
staffNo: userStore.id,
receiveDate: `${yyyy}-${mm}-${dd}`,
startNo: '',
endNo: '',
usedNo: '',
_editing: true,
_error: false,
})
}
function onClose() {
proxy.$tab.closePage()
}
function tableRowClassName({ row }) {
return row._error ? 'error-row' : ''
}
function openEdit(row, index) {
editIndex.value = index
dialogTitle.value = '编辑门诊号码段'
editForm.receiveDate = row.receiveDate
editForm.startNo = row.startNo
editForm.endNo = row.endNo
editForm.usedNo = row.usedNo
editForm.operatorName = row.operatorName
editForm.staffNo = row.staffNo
dialogVisible.value = true
}
function confirmEdit() {
const tmp = { ...tableData.value[editIndex.value], ...editForm }
if (!validateRow(tmp, editIndex.value)) return
tableData.value[editIndex.value] = {
...tableData.value[editIndex.value],
...editForm,
_dirty: true, // 标记为已修改,顶部保存时提交
}
dialogVisible.value = false
}
function extractPrefix(value) {
if (!value) return ''
const chars = value.split('')
for (let i = chars.length - 1; i >= 0; i--) {
if (/[A-Za-z]/.test(chars[i])) {
return value.slice(0, i)
}
}
return ''
}
function extractTailNumber(value) {
if (!value) return NaN
const m = value.match(/(\d+)$/)
if (!m) return NaN
return parseInt(m[1], 10)
}
function lengthWithinLimit(value) {
if (!value) return false
const m = value.match(/(\d+)$/)
if (!m) return false
return m[1].length <= 12
}
function rangesOverlap(aStart, aEnd, bStart, bEnd) {
return Math.max(aStart, bStart) <= Math.min(aEnd, bEnd)
}
function alertWarn(msg) {
if (proxy.$modal && proxy.$modal.alertWarning) {
proxy.$modal.alertWarning(msg)
} else if (proxy.$message) {
proxy.$message.warning(msg)
}
}
// 校验必须以数字结尾且尾部数字长度≤12
function isTailDigitsValid(value) {
const m = String(value || '').match(/(\d+)$/)
return !!m && m[1].length <= 12
}
function validateNumField(row, field, rowIndex) {
if (!isTailDigitsValid(row[field])) {
const idxInTable = typeof rowIndex === 'number' ? rowIndex : tableData.value.indexOf(row)
const lineNo = idxInTable >= 0 ? idxInTable + 1 : (editIndex.value >= 0 ? editIndex.value + 1 : undefined)
const msg = lineNo ? `第【${lineNo}】行数据中最大位数为12位且必须以数字结尾` : '最大位数为12位且必须以数字结尾'
alertWarn(msg)
row._error = true
row._warned = row._warned || {}
row._warned[field] = true
return false
}
row._error = false
row._warned = row._warned || {}
row._warned[field] = false
return true
}
function onNumberInput(row, field) {
row._warned = row._warned || {}
const valid = isTailDigitsValid(row[field])
if (!valid && !row._warned[field]) {
alertWarn('最大位数为12位且必须以数字结尾')
row._warned[field] = true
}
if (valid) {
row._warned[field] = false
}
}
function validateRow(row, rowIndex) {
row._error = false
if (!lengthWithinLimit(row.startNo) || !lengthWithinLimit(row.endNo) || !lengthWithinLimit(row.usedNo)) {
const idxInTable = typeof rowIndex === 'number' ? rowIndex : tableData.value.indexOf(row)
const lineNo = idxInTable >= 0 ? idxInTable + 1 : (editIndex.value >= 0 ? editIndex.value + 1 : undefined)
const msg = lineNo ? `第【${lineNo}】行数据中最大位数为12位且必须以数字结尾` : '最大位数为12位且必须以数字结尾'
alertWarn(msg)
row._error = true
return false
}
if ((row.startNo?.length || 0) !== (row.endNo?.length || 0)) {
const idxInTable = typeof rowIndex === 'number' ? rowIndex : tableData.value.indexOf(row)
const lineNo = idxInTable >= 0 ? idxInTable + 1 : (editIndex.value >= 0 ? editIndex.value + 1 : undefined)
const msg = lineNo ? `第【${lineNo}】行数据中,起始号码与终止号码长度必须一致,请修改!` : '起始号码与终止号码长度必须一致'
alertWarn(msg)
row._error = true
return false
}
const p1 = extractPrefix(row.startNo)
const p2 = extractPrefix(row.endNo)
const p3 = extractPrefix(row.usedNo)
if (!(p1 === p2 && p2 === p3)) {
const idxInTable = typeof rowIndex === 'number' ? rowIndex : tableData.value.indexOf(row)
const lineNo = idxInTable >= 0 ? idxInTable + 1 : (editIndex.value >= 0 ? editIndex.value + 1 : undefined)
const msg = lineNo ? `第【${lineNo}】行数据中,门诊号码的字母前缀必须相同,请修改!` : '行数据中,门诊号码的字母前缀必须相同,请修改!'
alertWarn(msg)
row._error = true
return false
}
const sNum = extractTailNumber(row.startNo)
const eNum = extractTailNumber(row.endNo)
if (Number.isNaN(sNum) || Number.isNaN(eNum) || sNum > eNum) {
const idxInTable = typeof rowIndex === 'number' ? rowIndex : tableData.value.indexOf(row)
const lineNo = idxInTable >= 0 ? idxInTable + 1 : (editIndex.value >= 0 ? editIndex.value + 1 : undefined)
const msg = lineNo ? `第【${lineNo}】行数据中,起始/终止号码不合法` : '起始/终止号码不合法'
alertWarn(msg)
row._error = true
return false
}
// 使用号码必须在起始与终止范围内
const uNum = extractTailNumber(row.usedNo)
if (Number.isNaN(uNum) || uNum < sNum || uNum > eNum) {
const idxInTable = typeof rowIndex === 'number' ? rowIndex : tableData.value.indexOf(row)
const lineNo = idxInTable >= 0 ? idxInTable + 1 : (editIndex.value >= 0 ? editIndex.value + 1 : undefined)
const msg = lineNo ? `第【${lineNo}】行数据中,使用号码必须在起始与终止号码范围内` : '使用号码必须在起始与终止号码范围内'
alertWarn(msg)
row._error = true
return false
}
const prefix = p1
for (let i = 0; i < tableData.value.length; i++) {
const other = tableData.value[i]
if (other === row || !other.startNo || !other.endNo) continue
if (extractPrefix(other.startNo) !== prefix) continue
const os = extractTailNumber(other.startNo)
const oe = extractTailNumber(other.endNo)
if (!Number.isNaN(os) && !Number.isNaN(oe)) {
if (rangesOverlap(sNum, eNum, os, oe)) {
const idxInTable = typeof rowIndex === 'number' ? rowIndex : tableData.value.indexOf(row)
const lineNo = idxInTable >= 0 ? idxInTable + 1 : (editIndex.value >= 0 ? editIndex.value + 1 : undefined)
const msg = lineNo ? `第【${lineNo}】行数据中,门诊号码和【${i + 1}】行的门诊号码有冲突,请修改!` : '门诊号码设置重复!'
alertWarn(msg)
row._error = true
return false
}
}
}
return true
}
function onSave(row) {
const rows = row ? [row] : tableData.value.filter(r => ids.value.includes(r.id) || r._dirty || r._editing)
if (!rows.length) return
for (const r of rows) {
const idx = tableData.value.indexOf(r)
if (!validateRow(r, idx)) return
}
const ok = lcUpsertMany(rows.map((r) => ({
id: r.id,
operatorId: r.operatorId,
operatorName: r.operatorName,
staffNo: r.staffNo,
receiveDate: r.receiveDate,
startNo: r.startNo,
endNo: r.endNo,
usedNo: r.usedNo,
})))
if (!ok) return
if (proxy.$modal?.alertSuccess) {
proxy.$modal.alertSuccess('保存成功!')
} else {
proxy.$message.success('保存成功!')
}
getList()
}
function onDelete() {
const rows = tableData.value.filter((r) => ids.value.includes(r.id))
if (!rows.length) return
for (const r of rows) {
const canDeleteSelf = String(r.operatorId) === String(userStore.id)
const neverUsed = r.usedNo === r.startNo
if (!canDeleteSelf) {
alertWarn('只能删除自己维护的门诊号码段')
return
}
if (!neverUsed) {
alertWarn('已有门诊号码段已有使用的门诊号码,请核对!')
return
}
}
const doRealDelete = () => {
lcDeleteByIds(rows.map((r) => r.id))
if (proxy.$modal?.alertSuccess) {
proxy.$modal.alertSuccess('删除成功')
} else {
proxy.$message.success('删除成功')
}
getList()
}
if (proxy.$modal?.confirm) {
proxy.$modal.confirm('是否确认删除选中数据项?').then(doRealDelete)
} else {
doRealDelete()
}
}
function getList() {
loading.value = true
queryParams.value.onlySelf = !viewAll.value
const res = lcList({ ...queryParams.value })
tableData.value = res.records.map((it) => ({
...it,
_editing: false,
_error: false,
_dirty: false,
}))
total.value = res.total
loading.value = false
}
// (测试辅助功能已移除)
// 纯前端本地持久化方法localStorage
const STORAGE_KEY = 'ohis_outpatient_no_segments'
function lcReadAll() {
try {
const raw = localStorage.getItem(STORAGE_KEY)
const arr = raw ? JSON.parse(raw) : []
return Array.isArray(arr) ? arr : []
} catch (e) {
return []
}
}
function lcWriteAll(list) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(list || []))
}
function lcList({ pageNo = 1, pageSize = 10, onlySelf = true }) {
const all = lcReadAll()
const filtered = onlySelf ? all.filter((x) => String(x.operatorId) === String(userStore.id)) : all
const start = (pageNo - 1) * pageSize
const end = start + pageSize
return { records: filtered.slice(start, end), total: filtered.length, all }
}
function checkOverlapAll(row, all) {
const prefix = extractPrefix(row.startNo)
const sNum = extractTailNumber(row.startNo)
const eNum = extractTailNumber(row.endNo)
for (const it of all) {
if (row.id && it.id === row.id) continue
if (!it.startNo || !it.endNo) continue
if (extractPrefix(it.startNo) !== prefix) continue
const os = extractTailNumber(it.startNo)
const oe = extractTailNumber(it.endNo)
if (!Number.isNaN(os) && !Number.isNaN(oe)) {
if (rangesOverlap(sNum, eNum, os, oe)) return true
}
}
return false
}
function lcUpsertMany(rows) {
const all = lcReadAll()
for (const r of rows) {
if (checkOverlapAll(r, all)) {
alertWarn('门诊号码设置重复!')
return false
}
if (!r.id) {
r.id = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
}
const idx = all.findIndex((x) => x.id === r.id)
if (idx >= 0) all[idx] = { ...all[idx], ...r }
else all.push({ ...r })
}
lcWriteAll(all)
return true
}
function lcDeleteByIds(idList) {
const all = lcReadAll()
const remain = all.filter((x) => !idList.includes(x.id))
lcWriteAll(remain)
}
</script>
<style scoped>
.error-row { --el-table-tr-bg-color: #fff7e6; }
.mb8 { margin-bottom: 8px; }
</style>