物料-采购件入库页面功能实现

This commit is contained in:
dxc
2026-01-27 15:50:23 +08:00
parent 2f8a5c55b1
commit 3afea217b7
45 changed files with 1522 additions and 2756 deletions

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
// 不需要引入组件,由 router-view 控制
// 1. 引入需要的图标组件
import { InfoFilled } from '@element-plus/icons-vue'
</script>
<template>
@ -7,8 +8,8 @@
<header class="app-header">
<div class="logo-container">
<router-link to="/" class="home-link">
<img src="./assets/iris.png" class="logo" alt="Logo" />
<span class="system-title">库存管理系统</span>
<img src="@/assets/iris.png" class="logo" alt="Logo" />
<span class="system-title">IRIS 库存管理系统</span>
</router-link>
</div>
@ -30,23 +31,32 @@
</template>
<style>
/* 全局重置 */
/* 注意App.vue 中的 style 标签通常不加 scoped
或者将全局样式(html, body)单独放在一个 style 标签中,
以确保 html, body 的高度设置能生效
*/
/* --- 全局重置样式 Start --- */
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f5f7fa;
background-color: #f5f7fa; /* 整体背景色 */
overflow: hidden; /* 防止最外层出现双滚动条 */
}
#app {
height: 100%;
}
/* --- 全局重置样式 End --- */
.app-wrapper {
display: flex;
flex-direction: column;
height: 100vh; /* 占满全屏高度 */
height: 100vh; /* 强制占满视口高度 */
overflow: hidden;
}
/* 顶部栏样式 */
@ -59,7 +69,8 @@ html, body {
justify-content: space-between;
padding: 0 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
flex-shrink: 0; /* 止被压缩 */
flex-shrink: 0; /* 止被压缩 */
z-index: 1000; /* 确保头部在最上层 */
}
.logo-container {
@ -73,43 +84,52 @@ html, body {
gap: 15px;
text-decoration: none;
cursor: pointer;
user-select: none;
}
.logo {
height: 40px;
height: 36px; /* 稍微调整高度适配 */
width: auto;
}
.system-title {
font-size: 18px;
font-weight: bold;
font-size: 20px;
font-weight: 600;
color: #303133;
letter-spacing: 1px;
}
/* 内容区样式 */
.app-content {
flex: 1; /* 关键:这会让内容区自动撑开,把 footer 挤到最底下 */
overflow: auto;
padding: 20px;
flex: 1; /* 自动占据剩余空间 */
overflow: hidden; /* 这里设为 hidden让内部的 Layout 组件去处理滚动 */
position: relative;
/* 如果您希望整个页面有内边距,可以加 padding
但通常建议 padding 加在具体的业务页面里,保持 Layout 铺满 */
padding: 0;
}
/* 新增:底部栏样式 */
/* 底部栏样式 */
.app-footer {
height: 30px; /* 固定高度 */
background-color: #e9e9eb; /* 稍微深一点的灰色,区分内容区 */
border-top: 1px solid #dcdfe6;
height: 36px;
background-color: #f0f2f5;
border-top: 1px solid #e4e7ed;
display: flex;
align-items: center;
justify-content: center; /* 文字居中 */
flex-shrink: 0; /* 止被压缩 */
justify-content: center;
flex-shrink: 0; /* 止被压缩 */
font-size: 12px;
color: #909399;
z-index: 1000;
}
.version-tag {
display: flex;
align-items: center;
font-weight: 500;
color: #e6a23c; /* 使用橙色,表示“测试/警告”意味 */
color: #e6a23c; /* 橙色警告色 */
background: rgba(230, 162, 60, 0.1); /* 淡橙色背景 */
padding: 2px 8px;
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,19 @@
import request from '@/utils/request'
export function getBuyList(params: any) {
return request({ url: '/inbound/buy/list', method: 'get', params })
}
export function createBuyInbound(data: any) {
return request({ url: '/inbound/buy/submit', method: 'post', data })
}
// 新增:更新接口
export function updateBuyInbound(id: number, data: any) {
return request({ url: `/inbound/buy/${id}`, method: 'put', data })
}
// 新增:删除接口
export function deleteBuyInbound(id: number) {
return request({ url: `/inbound/buy/${id}`, method: 'delete' })
}

View File

View File

View File

@ -1,39 +0,0 @@
import request from '@/utils/request'
// 注意baseURL 已经是 '/api/v1' 了,所以这里只需要写剩下的部分
// 获取入库列表
// 最终请求: /api/v1 + /stocks/inbound = /api/v1/stocks/inbound
export function getInboundList(params: any) {
return request({
url: '/stocks/inbound', // <--- 修改点:去掉了 /api/v1
method: 'get',
params
})
}
// 新增入库
export function createInbound(data: any) {
return request({
url: '/stocks/inbound', // <--- 修改点
method: 'post',
data
})
}
// 修改入库
export function updateInbound(id: number, data: any) {
return request({
url: `/stocks/inbound/${id}`, // <--- 修改点
method: 'put',
data
})
}
// 删除入库
export function deleteInbound(id: number) {
return request({
url: `/stocks/inbound/${id}`, // <--- 修改点
method: 'delete'
})
}

View File

@ -0,0 +1,36 @@
<template>
<section class="app-main">
<router-view v-slot="{ Component }">
<transition name="fade-transform" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</section>
</template>
<script setup lang="ts">
</script>
<style scoped>
.app-main {
/* 确保占满容器 */
width: 100%;
position: relative;
}
/* 简单的页面切换动画 */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.3s;
}
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<el-menu
:default-active="activeMenu"
background-color="#304156"
text-color="#bfcbd9"
active-text-color="#409EFF"
:unique-opened="true"
router
class="el-menu-vertical"
>
<template v-for="route in menuRoutes" :key="route.path">
<el-menu-item
v-if="!route.children || route.children.length === 1"
:index="resolvePath(route)"
>
<el-icon v-if="getMeta(route).icon">
<component :is="getMeta(route).icon" />
</el-icon>
<span>{{ getMeta(route).title }}</span>
</el-menu-item>
<el-sub-menu v-else :index="route.path">
<template #title>
<el-icon v-if="route.meta && route.meta.icon">
<component :is="route.meta.icon" />
</el-icon>
<span>{{ route.meta?.title }}</span>
</template>
<el-menu-item
v-for="child in route.children"
:key="child.path"
:index="resolvePath(route, child)"
>
<template #title>
<span>{{ child.meta?.title }}</span>
</template>
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 1. 获取当前激活的菜单路径
const activeMenu = computed(() => {
return route.path
})
// 2. 获取需要在菜单中显示的路由(过滤掉 hidden 的路由)
const menuRoutes = computed(() => {
return router.options.routes.filter((r: any) => !r.meta?.hidden)
})
// 3. 辅助函数:获取 meta 信息
const getMeta = (route: any) => {
if (route.meta) return route.meta
// 如果是 layout 嵌套层(如首页),取第一个子路由的 meta
if (route.children && route.children.length > 0) {
return route.children[0].meta
}
return {}
}
// 4. 辅助函数:拼接路径
const resolvePath = (parent: any, child?: any) => {
// 如果是首页这种 layout 嵌套结构
if (!child && parent.children && parent.children.length === 1) {
return parent.path === '/' ? '/dashboard' : parent.path + '/' + parent.children[0].path
}
// 如果是普通子菜单
if (child) {
return parent.path + '/' + child.path
}
return parent.path
}
</script>
<style scoped>
.el-menu-vertical {
border-right: none; /* 去掉 Element Plus 菜单默认的右边框 */
width: 100%;
}
:deep(.el-menu-item.is-active) {
background-color: #263445 !important;
}
</style>

View File

@ -1,11 +1,43 @@
<script setup lang="ts">
</script>
<template>
<div class="layout-wrapper">
<Sidebar class="sidebar-container" />
<div class="main-container">
<AppMain />
</div>
</div>
</template>
<style scoped>
<script setup lang="ts">
import Sidebar from './components/Sidebar/index.vue'
import AppMain from './components/AppMain.vue'
</script>
<style scoped>
.layout-wrapper {
display: flex;
width: 100%;
height: 100%; /* 继承 App.vue 中 app-content 的高度 */
overflow: hidden; /* 防止最外层出现滚动条 */
}
.sidebar-container {
width: 210px; /* 固定侧边栏宽度 */
height: 100%;
background-color: #304156; /* 侧边栏背景色 */
flex-shrink: 0; /* 防止被挤压 */
overflow-y: auto; /* 侧边栏内容过多时允许滚动 */
overflow-x: hidden;
}
.main-container {
flex: 1; /* 自动占满右侧剩余空间 */
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto; /* 关键:页面内容过多时,只在右侧区域滚动 */
background-color: #f0f2f5; /* 右侧灰色背景,让白色卡片更明显 */
padding: 20px; /* 给内部页面留出边距 */
box-sizing: border-box;
}
</style>

View File

@ -1,18 +1,148 @@
import { createRouter, createWebHistory } from 'vue-router'
// 核心修改点:使用 'type' 关键字导入 RouteRecordRaw或者将其分开导入
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/layout/index.vue'
const routes = [
// --- 修改点:根路径不再重定向,而是显示 Dashboard 首页 ---
const routes: Array<RouteRecordRaw> = [
// 1. 首页 Dashboard
{
path: '/',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue')
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '首页', icon: 'HomeFilled' }
}
]
},
// --- 保持原有的入库页路由不变 ---
// 2. 基础物料 (对应 views/material/list.vue)
{
path: '/stock/inbound',
name: 'StockInbound',
component: () => import('@/views/stock/inbound.vue')
path: '/material',
component: Layout,
redirect: '/material/index',
children: [
{
path: 'index',
name: 'MaterialBase',
// 基础物料列表
component: () => import('@/views/material/list.vue'),
meta: { title: '基础物料', icon: 'Box' }
}
]
},
// 3. 库存管理 (采购/半成品/成品/权益)
{
path: '/inventory',
component: Layout,
meta: { title: '库存管理', icon: 'Shop' },
redirect: '/inventory/buy',
children: [
{
path: 'buy',
name: 'InventoryBuy',
// 采购入库页面
component: () => import('@/views/stock/inbound/buy.vue'),
meta: { title: '采购件' }
},
{
path: 'semi',
name: 'InventorySemi',
// 半成品页面
component: () => import('@/views/stock/inbound/semi.vue'),
meta: { title: '半成品' }
},
{
path: 'product',
name: 'InventoryProduct',
// 成品页面
component: () => import('@/views/stock/inbound/product.vue'),
meta: { title: '成品' }
},
{
path: 'service',
name: 'InventoryService',
// 服务权益页面
component: () => import('@/views/stock/inbound/service.vue'),
meta: { title: '服务权益' }
}
]
},
// 4. 业务操作 (借库/维修/报废)
{
path: '/operation',
component: Layout,
meta: { title: '业务操作', icon: 'Operation' },
redirect: '/operation/borrow',
children: [
{
path: 'borrow',
name: 'OpBorrow',
// 借库页面
component: () => import('@/views/transaction/borrow.vue'),
meta: { title: '借库' }
},
{
path: 'repair',
name: 'OpRepair',
// 维修页面 (指向 return.vue)
component: () => import('@/views/transaction/return.vue'),
meta: { title: '维修' }
},
{
path: 'scrap',
name: 'OpScrap',
// 报废页面
component: () => import('@/views/transaction/scrap.vue'),
meta: { title: '报废' }
}
]
},
/* * 暂时屏蔽 BOM 和 系统管理
*/
// {
// path: '/bom',
// component: Layout,
// children: [
// {
// path: 'index',
// name: 'BOM',
// component: () => import('@/views/bom/index.vue'),
// meta: { title: 'BOM管理', icon: 'List' }
// }
// ]
// },
// {
// path: '/system',
// component: Layout,
// meta: { title: '系统管理', icon: 'Setting' },
// children: [
// {
// path: 'user',
// name: 'UserManage',
// component: () => import('@/views/system/user.vue'),
// meta: { title: '用户管理', icon: 'User' }
// },
// {
// path: 'log',
// name: 'OpLog',
// component: () => import('@/views/system/log.vue'),
// meta: { title: '操作日志', icon: 'Document' }
// }
// ]
// },
// 404 路由
{
path: '/:pathMatch(.*)*',
redirect: '/dashboard',
meta: { hidden: true }
}
]

