Files
his/openhis-ui-vue3/src/views/basicmanage/outpatientNoManagement/index.vue

514 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="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>