feat: add spare part management frontend pages
This commit is contained in:
parent
5500238be3
commit
5c7728e3db
|
|
@ -0,0 +1,148 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// ==================== 备件相关类型 ====================
|
||||||
|
|
||||||
|
// 备件分类
|
||||||
|
export interface SparePartCategory {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备件
|
||||||
|
export interface SparePart {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
categoryId?: string
|
||||||
|
categoryName?: string
|
||||||
|
projectId: string
|
||||||
|
projectName?: string
|
||||||
|
unit?: string
|
||||||
|
currentStock?: number
|
||||||
|
safeStock?: number
|
||||||
|
lowStockWarning?: boolean
|
||||||
|
description?: string
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备件表单
|
||||||
|
export interface SparePartForm {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
categoryId?: string
|
||||||
|
projectId: string
|
||||||
|
unit?: string
|
||||||
|
currentStock?: number
|
||||||
|
safeStock?: number
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 库存记录
|
||||||
|
export interface StockRecord {
|
||||||
|
id: string
|
||||||
|
sparePartId: string
|
||||||
|
sparePartName?: string
|
||||||
|
operationType: 'IN' | 'OUT'
|
||||||
|
quantity: number
|
||||||
|
beforeStock?: number
|
||||||
|
afterStock?: number
|
||||||
|
relatedOrderId?: string
|
||||||
|
relatedOrderNo?: string
|
||||||
|
operatorId?: string
|
||||||
|
operatorName?: string
|
||||||
|
remark?: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 入库请求
|
||||||
|
export interface InStockRequest {
|
||||||
|
sparePartId: string
|
||||||
|
quantity: number
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 出库请求
|
||||||
|
export interface OutStockRequest {
|
||||||
|
sparePartId: string
|
||||||
|
quantity: number
|
||||||
|
relatedOrderId?: string
|
||||||
|
relatedOrderNo?: string
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页响应
|
||||||
|
export interface PageResponse<T> {
|
||||||
|
content: T[]
|
||||||
|
totalElements: number
|
||||||
|
totalPages: number
|
||||||
|
size: number
|
||||||
|
number: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 备件分类 API ====================
|
||||||
|
|
||||||
|
// 获取分类列表
|
||||||
|
export function getSparePartCategories() {
|
||||||
|
return request.get<SparePartCategory[]>('/api/v1/ops/spare-parts/categories')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建分类
|
||||||
|
export function createSparePartCategory(data: { name: string; description?: string }) {
|
||||||
|
return request.post('/api/v1/ops/spare-parts/categories', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 备件 API ====================
|
||||||
|
|
||||||
|
// 获取备件列表
|
||||||
|
export function getSparePartList(projectId: string, categoryId?: string) {
|
||||||
|
return request.get<PageResponse<SparePart>>('/api/v1/ops/spare-parts', {
|
||||||
|
params: { projectId, categoryId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取备件详情
|
||||||
|
export function getSparePartDetail(id: string) {
|
||||||
|
return request.get<SparePart>(`/api/v1/ops/spare-parts/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建备件
|
||||||
|
export function createSparePart(data: SparePartForm) {
|
||||||
|
return request.post('/api/v1/ops/spare-parts', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新备件
|
||||||
|
export function updateSparePart(id: string, data: SparePartForm) {
|
||||||
|
return request.put(`/api/v1/ops/spare-parts/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除备件
|
||||||
|
export function deleteSparePart(id: string) {
|
||||||
|
return request.delete(`/api/v1/ops/spare-parts/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取低库存备件
|
||||||
|
export function getLowStockSpareParts(projectId: string) {
|
||||||
|
return request.get<SparePart[]>('/api/v1/ops/spare-parts/low-stock', {
|
||||||
|
params: { projectId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 入库
|
||||||
|
export function inStock(data: InStockRequest) {
|
||||||
|
return request.post('/api/v1/ops/spare-parts/in-stock', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 出库
|
||||||
|
export function outStock(data: OutStockRequest) {
|
||||||
|
return request.post('/api/v1/ops/spare-parts/out-stock', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取备件记录
|
||||||
|
export function getSparePartRecords(id: string) {
|
||||||
|
return request.get<StockRecord[]>(`/api/v1/ops/spare-parts/${id}/records`)
|
||||||
|
}
|
||||||
|
|
@ -110,6 +110,30 @@ const router = createRouter({
|
||||||
name: 'EnergyStatistics',
|
name: 'EnergyStatistics',
|
||||||
component: () => import('@/views/energy/EnergyStatistics.vue'),
|
component: () => import('@/views/energy/EnergyStatistics.vue'),
|
||||||
meta: { title: '能耗统计' }
|
meta: { title: '能耗统计' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sparepart/list',
|
||||||
|
name: 'SparePartList',
|
||||||
|
component: () => import('@/views/sparepart/SparePartList.vue'),
|
||||||
|
meta: { title: '备件管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sparepart/detail/:id',
|
||||||
|
name: 'SparePartDetail',
|
||||||
|
component: () => import('@/views/sparepart/SparePartDetail.vue'),
|
||||||
|
meta: { title: '备件详情' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sparepart/stock/in',
|
||||||
|
name: 'SparePartInStock',
|
||||||
|
component: () => import('@/views/sparepart/StockOperation.vue'),
|
||||||
|
meta: { title: '备件入库' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sparepart/stock/out',
|
||||||
|
name: 'SparePartOutStock',
|
||||||
|
component: () => import('@/views/sparepart/StockOperation.vue'),
|
||||||
|
meta: { title: '备件出库' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,11 @@ const menuItems: MenuProps['items'] = [
|
||||||
label: '运营管理',
|
label: '运营管理',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
key: '/sparepart/list',
|
||||||
|
icon: () => h(ToolOutlined),
|
||||||
|
label: '备件管理'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: '/maintenance/plans',
|
key: '/maintenance/plans',
|
||||||
icon: () => h(ToolOutlined),
|
icon: () => h(ToolOutlined),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { Descriptions, Button, Space, Card, Statistic, Row, Col, message } from 'ant-design-vue'
|
||||||
|
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||||
|
import {
|
||||||
|
ArrowLeftOutlined,
|
||||||
|
InboxOutlined,
|
||||||
|
ExportOutlined,
|
||||||
|
ReloadOutlined
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import {
|
||||||
|
getSparePartDetail,
|
||||||
|
getSparePartRecords,
|
||||||
|
type SparePart,
|
||||||
|
type StockRecord
|
||||||
|
} from '@/api/sparepart'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const recordLoading = ref(false)
|
||||||
|
const sparePart = ref<SparePart | null>(null)
|
||||||
|
const records = ref<StockRecord[]>([])
|
||||||
|
|
||||||
|
const id = route.params.id as string
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const recordColumns: ColumnsType = [
|
||||||
|
{ title: '操作类型', dataIndex: 'operationType', key: 'operationType', width: 100 },
|
||||||
|
{ title: '数量', dataIndex: 'quantity', key: 'quantity', width: 80 },
|
||||||
|
{ title: '操作前库存', dataIndex: 'beforeStock', key: 'beforeStock', width: 100 },
|
||||||
|
{ title: '操作后库存', dataIndex: 'afterStock', key: 'afterStock', width: 100 },
|
||||||
|
{ title: '关联工单', dataIndex: 'relatedOrderNo', key: 'relatedOrderNo', width: 140 },
|
||||||
|
{ title: '操作人', dataIndex: 'operatorName', key: 'operatorName', width: 100 },
|
||||||
|
{ title: '备注', dataIndex: 'remark', key: 'remark' },
|
||||||
|
{ title: '操作时间', dataIndex: 'createdAt', key: 'createdAt', width: 180 }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 获取备件详情
|
||||||
|
const fetchDetail = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getSparePartDetail(id)
|
||||||
|
sparePart.value = res.data.data
|
||||||
|
} catch {
|
||||||
|
message.error('获取备件详情失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取记录
|
||||||
|
const fetchRecords = async () => {
|
||||||
|
recordLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getSparePartRecords(id)
|
||||||
|
records.value = res.data.data || []
|
||||||
|
} catch {
|
||||||
|
message.error('获取库存记录失败')
|
||||||
|
} finally {
|
||||||
|
recordLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回列表
|
||||||
|
const handleBack = () => {
|
||||||
|
router.push('/sparepart/list')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 入库
|
||||||
|
const handleInStock = () => {
|
||||||
|
router.push(`/sparepart/stock/in?sparePartId=${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 出库
|
||||||
|
const handleOutStock = () => {
|
||||||
|
router.push(`/sparepart/stock/out?sparePartId=${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作类型显示
|
||||||
|
const getOperationType = (type: string) => {
|
||||||
|
return type === 'IN' ? '入库' : '出库'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDetail()
|
||||||
|
fetchRecords()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<Space>
|
||||||
|
<Button @click="handleBack">
|
||||||
|
<ArrowLeftOutlined /> 返回
|
||||||
|
</Button>
|
||||||
|
<h2 class="page-title">备件详情</h2>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 备件基本信息 -->
|
||||||
|
<Card title="基本信息" style="margin-bottom: 16px">
|
||||||
|
<Descriptions :column="3" bordered>
|
||||||
|
<Descriptions.Item label="备件编码">{{ sparePart?.code || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="备件名称">{{ sparePart?.name || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="分类">{{ sparePart?.categoryName || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="单位">{{ sparePart?.unit || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="所属项目">{{ sparePart?.projectName || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="描述">{{ sparePart?.description || '-' }}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 库存信息 -->
|
||||||
|
<Card title="库存信息" style="margin-bottom: 16px">
|
||||||
|
<Row :gutter="24">
|
||||||
|
<Col :span="6">
|
||||||
|
<Statistic
|
||||||
|
title="当前库存"
|
||||||
|
:value="sparePart?.currentStock || 0"
|
||||||
|
:suffix="sparePart?.unit || '个'"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col :span="6">
|
||||||
|
<Statistic
|
||||||
|
title="安全库存"
|
||||||
|
:value="sparePart?.safeStock || 0"
|
||||||
|
:suffix="sparePart?.unit || '个'"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col :span="6">
|
||||||
|
<Statistic
|
||||||
|
title="库存状态"
|
||||||
|
:value="sparePart?.lowStockWarning ? '低库存' : '正常'"
|
||||||
|
:value-style="sparePart?.lowStockWarning ? { color: '#cf1322' } : { color: '#3f8600' }"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col :span="6">
|
||||||
|
<Space direction="vertical">
|
||||||
|
<Button type="primary" @click="handleInStock">
|
||||||
|
<InboxOutlined /> 入库
|
||||||
|
</Button>
|
||||||
|
<Button @click="handleOutStock">
|
||||||
|
<ExportOutlined /> 出库
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 库存记录 -->
|
||||||
|
<Card title="库存记录">
|
||||||
|
<template #extra>
|
||||||
|
<Button @click="fetchRecords" :loading="recordLoading">
|
||||||
|
<ReloadOutlined /> 刷新
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<a-table
|
||||||
|
:columns="recordColumns"
|
||||||
|
:data-source="records"
|
||||||
|
:loading="recordLoading"
|
||||||
|
:row-key="(record: StockRecord) => record.id"
|
||||||
|
:pagination="{
|
||||||
|
pageSize: 10,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total: number) => `共 ${total} 条`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'operationType'">
|
||||||
|
<a-tag :color="record.operationType === 'IN' ? 'green' : 'blue'">
|
||||||
|
{{ getOperationType(record.operationType) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'quantity'">
|
||||||
|
<span :style="{ color: record.operationType === 'IN' ? '#3f8600' : '#cf1322' }">
|
||||||
|
{{ record.operationType === 'IN' ? '+' : '-' }}{{ record.quantity }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'createdAt'">
|
||||||
|
{{ record.createdAt ? new Date(record.createdAt).toLocaleString() : '-' }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,445 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { Button, Select, Space, message, Tag, Modal, Form, Input, InputNumber, Popconfirm } from 'ant-design-vue'
|
||||||
|
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
ExclamationCircleOutlined
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import {
|
||||||
|
getSparePartList,
|
||||||
|
getSparePartCategories,
|
||||||
|
createSparePart,
|
||||||
|
updateSparePart,
|
||||||
|
deleteSparePart,
|
||||||
|
getLowStockSpareParts,
|
||||||
|
type SparePart,
|
||||||
|
type SparePartCategory,
|
||||||
|
type SparePartForm
|
||||||
|
} from '@/api/sparepart'
|
||||||
|
import { getProjectSelectorList } from '@/api/project'
|
||||||
|
import { TableActions } from '@/components'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns: ColumnsType = [
|
||||||
|
{ title: '备件编码', dataIndex: 'code', key: 'code', width: 140 },
|
||||||
|
{ title: '备件名称', dataIndex: 'name', key: 'name', width: 180 },
|
||||||
|
{ title: '分类', dataIndex: 'categoryName', key: 'categoryName', width: 120 },
|
||||||
|
{ title: '单位', dataIndex: 'unit', key: 'unit', width: 80 },
|
||||||
|
{ title: '当前库存', dataIndex: 'currentStock', key: 'currentStock', width: 100 },
|
||||||
|
{ title: '安全库存', dataIndex: 'safeStock', key: 'safeStock', width: 100 },
|
||||||
|
{
|
||||||
|
title: '库存状态',
|
||||||
|
dataIndex: 'lowStockWarning',
|
||||||
|
key: 'lowStockWarning',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{ title: '操作', key: 'action', width: 180, fixed: 'right' as const }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 项目选择选项
|
||||||
|
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||||
|
|
||||||
|
// 分类选项
|
||||||
|
const categoryOptions = ref<{ value: string; label: string }[]>([])
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = reactive({
|
||||||
|
projectId: '',
|
||||||
|
categoryId: undefined as string | undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// 数据状态
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableData = ref<SparePart[]>([])
|
||||||
|
const lowStockData = ref<SparePart[]>([])
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模态框状态
|
||||||
|
const modalVisible = ref(false)
|
||||||
|
const modalTitle = ref('新建备件')
|
||||||
|
const modalLoading = ref(false)
|
||||||
|
const editingSparePart = ref<SparePart | null>(null)
|
||||||
|
|
||||||
|
// 表单
|
||||||
|
const formRef = ref()
|
||||||
|
const formState = reactive<SparePartForm>({
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
categoryId: undefined,
|
||||||
|
projectId: '',
|
||||||
|
unit: '',
|
||||||
|
currentStock: 0,
|
||||||
|
safeStock: 0,
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入备件名称' }],
|
||||||
|
code: [{ required: true, message: '请输入备件编码' }],
|
||||||
|
projectId: [{ required: true, message: '请选择项目' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取项目列表
|
||||||
|
const fetchProjects = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getProjectSelectorList()
|
||||||
|
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||||
|
value: item.id,
|
||||||
|
label: item.name
|
||||||
|
}))
|
||||||
|
} catch {
|
||||||
|
message.error('获取项目列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分类列表
|
||||||
|
const fetchCategories = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getSparePartCategories()
|
||||||
|
categoryOptions.value = (res.data.data || []).map((item: SparePartCategory) => ({
|
||||||
|
value: item.id,
|
||||||
|
label: item.name
|
||||||
|
}))
|
||||||
|
} catch {
|
||||||
|
message.error('获取分类列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取备件列表
|
||||||
|
const fetchSparePartList = async () => {
|
||||||
|
if (!queryParams.projectId) {
|
||||||
|
message.warning('请先选择项目')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getSparePartList(queryParams.projectId, queryParams.categoryId)
|
||||||
|
const data = res.data.data
|
||||||
|
tableData.value = data.content || []
|
||||||
|
pagination.total = data.totalElements || 0
|
||||||
|
} catch {
|
||||||
|
message.error('获取备件列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取低库存备件
|
||||||
|
const fetchLowStockSpareParts = async () => {
|
||||||
|
if (!queryParams.projectId) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getLowStockSpareParts(queryParams.projectId)
|
||||||
|
lowStockData.value = res.data.data || []
|
||||||
|
} catch {
|
||||||
|
message.error('获取低库存备件失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.current = 1
|
||||||
|
fetchSparePartList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
queryParams.projectId = ''
|
||||||
|
queryParams.categoryId = undefined
|
||||||
|
pagination.current = 1
|
||||||
|
tableData.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
|
const handlePageChange = (page: number, pageSize: number) => {
|
||||||
|
pagination.current = page
|
||||||
|
pagination.pageSize = pageSize
|
||||||
|
fetchSparePartList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开新建模态框
|
||||||
|
const handleAdd = () => {
|
||||||
|
editingSparePart.value = null
|
||||||
|
modalTitle.value = '新建备件'
|
||||||
|
Object.assign(formState, {
|
||||||
|
id: undefined,
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
categoryId: undefined,
|
||||||
|
projectId: queryParams.projectId,
|
||||||
|
unit: '',
|
||||||
|
currentStock: 0,
|
||||||
|
safeStock: 0,
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开编辑模态框
|
||||||
|
const handleEdit = (record: SparePart) => {
|
||||||
|
editingSparePart.value = record
|
||||||
|
modalTitle.value = '编辑备件'
|
||||||
|
Object.assign(formState, {
|
||||||
|
id: record.id,
|
||||||
|
name: record.name,
|
||||||
|
code: record.code,
|
||||||
|
categoryId: record.categoryId,
|
||||||
|
projectId: record.projectId,
|
||||||
|
unit: record.unit || '',
|
||||||
|
currentStock: record.currentStock || 0,
|
||||||
|
safeStock: record.safeStock || 0,
|
||||||
|
description: record.description || ''
|
||||||
|
})
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleView = (record: SparePart) => {
|
||||||
|
router.push(`/sparepart/detail/${record.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除备件
|
||||||
|
const handleDelete = (record: SparePart) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除备件 "${record.name}" 吗?`,
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await deleteSparePart(record.id)
|
||||||
|
message.success('删除成功')
|
||||||
|
fetchSparePartList()
|
||||||
|
} catch {
|
||||||
|
message.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
modalLoading.value = true
|
||||||
|
if (editingSparePart.value) {
|
||||||
|
await updateSparePart(editingSparePart.value.id, formState)
|
||||||
|
message.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createSparePart(formState)
|
||||||
|
message.success('创建成功')
|
||||||
|
}
|
||||||
|
modalVisible.value = false
|
||||||
|
fetchSparePartList()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('表单验证失败', error)
|
||||||
|
} finally {
|
||||||
|
modalLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 入库
|
||||||
|
const handleInStock = (record: SparePart) => {
|
||||||
|
router.push(`/sparepart/stock/in?sparePartId=${record.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 出库
|
||||||
|
const handleOutStock = (record: SparePart) => {
|
||||||
|
router.push(`/sparepart/stock/out?sparePartId=${record.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 库存状态显示
|
||||||
|
const getStockStatus = (record: SparePart) => {
|
||||||
|
if (record.lowStockWarning) {
|
||||||
|
return { color: 'error', text: '低库存' }
|
||||||
|
}
|
||||||
|
return { color: 'success', text: '正常' }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchProjects()
|
||||||
|
fetchCategories()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h2 class="page-title">备件管理</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选区 -->
|
||||||
|
<div class="filter-bar">
|
||||||
|
<Space>
|
||||||
|
<Select
|
||||||
|
v-model:value="queryParams.projectId"
|
||||||
|
placeholder="请选择项目"
|
||||||
|
style="width: 240px"
|
||||||
|
allow-clear
|
||||||
|
:options="projectOptions"
|
||||||
|
@change="handleSearch"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
v-model:value="queryParams.categoryId"
|
||||||
|
placeholder="请选择分类"
|
||||||
|
style="width: 160px"
|
||||||
|
allow-clear
|
||||||
|
:options="categoryOptions"
|
||||||
|
@change="handleSearch"
|
||||||
|
/>
|
||||||
|
<Button type="primary" @click="handleSearch">
|
||||||
|
<SearchOutlined /> 查询
|
||||||
|
</Button>
|
||||||
|
<Button @click="handleReset">
|
||||||
|
<ReloadOutlined /> 重置
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<div class="table-card">
|
||||||
|
<div style="margin-bottom: 16px">
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" @click="handleAdd" :disabled="!queryParams.projectId">
|
||||||
|
<PlusOutlined /> 新建备件
|
||||||
|
</Button>
|
||||||
|
<Button @click="fetchLowStockSpareParts" :disabled="!queryParams.projectId">
|
||||||
|
<ExclamationCircleOutlined /> 低库存备件
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="tableData"
|
||||||
|
:loading="loading"
|
||||||
|
:row-key="(record: SparePart) => record.id"
|
||||||
|
:pagination="{
|
||||||
|
current: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
total: pagination.total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total: number) => `共 ${total} 条`
|
||||||
|
}"
|
||||||
|
@change="(pag: any) => handlePageChange(pag.current, pag.pageSize)"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'lowStockWarning'">
|
||||||
|
<Tag v-if="record.lowStockWarning" color="red">
|
||||||
|
{{ record.lowStockWarning ? '低库存' : '正常' }}
|
||||||
|
</Tag>
|
||||||
|
<Tag v-else color="green">正常</Tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'action'">
|
||||||
|
<Space>
|
||||||
|
<Button type="link" size="small" @click="handleView(record)">查看</Button>
|
||||||
|
<Button type="link" size="small" @click="handleEdit(record)">编辑</Button>
|
||||||
|
<Button type="link" size="small" @click="handleInStock(record)">入库</Button>
|
||||||
|
<Button type="link" size="small" @click="handleOutStock(record)">出库</Button>
|
||||||
|
<Button type="link" size="small" danger @click="handleDelete(record)">删除</Button>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
|
||||||
|
<!-- 未选择项目提示 -->
|
||||||
|
<a-empty v-if="!queryParams.projectId && tableData.length === 0" description="请先选择项目" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新建/编辑模态框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="modalVisible"
|
||||||
|
:title="modalTitle"
|
||||||
|
:confirm-loading="modalLoading"
|
||||||
|
@ok="handleSubmit"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<a-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formState"
|
||||||
|
:rules="rules"
|
||||||
|
:label-col="{ span: 6 }"
|
||||||
|
:wrapper-col="{ span: 16 }"
|
||||||
|
>
|
||||||
|
<a-form-item label="备件编码" name="code">
|
||||||
|
<a-input v-model:value="formState.code" placeholder="请输入备件编码" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="备件名称" name="name">
|
||||||
|
<a-input v-model:value="formState.name" placeholder="请输入备件名称" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="分类" name="categoryId">
|
||||||
|
<a-select
|
||||||
|
v-model:value="formState.categoryId"
|
||||||
|
placeholder="请选择分类"
|
||||||
|
:options="categoryOptions"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="单位" name="unit">
|
||||||
|
<a-input v-model:value="formState.unit" placeholder="如:个、件、套" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="当前库存" name="currentStock">
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="formState.currentStock"
|
||||||
|
:min="0"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="安全库存" name="safeStock">
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="formState.safeStock"
|
||||||
|
:min="0"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="备注" name="description">
|
||||||
|
<a-textarea v-model:value="formState.description" placeholder="请输入备注" :rows="3" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { Card, Form, InputNumber, Input, Button, Space, message, Result } from 'ant-design-vue'
|
||||||
|
import {
|
||||||
|
ArrowLeftOutlined,
|
||||||
|
InboxOutlined,
|
||||||
|
ExportOutlined
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import {
|
||||||
|
getSparePartDetail,
|
||||||
|
inStock,
|
||||||
|
outStock,
|
||||||
|
type SparePart,
|
||||||
|
type InStockRequest,
|
||||||
|
type OutStockRequest
|
||||||
|
} from '@/api/sparepart'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const sparePart = ref<SparePart | null>(null)
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const formState = reactive({
|
||||||
|
quantity: 0,
|
||||||
|
relatedOrderNo: '',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
quantity: [{ required: true, message: '请输入数量', type: 'number' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInStock = computed(() => route.path.includes('/stock/in'))
|
||||||
|
const operationType = computed(() => isInStock.value ? '入库' : '出库')
|
||||||
|
const operationIcon = computed(() => isInStock.value ? InboxOutlined : ExportOutlined)
|
||||||
|
|
||||||
|
const sparePartId = route.query.sparePartId as string
|
||||||
|
|
||||||
|
// 获取备件信息
|
||||||
|
const fetchSparePart = async () => {
|
||||||
|
if (!sparePartId) {
|
||||||
|
message.error('缺少备件ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getSparePartDetail(sparePartId)
|
||||||
|
sparePart.value = res.data.data
|
||||||
|
} catch {
|
||||||
|
message.error('获取备件信息失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
submitLoading.value = true
|
||||||
|
|
||||||
|
if (isInStock.value) {
|
||||||
|
const data: InStockRequest = {
|
||||||
|
sparePartId,
|
||||||
|
quantity: formState.quantity,
|
||||||
|
remark: formState.remark
|
||||||
|
}
|
||||||
|
await inStock(data)
|
||||||
|
message.success('入库成功')
|
||||||
|
} else {
|
||||||
|
const data: OutStockRequest = {
|
||||||
|
sparePartId,
|
||||||
|
quantity: formState.quantity,
|
||||||
|
relatedOrderNo: formState.relatedOrderNo,
|
||||||
|
remark: formState.remark
|
||||||
|
}
|
||||||
|
await outStock(data)
|
||||||
|
message.success('出库成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
router.back()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('操作失败', error)
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchSparePart()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<Space>
|
||||||
|
<Button @click="handleBack">
|
||||||
|
<ArrowLeftOutlined /> 返回
|
||||||
|
</Button>
|
||||||
|
<h2 class="page-title">{{ operationType }}</h2>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card v-if="sparePart">
|
||||||
|
<!-- 备件信息 -->
|
||||||
|
<div class="sparepart-info">
|
||||||
|
<span class="info-label">备件信息:</span>
|
||||||
|
<span class="info-value">
|
||||||
|
{{ sparePart.name }} ({{ sparePart.code }})
|
||||||
|
</span>
|
||||||
|
<span class="info-label" style="margin-left: 24px">当前库存:</span>
|
||||||
|
<span class="info-value">
|
||||||
|
{{ sparePart.currentStock || 0 }} {{ sparePart.unit || '个' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 操作表单 -->
|
||||||
|
<Card :title="operationType" style="margin-top: 16px">
|
||||||
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formState"
|
||||||
|
:rules="rules"
|
||||||
|
:label-col="{ span: 6 }"
|
||||||
|
:wrapper-col="{ span: 16 }"
|
||||||
|
>
|
||||||
|
<Form.Item :label="operationType + '数量'" name="quantity">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="formState.quantity"
|
||||||
|
:min="1"
|
||||||
|
style="width: 200px"
|
||||||
|
:placeholder="'请输入' + operationType + '数量'"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item v-if="!isInStock" label="关联工单号" name="relatedOrderNo">
|
||||||
|
<Input
|
||||||
|
v-model:value="formState.relatedOrderNo"
|
||||||
|
placeholder="请输入关联工单号(可选)"
|
||||||
|
style="width: 300px"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="备注" name="remark">
|
||||||
|
<Input.TextArea
|
||||||
|
v-model:value="formState.remark"
|
||||||
|
placeholder="请输入备注(可选)"
|
||||||
|
:rows="3"
|
||||||
|
style="width: 400px"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item :wrapper-col="{ offset: 6, span: 16 }">
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||||
|
<component :is="operationIcon" /> {{ operationType }}
|
||||||
|
</Button>
|
||||||
|
<Button @click="handleBack">取消</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<Card v-if="loading" style="margin-top: 16px">
|
||||||
|
<Result title="加载中..." />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 缺少参数 -->
|
||||||
|
<Card v-if="!sparePartId && !loading" style="margin-top: 16px">
|
||||||
|
<Result
|
||||||
|
status="warning"
|
||||||
|
title="缺少参数"
|
||||||
|
sub-title="未指定备件ID,无法进行库存操作"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<Button type="primary" @click="handleBack">返回</Button>
|
||||||
|
</template>
|
||||||
|
</Result>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sparepart-info {
|
||||||
|
padding: 16px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
color: #262626;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue