|
|
|
@@ -0,0 +1,258 @@ |
|
|
|
{% extends "./layout.html" %} |
|
|
|
|
|
|
|
{% block title %}医院管理{% endblock %} |
|
|
|
|
|
|
|
{% block css %} |
|
|
|
<style> |
|
|
|
.drag-handle { cursor: move; color: #999; font-size: 18px; } |
|
|
|
.drag-handle:hover { color: #ff7800; } |
|
|
|
.sortable-ghost { opacity: 0.4; background: #fff7ed; } |
|
|
|
</style> |
|
|
|
{% endblock %} |
|
|
|
|
|
|
|
{% block content %} |
|
|
|
<div id="hospitalApp" v-cloak> |
|
|
|
<el-card shadow="never"> |
|
|
|
<template #header> |
|
|
|
<div class="flex flex-col gap-3"> |
|
|
|
<div class="flex items-center justify-between"> |
|
|
|
<div class="flex items-center gap-3"> |
|
|
|
<el-input v-model="keyword" placeholder="搜索医院名称" style="width:240px;" clearable @keyup.enter="onSearch"></el-input> |
|
|
|
<el-button type="primary" @click="onSearch">搜索</el-button> |
|
|
|
<el-button @click="resetQuery">重置</el-button> |
|
|
|
</div> |
|
|
|
<span class="text-xs text-gray-400">拖拽行可调整排序</span> |
|
|
|
</div> |
|
|
|
<div class="flex items-center gap-3"> |
|
|
|
<el-button type="primary" :icon="Plus" @click="showAdd">新增医院</el-button> |
|
|
|
<el-button :icon="Upload" @click="showImport = true">导入医院</el-button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<el-table :data="list" v-loading="loading" stripe border row-key="id"> |
|
|
|
<el-table-column width="60" align="center"> |
|
|
|
<template #header>排序</template> |
|
|
|
<template #default> |
|
|
|
<span class="drag-handle">⠿</span> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
<el-table-column prop="name" label="医院名称"></el-table-column> |
|
|
|
<el-table-column label="是否展示" width="120" align="center"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<el-switch :model-value="row.is_show === 1" @change="(v) => handleToggle(row, v)" /> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
<el-table-column label="操作" width="180" align="center"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<el-button type="primary" link @click="showEdit(row)">编辑</el-button> |
|
|
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
</el-table> |
|
|
|
|
|
|
|
<div class="flex justify-end mt-4"> |
|
|
|
<el-pagination background layout="total, sizes, prev, pager, next" :total="total" :page-sizes="[10, 20, 50, 100]" |
|
|
|
v-model:page-size="pageSize" v-model:current-page="page" @current-change="loadList" @size-change="onSizeChange"></el-pagination> |
|
|
|
</div> |
|
|
|
</el-card> |
|
|
|
|
|
|
|
<!-- 新增/编辑弹窗 --> |
|
|
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="460px" destroy-on-close draggable :close-on-click-modal="false"> |
|
|
|
<el-form :model="form" label-width="90px"> |
|
|
|
<el-form-item label="医院名称" required> |
|
|
|
<el-input v-model="form.name" placeholder="请输入医院名称" maxlength="100" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="排序"> |
|
|
|
<el-input-number v-model="form.sort" :min="0" :max="9999" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="是否展示"> |
|
|
|
<el-switch v-model="form.is_show" :active-value="1" :inactive-value="0" /> |
|
|
|
</el-form-item> |
|
|
|
</el-form> |
|
|
|
<template #footer> |
|
|
|
<el-button @click="dialogVisible = false">取消</el-button> |
|
|
|
<el-button type="primary" @click="handleSave" :loading="saving">确定</el-button> |
|
|
|
</template> |
|
|
|
</el-dialog> |
|
|
|
|
|
|
|
<!-- 批量导入弹窗 --> |
|
|
|
<el-dialog v-model="showImport" title="批量导入医院" width="500px" destroy-on-close :close-on-click-modal="false"> |
|
|
|
<p style="font-size:13px;color:var(--el-color-primary);margin-bottom:12px;">每行输入一个医院名称,重复的将自动跳过。</p> |
|
|
|
<el-input v-model="importText" type="textarea" :rows="10" :placeholder="importPlaceholder"></el-input> |
|
|
|
<template #footer> |
|
|
|
<div style="text-align:right;"> |
|
|
|
<el-button @click="showImport = false">取消</el-button> |
|
|
|
<el-button type="primary" @click="handleImport" :loading="importing">确认导入</el-button> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
</el-dialog> |
|
|
|
</div> |
|
|
|
{% endblock %} |
|
|
|
|
|
|
|
{% block js %} |
|
|
|
<script src="/static/lib/sortablejs/Sortable.min.js"></script> |
|
|
|
<script> |
|
|
|
const { createApp, ref, reactive, onMounted, nextTick } = Vue; |
|
|
|
const { Plus, Upload } = ElementPlusIconsVue; |
|
|
|
|
|
|
|
const app = createApp({ |
|
|
|
delimiters: ['${', '}'], |
|
|
|
setup() { |
|
|
|
const loading = ref(false); |
|
|
|
const list = ref([]); |
|
|
|
const keyword = ref(''); |
|
|
|
const total = ref(0); |
|
|
|
const page = ref(1); |
|
|
|
const pageSize = ref(10); |
|
|
|
const dialogVisible = ref(false); |
|
|
|
const dialogTitle = ref('新增医院'); |
|
|
|
const saving = ref(false); |
|
|
|
const form = reactive({ id: null, name: '', sort: 0, is_show: 1 }); |
|
|
|
const showImport = ref(false); |
|
|
|
const importText = ref(''); |
|
|
|
const importing = ref(false); |
|
|
|
const importPlaceholder = '北京协和医院\n复旦大学附属肿瘤医院\n中山大学肿瘤防治中心'; |
|
|
|
let sortableInstance = null; |
|
|
|
|
|
|
|
async function loadList() { |
|
|
|
loading.value = true; |
|
|
|
try { |
|
|
|
const params = new URLSearchParams({ |
|
|
|
keyword: keyword.value, page: page.value, pageSize: pageSize.value |
|
|
|
}); |
|
|
|
const res = await fetch('/admin/hospital/list?' + params).then(r => r.json()); |
|
|
|
if (res.code === 0) { |
|
|
|
list.value = res.data.data || []; |
|
|
|
total.value = res.data.count || 0; |
|
|
|
nextTick(() => initSortable()); |
|
|
|
} |
|
|
|
} finally { loading.value = false; } |
|
|
|
} |
|
|
|
|
|
|
|
function initSortable() { |
|
|
|
if (sortableInstance) { sortableInstance.destroy(); sortableInstance = null; } |
|
|
|
const tbody = document.querySelector('#hospitalApp .el-table__body-wrapper tbody'); |
|
|
|
if (!tbody) return; |
|
|
|
sortableInstance = Sortable.create(tbody, { |
|
|
|
handle: '.drag-handle', |
|
|
|
animation: 150, |
|
|
|
ghostClass: 'sortable-ghost', |
|
|
|
onEnd(evt) { |
|
|
|
if (evt.oldIndex === evt.newIndex) return; |
|
|
|
const arr = list.value.slice(); |
|
|
|
const item = arr.splice(evt.oldIndex, 1)[0]; |
|
|
|
arr.splice(evt.newIndex, 0, item); |
|
|
|
list.value = arr; |
|
|
|
fetch('/admin/hospital/sort', { |
|
|
|
method: 'POST', |
|
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
|
body: JSON.stringify({ ids: arr.map(i => i.id) }) |
|
|
|
}).then(r => r.json()).then(res => { |
|
|
|
if (res.code === 0) ElementPlus.ElMessage.success('排序已保存'); |
|
|
|
else loadList(); |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function showAdd() { |
|
|
|
dialogTitle.value = '新增医院'; |
|
|
|
form.id = null; form.name = ''; form.sort = 0; form.is_show = 1; |
|
|
|
dialogVisible.value = true; |
|
|
|
} |
|
|
|
|
|
|
|
function onSearch() { |
|
|
|
page.value = 1; |
|
|
|
loadList(); |
|
|
|
} |
|
|
|
|
|
|
|
function resetQuery() { |
|
|
|
keyword.value = ''; |
|
|
|
page.value = 1; |
|
|
|
loadList(); |
|
|
|
} |
|
|
|
|
|
|
|
function onSizeChange() { |
|
|
|
page.value = 1; |
|
|
|
loadList(); |
|
|
|
} |
|
|
|
function showEdit(row) { |
|
|
|
dialogTitle.value = '编辑医院'; |
|
|
|
form.id = row.id; form.name = row.name; form.sort = row.sort; form.is_show = row.is_show; |
|
|
|
dialogVisible.value = true; |
|
|
|
} |
|
|
|
|
|
|
|
async function handleSave() { |
|
|
|
if (!form.name.trim()) { ElementPlus.ElMessage.warning('请输入医院名称'); return; } |
|
|
|
saving.value = true; |
|
|
|
try { |
|
|
|
const url = form.id ? '/admin/hospital/edit' : '/admin/hospital/add'; |
|
|
|
const res = await fetch(url, { |
|
|
|
method: 'POST', |
|
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
|
body: JSON.stringify(form) |
|
|
|
}).then(r => r.json()); |
|
|
|
if (res.code === 0) { |
|
|
|
ElementPlus.ElMessage.success('保存成功'); |
|
|
|
dialogVisible.value = false; |
|
|
|
loadList(); |
|
|
|
} else { |
|
|
|
ElementPlus.ElMessage.error(res.msg || '保存失败'); |
|
|
|
} |
|
|
|
} finally { saving.value = false; } |
|
|
|
} |
|
|
|
|
|
|
|
async function handleDelete(row) { |
|
|
|
try { |
|
|
|
await ElementPlus.ElMessageBox.confirm(`确定要删除「${row.name}」吗?`, '提示', { type: 'warning' }); |
|
|
|
const res = await fetch('/admin/hospital/delete', { |
|
|
|
method: 'POST', |
|
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
|
body: JSON.stringify({ id: row.id }) |
|
|
|
}).then(r => r.json()); |
|
|
|
if (res.code === 0) { ElementPlus.ElMessage.success('删除成功'); loadList(); } |
|
|
|
else { ElementPlus.ElMessage.error(res.msg || '删除失败'); } |
|
|
|
} catch (e) {} |
|
|
|
} |
|
|
|
|
|
|
|
async function handleToggle(row, val) { |
|
|
|
const res = await fetch('/admin/hospital/toggleShow', { |
|
|
|
method: 'POST', |
|
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
|
body: JSON.stringify({ id: row.id, is_show: val ? 1 : 0 }) |
|
|
|
}).then(r => r.json()); |
|
|
|
if (res.code === 0) { row.is_show = val ? 1 : 0; ElementPlus.ElMessage.success('已更新'); } |
|
|
|
else { ElementPlus.ElMessage.error(res.msg || '操作失败'); } |
|
|
|
} |
|
|
|
|
|
|
|
async function handleImport() { |
|
|
|
if (!importText.value.trim()) { ElementPlus.ElMessage.warning('请输入医院名称'); return; } |
|
|
|
importing.value = true; |
|
|
|
try { |
|
|
|
const res = await fetch('/admin/hospital/import', { |
|
|
|
method: 'POST', |
|
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
|
body: JSON.stringify({ text: importText.value }) |
|
|
|
}).then(r => r.json()); |
|
|
|
if (res.code === 0) { |
|
|
|
ElementPlus.ElMessage.success(`导入成功 ${res.data.count} 条,跳过 ${res.data.skipped} 条`); |
|
|
|
showImport.value = false; |
|
|
|
importText.value = ''; |
|
|
|
loadList(); |
|
|
|
} else { |
|
|
|
ElementPlus.ElMessage.error(res.msg || '导入失败'); |
|
|
|
} |
|
|
|
} finally { importing.value = false; } |
|
|
|
} |
|
|
|
|
|
|
|
onMounted(() => loadList()); |
|
|
|
|
|
|
|
return { loading, list, keyword, total, page, pageSize, dialogVisible, dialogTitle, saving, form, showImport, importText, importing, importPlaceholder, loadList, onSearch, resetQuery, onSizeChange, showAdd, showEdit, handleSave, handleDelete, handleToggle, handleImport, Plus, Upload }; |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
app.use(ElementPlus, { locale: ElementPlusLocaleZhCn }); |
|
|
|
app.mount('#hospitalApp'); |
|
|
|
</script> |
|
|
|
{% endblock %} |