门诊号码管理维护界面

This commit is contained in:
2025-11-06 09:11:26 +08:00
committed by wzk
parent 9997f4f7c9
commit 5a99fe8234
3 changed files with 556 additions and 0 deletions

View File

@@ -0,0 +1,513 @@
<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="outpatientNoManagement">
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()
// 解决从标签页关闭后再次进入页面空白的问题:
// 当页面被 keep-alive 缓存后再次激活,主动刷新列表
onActivated(() => {
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 prefix = p1
for (let i = 0; i < tableData.value.length; i++) {
const other = tableData.value[i]
// 跳过自身:当从弹窗校验时 row 为临时对象,需用下标判断
if ((typeof rowIndex === 'number' && i === rowIndex) || 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>