View File

@ -3,9 +3,9 @@ import { ElMessage } from 'element-plus'
// 1. 创建 axios 实例
const service = axios.create({
// 这里的 '/api' 配合 vite.config.ts 的 proxy 使用
baseURL: '/api/v1',
timeout: 5000 // 请求超时时间
// 【修改这里】不要写死 '/api/v1',改为读取环境变量
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 5000
})
// 2. 请求拦截器 (可以在这里加 Token)

View File

@ -3,22 +3,36 @@
<el-card class="welcome-card">
<template #header>
<div class="card-header">
<span>👋 欢迎回来</span>
<span class="title">👋 欢迎回来Admin</span>
<el-tag type="success">系统运行正常</el-tag>
</div>
</template>
<div class="card-body">
<h2>欢迎使用 IRIS 库存管理系统</h2>
<p>请点击下方按钮进入具体业务模块</p>
<h2>IRIS 库存管理系统</h2>
<p class="subtitle">请选择您要进行的业务操作</p>
<div class="action-buttons">
<el-button type="primary" size="large" @click="$router.push('/stock/inbound')">
<el-icon style="margin-right: 5px"><Box /></el-icon>
进入采购入库
<el-button type="primary" size="large" @click="handleNav('/inventory/buy')">
<el-icon style="margin-right: 5px"><ShoppingCart /></el-icon>
采购入库
</el-button>
<el-button size="large" disabled>
<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 style="margin-top: 20px;">
<el-button text bg size="small" disabled>
<el-icon style="margin-right: 5px"><TrendCharts /></el-icon>
库存报表 (开发中)
数据大屏 (开发中)
</el-button>
</div>
</div>
@ -27,35 +41,75 @@
</template>
<script setup lang="ts">
import { Box, TrendCharts } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
// 引入需要的图标
import { Box, TrendCharts, ShoppingCart, Operation } from '@element-plus/icons-vue'
const router = useRouter()
// 统一跳转函数
const handleNav = (path: string) => {
router.push(path)
}
</script>
<style scoped>
.dashboard-container {
padding: 20px;
/* 使用 100% 宽度和高度,利用 Flex 居中显示 */
height: calc(100vh - 84px); /* 减去顶部导航栏的高度,防止出现双滚动条 */
display: flex;
justify-content: center;
margin-top: 50px;
align-items: center;
background-color: #f0f2f5; /* 给背景加个淡灰色,突出卡片 */
}
.welcome-card {
width: 600px;
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 {
color: #303133;
font-size: 28px;
color: #409EFF; /* 使用主题蓝 */
margin-bottom: 10px;
}
.card-body p {
color: #606266;
margin-bottom: 30px;
.subtitle {
color: #909399;
margin-bottom: 40px;
font-size: 14px;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap; /* 防止屏幕过窄时按钮挤压 */
}
/* 给按钮加一点悬浮效果 */
.el-button {
transition: all 0.3s;
}
.el-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -1,11 +1,38 @@
<script setup lang="ts">
</script>
<template>
<div class="app-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span class="title">基础物料管理</span>
<el-button type="primary">
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增物料
</el-button>
</div>
</template>
<div class="filter-container">
<el-input placeholder="请输入物料名称或编码" style="width: 200px; margin-right: 10px;" />
<el-select placeholder="物料类别" style="width: 150px; margin-right: 10px;">
<el-option label="电子元器件" value="elec" />
<el-option label="结构件" value="struct" />
</el-select>
<el-button type="primary" plain>搜索</el-button>
</div>
<el-table :data="tableData" border stripe style="width: 100%; margin-top: 20px">
<el-table-column prop="code" label="物料编码" width="120" />
<el-table-column prop="name" label="物料名称" width="180" />
<el-table-column prop="spec" label="规格型号" />
<el-table-column prop="unit" label="单位" width="80" />
<el-table-column prop="category" label="类别" width="120" />
<el-table-column label="操作" width="150">
<template #default>
<el-button link type="primary" size="small">编辑</el-button>
<el-button link type="danger" size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<style scoped>
</style>

View File

@ -1,237 +0,0 @@
<template>
<div class="app-container" style="padding: 20px;">
<div style="margin-bottom: 20px; display: flex; justify-content: space-between;">
<el-button type="primary" :icon="Plus" @click="handleCreate">新增入库</el-button>
<el-button :icon="Refresh" @click="fetchData">刷新</el-button>
</div>
<el-table v-loading="loading" :data="tableData" border stripe style="width: 100%">
<el-table-column prop="inbound_date" label="入库时间" width="160" />
<el-table-column prop="sku_code" label="SKU" width="120" fixed="left" />
<el-table-column prop="material_name" label="物料名称" min-width="150" />
<el-table-column prop="spec_model" label="规格" width="120" />
<el-table-column prop="category" label="分类" width="100" />
<el-table-column prop="unit" label="单位" width="60" />
<el-table-column prop="qty_inbound" label="入库量" width="100">
<template #default="{ row }">
<span style="font-weight: bold; color: #409EFF">{{ row.qty_inbound }}</span>
</template>
</el-table-column>
<el-table-column prop="price_unit" label="单价" width="100" />
<el-table-column prop="price_total" label="总价" width="100" />
<el-table-column prop="warehouse_loc" label="库位" width="100" />
<el-table-column prop="supplier_name" label="供应商" width="120" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleUpdate(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 20px; text-align: right;">
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.pageSize"
:total="total"
layout="total, prev, pager, next"
@current-change="fetchData"
/>
</div>
<el-dialog
:title="dialogType === 'create' ? '入库录入 (支持自动建档)' : '编辑入库单'"
v-model="dialogVisible"
width="650px"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<div v-if="dialogType === 'create'">
<el-divider content-position="left">物料基础信息</el-divider>
<el-alert title="输入SKU后如是新物料请补全名称和规格如是旧物料系统会自动关联。" type="info" :closable="false" style="margin-bottom:15px;" />
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="SKU编码" prop="sku_code">
<el-input v-model="form.sku_code" placeholder="唯一识别码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="物料名称" prop="material_name">
<el-input v-model="form.material_name" placeholder="新物料必填" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="规格型号" prop="spec_model">
<el-input v-model="form.spec_model" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="分类" prop="category">
<el-input v-model="form.category" placeholder="如: 电子料" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="单位" prop="unit">
<el-input v-model="form.unit" placeholder="个/包" />
</el-form-item>
</el-col>
</el-row>
</div>
<el-divider content-position="left">入库业务信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="入库数量" prop="qty_inbound">
<el-input-number v-model="form.qty_inbound" :min="0" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="单价" prop="price_unit">
<el-input-number v-model="form.price_unit" :min="0" :precision="2" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="库位" prop="warehouse_loc">
<el-input v-model="form.warehouse_loc" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="批次号" prop="batch_no">
<el-input v-model="form.batch_no" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="供应商" prop="supplier_name">
<el-input v-model="form.supplier_name" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submitForm">确认提交</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Plus, Refresh } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getInboundList, createInbound, updateInbound, deleteInbound } from '@/api/stock'
const loading = ref(false)
const submitting = ref(false)
const tableData = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const dialogType = ref<'create' | 'update'>('create')
const formRef = ref()
const queryParams = reactive({ page: 1, pageSize: 10 })
// 表单对象
const form = reactive({
id: undefined,
sku_code: '',
material_name: '',
spec_model: '',
category: '',
unit: '',
qty_inbound: 0,
price_unit: 0,
warehouse_loc: '',
batch_no: '',
supplier_name: ''
})
const rules = {
sku_code: [{ required: true, message: 'SKU不能为空', trigger: 'blur' }],
qty_inbound: [{ required: true, message: '数量必须大于0', trigger: 'blur' }]
}
const fetchData = async () => {
loading.value = true
try {
const res = await getInboundList(queryParams)
tableData.value = res.data.items
total.value = res.data.total
} finally {
loading.value = false
}
}
const handleCreate = () => {
dialogType.value = 'create'
// 重置表单
Object.assign(form, {
id: undefined, sku_code: '', material_name: '', spec_model: '', category: '', unit: '',
qty_inbound: 1, price_unit: 0, warehouse_loc: '', batch_no: '', supplier_name: ''
})
dialogVisible.value = true
}
const handleUpdate = (row: any) => {
dialogType.value = 'update'
// 仅允许修改入库相关信息
Object.assign(form, row)
dialogVisible.value = true
}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (valid) {
submitting.value = true
try {
if (dialogType.value === 'create') {
await createInbound(form)
ElMessage.success('入库成功')
} else {
// 编辑时只提交入库单ID和可修改字段
await updateInbound(form.id!, {
qty_inbound: form.qty_inbound,
price_unit: form.price_unit,
warehouse_loc: form.warehouse_loc,
batch_no: form.batch_no,
supplier_name: form.supplier_name
})
ElMessage.success('更新成功')
}
dialogVisible.value = false
fetchData()
} catch (e: any) {
ElMessage.error(e.response?.data?.msg || '操作失败')
} finally {
submitting.value = false
}
}
})
}
const handleDelete = (row: any) => {
ElMessageBox.confirm('确认删除该记录?', '警告', { type: 'warning' })
.then(async () => {
await deleteInbound(row.id)
ElMessage.success('已删除')
fetchData()
})
}
onMounted(() => fetchData())
</script>

