优化前端布局

This commit is contained in:
2026-07-07 09:34:41 +08:00
parent 2d7cadfa2b
commit cc3c1aba96
14 changed files with 167 additions and 74 deletions

View File

@@ -454,7 +454,7 @@ INSERT INTO `menu` (`id`, `parent_id`, `name`, `type`, `path`, `component`, `ico
-- ===== 药品管理(目录) ===== -- ===== 药品管理(目录) =====
INSERT INTO `menu` (`id`, `parent_id`, `name`, `type`, `path`, `component`, `icon`, `permission_code`, `sort_order`) VALUES INSERT INTO `menu` (`id`, `parent_id`, `name`, `type`, `path`, `component`, `icon`, `permission_code`, `sort_order`) VALUES
(21, 0, '药品管理', 1, NULL, NULL, 'MedicineBox', NULL, 2), (21, 0, '药品管理', 1, NULL, NULL, 'Goods', NULL, 2),
(22, 21, '药品列表', 2, '/medicine/list', 'views/medicine/MedicineList', NULL, 'medicine:list', 21), (22, 21, '药品列表', 2, '/medicine/list', 'views/medicine/MedicineList', NULL, 'medicine:list', 21),
(23, 22, '药品查询', 3, NULL, NULL, NULL, 'medicine:query', 211), (23, 22, '药品查询', 3, NULL, NULL, NULL, 'medicine:query', 211),
(24, 22, '新增药品', 3, NULL, NULL, NULL, 'medicine:add', 212), (24, 22, '新增药品', 3, NULL, NULL, NULL, 'medicine:add', 212),

View File

@@ -8,89 +8,157 @@ html, body, #app {
height: 100%; height: 100%;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', Arial, sans-serif; 'Microsoft YaHei', Arial, sans-serif;
color: #303133;
} }
// 页面通用样式 // 页面容器(卡片式)
.page-container { .page-container {
background: #fff; background: #fff;
border-radius: 4px; border-radius: 8px;
padding: 20px; padding: 24px;
min-height: calc(100vh - 140px); min-height: calc(100vh - 140px);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
} }
// 页面标题
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #303133;
display: flex;
align-items: center;
gap: 8px;
&::before {
content: '';
display: inline-block;
width: 4px;
height: 18px;
background: #409eff;
border-radius: 2px;
}
}
// 搜索区
.search-bar { .search-bar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; gap: 12px;
margin-bottom: 16px; margin-bottom: 16px;
padding: 16px; padding: 16px;
background: #fafafa; background: #f5f7fa;
border-radius: 4px; border-radius: 8px;
.el-input, .el-select, .el-cascader, .el-date-picker { .el-input, .el-select, .el-cascader, .el-date-picker {
width: 200px; width: 200px;
} }
} }
// 工具栏
.tool-bar { .tool-bar {
margin-bottom: 16px; margin-bottom: 16px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
} }
// 表格操作按钮 // 操作按钮
.table-actions { .table-actions {
display: flex; display: flex;
gap: 8px; gap: 6px;
}
// 弹窗表单
.dialog-form {
.el-form-item {
margin-bottom: 18px;
}
.el-input, .el-select, .el-cascader {
width: 100%;
}
} }
// 统计卡片 // 统计卡片
.stat-cards { .stat-cards {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 16px; gap: 20px;
margin-bottom: 20px; margin-bottom: 24px;
} }
.stat-card { .stat-card {
background: #fff; background: #fff;
border-radius: 8px; border-radius: 12px;
padding: 20px; padding: 24px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 20px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.stat-icon { .stat-icon {
width: 56px; width: 56px;
height: 56px; height: 56px;
border-radius: 12px; border-radius: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 28px; font-size: 28px;
color: #fff; color: #fff;
flex-shrink: 0;
} }
.stat-info { .stat-info {
.stat-label { .stat-label {
color: #909399; color: #909399;
font-size: 14px; font-size: 14px;
margin-bottom: 4px;
} }
.stat-value { .stat-value {
font-size: 28px; font-size: 30px;
font-weight: 700; font-weight: 700;
color: #303133; color: #303133;
line-height: 1.2;
} }
} }
} }
// 区域卡片
.section-card {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.05);
}
// 对话框表单
.dialog-form {
.el-form-item {
margin-bottom: 18px;
}
.el-input, .el-select, .el-cascader, .el-input-number {
width: 100%;
}
}
// 状态标签增强
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
// 响应式
@media (max-width: 1400px) {
.stat-cards { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
.stat-cards { grid-template-columns: 1fr; }
.page-container { padding: 16px; }
}

View File

@@ -60,6 +60,7 @@ onMounted(loadData)
<template> <template>
<div class="page-container"> <div class="page-container">
<div class="page-header"><span class="page-title">药品分类</span></div>
<div class="tool-bar"> <div class="tool-bar">
<el-button type="primary" v-permission="'category:add'" :icon="Plus" @click="handleAdd(0)">+ 新增顶级分类</el-button> <el-button type="primary" v-permission="'category:add'" :icon="Plus" @click="handleAdd(0)">+ 新增顶级分类</el-button>
</div> </div>

View File

@@ -22,6 +22,7 @@ onMounted(loadData)
<template> <template>
<div class="page-container"> <div class="page-container">
<div class="page-header"><span class="page-title">客户管理</span></div>
<div class="search-bar"> <div class="search-bar">
<el-input v-model="query.name" placeholder="客户名称" clearable /> <el-input v-model="query.name" placeholder="客户名称" clearable />
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button> <el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>

View File

@@ -3,8 +3,7 @@ import { ref, onMounted } from 'vue'
import { getStatistics, getExpiryWarning } from '@/api/dashboard' import { getStatistics, getExpiryWarning } from '@/api/dashboard'
import { getStockInList } from '@/api/stockIn' import { getStockInList } from '@/api/stockIn'
import { getStockOutList } from '@/api/stockOut' import { getStockOutList } from '@/api/stockOut'
import { Goods, Box, DocumentChecked, Warning } from '@element-plus/icons-vue' import { Goods, Box, DocumentChecked, Warning, Clock } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const statistics = ref({ medicineCount: 0, stockBatchCount: 0, pendingStockIn: 0, expiryWarningCount: 0 }) const statistics = ref({ medicineCount: 0, stockBatchCount: 0, pendingStockIn: 0, expiryWarningCount: 0 })
const expiryList = ref([]) const expiryList = ref([])
@@ -12,10 +11,10 @@ const recentInList = ref([])
const recentOutList = ref([]) const recentOutList = ref([])
const statCards = [ const statCards = [
{ key: 'medicineCount', label: '药品总数', color: '#409eff', icon: Goods }, { key: 'medicineCount', label: '药品总数', color: '#409eff', bg: 'linear-gradient(135deg, #409eff, #66b1ff)', icon: Goods },
{ key: 'stockBatchCount', label: '库存批次', color: '#67c23a', icon: Box }, { key: 'stockBatchCount', label: '库存批次', color: '#67c23a', bg: 'linear-gradient(135deg, #67c23a, #95d475)', icon: Box },
{ key: 'pendingStockIn', label: '待审入库', color: '#e6a23c', icon: DocumentChecked }, { key: 'pendingStockIn', label: '待审入库', color: '#e6a23c', bg: 'linear-gradient(135deg, #e6a23c, #f3d19e)', icon: DocumentChecked },
{ key: 'expiryWarningCount', label: '效期预警', color: '#f56c6c', icon: Warning }, { key: 'expiryWarningCount', label: '效期预警', color: '#f56c6c', bg: 'linear-gradient(135deg, #f56c6c, #fab6b6)', icon: Warning },
] ]
function expiryColor(date) { function expiryColor(date) {
@@ -30,22 +29,29 @@ onMounted(async () => {
try { try {
const [statRes, warnRes, inRes, outRes] = await Promise.allSettled([ const [statRes, warnRes, inRes, outRes] = await Promise.allSettled([
getStatistics(), getExpiryWarning(), getStatistics(), getExpiryWarning(),
getStockInList({ page: 1, size: 5 }), getStockInList({ page: 1, size: 4 }),
getStockOutList({ page: 1, size: 5 }), getStockOutList({ page: 1, size: 4 }),
]) ])
if (statRes.status === 'fulfilled') statistics.value = statRes.value.data || {} if (statRes.status === 'fulfilled') statistics.value = statRes.value.data || {}
if (warnRes.status === 'fulfilled') expiryList.value = warnRes.value.data?.records || warnRes.value.data || [] if (warnRes.status === 'fulfilled') expiryList.value = warnRes.value.data?.records || warnRes.value.data || []
if (inRes.status === 'fulfilled') recentInList.value = inRes.value.data?.records || [] if (inRes.status === 'fulfilled') recentInList.value = inRes.value.data?.records || []
if (outRes.status === 'fulfilled') recentOutList.value = outRes.value.data?.records || [] if (outRes.status === 'fulfilled') recentOutList.value = outRes.value.data?.records || []
} catch { /* 后端未启动 */ } } catch { /* */ }
}) })
</script> </script>
<template> <template>
<div> <div>
<div class="page-header">
<span class="page-title">仪表盘</span>
<span style="color:#909399;font-size:13px">
<el-icon><Clock /></el-icon> {{ new Date().toLocaleDateString('zh-CN', { year:'numeric', month:'long', day:'numeric', weekday:'long' }) }}
</span>
</div>
<div class="stat-cards"> <div class="stat-cards">
<div v-for="card in statCards" :key="card.key" class="stat-card"> <div v-for="card in statCards" :key="card.key" class="stat-card">
<div class="stat-icon" :style="{ background: card.color }"> <div class="stat-icon" :style="{ background: card.bg }">
<el-icon><component :is="card.icon" /></el-icon> <el-icon><component :is="card.icon" /></el-icon>
</div> </div>
<div class="stat-info"> <div class="stat-info">
@@ -55,53 +61,57 @@ onMounted(async () => {
</div> </div>
</div> </div>
<el-card class="section-card" header="效期预警90天内到期"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<el-table :data="expiryList" size="small" empty-text="暂无预警"> <div class="section-card">
<el-table-column prop="medicineName" label="药品" min-width="160" /> <h4 style="margin-bottom:12px;color:#e6a23c"><el-icon><Warning /></el-icon> 效期预警90天内到期</h4>
<el-table-column prop="batchNo" label="批号" width="120" /> <el-table :data="expiryList" size="small" empty-text="暂无预警 ">
<el-table-column prop="quantity" label="库存" width="80" /> <el-table-column prop="medicineName" label="药品" min-width="140" />
<el-table-column prop="expiryDate" label="有效期至" width="130"> <el-table-column prop="batchNo" label="批号" width="110" />
<template #default="{ row }"> <el-table-column prop="quantity" label="库存" width="70" align="center" />
<el-tag :type="expiryColor(row.expiryDate)" size="small">{{ row.expiryDate }}</el-tag> <el-table-column prop="expiryDate" label="有效期至" width="120" align="center">
</template> <template #default="{ row }">
</el-table-column> <el-tag :type="expiryColor(row.expiryDate)" size="small" effect="dark">{{ row.expiryDate }}</el-tag>
</el-table> </template>
</el-card> </el-table-column>
</el-table>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px"> </div>
<el-card header="最近入库"> <div class="section-card">
<template v-if="recentInList.length"> <h4 style="margin-bottom:12px;color:#409eff"><el-icon><DocumentChecked /></el-icon> 最近业务</h4>
<div v-for="item in recentInList" :key="item.id" class="record-item"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<span>{{ item.recordNo }}</span> <div>
<span style="color:#909399;font-size:13px">{{ item.supplierName }}</span> <h5 style="margin-bottom:8px;font-size:13px;color:#909399">最近入库</h5>
<el-tag :type="item.status===1?'success':item.status===0?'warning':'info'" size="small"> <div v-if="recentInList.length">
{{ item.status===1?'已入库':item.status===0?'待审核':'已取消' }} <div v-for="item in recentInList" :key="item.id" class="record-item">
</el-tag> <span style="font-size:13px;font-weight:500">{{ item.recordNo }}</span>
<el-tag :type="item.status===1?'success':item.status===0?'warning':'info'" size="small" effect="plain">
{{ item.status===1?'已入库':item.status===0?'待审核':'已取消' }}
</el-tag>
</div>
</div>
<el-empty v-else description="暂无" :image-size="40" />
</div> </div>
</template> <div>
<el-empty v-else description="暂无数据" /> <h5 style="margin-bottom:8px;font-size:13px;color:#909399">最近出库</h5>
</el-card> <div v-if="recentOutList.length">
<el-card header="最近出库"> <div v-for="item in recentOutList" :key="item.id" class="record-item">
<template v-if="recentOutList.length"> <span style="font-size:13px;font-weight:500">{{ item.recordNo }}</span>
<div v-for="item in recentOutList" :key="item.id" class="record-item"> <el-tag :type="item.status===1?'success':item.status===0?'warning':'info'" size="small" effect="plain">
<span>{{ item.recordNo }}</span> {{ item.status===1?'已出库':item.status===0?'待审核':'已取消' }}
<span style="color:#909399;font-size:13px">{{ item.customerName || '-' }}</span> </el-tag>
<el-tag :type="item.status===1?'success':item.status===0?'warning':'info'" size="small"> </div>
{{ item.status===1?'已出库':item.status===0?'待审核':'已取消' }} </div>
</el-tag> <el-empty v-else description="暂无" :image-size="40" />
</div> </div>
</template> </div>
<el-empty v-else description="暂无数据" /> </div>
</el-card>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.section-card { margin-bottom: 16px; }
.record-item { .record-item {
display: flex; justify-content: space-between; align-items: center; display: flex; justify-content: space-between; align-items: center;
padding: 10px 0; border-bottom: 1px solid #f0f0f0; padding: 8px 0; border-bottom: 1px dashed #ebeef5;
&:last-child { border-bottom: none; } &:last-child { border-bottom: none; }
} }
</style> </style>

View File

@@ -94,6 +94,10 @@ onMounted(async () => {
<template> <template>
<div class="page-container"> <div class="page-container">
<div class="page-header">
<span class="page-title">药品管理</span>
<span style="color:#909399;font-size:13px"> {{ total }} 条记录</span>
</div>
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="search-bar"> <div class="search-bar">
<el-input v-model="query.name" placeholder="药品名称" clearable @keyup.enter="loadData" /> <el-input v-model="query.name" placeholder="药品名称" clearable @keyup.enter="loadData" />

View File

@@ -73,6 +73,7 @@ onMounted(loadData)
<template> <template>
<div class="page-container"> <div class="page-container">
<div class="page-header"><span class="page-title">菜单管理</span></div>
<div class="tool-bar"> <div class="tool-bar">
<el-button type="primary" v-permission="'menu:add'" :icon="Plus" @click="handleAdd(0)">新增顶级菜单</el-button> <el-button type="primary" v-permission="'menu:add'" :icon="Plus" @click="handleAdd(0)">新增顶级菜单</el-button>
</div> </div>

View File

@@ -34,6 +34,7 @@ onMounted(loadData)
<template> <template>
<div class="page-container"> <div class="page-container">
<div class="page-header"><span class="page-title">角色管理</span></div>
<div class="tool-bar"> <div class="tool-bar">
<el-button type="primary" v-permission="'role:add'" @click="handleAdd">+ 新增角色</el-button> <el-button type="primary" v-permission="'role:add'" @click="handleAdd">+ 新增角色</el-button>
</div> </div>

View File

@@ -26,6 +26,7 @@ onMounted(loadData)
<template> <template>
<div class="page-container"> <div class="page-container">
<div class="page-header"><span class="page-title">库存管理</span></div>
<div class="search-bar"> <div class="search-bar">
<el-input v-model="query.medicineName" placeholder="药品名称" clearable /> <el-input v-model="query.medicineName" placeholder="药品名称" clearable />
<el-input v-model="query.batchNo" placeholder="批号" clearable /> <el-input v-model="query.batchNo" placeholder="批号" clearable />

View File

@@ -54,6 +54,7 @@ onMounted(loadData)
<template> <template>
<div class="page-container"> <div class="page-container">
<div class="page-header"><span class="page-title">入库管理</span></div>
<div class="search-bar"> <div class="search-bar">
<el-input v-model="query.recordNo" placeholder="入库单号" clearable style="width:200px" @keyup.enter="loadData" /> <el-input v-model="query.recordNo" placeholder="入库单号" clearable style="width:200px" @keyup.enter="loadData" />
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button> <el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>

View File

@@ -55,6 +55,7 @@ onMounted(loadData)
<template> <template>
<div class="page-container"> <div class="page-container">
<div class="page-header"><span class="page-title">出库管理</span></div>
<div class="search-bar"> <div class="search-bar">
<el-input v-model="query.recordNo" placeholder="出库单号" clearable style="width:200px" @keyup.enter="loadData" /> <el-input v-model="query.recordNo" placeholder="出库单号" clearable style="width:200px" @keyup.enter="loadData" />
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button> <el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>

View File

@@ -26,6 +26,9 @@ onMounted(loadData)
<template> <template>
<div class="page-container"> <div class="page-container">
<div class="page-header">
<span class="page-title">供应商管理</span>
</div>
<div class="search-bar"> <div class="search-bar">
<el-input v-model="query.name" placeholder="供应商名称" clearable @keyup.enter="loadData" /> <el-input v-model="query.name" placeholder="供应商名称" clearable @keyup.enter="loadData" />
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button> <el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>

View File

@@ -28,6 +28,7 @@ onMounted(async () => { try { allRoles.value = (await getAllRoles()).data || []
<template> <template>
<div class="page-container"> <div class="page-container">
<div class="page-header"><span class="page-title">用户管理</span></div>
<div class="search-bar"> <div class="search-bar">
<el-input v-model="query.username" placeholder="用户名" clearable /> <el-input v-model="query.username" placeholder="用户名" clearable />
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button> <el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>