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',
|
||||
component: () => import('@/views/energy/EnergyStatistics.vue'),
|
||||
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: '备件出库' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,32 +43,37 @@ const menuItems: MenuProps['items'] = [
|
|||
]
|
||||
},
|
||||
{
|
||||
key: 'operation',
|
||||
label: '运营管理',
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
key: '/maintenance/plans',
|
||||
icon: () => h(ToolOutlined),
|
||||
label: '维保计划'
|
||||
},
|
||||
{
|
||||
key: '/maintenance/tasks',
|
||||
icon: () => h(ToolOutlined),
|
||||
label: '维保任务'
|
||||
},
|
||||
{
|
||||
key: 'energy',
|
||||
icon: () => h(HeatMapOutlined),
|
||||
label: '能耗管理',
|
||||
key: 'operation',
|
||||
label: '运营管理',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ key: '/energy/meters', label: '计量点管理' },
|
||||
{ key: '/energy/consumption', label: '能耗录入' },
|
||||
{ key: '/energy/statistics', label: '能耗统计' }
|
||||
{
|
||||
key: '/sparepart/list',
|
||||
icon: () => h(ToolOutlined),
|
||||
label: '备件管理'
|
||||
},
|
||||
{
|
||||
key: '/maintenance/plans',
|
||||
icon: () => h(ToolOutlined),
|
||||
label: '维保计划'
|
||||
},
|
||||
{
|
||||
key: '/maintenance/tasks',
|
||||
icon: () => h(ToolOutlined),
|
||||
label: '维保任务'
|
||||
},
|
||||
{
|
||||
key: 'energy',
|
||||
icon: () => h(HeatMapOutlined),
|
||||
label: '能耗管理',
|
||||
children: [
|
||||
{ key: '/energy/meters', label: '计量点管理' },
|
||||
{ key: '/energy/consumption', label: '能耗录入' },
|
||||
{ key: '/energy/statistics', label: '能耗统计' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
label: '系统管理',
|
||||
|
|
|
|||
|
|
@ -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