View File

@ -0,0 +1,415 @@
<template>
<div class="buy-module">
<div class="header-tools">
<div class="left-actions">
<el-button type="primary" :icon="Plus" @click="handleCreate">全量采购入库登记</el-button>
<el-button :icon="Refresh" @click="fetchData">刷新数据</el-button>
</div>
<el-popover placement="bottom" title="显示列配置" :width="400" trigger="click">
<template #reference>
<el-button :icon="Setting">自定义表格表头</el-button>
</template>
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
<el-divider content-position="left">基础层字段</el-divider>
<el-checkbox v-for="c in baseColumns" :key="c.prop" :value="c.prop">{{ c.label }}</el-checkbox>
<el-divider content-position="left">库存/财务层字段</el-divider>
<el-checkbox v-for="c in stockColumns" :key="c.prop" :value="c.prop">{{ c.label }}</el-checkbox>
</el-checkbox-group>
</el-popover>
</div>
<el-table
v-loading="loading"
:data="tableData"
border
stripe
style="width: 100%"
size="small"
highlight-current-row
>
<template v-for="col in allColumns" :key="col.prop">
<el-table-column
v-if="visibleColumnProps.includes(col.prop)"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth || '120'"
show-overflow-tooltip
>
<template #default="scope" v-if="col.prop === 'serial_batch'">
<span v-if="scope.row.serial_number" style="color: #409EFF; font-weight: bold;">
SN: {{ scope.row.serial_number }}
</span>
<span v-else-if="scope.row.batch_number" style="color: #67C23A; font-weight: bold;">
BN: {{ scope.row.batch_number }}
</span>
<span v-else>-</span>
</template>
<template #default="scope" v-else-if="col.prop === 'status'">
<el-tag size="small" :type="getStatusType(scope.row.status)">
{{ scope.row.status }}
</el-tag>
</template>
<template #default="scope" v-else-if="['unit_price', 'total_price'].includes(col.prop)">
{{ formatMoney(scope.row[col.prop]) }}
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleUpdate(row)">编辑</el-button>
<el-popconfirm title="确定删除该条入库记录吗?" @confirm="handleDelete(row)">
<template #reference>
<el-button link type="danger" size="small">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination-container"
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[15, 30, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
/>
<el-dialog
v-model="visible"
:title="dialogStatus === 'create' ? '新增采购件入库' : '编辑入库信息'"
width="950px"
top="3vh"
destroy-on-close
:close-on-click-modal="false"
>
<el-form :model="form" label-width="120px" ref="formRef" :rules="rules" size="default">
<el-divider content-position="left"><b>1. 基础核心层 (Material Base)</b></el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="名称" prop="material_name">
<el-input v-model="form.material_name" :disabled="dialogStatus === 'update'" placeholder="必填" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="规格型号" prop="spec_model">
<el-input v-model="form.spec_model" :disabled="dialogStatus === 'update'" placeholder="必填: 内部货号" />
</el-form-item>
</el-col>
<el-col :span="8"><el-form-item label="计量单位" prop="unit"><el-input v-model="form.unit" /></el-form-item></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="类别" prop="category"><el-input v-model="form.category" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类型" prop="material_type"><el-input v-model="form.material_type" disabled /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="可见等级" prop="visibility_level"><el-input-number v-model="form.visibility_level" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
</el-row>
<el-divider content-position="left"><b>2. 实体库存层 (Stock Buy)</b></el-divider>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" placeholder="选填" /></el-form-item></el-col>
<el-col :span="8">
<el-form-item label="入库日期" prop="in_date">
<el-input v-model="form.in_date" disabled placeholder="系统自动生成">
<template #suffix><el-icon><Calendar /></el-icon></template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" /></el-form-item></el-col>
</el-row>
<el-row :gutter="20" style="background-color: #fffbf0; border-radius: 4px; padding-top:10px;">
<el-col :span="12">
<el-form-item label="序列号" prop="serial_number">
<el-input v-model="form.serial_number" placeholder="设备SN码" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="批号" prop="batch_number">
<el-input v-model="form.batch_number" placeholder="生产批次号" clearable />
</el-form-item>
</el-col>
<el-col :span="24">
<div style="font-size: 12px; color: #e6a23c; margin-left: 120px; margin-bottom: 10px; line-height: 1;">
* 规则序列号与批号互斥且必填其一 (填写一个会自动清空另一个)
</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 10px;">
<el-col :span="12"><el-form-item label="到检状态" prop="inspection_status"><el-input v-model="form.inspection_status" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="照片/到货图" prop="arrival_photo"><el-input v-model="form.arrival_photo" /></el-form-item></el-col>
</el-row>
<el-row :gutter="20" style="background-color: #f8fcfd; padding-top: 18px; border-radius: 4px; margin-top: 10px;">
<el-col :span="8">
<el-form-item label="入库量" prop="in_quantity">
<el-input-number
v-model="form.in_quantity"
:min="1"
:step="1"
:precision="0"
style="width:100%"
controls-position="right"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="库存数量" prop="stock_quantity">
<el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="可用数量" prop="available_quantity">
<el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left"><b>3. 财务与商务信息</b></el-divider>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="单价" prop="unit_price"><el-input-number v-model="form.unit_price" :precision="4" style="width:100%" controls-position="right" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="总价" prop="total_price"><el-input-number v-model="form.total_price" :precision="4" style="width:100%" disabled :controls="false" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="供应商" prop="supplier_name"><el-input v-model="form.supplier_name" /></el-form-item></el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submitForm">
{{ dialogStatus === 'create' ? '确认入库' : '保存修改' }}
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { Plus, Setting, Refresh, Calendar } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import { getBuyList, createBuyInbound, updateBuyInbound, deleteBuyInbound } from '@/api/inbound/buy'
// 状态控制
const loading = ref(false)
const submitting = ref(false)
const visible = ref(false)
const dialogStatus = ref<'create' | 'update'>('create')
const tableData = ref([])
const total = ref(0)
const formRef = ref()
const queryParams = reactive({ page: 1, pageSize: 15 })
// --- 1. 列定义 ---
const baseColumns = [
{ prop: 'material_name', label: '物料名称', minWidth: '150' },
{ prop: 'category', label: '类别', minWidth: '100' },
{ prop: 'spec_model', label: '规格型号', minWidth: '150' },
{ prop: 'unit', label: '单位', minWidth: '70' }
]
const stockColumns = [
{ prop: 'sku', label: '编码/SKU', minWidth: '140' },
{ prop: 'inbound_date', label: '入库日期', minWidth: '120' },
// 组合展示列
{ prop: 'serial_batch', label: '序列号/批号', minWidth: '160' },
{ prop: 'stock_quantity', label: '库存数', minWidth: '90' },
{ prop: 'available_quantity', label: '可用数', minWidth: '90' },
{ prop: 'in_quantity', label: '入库量', minWidth: '90' },
{ prop: 'unit_price', label: '单价', minWidth: '110' },
{ prop: 'total_price', label: '总价', minWidth: '110' },
{ prop: 'status', label: '状态', minWidth: '90' },
{ prop: 'warehouse_location', label: '库位', minWidth: '100' },
{ prop: 'supplier_name', label: '供应商', minWidth: '150' }
]
const allColumns = [...baseColumns, ...stockColumns]
// --- 2. 默认展示列 ---
const visibleColumnProps = ref([
'material_name', 'spec_model', 'inbound_date',
'serial_batch', 'stock_quantity', 'available_quantity', 'status'
])
// --- 3. 表单对象 ---
const form = reactive({
id: undefined, // 编辑时使用
material_name: '', category: '', spec_model: '', unit: '个',
material_type: '采购件', visibility_level: 0,
sku: '', in_date: '',
serial_number: '', batch_number: '',
status: '在库', inspection_status: '未检',
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
warehouse_location: '', unit_price: 0, total_price: 0,
supplier_name: '', arrival_photo: '', remark: ''
})
// --- 4. 校验逻辑 (互斥且必填其一) ---
const validateIdentity = (rule: any, value: any, callback: any) => {
if (!form.serial_number && !form.batch_number) {
callback(new Error('序列号和批号至少填写一项'))
} else {
// 清除交叉报错
if (formRef.value) {
if (rule.field === 'serial_number' && form.batch_number) formRef.value.clearValidate('batch_number')
if (rule.field === 'batch_number' && form.serial_number) formRef.value.clearValidate('serial_number')
}
callback()
}
}
const rules = {
material_name: [{ required: true, message: '必填', trigger: 'blur' }],
spec_model: [{ required: true, message: '必填', trigger: 'blur' }],
in_quantity: [{ required: true, message: '必填', trigger: 'blur' }],
serial_number: [{ validator: validateIdentity, trigger: 'blur' }],
batch_number: [{ validator: validateIdentity, trigger: 'blur' }]
}
// --- 5. 监听逻辑 ---
watch(() => form.serial_number, (val) => {
if (val && form.batch_number) form.batch_number = ''
})
watch(() => form.batch_number, (val) => {
if (val && form.serial_number) form.serial_number = ''
})
watch(() => form.in_quantity, (newVal) => {
if (newVal !== undefined) {
if (dialogStatus.value === 'create') {
form.stock_quantity = newVal
form.available_quantity = newVal
}
form.total_price = Number((newVal * form.unit_price).toFixed(4))
}
})
watch(() => form.unit_price, (newVal) => {
if (newVal !== undefined) {
form.total_price = Number((newVal * form.in_quantity).toFixed(4))
}
})
// --- 6. 核心操作 ---
const fetchData = async () => {
loading.value = true
try {
const res = await getBuyList(queryParams)
tableData.value = res.data.items || []
total.value = res.data.total || 0
} finally { loading.value = false }
}
const handleCreate = () => {
dialogStatus.value = 'create'
resetForm()
form.in_date = dayjs().format('YYYY-MM-DD HH:mm:ss')
visible.value = true
}
// 核心修改:手动映射后端数据到前端表单
const handleUpdate = (row: any) => {
dialogStatus.value = 'update'
// 先重置表单防止残留
resetForm()
// 1. 基础字段拷贝
Object.assign(form, row)
// 2. 修正字段映射 (后端Key -> 前端Form Key)
form.id = row.id
form.in_quantity = Number(row.qty_inbound) || 1
form.stock_quantity = Number(row.qty_inbound) || 1 // 这里假设库存没变或者应由后端传回stock_quantity
form.available_quantity = Number(row.qty_available) || 1
form.unit_price = Number(row.price_unit) || 0
form.warehouse_location = row.warehouse_loc || ''
form.serial_number = row.serial_number || ''
form.batch_number = row.batch_number || ''
form.status = row.status || '在库'
// 3. 计算总价
form.total_price = Number((form.in_quantity * form.unit_price).toFixed(2))
// 4. 补充日期
if (!form.in_date) form.in_date = dayjs().format('YYYY-MM-DD HH:mm:ss')
visible.value = true
}
const handleDelete = async (row: any) => {
try {
await deleteBuyInbound(row.id)
ElMessage.success('删除成功')
fetchData()
} catch (e: any) {
ElMessage.error(e.message || '删除失败')
}
}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (valid) {
submitting.value = true
try {
if (dialogStatus.value === 'create') {
await createBuyInbound(form)
ElMessage.success('入库成功')
} else {
await updateBuyInbound(form.id!, form)
ElMessage.success('更新成功')
}
visible.value = false
fetchData()
} catch (e: any) {
ElMessage.error(e.message || '提交失败')
} finally { submitting.value = false }
}
})
}
const resetForm = () => {
Object.assign(form, {
id: undefined,
material_name: '', category: '', spec_model: '', unit: '个',
material_type: '采购件', visibility_level: 0,
sku: '', in_date: '',
serial_number: '', batch_number: '',
status: '在库', inspection_status: '未检',
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
warehouse_location: '', unit_price: 0, total_price: 0,
supplier_name: '', arrival_photo: '', remark: ''
})
}
const getStatusType = (status: string) => {
return status === '在库' ? 'success' : 'info'
}
const formatMoney = (val: number) => {
return val ? `¥ ${Number(val).toFixed(2)}` : '-'
}
onMounted(() => fetchData())
</script>
<style scoped>
.buy-module { background: #fff; border-radius: 8px; }
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
.column-selector { display: flex; flex-direction: column; padding: 10px; max-height: 400px; overflow-y: auto; }
.pagination-container { margin-top: 20px; display: flex; justify-content: flex-end; }
:deep(.el-divider--horizontal) { margin: 15px 0 15px 0; }
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="inbound-container" style="padding: 20px;">
<el-tabs v-model="activeModule" type="border-card">
<el-tab-pane label="物料采购入库" name="buy">
<BuyInbound v-if="activeModule === 'buy'" />
</el-tab-pane>
<el-tab-pane label="半成品入库" name="semi">
<SemiInbound v-if="activeModule === 'semi'" />
</el-tab-pane>
<el-tab-pane label="成品入库" name="product">
<ProductInbound v-if="activeModule === 'product'" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 因为在同一个文件夹下,直接用 ./ 即可
import BuyInbound from './buy.vue'
import SemiInbound from './semi.vue'
import ProductInbound from './product.vue'
const activeModule = ref('buy')
</script>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>

View File

@ -0,0 +1 @@
<template><div style="padding:20px;"><h2>服务权益管理</h2></div></template>

View File

@ -1,11 +1 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>
<template><div style="padding:20px;"><h2>借库申请</h2></div></template>

View File

@ -1,11 +1 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>
<template><div style="padding:20px;"><h2>维修登记</h2></div></template>

View File

@ -1,11 +1 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>
<template><div style="padding:20px;"><h2>报废处理</h2></div></template>