Files
KCGL/inventory-web/src/views/dashboard/index.vue

651 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="dashboard-container">
<el-card class="welcome-card">
<template #header>
<div class="card-header">
<span class="title">👋 欢迎回来{{ userStore.username }}</span>
<div style="display: flex; align-items: center; gap: 10px;">
<el-button type="primary" plain size="small" @click="openWarehouseDialog" :icon="Location" class="warehouse-btn">
库位设置
</el-button>
<el-tag type="success">系统运行正常</el-tag>
<el-button type="info" plain size="small" @click="openPrinterDialog" :icon="Setting" class="printer-btn">
打印设置
</el-button>
</div>
</div>
</template>
<div class="card-body">
<h2>IRIS 库存管理系统</h2>
<p class="subtitle">请选择您要进行的业务操作</p>
<div class="action-buttons">
<el-button type="primary" size="large" @click="handleNav('/inventory/buy')">
<el-icon style="margin-right: 5px"><ShoppingCart /></el-icon>
采购入库
</el-button>
<el-button type="success" size="large" @click="handleNav('/material/index')">
<el-icon style="margin-right: 5px"><Box /></el-icon>
基础信息
</el-button>
<el-button type="warning" size="large" @click="handleNav('/operation/borrow')">
<el-icon style="margin-right: 5px"><Operation /></el-icon>
借库申请
</el-button>
</div>
<!-- 修改密码温馨提示 -->
<div class="password-tip">
💡 提示为了您的账号安全请点击右上角<strong>个人头像</strong>修改默认登录密码
</div>
</div>
</el-card>
<!-- 打印机配置弹窗 -->
<el-dialog v-model="printerDialogVisible" title="打印机 IP 配置" width="500px" destroy-on-close>
<el-form :model="printerForm" label-width="120px">
<el-form-item label="标签打印机 IP">
<el-input v-model="printerForm.label_ip" placeholder="例如 192.168.9.221" />
</el-form-item>
<el-form-item label="标签打印机端口">
<el-input v-model.number="printerForm.label_port" placeholder="例如 9100" />
</el-form-item>
<el-form-item label="网络打印机 IP">
<el-input v-model="printerForm.network_ip" placeholder="例如 192.168.9.250" />
</el-form-item>
<el-form-item label="网络打印机端口">
<el-input v-model.number="printerForm.network_port" placeholder="例如 9100" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="printerDialogVisible = false">取消</el-button>
<el-button type="primary" @click="savePrinterConfig" :loading="loading">保存</el-button>
</span>
</template>
</el-dialog>
<!-- 库位管理弹窗 -->
<el-dialog v-model="warehouseDialogVisible" title="库位管理" width="700px" destroy-on-close :close-on-click-modal="false">
<div class="warehouse-dialog">
<div class="warehouse-header">
<!-- 非批量模式 -->
<template v-if="!isBulkDeleteMode">
<el-button type="primary" @click="handleAddTopLevel" :icon="Plus">
新增顶级区域
</el-button>
<el-button type="success" @click="openBatchGenerate" :icon="Plus">
批量生成
</el-button>
<el-button type="danger" @click="isBulkDeleteMode = true" :icon="Delete">
批量删除
</el-button>
</template>
<!-- 批量模式 -->
<template v-else>
<el-button @click="cancelBatchMode" :icon="Close">
取消
</el-button>
<el-button type="danger" @click="handleBatchDelete" :disabled="selectedIds.length === 0" :icon="Delete">
确认删除 {{ selectedIds.length > 0 ? `(${selectedIds.length}项)` : '' }}
</el-button>
</template>
</div>
<el-scrollbar height="450px">
<el-tree
ref="treeRef"
:data="treeData"
node-key="id"
:props="treeProps"
:expand-on-click-node="false"
:default-expand-all="false"
:show-checkbox="isBulkDeleteMode"
@check="handleTreeCheck"
>
<template #default="{ node, data }">
<div class="tree-node">
<span class="node-label">{{ node.label }}</span>
<span class="node-actions">
<el-button
:type="getAddBtnType(node.level)"
link
size="small"
@click="handleAddChild(data)"
:icon="Plus"
>
新增下级
</el-button>
<el-button
type="warning"
link
size="small"
@click="handleEdit(data)"
:icon="Edit"
>
编辑
</el-button>
<el-button
type="danger"
link
size="small"
@click="handleDelete(data)"
:icon="Delete"
>
删除
</el-button>
</span>
</div>
</template>
</el-tree>
</el-scrollbar>
</div>
</el-dialog>
<!-- 新增/编辑库位弹窗 -->
<el-dialog v-model="locationFormVisible" :title="locationFormTitle" width="400px" destroy-on-close>
<el-form :model="locationForm" label-width="80px">
<el-form-item label="上级库位">
<el-input :value="locationForm.parentName" disabled />
</el-form-item>
<el-form-item label="名称" required>
<el-input v-model="locationForm.name" placeholder="请输入库位名称" />
</el-form-item>
<el-form-item label="启用状态">
<el-switch v-model="locationForm.is_enabled" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="locationFormVisible = false">取消</el-button>
<el-button type="primary" @click="saveLocation" :loading="locationLoading">保存</el-button>
</template>
</el-dialog>
<!-- 批量生成库位弹窗 -->
<el-dialog v-model="batchGenerateVisible" title="批量生成库位" width="800px" destroy-on-close>
<el-form :model="batchGenerateForm" label-width="100px">
<el-form-item label="父级库位">
<el-input :value="batchGenerateForm.parentName" disabled />
<el-button link size="small" @click="batchGenerateForm.parent_id = null; batchGenerateForm.parentName = '无(顶级)'">清空选择</el-button>
</el-form-item>
<el-divider>层级规则按顺序生成</el-divider>
<div v-for="(rule, index) in batchGenerateForm.rules" :key="index" class="rule-item">
<el-row :gutter="10">
<el-col :span="5">
<el-input v-model="rule.prefix" placeholder="前缀" />
</el-col>
<el-col :span="6">
<el-input-number v-model="rule.start" :min="1" placeholder="起始" style="width: 100%" controls-position="right" />
</el-col>
<el-col :span="6">
<el-input-number v-model="rule.end" :min="1" placeholder="结束" style="width: 100%" controls-position="right" />
</el-col>
<el-col :span="5">
<el-input-number v-model="rule.pad" :min="1" :max="5" placeholder="补零" style="width: 100%" controls-position="right" />
</el-col>
<el-col :span="2">
<el-button type="danger" :icon="Delete" @click="batchGenerateForm.rules.splice(index, 1)" style="width: 100%" />
</el-col>
</el-row>
</div>
<el-button type="primary" plain @click="batchGenerateForm.rules.push({ prefix: '', start: 1, end: 10, pad: 1 })" :icon="Plus">
添加层级
</el-button>
<div class="batch-preview">
<span v-if="previewCount > 0">即将生成 <strong>{{ previewCount }}</strong> 个库位</span>
<span v-else>请完善规则</span>
</div>
</el-form>
<template #footer>
<el-button @click="batchGenerateVisible = false">取消</el-button>
<el-button type="primary" @click="handleBatchGenerate" :loading="batchLoading" :disabled="previewCount === 0 || previewCount > 3000">
生成 {{ previewCount > 3000 ? '(超限)' : '' }}
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { useRouter } from 'vue-router'
// 1. 引入 User Store
import { useUserStore } from '@/stores/user'
// 引入需要的图标
import { Box, TrendCharts, ShoppingCart, Operation, Setting, Location, Plus, Edit, Delete, Close } from '@element-plus/icons-vue'
import { getPrinterConfig, updatePrinterConfig } from '@/api/common/print'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getWarehouseTree, createWarehouse, updateWarehouse, deleteWarehouse, batchDeleteWarehouse, batchGenerateWarehouse } from '@/api/common/warehouse'
const router = useRouter()
// 2. 实例化 store
const userStore = useUserStore()
// 打印机配置相关
const printerDialogVisible = ref(false)
const printerForm = reactive({
label_ip: '',
label_port: '',
network_ip: '',
network_port: ''
})
const loading = ref(false)
const openPrinterDialog = async () => {
try {
loading.value = true
const res = await getPrinterConfig()
if (res.code === 200) {
const config = res.data
printerForm.label_ip = config.label_printer?.ip || ''
printerForm.label_port = config.label_printer?.port || ''
printerForm.network_ip = config.network_printer?.ip || ''
printerForm.network_port = config.network_printer?.port || ''
}
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
printerDialogVisible.value = true
}
const savePrinterConfig = async () => {
try {
loading.value = true
const config = {
label_printer: {
ip: printerForm.label_ip,
port: Number(printerForm.label_port)
},
network_printer: {
ip: printerForm.network_ip,
port: Number(printerForm.network_port)
}
}
const res = await updatePrinterConfig(config)
if (res.code === 200) {
ElMessage.success('保存成功')
printerDialogVisible.value = false
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch (e) {
ElMessage.error('请求异常')
} finally {
loading.value = false
}
}
// ==================== 库位管理相关 ====================
// 根据层级返回"新增下级"按钮颜色
// el-tree中顶层节点level=1第1层=primary蓝色第2层=success绿色第3层=warning橙色第4层及以上=danger红色
const getAddBtnType = (level: number) => {
const levelTypes: Record<number, string> = {
1: 'primary', // 第1层(顶级)
2: 'success', // 第2层
3: 'warning', // 第3层
}
return levelTypes[level] || 'danger' // 第4层及以上
}
const warehouseDialogVisible = ref(false)
const treeRef = ref()
const treeData = ref<any[]>([])
const treeProps = {
label: 'name',
children: 'children'
}
// 库位表单
const locationFormVisible = ref(false)
const locationFormTitle = ref('')
const locationForm = reactive({
id: undefined as number | undefined,
parent_id: null as number | null,
parentName: '无(顶级)',
name: '',
is_enabled: true
})
const locationLoading = ref(false)
// 批量删除相关
const selectedIds = ref<number[]>([])
const isBulkDeleteMode = ref(false)
const handleTreeCheck = (data: any, checked: any) => {
// 使用 getCheckedKeys(true) 只获取叶子节点,防止 el-tree 自动连带选中父节点导致误删
if (treeRef.value) {
selectedIds.value = treeRef.value.getCheckedKeys(true) as number[]
}
}
const cancelBatchMode = () => {
isBulkDeleteMode.value = false
selectedIds.value = []
if (treeRef.value) {
treeRef.value.setCheckedKeys([])
}
}
const handleBatchDelete = async () => {
if (selectedIds.value.length === 0) {
ElMessage.warning('请先选择要删除的库位')
return
}
try {
await ElMessageBox.confirm(
`确定要删除选中的 ${selectedIds.value.length} 个库位吗?该操作将同时删除所有子库位。`,
'警告',
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
)
const res = await batchDeleteWarehouse(selectedIds.value)
if (res.code === 200) {
ElMessage.success('批量删除成功')
cancelBatchMode()
await loadWarehouseTree()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (e) {
// 用户取消
}
}
// 批量生成相关
const batchGenerateVisible = ref(false)
const batchGenerateForm = reactive({
parent_id: null as number | null,
parentName: '无(顶级)',
rules: [{ prefix: '', start: 1, end: 10, pad: 1 }] as { prefix: string, start: number, end: number, pad: number }[]
})
const batchLoading = ref(false)
const previewCount = computed(() => {
let count = 1
for (const rule of batchGenerateForm.rules) {
if (rule.start && rule.end && rule.start <= rule.end) {
count *= (rule.end - rule.start + 1)
} else {
return 0
}
}
return count
})
const openBatchGenerate = () => {
batchGenerateForm.parent_id = null
batchGenerateForm.parentName = '无(顶级)'
batchGenerateForm.rules = [{ prefix: '', start: 1, end: 10, pad: 1 }]
batchGenerateVisible.value = true
}
const handleBatchGenerate = async () => {
if (previewCount.value === 0 || previewCount.value > 3000) {
ElMessage.warning('生成数量无效或超过限制')
return
}
try {
batchLoading.value = true
const res = await batchGenerateWarehouse({
parent_id: batchGenerateForm.parent_id,
rules: batchGenerateForm.rules
})
if (res.code === 200) {
ElMessage.success(`生成成功,共生成 ${res.data.generated_count} 个库位`)
batchGenerateVisible.value = false
await loadWarehouseTree()
} else {
ElMessage.error(res.msg || '生成失败')
}
} catch (e) {
ElMessage.error('请求异常')
} finally {
batchLoading.value = false
}
}
// 打开库位管理弹窗
const openWarehouseDialog = async () => {
await loadWarehouseTree()
warehouseDialogVisible.value = true
}
// 加载库位树
const loadWarehouseTree = async () => {
try {
const res = await getWarehouseTree()
if (res.code === 200) {
treeData.value = res.data || []
}
} catch (e) {
console.error(e)
}
}
// 新增顶级区域
const handleAddTopLevel = () => {
locationForm.id = undefined
locationForm.parent_id = null
locationForm.parentName = '无(顶级)'
locationForm.name = ''
locationForm.is_enabled = true
locationFormTitle.value = '新增顶级库位'
locationFormVisible.value = true
}
// 新增下级
const handleAddChild = (data: any) => {
locationForm.id = undefined
locationForm.parent_id = data.id
locationForm.parentName = data.full_path || data.name
locationForm.name = ''
locationForm.is_enabled = true
locationFormTitle.value = `新增下级库位:${data.name}`
locationFormVisible.value = true
}
// 编辑
const handleEdit = (data: any) => {
locationForm.id = data.id
locationForm.parent_id = data.parent_id
locationForm.parentName = data.parent_id ? data.parent_full_path || '无(顶级)' : '无(顶级)'
locationForm.name = data.name
locationForm.is_enabled = data.is_enabled
locationFormTitle.value = `编辑库位:${data.name}`
locationFormVisible.value = true
}
// 删除
const handleDelete = async (data: any) => {
try {
await ElMessageBox.confirm(
`确定要删除库位「${data.name}」吗?${data.children && data.children.length > 0 ? '该操作将同时删除所有子库位。' : ''}`,
'警告',
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
)
const res = await deleteWarehouse(data.id)
if (res.code === 200) {
ElMessage.success('删除成功')
await loadWarehouseTree()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (e) {
// 用户取消
}
}
// 保存库位
const saveLocation = async () => {
if (!locationForm.name.trim()) {
ElMessage.warning('请输入库位名称')
return
}
try {
locationLoading.value = true
const payload: any = {
name: locationForm.name.trim(),
is_enabled: locationForm.is_enabled
}
if (locationForm.parent_id) {
payload.parent_id = locationForm.parent_id
}
let res
if (locationForm.id) {
// 编辑
payload.id = locationForm.id
res = await updateWarehouse(payload)
} else {
// 新增
res = await createWarehouse(payload)
}
if (res.code === 200) {
ElMessage.success('保存成功')
locationFormVisible.value = false
await loadWarehouseTree()
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch (e) {
ElMessage.error('请求异常')
} finally {
locationLoading.value = false
}
}
// 统一跳转函数
const handleNav = (path: string) => {
router.push(path)
}
</script>
<style scoped>
.dashboard-container {
/* 使用 100% 宽度和高度,利用 Flex 居中显示 */
height: calc(100vh - 84px); /* 减去顶部导航栏的高度,防止出现双滚动条 */
display: flex;
justify-content: center;
align-items: center;
background-color: #f0f2f5; /* 给背景加个淡灰色,突出卡片 */
}
.welcome-card {
width: 800px; /*稍微加宽一点 */
text-align: center;
border-radius: 8px; /* 圆角更好看 */
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 18px;
font-weight: bold;
color: #303133;
}
.card-body {
padding: 20px 0;
}
.card-body h2 {
font-size: 28px;
color: #409EFF; /* 使用主题蓝 */
margin-bottom: 10px;
}
.subtitle {
color: #909399;
margin-bottom: 40px;
font-size: 14px;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap; /* 防止屏幕过窄时按钮挤压 */
}
/* 修改密码温馨提示 */
.password-tip {
text-align: center;
font-size: 13px;
color: #909399;
margin-top: 20px;
padding: 10px 16px;
background-color: #f4f4f5;
border-radius: 6px;
line-height: 1.6;
}
.password-tip strong {
color: #409eff;
}
/* 给按钮加一点悬浮效果 */
.el-button {
transition: all 0.3s;
}
.el-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 库位管理弹窗样式 */
.warehouse-dialog {
padding: 10px 0;
}
.warehouse-header {
margin-bottom: 15px;
}
.tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 10px;
}
.node-label {
font-size: 14px;
}
.node-actions {
display: flex;
gap: 5px;
}
/* 批量生成规则样式 */
.rule-item {
margin-bottom: 10px;
padding: 10px;
background: #f5f7fa;
border-radius: 4px;
}
.batch-preview {
margin-top: 15px;
padding: 10px;
text-align: center;
background: #ecf5ff;
border-radius: 4px;
}
.batch-preview strong {
color: #409eff;
font-size: 16px;
}
</style>