完善前端项目结构和依赖配置

- 更新 package.json 中的依赖版本号,包括 element-plus、axios、pinia、vue-router 等
- 删除 HelloWorld 组件,替换为 router-view 以支持路由功能
- 添加 auth、category、customer、dashboard、medicine、role、stock、stockIn、stockOut、
  supplier、user 等多个 API 接口文件
- 创建权限指令、主布局组件和全局样式
- 集成 Element Plus UI 框架并注册图标组件
- 实现应用状态管理和用户状态管理 Store
- 配置 Vue Router 路由系统,支持动态菜单路由生成
- 移除旧的 CSS 样式文件,改用 SCSS 全局样式
```
This commit is contained in:
2026-07-06 12:07:23 +08:00
parent 3fcf927aef
commit 15da4fe492
38 changed files with 2057 additions and 409 deletions

View File

@@ -0,0 +1,108 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getCategoryList, createCategory, updateCategory, deleteCategory } from '@/api/category'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
const tableData = ref([])
const dialogVisible = ref(false)
const dialogTitle = ref('新增分类')
const formRef = ref(null)
const parentOptions = ref([])
const form = reactive({
id: null, parentId: 0, name: '', code: '', sortOrder: 0, description: '',
})
async function loadData() {
try {
const res = await getCategoryList()
tableData.value = res.data || []
parentOptions.value = (res.data || []).map((c) => ({ id: c.id, name: c.name, parentId: c.parentId }))
} catch { /* */ }
}
function handleAdd(parentId = 0) {
dialogTitle.value = '新增分类'
Object.assign(form, { id: null, parentId, name: '', code: '', sortOrder: 0, description: '' })
dialogVisible.value = true
}
function handleEdit(row) {
dialogTitle.value = '编辑分类'
Object.assign(form, { ...row, parentId: row.parentId || 0 })
dialogVisible.value = true
}
async function handleDelete(row) {
await ElMessageBox.confirm('确定删除该分类吗?子分类也会被删除。', '确认删除', { type: 'warning' })
await deleteCategory(row.id)
ElMessage.success('删除成功')
loadData()
}
async function handleSubmit() {
const data = { ...form }
delete data.id
if (form.id) {
await updateCategory(form.id, data)
ElMessage.success('编辑成功')
} else {
await createCategory(data)
ElMessage.success('新增成功')
}
dialogVisible.value = false
loadData()
}
onMounted(loadData)
</script>
<template>
<div class="page-container">
<div class="tool-bar">
<el-button type="primary" v-permission="'category:add'" :icon="Plus" @click="handleAdd(0)">+ 新增顶级分类</el-button>
</div>
<el-table :data="tableData" border stripe row-key="id" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" default-expand-all>
<el-table-column prop="name" label="分类名称" min-width="200" />
<el-table-column prop="code" label="分类编码" width="150" />
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<el-button size="small" v-permission="'category:edit'" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" v-permission="'category:add'" @click="handleAdd(row.id)">添加子分类</el-button>
<el-button size="small" type="danger" v-permission="'category:delete'" @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form ref="formRef" :model="form" label-width="80px">
<el-form-item label="上级分类">
<el-select v-model="form.parentId" placeholder="不选则为顶级分类" clearable style="width:100%">
<el-option v-for="p in parentOptions.filter(x => x.id !== form.id)" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
</el-form-item>
<el-form-item label="名称" required>
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="编码" required>
<el-input v-model="form.code" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sortOrder" :min="0" style="width:100%" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit"> </el-button>
</template>
</el-dialog>
</div>
</template>

View File

@@ -0,0 +1,58 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getCustomerList, createCustomer, updateCustomer, deleteCustomer } from '@/api/customer'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh } from '@element-plus/icons-vue'
const query = reactive({ page: 1, size: 10, name: '' })
const tableData = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const dialogTitle = ref('新增客户')
const form = reactive({ id: null, name: '', contactPerson: '', phone: '', email: '', address: '', type: '个人', remark: '' })
async function loadData() { try { const r = await getCustomerList(query); tableData.value = r.data?.records || []; total.value = r.data?.total || 0 } catch { /* */ } }
function handleAdd() { dialogTitle.value = '新增客户'; Object.keys(form).forEach(k => form[k] = typeof form[k] === 'number' ? null : ''); form.type = '个人'; form.id = null; dialogVisible.value = true }
function handleEdit(row) { dialogTitle.value = '编辑客户'; Object.assign(form, row); dialogVisible.value = true }
async function handleDelete(row) { await ElMessageBox.confirm('确定删除?', '确认', { type: 'warning' }); await deleteCustomer(row.id); ElMessage.success('删除成功'); loadData() }
async function handleSubmit() { const d = { ...form }; delete d.id; if (form.id) { await updateCustomer(form.id, d); ElMessage.success('编辑成功') } else { await createCustomer(d); ElMessage.success('新增成功') }; dialogVisible.value = false; loadData() }
onMounted(loadData)
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="query.name" placeholder="客户名称" clearable />
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>
<el-button :icon="Refresh" @click="query.name='';loadData()">重置</el-button>
</div>
<div class="tool-bar">
<el-button type="primary" v-permission="'customer:add'" @click="handleAdd">+ 新增客户</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="name" label="客户名称" min-width="180" />
<el-table-column prop="contactPerson" label="联系人" width="100" />
<el-table-column prop="phone" label="电话" width="130" />
<el-table-column prop="type" label="类型" width="100" />
<el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }"><div class="table-actions"><el-button size="small" v-permission="'customer:edit'" @click="handleEdit(row)">编辑</el-button><el-button size="small" type="danger" v-permission="'customer:delete'" @click="handleDelete(row)">删除</el-button></div></template>
</el-table-column>
</el-table>
<div style="display:flex;justify-content:flex-end;margin-top:16px"><el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next" @current-change="loadData" @size-change="query.page=1;loadData()" /></div>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form :model="form" label-width="80px">
<el-form-item label="名称" required><el-input v-model="form.name" /></el-form-item>
<el-form-item label="联系人"><el-input v-model="form.contactPerson" /></el-form-item>
<el-form-item label="电话"><el-input v-model="form.phone" /></el-form-item>
<el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item>
<el-form-item label="类型"><el-select v-model="form.type" style="width:100%"><el-option label="个人" value="个人" /><el-option label="医院" value="医院" /><el-option label="药店" value="药店" /><el-option label="诊所" value="诊所" /><el-option label="批发商" value="批发商" /></el-select></el-form-item>
<el-form-item label="地址"><el-input v-model="form.address" /></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></el-form-item>
</el-form>
<template #footer><el-button @click="dialogVisible=false">取消</el-button><el-button type="primary" @click="handleSubmit">确定</el-button></template>
</el-dialog>
</div>
</template>

View File

@@ -0,0 +1,120 @@
<script setup>
import { ref, onMounted } from 'vue'
import { getStatistics, getExpiryWarning } from '@/api/dashboard'
import { getStockInList } from '@/api/stockIn'
import { getStockOutList } from '@/api/stockOut'
import { Goods, Box, DocumentChecked, Warning } from '@element-plus/icons-vue'
const statistics = ref({ medicineCount: 0, stockBatchCount: 0, pendingStockIn: 0, expiryWarningCount: 0 })
const expiryList = ref([])
const recentInList = ref([])
const recentOutList = ref([])
const statCards = [
{ key: 'medicineCount', label: '药品总数', color: '#409eff', icon: Goods },
{ key: 'stockBatchCount', label: '库存批次', color: '#67c23a', icon: Box },
{ key: 'pendingStockIn', label: '待审入库', color: '#e6a23c', icon: DocumentChecked },
{ key: 'expiryWarningCount', label: '效期预警', color: '#f56c6c', icon: Warning },
]
function expiryColor(date) {
const days = Math.ceil((new Date(date) - new Date()) / (1000 * 60 * 60 * 24))
if (days < 30) return 'danger'
if (days < 90) return 'warning'
return ''
}
onMounted(async () => {
try {
const [statRes, warnRes, inRes, outRes] = await Promise.all([
getStatistics(),
getExpiryWarning(),
getStockInList({ page: 1, size: 5 }),
getStockOutList({ page: 1, size: 5 }),
])
statistics.value = statRes.data
expiryList.value = warnRes.data?.records || warnRes.data || []
recentInList.value = inRes.data?.records || []
recentOutList.value = outRes.data?.records || []
} catch { /* 后端未启动,静默处理 */ }
})
</script>
<template>
<div>
<!-- 统计卡片 -->
<div class="stat-cards">
<div v-for="card in statCards" :key="card.key" class="stat-card">
<div class="stat-icon" :style="{ background: card.color }">
<el-icon><component :is="card.icon" /></el-icon>
</div>
<div class="stat-info">
<div class="stat-label">{{ card.label }}</div>
<div class="stat-value">{{ statistics[card.key] || 0 }}</div>
</div>
</div>
</div>
<!-- 效期预警 -->
<el-card class="section-card" header="效期预警近90天">
<el-table :data="expiryList" size="small" empty-text="暂无预警数据">
<el-table-column prop="medicineName" label="药品名称" />
<el-table-column prop="batchNo" label="批号" />
<el-table-column prop="quantity" label="库存" width="80" />
<el-table-column prop="expiryDate" label="有效期至" width="120">
<template #default="{ row }">
<el-tag :type="expiryColor(row.expiryDate || row.expiry_date)" size="small">
{{ row.expiryDate || row.expiry_date }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 最近记录 -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px;">
<el-card header="最近入库">
<template v-if="recentInList.length">
<div v-for="item in recentInList" :key="item.id" class="record-item">
<span>{{ item.recordNo || item.record_no }}</span>
<span>{{ item.supplierName || '供应商' }}</span>
<el-tag :type="item.status === 1 ? 'success' : 'warning'" size="small">
{{ item.status === 1 ? '已入库' : item.status === 0 ? '待审核' : '已取消' }}
</el-tag>
</div>
</template>
<el-empty v-else description="暂无数据" />
</el-card>
<el-card header="最近出库">
<template v-if="recentOutList.length">
<div v-for="item in recentOutList" :key="item.id" class="record-item">
<span>{{ item.recordNo || item.record_no }}</span>
<span>{{ item.customerName || '客户' }}</span>
<el-tag :type="item.status === 1 ? 'success' : 'warning'" size="small">
{{ item.status === 1 ? '已出库' : item.status === 0 ? '待审核' : '已取消' }}
</el-tag>
</div>
</template>
<el-empty v-else description="暂无数据" />
</el-card>
</div>
</div>
</template>
<style lang="scss" scoped>
.section-card {
margin-bottom: 16px;
}
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
</style>

View File

@@ -0,0 +1,110 @@
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { User, Lock } from '@element-plus/icons-vue'
const router = useRouter()
const userStore = useUserStore()
const loginFormRef = ref(null)
const loading = ref(false)
const loginForm = reactive({
username: 'admin',
password: 'admin123',
remember: false,
})
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
}
async function handleLogin() {
const valid = await loginFormRef.value.validate().catch(() => false)
if (!valid) return
loading.value = true
try {
await userStore.login({
username: loginForm.username,
password: loginForm.password,
})
router.push('/dashboard')
} catch {
// 错误已在拦截器中处理
} finally {
loading.value = false
}
}
</script>
<template>
<div class="login-page">
<div class="login-card">
<h2 class="login-title">药品管理系统</h2>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="rules"
size="large"
@keyup.enter="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="用户名"
:prefix-icon="User"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
show-password
placeholder="密码"
:prefix-icon="Lock"
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="loginForm.remember">记住我</el-checkbox>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="loading"
style="width: 100%"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登 录' }}
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<style lang="scss" scoped>
.login-page {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #051f96 100%);
}
.login-card {
width: 420px;
padding: 40px;
background: #fff;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
}
.login-title {
text-align: center;
margin-bottom: 32px;
color: #303133;
font-size: 24px;
}
</style>

View File

@@ -0,0 +1,254 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getMedicineList, createMedicine, updateMedicine, deleteMedicine } from '@/api/medicine'
import { getCategoryTree } from '@/api/category'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh } from '@element-plus/icons-vue'
const query = reactive({ page: 1, size: 10, name: '', categoryId: null })
const tableData = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const dialogTitle = ref('新增药品')
const formRef = ref(null)
const categoryOptions = ref([])
const form = reactive({
id: null, categoryId: null, name: '', genericName: '', specification: '',
manufacturer: '', approvalNumber: '', barcode: '', dosageForm: '',
unit: '盒', retailPrice: 0, wholesalePrice: 0, storageCondition: '',
shelfLife: 24, warningStock: 10, description: '',
})
const rules = {
categoryId: [{ required: true, message: '请选择分类', trigger: 'change' }],
name: [{ required: true, message: '请输入药品名称', trigger: 'blur' }],
specification: [{ required: true, message: '请输入规格', trigger: 'blur' }],
manufacturer: [{ required: true, message: '请输入生产厂家', trigger: 'blur' }],
approvalNumber: [{ required: true, message: '请输入批准文号', trigger: 'blur' }],
}
async function loadData() {
try {
const res = await getMedicineList(query)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} catch { /* 后端未启动 */ }
}
function resetSearch() {
query.name = ''
query.categoryId = null
query.page = 1
loadData()
}
function handleAdd() {
dialogTitle.value = '新增药品'
Object.assign(form, { id: null, categoryId: null, name: '', genericName: '', specification: '',
manufacturer: '', approvalNumber: '', barcode: '', dosageForm: '', unit: '盒',
retailPrice: 0, wholesalePrice: 0, storageCondition: '', shelfLife: 24, warningStock: 10, description: '' })
dialogVisible.value = true
}
function handleEdit(row) {
dialogTitle.value = '编辑药品'
Object.assign(form, row)
dialogVisible.value = true
}
async function handleDelete(row) {
await ElMessageBox.confirm('确定删除该药品吗?', '确认删除', { type: 'warning' })
await deleteMedicine(row.id)
ElMessage.success('删除成功')
loadData()
}
async function handleSubmit() {
const valid = await formRef.value.validate().catch(() => false)
if (!valid) return
const data = { ...form }
delete data.id
if (form.id) {
await updateMedicine(form.id, data)
ElMessage.success('编辑成功')
} else {
await createMedicine(data)
ElMessage.success('新增成功')
}
dialogVisible.value = false
loadData()
}
function handleSizeChange(size) {
query.size = size
query.page = 1
loadData()
}
onMounted(async () => {
try { categoryOptions.value = (await getCategoryTree()).data || [] } catch { /* */ }
loadData()
})
</script>
<template>
<div class="page-container">
<!-- 搜索栏 -->
<div class="search-bar">
<el-input v-model="query.name" placeholder="药品名称" clearable @keyup.enter="loadData" />
<el-cascader
v-model="query.categoryId"
:options="categoryOptions"
:props="{ value: 'id', label: 'name', checkStrictly: true, emitPath: false }"
placeholder="药品分类"
clearable
/>
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
</div>
<!-- 工具栏 -->
<div class="tool-bar">
<el-button type="primary" v-permission="'medicine:add'" @click="handleAdd">+ 新增药品</el-button>
</div>
<!-- 表格 -->
<el-table :data="tableData" border stripe v-loading="false">
<el-table-column prop="name" label="药品名称" min-width="160" />
<el-table-column prop="specification" label="规格" width="120" />
<el-table-column prop="manufacturer" label="生产厂家" min-width="160" />
<el-table-column prop="approvalNumber" label="批准文号" width="140" show-overflow-tooltip />
<el-table-column prop="dosageForm" label="剂型" width="80" />
<el-table-column prop="retailPrice" label="零售价" width="90">
<template #default="{ row }">¥{{ row.retailPrice }}</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<el-button size="small" v-permission="'medicine:edit'" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" v-permission="'medicine:delete'" @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div style="display: flex; justify-content: flex-end; margin-top: 16px;">
<el-pagination
v-model:current-page="query.page"
v-model:page-size="query.size"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="loadData"
/>
</div>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="650px" @closed="formRef?.resetFields()">
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px" class="dialog-form">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="药品名称" prop="name">
<el-input v-model="form.name" placeholder="通用名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="英文名称" prop="genericName">
<el-input v-model="form.genericName" placeholder="化学通用名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="药品分类" prop="categoryId">
<el-cascader
v-model="form.categoryId"
:options="categoryOptions"
:props="{ value: 'id', label: 'name', checkStrictly: true, emitPath: false }"
placeholder="选择分类"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="规格" prop="specification">
<el-input v-model="form.specification" placeholder="如 0.25g×24粒" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="生产厂家" prop="manufacturer">
<el-input v-model="form.manufacturer" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="批准文号" prop="approvalNumber">
<el-input v-model="form.approvalNumber" placeholder="国药准字XXXX" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="条形码">
<el-input v-model="form.barcode" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="剂型">
<el-select v-model="form.dosageForm" placeholder="选择剂型">
<el-option label="片剂" value="片剂" />
<el-option label="胶囊剂" value="胶囊剂" />
<el-option label="注射液" value="注射液" />
<el-option label="口服液" value="口服液" />
<el-option label="颗粒剂" value="颗粒剂" />
<el-option label="乳膏剂" value="乳膏剂" />
<el-option label="丸剂" value="丸剂" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="单位">
<el-select v-model="form.unit">
<el-option label="盒" value="盒" />
<el-option label="瓶" value="瓶" />
<el-option label="支" value="支" />
<el-option label="袋" value="袋" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="零售价">
<el-input-number v-model="form.retailPrice" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="批发价">
<el-input-number v-model="form.wholesalePrice" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="储存条件">
<el-input v-model="form.storageCondition" placeholder="常温/阴凉/冷藏" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="有效期(月)">
<el-input-number v-model="form.shelfLife" :min="1" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="预警阈值">
<el-input-number v-model="form.warningStock" :min="0" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="form.description" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit"> </el-button>
</template>
</el-dialog>
</div>
</template>

View File

@@ -0,0 +1,67 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getRoleList, createRole, updateRole, deleteRole, assignMenus } from '@/api/role'
import { getMenusApi } from '@/api/auth'
import { ElMessage, ElMessageBox } from 'element-plus'
const tableData = ref([])
const dialogVisible = ref(false)
const dialogTitle = ref('新增角色')
const form = reactive({ id: null, name: '', code: '', description: '', status: 1 })
const menuVisible = ref(false)
const menuForm = reactive({ roleId: null, menuIds: [] })
const menuTree = ref([])
const menuCheckKeys = ref([])
async function loadData() { try { const r = await getRoleList(); tableData.value = r.data?.records || r.data || [] } catch { /* */ } }
function handleAdd() { dialogTitle.value = '新增角色'; Object.keys(form).forEach(k => form[k] = k === 'status' ? 1 : ''); form.id = null; dialogVisible.value = true }
function handleEdit(row) { dialogTitle.value = '编辑角色'; Object.assign(form, row); dialogVisible.value = true }
async function handleDelete(row) { await ElMessageBox.confirm('确定删除?', '确认', { type: 'warning' }); await deleteRole(row.id); ElMessage.success('删除成功'); loadData() }
async function handleSubmit() { const d = { ...form }; delete d.id; if (form.id) { await updateRole(form.id, d); ElMessage.success('编辑成功') } else { await createRole(d); ElMessage.success('新增成功') }; dialogVisible.value = false; loadData() }
async function handleMenu(row) { menuForm.roleId = row.id; const menus = await getMenusApi(); menuTree.value = buildTree(menus.data || menus || []); menuCheckKeys.value = collectMenuIds(row.menuIds || []); menuVisible.value = true }
function buildTree(list, parentId = 0) { return list.filter(m => m.parentId === parentId).map(m => ({ ...m, children: buildTree(list, m.id) })) }
function collectMenuIds(ids) { return ids }
async function saveMenus() {
const ids = menuCheckKeys.value
await assignMenus(menuForm.roleId, { menuIds: ids })
ElMessage.success('权限分配成功'); menuVisible.value = false; loadData()
}
onMounted(loadData)
</script>
<template>
<div class="page-container">
<div class="tool-bar">
<el-button type="primary" v-permission="'role:add'" @click="handleAdd">+ 新增角色</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="name" label="角色名称" width="140" />
<el-table-column prop="code" label="角色编码" width="140" />
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80" align="center"><template #default="{row}"><el-tag :type="row.status===1?'success':'danger'" size="small">{{row.status===1?'启用':'停用'}}</el-tag></template></el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{row}"><div class="table-actions"><el-button size="small" v-permission="'role:assign-menu'" @click="handleMenu(row)">权限</el-button><el-button size="small" v-permission="'role:edit'" @click="handleEdit(row)">编辑</el-button><el-button size="small" type="danger" v-permission="'role:delete'" @click="handleDelete(row)">删除</el-button></div></template>
</el-table-column>
</el-table>
<!-- 编辑角色弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="450px">
<el-form :model="form" label-width="70px">
<el-form-item label="名称" required><el-input v-model="form.name" /></el-form-item>
<el-form-item label="编码" required><el-input v-model="form.code" /></el-form-item>
<el-form-item label="描述"><el-input v-model="form.description" type="textarea" :rows="2" /></el-form-item>
<el-form-item label="状态"><el-switch v-model="form.status" :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="handleSubmit">确定</el-button></template>
</el-dialog>
<!-- 权限分配弹窗 -->
<el-dialog v-model="menuVisible" title="分配权限" width="500px">
<el-tree ref="menuTreeRef" :data="menuTree" show-checkbox node-key="id" :props="{label:'name',children:'children'}" :default-checked-keys="menuCheckKeys" @check="(_,{checkedKeys})=>menuCheckKeys=checkedKeys" />
<template #footer><el-button @click="menuVisible=false">取消</el-button><el-button type="primary" @click="saveMenus">保存</el-button></template>
</el-dialog>
</div>
</template>

View File

@@ -0,0 +1,50 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getStockList } from '@/api/stock'
import { Search, Refresh } from '@element-plus/icons-vue'
const query = reactive({ page: 1, size: 10, medicineName: '', batchNo: '' })
const tableData = ref([])
const total = ref(0)
function expiryColor(date) {
if (!date) return ''
const days = Math.ceil((new Date(date) - new Date()) / (1000 * 60 * 60 * 24))
if (days < 30) return 'danger'
if (days < 90) return 'warning'
return 'success'
}
function daysLeft(date) {
if (!date) return ''
return Math.ceil((new Date(date) - new Date()) / (1000 * 60 * 60 * 24))
}
async function loadData() { try { const r = await getStockList(query); tableData.value = r.data?.records || []; total.value = r.data?.total || 0 } catch { /* */ } }
onMounted(loadData)
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="query.medicineName" placeholder="药品名称" clearable />
<el-input v-model="query.batchNo" placeholder="批号" clearable />
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>
<el-button :icon="Refresh" @click="query.medicineName='';query.batchNo='';loadData()">重置</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="medicineName" label="药品名称" min-width="160" />
<el-table-column prop="batchNo" label="批号" width="120" />
<el-table-column prop="quantity" label="库存数量" width="100" align="center" />
<el-table-column prop="costPrice" label="成本价" width="100"><template #default="{row}">¥{{row.costPrice}}</template></el-table-column>
<el-table-column prop="expiryDate" label="有效期至" width="120">
<template #default="{row}">
<el-tag :type="expiryColor(row.expiryDate)" size="small">{{ row.expiryDate }} <span v-if="daysLeft(row.expiryDate) < 90">({{daysLeft(row.expiryDate)}})</span></el-tag>
</template>
</el-table-column>
<el-table-column prop="warehouseName" label="仓库" width="120" />
<el-table-column prop="productionDate" label="生产日期" width="120" />
</el-table>
<div style="display:flex;justify-content:flex-end;margin-top:16px"><el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next" @current-change="loadData" @size-change="query.page=1;loadData()" /></div>
</div>
</template>

View File

@@ -0,0 +1,74 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getAllSuppliers } from '@/api/supplier'
import { getMedicineList } from '@/api/medicine'
import { createStockIn } from '@/api/stockIn'
import { ElMessage } from 'element-plus'
const router = useRouter()
const supplierOptions = ref([])
const medicineOptions = ref([])
const form = reactive({ supplierId: null, warehouseId: 1, inDate: new Date().toISOString().slice(0, 10), remark: '' })
const details = ref([])
function addDetail() {
details.value.push({ medicineId: null, batchNo: '', quantity: 1, costPrice: 0, productionDate: '', expiryDate: '' })
}
function removeDetail(index) { details.value.splice(index, 1) }
function detailTotal(d) { return (d.quantity || 0) * (d.costPrice || 0) }
const totalAmount = ref(0)
function updateTotal() { totalAmount.value = details.value.reduce((s, d) => s + detailTotal(d), 0) }
async function loadOptions() {
try {
const [sRes, mRes] = await Promise.all([getAllSuppliers(), getMedicineList({ page: 1, size: 999 })])
supplierOptions.value = sRes.data || []
medicineOptions.value = mRes.data?.records || []
} catch { /* */ }
}
async function handleSubmit() {
if (!form.supplierId) { ElMessage.warning('请选择供应商'); return }
if (!details.value.length) { ElMessage.warning('请添加药品明细'); return }
updateTotal()
await createStockIn({ ...form, details: details.value, totalAmount: totalAmount.value })
ElMessage.success('入库单已创建,等待审核')
router.push('/stock-in/list')
}
onMounted(loadOptions)
</script>
<template>
<div class="page-container">
<h3 style="margin-bottom:16px">新建入库单</h3>
<el-form :model="form" label-width="90px" style="max-width:700px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="供应商" required><el-select v-model="form.supplierId" placeholder="选择供应商" filterable style="width:100%"><el-option v-for="s in supplierOptions" :key="s.id" :label="s.name" :value="s.id" /></el-select></el-form-item></el-col>
<el-col :span="12"><el-form-item label="入库日期" required><el-date-picker v-model="form.inDate" type="date" placeholder="选择日期" style="width:100%" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></el-form-item></el-col>
</el-row>
</el-form>
<!-- 药品明细 -->
<div style="margin:20px 0"><el-button type="primary" @click="addDetail">+ 添加药品</el-button><span style="margin-left:12px;color:#909399">总计: ¥{{ totalAmount }}</span></div>
<el-table :data="details" border>
<el-table-column label="药品" min-width="200">
<template #default="{row}"><el-select v-model="row.medicineId" placeholder="选择药品" filterable style="width:100%"><el-option v-for="m in medicineOptions" :key="m.id" :label="`${m.name} (${m.specification})`" :value="m.id" /></el-select></template>
</el-table-column>
<el-table-column label="批号" width="140"><template #default="{row}"><el-input v-model="row.batchNo" size="small" /></template></el-table-column>
<el-table-column label="数量" width="100"><template #default="{row}"><el-input-number v-model="row.quantity" :min="1" size="small" style="width:100%" @change="updateTotal" /></template></el-table-column>
<el-table-column label="进价" width="110"><template #default="{row}"><el-input-number v-model="row.costPrice" :min="0" :precision="2" size="small" style="width:100%" @change="updateTotal" /></template></el-table-column>
<el-table-column label="小计" width="110"><template #default="{row}">¥{{detailTotal(row)}}</template></el-table-column>
<el-table-column label="生产日期" width="140"><template #default="{row}"><el-date-picker v-model="row.productionDate" type="date" size="small" /></template></el-table-column>
<el-table-column label="有效期至" width="140"><template #default="{row}"><el-date-picker v-model="row.expiryDate" type="date" size="small" /></template></el-table-column>
<el-table-column label="操作" width="80"><template #default="{ $index }"><el-button size="small" type="danger" @click="removeDetail($index)">移除</el-button></template></el-table-column>
</el-table>
<div style="margin-top:20px">
<el-button @click="router.back()">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交入库单</el-button>
</div>
</div>
</template>

View File

@@ -0,0 +1,57 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getStockInList, auditStockIn } from '@/api/stockIn'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const router = useRouter()
const query = reactive({ page: 1, size: 10 })
const tableData = ref([])
const total = ref(0)
function statusTag(status) {
return { 0: 'warning', 1: 'success', 2: 'info' }[status] || 'info'
}
function statusText(status) {
return { 0: '待审核', 1: '已入库', 2: '已取消' }[status] || '未知'
}
async function loadData() { try { const r = await getStockInList(query); tableData.value = r.data?.records || []; total.value = r.data?.total || 0 } catch { /* */ } }
function handleCreate() { router.push('/stock-in/add') }
function handleView(row) { /* 查看详情弹窗 */ ElMessage.info('入库单号: ' + (row.recordNo || row.record_no)) }
async function handleAudit(row) { await auditStockIn(row.id, { status: 1 }); ElMessage.success('审核通过,库存已更新'); loadData() }
onMounted(loadData)
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="query.recordNo" placeholder="入库单号" clearable style="width:180px" />
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>
<el-button :icon="Refresh" @click="Object.keys(query).forEach(k=>query[k]=k==='page'?1:k==='size'?10:'');loadData()">重置</el-button>
</div>
<div class="tool-bar">
<el-button type="primary" v-permission="'stock-in:add'" :icon="Plus" @click="handleCreate">+ 新建入库单</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="recordNo" label="入库单号" width="150" />
<el-table-column prop="supplierName" label="供应商" min-width="180" />
<el-table-column prop="totalAmount" label="总金额" width="110"><template #default="{row}">¥{{row.totalAmount}}</template></el-table-column>
<el-table-column prop="inDate" label="入库日期" width="120" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{row}"><el-tag :type="statusTag(row.status)" size="small">{{statusText(row.status)}}</el-tag></template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{row}">
<div class="table-actions">
<el-button size="small" @click="handleView(row)">查看</el-button>
<el-button v-if="row.status===0" size="small" type="success" v-permission="'stock-in:audit'" @click="handleAudit(row)">审核</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div style="display:flex;justify-content:flex-end;margin-top:16px"><el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next" @current-change="loadData" @size-change="query.page=1;loadData()" /></div>
</div>
</template>

View File

@@ -0,0 +1,86 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getAllCustomers } from '@/api/customer'
import { getStockList } from '@/api/stock'
import { createStockOut } from '@/api/stockOut'
import { ElMessage } from 'element-plus'
const router = useRouter()
const customerOptions = ref([])
const stockOptions = ref([])
const form = reactive({ customerId: null, warehouseId: 1, outDate: new Date().toISOString().slice(0, 10), outType: 1, remark: '' })
const details = ref([])
function addDetail() {
details.value.push({ medicineId: null, batchNo: '', quantity: 1, unitPrice: 0 })
}
function removeDetail(i) { details.value.splice(i, 1) }
function detailTotal(d) { return (d.quantity || 0) * (d.unitPrice || 0) }
const totalAmount = ref(0)
function updateTotal() { totalAmount.value = details.value.reduce((s, d) => s + detailTotal(d), 0) }
async function loadOptions() {
try {
const [cRes, sRes] = await Promise.all([getAllCustomers(), getStockList({ page: 1, size: 999 })])
customerOptions.value = cRes.data || []
stockOptions.value = sRes.data?.records || []
} catch { /* */ }
}
function onMedicineSelected(index, stockId) {
const stock = stockOptions.value.find(s => s.id === stockId)
if (stock) {
details.value[index].medicineId = stock.medicineId
details.value[index].batchNo = stock.batchNo
details.value[index].unitPrice = stock.costPrice || 0
details.value[index]._maxQty = stock.quantity
}
}
async function handleSubmit() {
if (!form.customerId) { ElMessage.warning('请选择客户'); return }
if (!details.value.length) { ElMessage.warning('请添加药品明细'); return }
updateTotal()
await createStockOut({ ...form, details: details.value, totalAmount: totalAmount.value })
ElMessage.success('出库单已创建,等待审核')
router.push('/stock-out/list')
}
onMounted(loadOptions)
</script>
<template>
<div class="page-container">
<h3 style="margin-bottom:16px">新建出库单</h3>
<el-form :model="form" label-width="90px" style="max-width:700px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="客户" required><el-select v-model="form.customerId" placeholder="选择客户" filterable style="width:100%"><el-option v-for="c in customerOptions" :key="c.id" :label="c.name" :value="c.id" /></el-select></el-form-item></el-col>
<el-col :span="12"><el-form-item label="出库类型"><el-select v-model="form.outType" style="width:100%"><el-option label="销售出库" :value="1" /><el-option label="领用出库" :value="2" /><el-option label="报损出库" :value="3" /><el-option label="退货出库" :value="4" /></el-select></el-form-item></el-col>
<el-col :span="12"><el-form-item label="出库日期" required><el-date-picker v-model="form.outDate" type="date" style="width:100%" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></el-form-item></el-col>
</el-row>
</el-form>
<div style="margin:20px 0"><el-button type="primary" @click="addDetail">+ 添加药品</el-button><span style="margin-left:12px;color:#909399">总计: ¥{{ totalAmount }}</span></div>
<el-table :data="details" border>
<el-table-column label="库存批次" min-width="220">
<template #default="{row, $index}">
<el-select :model-value="row._stockId" placeholder="选择药品批次" filterable style="width:100%" @change="onMedicineSelected($index, $event)">
<el-option v-for="s in stockOptions" :key="s.id" :label="`${s.medicineName} - ${s.batchNo} (库存${s.quantity})`" :value="s.id" />
</el-select>
</template>
</el-table-column>
<el-table-column label="批号" width="120"><template #default="{row}">{{row.batchNo}}</template></el-table-column>
<el-table-column label="数量" width="100"><template #default="{row}"><el-input-number v-model="row.quantity" :min="1" :max="row._maxQty||9999" size="small" style="width:100%" @change="updateTotal" /></template></el-table-column>
<el-table-column label="单价" width="110"><template #default="{row}"><el-input-number v-model="row.unitPrice" :min="0" :precision="2" size="small" style="width:100%" @change="updateTotal" /></template></el-table-column>
<el-table-column label="小计" width="110"><template #default="{row}">¥{{detailTotal(row)}}</template></el-table-column>
<el-table-column label="操作" width="80"><template #default="{ $index }"><el-button size="small" type="danger" @click="removeDetail($index)">移除</el-button></template></el-table-column>
</el-table>
<div style="margin-top:20px">
<el-button @click="router.back()">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交出库单</el-button>
</div>
</div>
</template>

View File

@@ -0,0 +1,55 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getStockOutList, auditStockOut } from '@/api/stockOut'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const router = useRouter()
const query = reactive({ page: 1, size: 10 })
const tableData = ref([])
const total = ref(0)
function statusTag(s) { return { 0: 'warning', 1: 'success', 2: 'info' }[s] || 'info' }
function statusText(s) { return { 0: '待审核', 1: '已出库', 2: '已取消' }[s] || '未知' }
function typeText(s) { return { 1: '销售出库', 2: '领用出库', 3: '报损出库', 4: '退货出库' }[s] || '未知' }
async function loadData() { try { const r = await getStockOutList(query); tableData.value = r.data?.records || []; total.value = r.data?.total || 0 } catch { /* */ } }
function handleCreate() { router.push('/stock-out/add') }
function handleView(row) { ElMessage.info('出库单号: ' + (row.recordNo || row.record_no)) }
async function handleAudit(row) { await auditStockOut(row.id, { status: 1 }); ElMessage.success('审核通过,库存已扣减'); loadData() }
onMounted(loadData)
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="query.recordNo" placeholder="出库单号" clearable style="width:180px" />
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>
<el-button :icon="Refresh" @click="Object.keys(query).forEach(k=>query[k]=k==='page'?1:k==='size'?10:'');loadData()">重置</el-button>
</div>
<div class="tool-bar">
<el-button type="primary" v-permission="'stock-out:add'" :icon="Plus" @click="handleCreate">+ 新建出库单</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="recordNo" label="出库单号" width="150" />
<el-table-column prop="customerName" label="客户" min-width="160" />
<el-table-column prop="outType" label="类型" width="100"><template #default="{row}">{{typeText(row.outType)}}</template></el-table-column>
<el-table-column prop="totalAmount" label="总金额" width="110"><template #default="{row}">¥{{row.totalAmount}}</template></el-table-column>
<el-table-column prop="outDate" label="出库日期" width="120" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{row}"><el-tag :type="statusTag(row.status)" size="small">{{statusText(row.status)}}</el-tag></template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{row}">
<div class="table-actions">
<el-button size="small" @click="handleView(row)">查看</el-button>
<el-button v-if="row.status===0" size="small" type="success" v-permission="'stock-out:audit'" @click="handleAudit(row)">审核</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div style="display:flex;justify-content:flex-end;margin-top:16px"><el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next" @current-change="loadData" @size-change="query.page=1;loadData()" /></div>
</div>
</template>

View File

@@ -0,0 +1,78 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getSupplierList, createSupplier, updateSupplier, deleteSupplier } from '@/api/supplier'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh } from '@element-plus/icons-vue'
const query = reactive({ page: 1, size: 10, name: '', phone: '' })
const tableData = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const dialogTitle = ref('新增供应商')
const formRef = ref(null)
const form = reactive({ id: null, name: '', contactPerson: '', phone: '', email: '', address: '', bankAccount: '', bankName: '', taxId: '', licenseNumber: '', status: 1, remark: '' })
async function loadData() {
try { const res = await getSupplierList(query); tableData.value = res.data?.records || []; total.value = res.data?.total || 0 } catch { /* */ }
}
function handleAdd() { dialogTitle.value = '新增供应商'; Object.keys(form).forEach(k => form[k] = k === 'status' ? 1 : null); form.id = null; dialogVisible.value = true }
function handleEdit(row) { dialogTitle.value = '编辑供应商'; Object.assign(form, row); dialogVisible.value = true }
async function handleDelete(row) { await ElMessageBox.confirm('确定删除?', '确认', { type: 'warning' }); await deleteSupplier(row.id); ElMessage.success('删除成功'); loadData() }
async function handleSubmit() { const data = { ...form }; delete data.id; if (form.id) { await updateSupplier(form.id, data); ElMessage.success('编辑成功') } else { await createSupplier(data); ElMessage.success('新增成功') }; dialogVisible.value = false; loadData() }
async function toggleStatus(row) { await updateSupplier(row.id, { status: row.status }); ElMessage.success('状态已更新') }
onMounted(loadData)
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="query.name" placeholder="供应商名称" clearable @keyup.enter="loadData" />
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>
<el-button :icon="Refresh" @click="query.name = ''; loadData()">重置</el-button>
</div>
<div class="tool-bar">
<el-button type="primary" v-permission="'supplier:add'" @click="handleAdd">+ 新增供应商</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="name" label="供应商名称" min-width="180" />
<el-table-column prop="contactPerson" label="联系人" width="100" />
<el-table-column prop="phone" label="电话" width="130" />
<el-table-column prop="email" label="邮箱" width="180" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-switch v-model="row.status" :active-value="1" :inactive-value="0" @change="toggleStatus(row)" />
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<el-button size="small" v-permission="'supplier:edit'" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" v-permission="'supplier:delete'" @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div style="display:flex;justify-content:flex-end;margin-top:16px">
<el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next" @current-change="loadData" @size-change="query.page=1;loadData()" />
</div>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px">
<el-form ref="formRef" :model="form" label-width="90px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="名称" required><el-input v-model="form.name" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="联系人"><el-input v-model="form.contactPerson" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="电话"><el-input v-model="form.phone" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="银行账号"><el-input v-model="form.bankAccount" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="开户银行"><el-input v-model="form.bankName" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="税号"><el-input v-model="form.taxId" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="许可证号"><el-input v-model="form.licenseNumber" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="地址"><el-input v-model="form.address" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></el-form-item></el-col>
</el-row>
</el-form>
<template #footer><el-button @click="dialogVisible=false">取消</el-button><el-button type="primary" @click="handleSubmit">确定</el-button></template>
</el-dialog>
</div>
</template>

View File

@@ -0,0 +1,72 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getUserList, createUser, updateUser, deleteUser, assignRoles } from '@/api/user'
import { getAllRoles } from '@/api/role'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh } from '@element-plus/icons-vue'
const query = reactive({ page: 1, size: 10, username: '' })
const tableData = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const dialogTitle = ref('新增用户')
const form = reactive({ id: null, username: '', password: '', realName: '', phone: '', email: '', status: 1 })
const roleVisible = ref(false)
const roleForm = reactive({ userId: null, roleIds: [] })
const allRoles = ref([])
async function loadData() { try { const r = await getUserList(query); tableData.value = r.data?.records || []; total.value = r.data?.total || 0 } catch { /* */ } }
function handleAdd() { dialogTitle.value = '新增用户'; Object.keys(form).forEach(k => form[k] = k === 'status' ? 1 : ''); form.id = null; dialogVisible.value = true }
function handleEdit(row) { dialogTitle.value = '编辑用户'; Object.assign(form, row); form.password = ''; dialogVisible.value = true }
async function handleDelete(row) { await ElMessageBox.confirm('确定删除?', '确认', { type: 'warning' }); await deleteUser(row.id); ElMessage.success('删除成功'); loadData() }
async function handleSubmit() { const d = { ...form }; delete d.id; if (!d.password) delete d.password; if (form.id) { await updateUser(form.id, d); ElMessage.success('编辑成功') } else { await createUser(d); ElMessage.success('新增成功') }; dialogVisible.value = false; loadData() }
async function handleRole(row) { roleForm.userId = row.id; roleForm.roleIds = row.roleIds || []; roleVisible.value = true }
async function saveRoles() { await assignRoles(roleForm.userId, { roleIds: roleForm.roleIds }); ElMessage.success('角色分配成功'); roleVisible.value = false; loadData() }
onMounted(async () => { try { allRoles.value = (await getAllRoles()).data || [] } catch { /* */ }; loadData() })
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="query.username" placeholder="用户名" clearable />
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>
<el-button :icon="Refresh" @click="query.username='';loadData()">重置</el-button>
</div>
<div class="tool-bar">
<el-button type="primary" v-permission="'user:add'" @click="handleAdd">+ 新增用户</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="realName" label="姓名" width="100" />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column prop="email" label="邮箱" width="180" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80" align="center"><template #default="{row}"><el-tag :type="row.status===1?'success':'danger'" size="small">{{row.status===1?'启用':'停用'}}</el-tag></template></el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{row}"><div class="table-actions"><el-button size="small" v-permission="'user:edit'" @click="handleEdit(row)">编辑</el-button><el-button size="small" v-permission="'user:assign-role'" @click="handleRole(row)">角色</el-button><el-button size="small" type="danger" v-permission="'user:delete'" @click="handleDelete(row)">删除</el-button></div></template>
</el-table-column>
</el-table>
<div style="display:flex;justify-content:flex-end;margin-top:16px"><el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next" @current-change="loadData" @size-change="query.page=1;loadData()" /></div>
<!-- 用户弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form :model="form" label-width="80px">
<el-form-item label="用户名" required><el-input v-model="form.username" /></el-form-item>
<el-form-item label="密码" :required="!form.id"><el-input v-model="form.password" type="password" show-password :placeholder="form.id?'留空则不修改':''" /></el-form-item>
<el-form-item label="姓名"><el-input v-model="form.realName" /></el-form-item>
<el-form-item label="手机号"><el-input v-model="form.phone" /></el-form-item>
<el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item>
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="停用" /></el-form-item>
</el-form>
<template #footer><el-button @click="dialogVisible=false">取消</el-button><el-button type="primary" @click="handleSubmit">确定</el-button></template>
</el-dialog>
<!-- 角色分配弹窗 -->
<el-dialog v-model="roleVisible" title="分配角色" width="450px">
<el-checkbox-group v-model="roleForm.roleIds">
<el-checkbox v-for="r in allRoles" :key="r.id" :label="r.id" :value="r.id">{{ r.name }}</el-checkbox>
</el-checkbox-group>
<template #footer><el-button @click="roleVisible=false">取消</el-button><el-button type="primary" @click="saveRoles">保存</el-button></template>
</el-dialog>
</div>
</template>