药房管理

This commit is contained in:
zoujiandong
2026-09-07 10:37:32 +08:00
parent 19de6e0e26
commit 5231542d73
3 changed files with 1003 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
import request from '@/utils/request';
/**
* 获取药房分页列表
* @param {Object} data - 查询参数及分页参数
* @returns {Promise}
*/
export function getPharmacyPage(data) {
return request({
url: '/admin/basic/pharmacy/page',
method: 'post',
data,
});
}
/**
* 获取药房列表(下拉选择框)
* @param {Object} params - 过滤条件
* @returns {Promise}
*/
export function getPharmacyList(params) {
return request({
url: '/admin/basic/pharmacy/list',
method: 'get',
params,
});
}
/**
* 获取药房详情
* @param {String} pharmacy_id - 药房 ID
* @returns {Promise}
*/
export function getPharmacyDetail(pharmacy_id) {
return request({
url: `/admin/basic/pharmacy/${pharmacy_id}`,
method: 'get',
});
}
/**
* 新增药房
* @param {Object} data - 药房实体参数
* @returns {Promise}
*/
export function addPharmacy(data) {
return request({
url: '/admin/basic/pharmacy',
method: 'post',
data,
});
}
/**
* 修改药房
* @param {String} pharmacy_id - 药房 ID
* @param {Object} data - 药房实体参数
* @returns {Promise}
*/
export function updatePharmacy(pharmacy_id, data) {
return request({
url: `/admin/basic/pharmacy/${pharmacy_id}`,
method: 'put',
data,
});
}
/**
* 切换药房状态(快捷开关)
* @param {String} pharmacy_id - 药房 ID
* @param {Number} status - 0: 禁用, 1: 正常
* @returns {Promise}
*/
export function updatePharmacyStatus(pharmacy_id, status) {
return request({
url: `/admin/basic/pharmacy/status/${pharmacy_id}`,
method: 'put',
data: { status },
});
}
/**
* 删除药房(软删除)
* @param {String} pharmacy_id - 药房 ID
* @returns {Promise}
*/
export function deletePharmacy(pharmacy_id) {
return request({
url: `/admin/basic/pharmacy/${pharmacy_id}`,
method: 'delete',
});
}
@@ -0,0 +1,416 @@
<template>
<a-modal
v-model:visible="visible"
:title="modalTitle"
:width="760"
:ok-loading="submitLoading"
:ok-text="'保存'"
:cancel-text="'取消'"
:footer="mode !== 'detail'"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
:disabled="mode === 'detail'"
auto-label-width
layout="horizontal"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="pharmacy_name" label="药房名称">
<a-input
v-model="formData.pharmacy_name"
placeholder="请输入药房名称"
allow-clear
max-length="50"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="pharmacy_code" label="药房代码">
<a-input
v-model="formData.pharmacy_code"
placeholder="请输入药房代码(如 PHARM_001)"
allow-clear
max-length="50"
:disabled="mode === 'edit'"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="postage" label="基础邮费 (元)">
<a-input-number
v-model="formData.postage"
placeholder="请输入基础邮费"
:min="0"
:precision="2"
:step="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="free_shipping_threshold" label="包邮门槛 (元)">
<a-input-number
v-model="formData.free_shipping_threshold"
placeholder="0表示不设门槛"
:min="0"
:precision="2"
:step="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="is_pickup" label="支持自提">
<a-radio-group v-model="formData.is_pickup">
<a-radio :value="1">支持</a-radio>
<a-radio :value="0">不支持</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="status" label="药房状态">
<a-radio-group v-model="formData.status">
<a-radio :value="1">正常</a-radio>
<a-radio :value="0">禁用</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="telephone" label="联系电话">
<a-input
v-model="formData.telephone"
placeholder="请输入联系电话"
allow-clear
max-length="20"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="所属区域">
<a-space style="width: 100%">
<a-select
v-model="formData.province_id"
placeholder="选择省份"
style="width: 170px"
allow-clear
@change="handleProvinceChange"
@clear="handleProvinceClear"
>
<a-option
v-for="item in provinceList"
:key="item.area_id"
:value="Number(item.area_id)"
:label="item.area_name"
>
{{ item.area_name }}
</a-option>
</a-select>
<a-select
v-model="formData.city_id"
placeholder="选择城市"
style="width: 170px"
allow-clear
:disabled="!formData.province_id"
@change="handleCityChange"
@clear="handleCityClear"
>
<a-option
v-for="item in cityList"
:key="item.area_id"
:value="Number(item.area_id)"
:label="item.area_name"
>
{{ item.area_name }}
</a-option>
</a-select>
<a-select
v-model="formData.county_id"
placeholder="选择区县"
style="width: 170px"
allow-clear
:disabled="!formData.city_id"
>
<a-option
v-for="item in countyList"
:key="item.area_id"
:value="Number(item.area_id)"
:label="item.area_name"
>
{{ item.area_name }}
</a-option>
</a-select>
</a-space>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item field="address" label="详细地址">
<a-textarea
v-model="formData.address"
placeholder="请输入详细地址,如街道、门牌号等"
:auto-size="{ minRows: 2, maxRows: 4 }"
allow-clear
max-length="200"
show-word-limit
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive, computed } from 'vue';
import { Message } from '@arco-design/web-vue';
import { addPharmacy, updatePharmacy, getPharmacyDetail } from '@/api/basic/pharmacy';
import { getAdminAreaList } from '@/api/basic/list';
const emit = defineEmits(['success']);
const visible = ref(false);
const mode = ref('add'); // 'add' | 'edit' | 'detail'
const currentId = ref('');
const submitLoading = ref(false);
const formRef = ref(null);
const provinceList = ref([]);
const cityList = ref([]);
const countyList = ref([]);
const defaultFormData = {
pharmacy_name: '',
pharmacy_code: '',
postage: 0,
free_shipping_threshold: 0,
is_pickup: 0,
telephone: '',
province_id: undefined,
city_id: undefined,
county_id: undefined,
address: '',
status: 1,
};
const formData = reactive({ ...defaultFormData });
const modalTitle = computed(() => {
if (mode.value === 'add') return '新增药房';
if (mode.value === 'edit') return '修改药房';
return '药房详情';
});
const rules = {
pharmacy_name: [
{ required: true, message: '请输入药房名称' },
{ maxLength: 50, message: '药房名称不能超过50个字符' },
],
pharmacy_code: [
{ required: true, message: '请输入药房代码' },
{ maxLength: 50, message: '药房代码不能超过50个字符' },
],
postage: [
{ required: true, message: '请输入基础邮费' },
],
free_shipping_threshold: [
{ required: true, message: '请输入满额包邮门槛金额' },
],
is_pickup: [
{ required: true, message: '请选择是否支持自提' },
],
address: [
{ required: true, message: '请输入详细地址' },
{ maxLength: 200, message: '详细地址不能超过200个字符' },
],
};
// 获取区域列表 (area_type: 2 省份, 3 城市, 4 区县)
const fetchAreaList = async (parentId = '', areaType = 2) => {
try {
const res = await getAdminAreaList({
parent_id: parentId,
area_type: areaType,
});
if (res.code === 200) {
if (areaType === 2) {
provinceList.value = res.data || [];
} else if (areaType === 3) {
cityList.value = res.data || [];
} else if (areaType === 4) {
countyList.value = res.data || [];
}
}
} catch (error) {
console.error('获取区域失败:', error);
}
};
const handleProvinceChange = (value) => {
formData.city_id = undefined;
formData.county_id = undefined;
cityList.value = [];
countyList.value = [];
if (value) {
fetchAreaList(value, 3);
}
};
const handleProvinceClear = () => {
formData.province_id = undefined;
formData.city_id = undefined;
formData.county_id = undefined;
cityList.value = [];
countyList.value = [];
};
const handleCityChange = (value) => {
formData.county_id = undefined;
countyList.value = [];
if (value) {
fetchAreaList(value, 4);
}
};
const handleCityClear = () => {
formData.city_id = undefined;
formData.county_id = undefined;
countyList.value = [];
};
const resetForm = () => {
Object.assign(formData, defaultFormData);
cityList.value = [];
countyList.value = [];
if (formRef.value) {
formRef.value.resetFields();
}
};
// 打开弹窗
const open = async (openMode = 'add', id = '') => {
mode.value = openMode;
currentId.value = id;
resetForm();
visible.value = true;
// 保证省份数据已加载
if (provinceList.value.length === 0) {
await fetchAreaList('', 2);
}
if ((openMode === 'edit' || openMode === 'detail') && id) {
try {
const res = await getPharmacyDetail(id);
if (res.code === 200 && res.data) {
const d = res.data;
Object.assign(formData, {
pharmacy_name: d.pharmacy_name || '',
pharmacy_code: d.pharmacy_code || '',
postage: Number(d.postage) || 0,
free_shipping_threshold: Number(d.free_shipping_threshold) || 0,
is_pickup: d.is_pickup ?? 0,
telephone: d.telephone || '',
province_id: d.province_id ? Number(d.province_id) : undefined,
city_id: d.city_id ? Number(d.city_id) : undefined,
county_id: d.county_id ? Number(d.county_id) : undefined,
address: d.address || '',
status: d.status ?? 1,
});
// 联动加载已有城市与区县选项
if (formData.province_id) {
await fetchAreaList(formData.province_id, 3);
}
if (formData.city_id) {
await fetchAreaList(formData.city_id, 4);
}
} else {
Message.error(res.message || '获取药房详情失败');
}
} catch (err) {
Message.error('获取药房详情出现异常');
}
}
};
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return;
const errors = await formRef.value.validate();
if (errors) {
return;
}
const payload = {
pharmacy_name: formData.pharmacy_name,
pharmacy_code: formData.pharmacy_code,
postage: Number(formData.postage),
free_shipping_threshold: Number(formData.free_shipping_threshold),
is_pickup: Number(formData.is_pickup),
telephone: formData.telephone || '',
province_id: formData.province_id ? Number(formData.province_id) : undefined,
city_id: formData.city_id ? Number(formData.city_id) : undefined,
county_id: formData.county_id ? Number(formData.county_id) : undefined,
address: formData.address,
status: Number(formData.status),
};
submitLoading.value = true;
try {
let res;
if (mode.value === 'add') {
res = await addPharmacy(payload);
} else {
res = await updatePharmacy(currentId.value, payload);
}
if (res.code === 200) {
Message.success(mode.value === 'add' ? '添加药房成功' : '修改药房成功');
visible.value = false;
emit('success');
} else {
Message.error(res.message || '操作失败');
}
} catch (error) {
console.error('提交药房表单异常:', error);
} finally {
submitLoading.value = false;
}
};
const handleCancel = () => {
visible.value = false;
resetForm();
};
defineExpose({
open,
});
</script>
<style scoped>
:deep(.arco-form-item) {
margin-bottom: 20px;
}
</style>
+495
View File
@@ -0,0 +1,495 @@
<template>
<div class="app-container">
<!-- 搜索筛选表单 -->
<a-form :model="queryForm" ref="queryFormRef" layout="inline" class="search-form">
<a-form-item field="pharmacy_name" label="药房名称">
<a-input
v-model="queryForm.pharmacy_name"
placeholder="请输入药房名称"
allow-clear
style="width: 180px"
@press-enter="handleQuery"
/>
</a-form-item>
<a-form-item field="pharmacy_code" label="药房代码">
<a-input
v-model="queryForm.pharmacy_code"
placeholder="请输入药房代码"
allow-clear
style="width: 180px"
@press-enter="handleQuery"
/>
</a-form-item>
<a-form-item field="status" label="状态">
<a-select
v-model="queryForm.status"
placeholder="全部状态"
allow-clear
style="width: 140px"
>
<a-option :value="1">正常</a-option>
<a-option :value="0">禁用</a-option>
</a-select>
</a-form-item>
<a-form-item field="is_pickup" label="支持自提">
<a-select
v-model="queryForm.is_pickup"
placeholder="全部"
allow-clear
style="width: 130px"
>
<a-option :value="1">支持</a-option>
<a-option :value="0">不支持</a-option>
</a-select>
</a-form-item>
<a-form-item field="province_id" label="省份">
<a-select
v-model="queryForm.province_id"
placeholder="全部省份"
allow-clear
style="width: 150px"
@change="changeProvince"
@clear="clearProvince"
>
<a-option
v-for="item in provinceData"
:key="item.area_id"
:value="Number(item.area_id)"
:label="item.area_name"
>
{{ item.area_name }}
</a-option>
</a-select>
</a-form-item>
<a-form-item field="city_id" label="城市">
<a-select
v-model="queryForm.city_id"
placeholder="全部城市"
allow-clear
style="width: 150px"
:disabled="!queryForm.province_id"
@change="changeCity"
@clear="clearCity"
>
<a-option
v-for="item in cityData"
:key="item.area_id"
:value="Number(item.area_id)"
:label="item.area_name"
>
{{ item.area_name }}
</a-option>
</a-select>
</a-form-item>
<a-form-item field="county_id" label="区县">
<a-select
v-model="queryForm.county_id"
placeholder="全部区县"
allow-clear
style="width: 150px"
:disabled="!queryForm.city_id"
>
<a-option
v-for="item in countyData"
:key="item.area_id"
:value="Number(item.area_id)"
:label="item.area_name"
>
{{ item.area_name }}
</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleQuery">
<template #icon><icon-search /></template>
搜索
</a-button>
<a-button @click="handleResetQuery">
<template #icon><icon-loop /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider style="margin: 12px 0 16px" />
<!-- 操作工具栏 -->
<div class="table-action-bar">
<a-space>
<a-button type="primary" @click="handleAdd">
<template #icon><icon-plus /></template>
新增药房
</a-button>
</a-space>
</div>
<!-- 数据表格 -->
<a-table
:columns="columns"
:data="tableData"
:loading="tableLoading"
:pagination="paginationProps"
row-key="pharmacy_id"
:scroll="{ x: 1600 }"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<!-- 序号列 -->
<template #index="{ rowIndex }">
{{ (pager.page - 1) * pager.page_size + rowIndex + 1 }}
</template>
<!-- 基础邮费 -->
<template #postage="{ record }">
<span class="price-text">¥{{ Number(record.postage || 0).toFixed(2) }}</span>
</template>
<!-- 包邮门槛 -->
<template #free_shipping_threshold="{ record }">
<span v-if="Number(record.free_shipping_threshold) > 0" class="price-text">
¥{{ Number(record.free_shipping_threshold).toFixed(2) }}
</span>
<a-tag v-else size="small" color="blue">不设门槛</a-tag>
</template>
<!-- 是否支持自提 -->
<template #is_pickup="{ record }">
<a-tag v-if="record.is_pickup === 1" color="green" size="small">支持</a-tag>
<a-tag v-else color="gray" size="small">不支持</a-tag>
</template>
<!-- 详细地址(带 Tooltip) -->
<template #full_address="{ record }">
<a-tooltip :content="record.full_address || record.address || '-'">
<div class="ellipsis-text">
{{ record.full_address || record.address || '-' }}
</div>
</a-tooltip>
</template>
<!-- 状态快捷开关 -->
<template #status="{ record }">
<a-switch
:model-value="record.status === 1"
:loading="record._statusLoading"
type="round"
size="small"
@change="(val) => handleStatusToggle(record, val)"
>
<template #checked>正常</template>
<template #unchecked>禁用</template>
</a-switch>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleDetail(record)">
<template #icon><icon-eye /></template>
详情
</a-button>
<a-button type="text" size="small" @click="handleEdit(record)">
<template #icon><icon-edit /></template>
修改
</a-button>
<a-popconfirm
content="确定要删除此药房吗?删除后不可恢复!"
type="warning"
@ok="handleDelete(record)"
>
<a-button type="text" status="danger" size="small">
<template #icon><icon-delete /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
<!-- 新增 / 编辑 / 详情 弹窗 -->
<PharmacyModal ref="modalRef" @success="handleQuery" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { Message } from '@arco-design/web-vue';
import { getPharmacyPage, updatePharmacyStatus, deletePharmacy } from '@/api/basic/pharmacy';
import { getAdminAreaList } from '@/api/basic/list';
import PharmacyModal from './components/pharmacyModal.vue';
const queryFormRef = ref(null);
const modalRef = ref(null);
const tableLoading = ref(false);
const tableData = ref([]);
// 区域数据
const provinceData = ref([]);
const cityData = ref([]);
const countyData = ref([]);
// 分页信息
const pager = reactive({
page: 1,
page_size: 10,
total: 0,
});
const paginationProps = computed(() => ({
total: pager.total,
current: pager.page,
pageSize: pager.page_size,
showTotal: true,
showJumper: true,
showPageSize: true,
pageSizeOptions: [10, 20, 50, 100],
}));
// 查询表单
const queryForm = reactive({
pharmacy_name: '',
pharmacy_code: '',
status: undefined,
is_pickup: undefined,
province_id: undefined,
city_id: undefined,
county_id: undefined,
});
// 表格列配置
const columns = [
{ title: '#', slotName: 'index', width: 60, align: 'center' },
{ title: '药房代码', dataIndex: 'pharmacy_code', width: 140 },
{ title: '药房名称', dataIndex: 'pharmacy_name', width: 200, ellipsis: true },
{ title: '基础邮费', slotName: 'postage', width: 120, align: 'right' },
{ title: '满额包邮门槛', slotName: 'free_shipping_threshold', width: 140, align: 'right' },
{ title: '自提', slotName: 'is_pickup', width: 90, align: 'center' },
{ title: '联系电话', dataIndex: 'telephone', width: 140 },
{ title: '地址', slotName: 'full_address', minWidth: 240 },
{ title: '状态', slotName: 'status', width: 100, align: 'center' },
{ title: '创建时间', dataIndex: 'created_at', width: 180 },
{ title: '操作', slotName: 'action', width: 190, fixed: 'right', align: 'center' },
];
// 获取区域信息
const fetchAreaList = async (parentId = '', areaType = 2) => {
try {
const res = await getAdminAreaList({
parent_id: parentId,
area_type: areaType,
});
if (res.code === 200) {
if (areaType === 2) provinceData.value = res.data || [];
if (areaType === 3) cityData.value = res.data || [];
if (areaType === 4) countyData.value = res.data || [];
}
} catch (error) {
console.error('获取区域失败:', error);
}
};
const changeProvince = (val) => {
queryForm.city_id = undefined;
queryForm.county_id = undefined;
cityData.value = [];
countyData.value = [];
if (val) {
fetchAreaList(val, 3);
}
};
const clearProvince = () => {
queryForm.province_id = undefined;
queryForm.city_id = undefined;
queryForm.county_id = undefined;
cityData.value = [];
countyData.value = [];
};
const changeCity = (val) => {
queryForm.county_id = undefined;
countyData.value = [];
if (val) {
fetchAreaList(val, 4);
}
};
const clearCity = () => {
queryForm.city_id = undefined;
queryForm.county_id = undefined;
countyData.value = [];
};
// 获取表格数据
const fetchTableData = async () => {
tableLoading.value = true;
try {
const params = {
page: pager.page,
page_size: pager.page_size,
};
if (queryForm.pharmacy_name) params.pharmacy_name = queryForm.pharmacy_name;
if (queryForm.pharmacy_code) params.pharmacy_code = queryForm.pharmacy_code;
if (queryForm.status !== undefined && queryForm.status !== null) params.status = queryForm.status;
if (queryForm.is_pickup !== undefined && queryForm.is_pickup !== null) params.is_pickup = queryForm.is_pickup;
if (queryForm.province_id) params.province_id = queryForm.province_id;
if (queryForm.city_id) params.city_id = queryForm.city_id;
if (queryForm.county_id) params.county_id = queryForm.county_id;
const res = await getPharmacyPage(params);
if (res.code === 200 && res.data) {
tableData.value = (res.data.data || []).map((item) => ({
...item,
_statusLoading: false,
}));
pager.total = res.data.total || 0;
pager.page = res.data.page || 1;
pager.page_size = res.data.page_size || 10;
} else {
Message.error(res.message || '获取药房列表失败');
}
} catch (err) {
console.error('获取药房列表异常:', err);
} finally {
tableLoading.value = false;
}
};
// 搜索
const handleQuery = () => {
pager.page = 1;
fetchTableData();
};
// 重置
const handleResetQuery = () => {
if (queryFormRef.value) {
queryFormRef.value.resetFields();
}
queryForm.pharmacy_name = '';
queryForm.pharmacy_code = '';
queryForm.status = undefined;
queryForm.is_pickup = undefined;
queryForm.province_id = undefined;
queryForm.city_id = undefined;
queryForm.county_id = undefined;
cityData.value = [];
countyData.value = [];
pager.page = 1;
fetchTableData();
};
// 分页变化
const handlePageChange = (page) => {
pager.page = page;
fetchTableData();
};
const handlePageSizeChange = (pageSize) => {
pager.page_size = pageSize;
pager.page = 1;
fetchTableData();
};
// 新增
const handleAdd = () => {
modalRef.value?.open('add');
};
// 详情
const handleDetail = (record) => {
modalRef.value?.open('detail', record.pharmacy_id);
};
// 编辑
const handleEdit = (record) => {
modalRef.value?.open('edit', record.pharmacy_id);
};
// 状态切换
const handleStatusToggle = async (record, val) => {
const newStatus = val ? 1 : 0;
const originalStatus = record.status;
record._statusLoading = true;
try {
const res = await updatePharmacyStatus(record.pharmacy_id, newStatus);
if (res.code === 200) {
record.status = newStatus;
Message.success(newStatus === 1 ? '已启用该药房' : '已禁用该药房');
} else {
record.status = originalStatus;
Message.error(res.message || '修改状态失败');
}
} catch (err) {
record.status = originalStatus;
Message.error('切换状态异常');
} finally {
record._statusLoading = false;
}
};
// 删除
const handleDelete = async (record) => {
try {
const res = await deletePharmacy(record.pharmacy_id);
if (res.code === 200) {
Message.success('药房删除成功');
// 如果当前页只有一条且非第一页,删除后向前退一页
if (tableData.value.length === 1 && pager.page > 1) {
pager.page -= 1;
}
fetchTableData();
} else {
Message.error(res.message || '删除药房失败');
}
} catch (err) {
Message.error('删除药房异常');
}
};
onMounted(() => {
fetchTableData();
fetchAreaList('', 2);
});
</script>
<style lang="scss" scoped>
.app-container {
padding: 16px;
background-color: #fff;
border-radius: 4px;
}
.search-form {
:deep(.arco-form-item) {
margin-bottom: 12px;
}
}
.table-action-bar {
margin-bottom: 16px;
}
.price-text {
font-weight: 500;
color: #ff5722;
}
.ellipsis-text {
max-width: 320px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>