feat(qc): implement material library with CRUD, stock-in, usage tracking and task deduction
This commit is contained in:
1 parent
4137c54588
commit
e6072457fd
9 files changed
+1638
-22
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
type PageList<T> = {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export type QcMaterialUsage = {
|
||||
id: string
|
||||
materialId: string
|
||||
materialCode: string
|
||||
materialName: string
|
||||
changeType: string
|
||||
changeQty: number
|
||||
stockAfter: number
|
||||
qcTaskId: string
|
||||
qcTaskNo: string
|
||||
sourceType: string
|
||||
remark: string
|
||||
createBy: string
|
||||
createTime: string
|
||||
}
|
||||
|
||||
export function fetchQcMaterialUsageList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
materialId?: string
|
||||
changeType?: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
}) {
|
||||
return request.get<PageList<QcMaterialUsage>>({
|
||||
url: '/web/admin/qc-material-usages',
|
||||
params
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
type PageList<T> = {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export type QcMaterial = {
|
||||
id: string
|
||||
materialCode: string
|
||||
materialName: string
|
||||
specModel: string
|
||||
unit: string
|
||||
stockQty: number
|
||||
warningQty: number
|
||||
status: string
|
||||
tenantNo: string
|
||||
orderNum: number
|
||||
remark: string
|
||||
createBy: string
|
||||
createTime: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
export type QcMaterialOption = {
|
||||
id: string
|
||||
label: string
|
||||
materialCode: string
|
||||
materialName: string
|
||||
specModel: string
|
||||
unit: string
|
||||
stockQty: number
|
||||
}
|
||||
|
||||
export function fetchQcMaterialList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
status?: string
|
||||
}) {
|
||||
return request.get<PageList<QcMaterial>>({
|
||||
url: '/web/admin/qc-materials',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchQcMaterialOptions() {
|
||||
return request.get<QcMaterialOption[]>({
|
||||
url: '/web/admin/qc-materials/options'
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCreateQcMaterial(data: Record<string, any>) {
|
||||
return request.post<QcMaterial>({
|
||||
url: '/web/admin/qc-materials',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUpdateQcMaterial(id: string, data: Record<string, any>) {
|
||||
return request.put<QcMaterial>({
|
||||
url: `/web/admin/qc-materials/${id}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchDeleteQcMaterial(id: string) {
|
||||
return request.delete<void>({
|
||||
url: `/web/admin/qc-materials/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchStockInQcMaterial(id: string, data: { qty: number; remark?: string }) {
|
||||
return request.post<QcMaterial>({
|
||||
url: `/web/admin/qc-materials/${id}/stock-in`,
|
||||
data
|
||||
})
|
||||
}
|
||||
@@ -191,6 +191,13 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
path: 'sample-code',
|
||||
component: '/basic-info/sample-code/index',
|
||||
meta: { title: '样本编码管理', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '40000000000000000000000000000007',
|
||||
name: 'BasicInfoInsuranceCompany',
|
||||
path: 'insurance-company',
|
||||
component: '/basic-info/insurance-company/index',
|
||||
meta: { title: '保险公司及项目价格', keepAlive: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -240,6 +247,20 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
path: 'refund-confirm',
|
||||
component: '/cell-storage/refund-confirm/index',
|
||||
meta: { title: '退费确认(财务)', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000006',
|
||||
name: 'CellStorageStorageCert',
|
||||
path: 'storage-cert',
|
||||
component: '/cell-storage/storage-cert/index',
|
||||
meta: { title: '储存证管理', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000008',
|
||||
name: 'CellStorageFullmoonTodo',
|
||||
path: 'fullmoon-todo',
|
||||
component: '/cell-storage/fullmoon-todo/index',
|
||||
meta: { title: '满月待办管理', keepAlive: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -344,6 +365,20 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true,
|
||||
menuCode: 'biz:qcTask:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '70000000000000000000000000000009',
|
||||
name: 'QualityInspectionQcMaterial',
|
||||
path: 'qc-material',
|
||||
component: '/quality-inspection/qc-material/index',
|
||||
meta: { title: '质检物料库', keepAlive: true, menuCode: 'biz:qcMaterial:list' }
|
||||
},
|
||||
{
|
||||
id: '70000000000000000000000000000010',
|
||||
name: 'QualityInspectionQcMaterialUsage',
|
||||
path: 'qc-material-usage',
|
||||
component: '/quality-inspection/qc-material-usage/index',
|
||||
meta: { title: '物料变动记录', keepAlive: true, menuCode: 'biz:qcMaterialUsage:list' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
<!-- 物料变动记录 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>物料变动记录</h1>
|
||||
<p class="page-desc">查看检测物料的使用与入库变动流水,支持按物料、变动类型和时间范围筛选。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2>变动记录列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="物料筛选">
|
||||
<el-select
|
||||
v-model="searchForm.materialId"
|
||||
placeholder="全部物料"
|
||||
clearable
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="m in materialOptions"
|
||||
:key="m.id"
|
||||
:label="m.label"
|
||||
:value="m.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="变动类型">
|
||||
<el-select v-model="searchForm.changeType" placeholder="全部" clearable>
|
||||
<el-option label="使用" value="use" />
|
||||
<el-option label="入库" value="in" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">按物料、变动类型和时间范围查询变动记录。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-end">
|
||||
<el-tooltip content="列设置" placement="top">
|
||||
<el-button class="btn-ghost" circle @click="showColumnSettings = true">
|
||||
<el-icon><Setting /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button class="btn-ghost" circle @click="loadData">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="120" />
|
||||
<el-table-column prop="materialName" label="物料名称" min-width="140" />
|
||||
<el-table-column prop="changeType" label="变动类型" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.changeType === 'use' ? 'warning' : 'success'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.changeType === 'use' ? '使用' : '入库' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="changeQty" label="变动数量" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="row.changeQty < 0 ? 'qty-negative' : 'qty-positive'">
|
||||
{{ row.changeQty < 0 ? row.changeQty : `+${row.changeQty}` }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stockAfter" label="变动后库存" min-width="120" align="right" />
|
||||
<el-table-column prop="qcTaskNo" label="关联任务号" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.qcTaskNo || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sourceType" label="来源类型" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="getSourceTagType(row.sourceType)"
|
||||
size="small"
|
||||
>
|
||||
{{ getSourceLabel(row.sourceType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="createBy" label="操作人" min-width="100" />
|
||||
<el-table-column prop="createTime" label="变动时间" min-width="170" />
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||||
@current-change="(val) => ((pagination.page = val), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 列设置抽屉 -->
|
||||
<el-drawer v-model="showColumnSettings" title="列设置" size="300px">
|
||||
<p>列设置功能预留</p>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Search, RefreshLeft, Refresh, Setting } from '@element-plus/icons-vue'
|
||||
import { fetchQcMaterialUsageList } from '@/api/qc-material-usage'
|
||||
import type { QcMaterialUsage } from '@/api/qc-material-usage'
|
||||
import { fetchQcMaterialOptions } from '@/api/qc-material'
|
||||
import type { QcMaterialOption } from '@/api/qc-material'
|
||||
|
||||
defineOptions({ name: 'QcMaterialUsage' })
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const searchForm = reactive<{
|
||||
materialId: string
|
||||
changeType: string
|
||||
}>({
|
||||
materialId: '',
|
||||
changeType: ''
|
||||
})
|
||||
|
||||
const dateRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<QcMaterialUsage[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const materialOptions = ref<QcMaterialOption[]>([])
|
||||
const showColumnSettings = ref(false)
|
||||
|
||||
function getSourceTagType(sourceType: string) {
|
||||
if (sourceType === 'task_complete') return 'primary'
|
||||
if (sourceType === 'manual_in') return 'success'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function getSourceLabel(sourceType: string) {
|
||||
if (sourceType === 'task_complete') return '任务完成'
|
||||
if (sourceType === 'manual_in') return '手动入库'
|
||||
return sourceType || '-'
|
||||
}
|
||||
|
||||
async function loadMaterialOptions() {
|
||||
try {
|
||||
const res = await fetchQcMaterialOptions()
|
||||
materialOptions.value = res || []
|
||||
} catch {
|
||||
materialOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: Record<string, any> = {
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
}
|
||||
if (searchForm.materialId) params.materialId = searchForm.materialId
|
||||
if (searchForm.changeType) params.changeType = searchForm.changeType
|
||||
if (dateRange.value && dateRange.value[0]) {
|
||||
params.startTime = dateRange.value[0]
|
||||
params.endTime = dateRange.value[1]
|
||||
}
|
||||
const res = await fetchQcMaterialUsageList(params)
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.materialId = ''
|
||||
searchForm.changeType = ''
|
||||
dateRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadMaterialOptions()
|
||||
if (route.query.materialId) {
|
||||
searchForm.materialId = route.query.materialId as string
|
||||
}
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.qty-positive {
|
||||
color: var(--el-color-success);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.qty-negative {
|
||||
color: var(--el-color-danger);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,472 @@
|
||||
<!-- 质检物料库 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>质检物料库</h1>
|
||||
<p class="page-desc">维护质检物料的编码、名称、规格、库存预警等信息,支持入库操作。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2>物料列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="物料名称/编码" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 100%">
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">按物料名称、编码模糊查询。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">重置</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleStockInFromToolbar"
|
||||
>
|
||||
入库
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="materialName" label="物料名称" min-width="140" align="center" header-align="center" />
|
||||
<el-table-column prop="specModel" label="规格型号" min-width="140" align="center" header-align="center" />
|
||||
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
|
||||
<el-table-column label="当前库存" min-width="100" align="right" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.stockQty ?? 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预警库存" min-width="100" align="right" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.warningQty ?? 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存状态" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.stockQty <= row.warningQty ? 'danger' : 'success'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.stockQty <= row.warningQty ? '库存预警' : '正常' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="80" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
|
||||
{{ row.status === '1' ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="140" align="center" header-align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" header-align="center" />
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleStockIn(row)">入库</el-button>
|
||||
<el-button class="btn-op-item" @click="handleViewUsage(row)">详情</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||||
@current-change="(val) => ((pagination.page = val), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑 Dialog -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
class="material-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="物料编码" prop="materialCode">
|
||||
<el-input v-model="formData.materialCode" placeholder="请输入物料编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="物料名称" prop="materialName">
|
||||
<el-input v-model="formData.materialName" placeholder="请输入物料名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="规格型号">
|
||||
<el-input v-model="formData.specModel" placeholder="请输入规格型号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单位">
|
||||
<el-input v-model="formData.unit" placeholder="请输入单位" />
|
||||
</el-form-item>
|
||||
<el-form-item label="预警库存">
|
||||
<el-input-number
|
||||
v-model="formData.warningQty"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
placeholder="请输入预警库存"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio value="1">启用</el-radio>
|
||||
<el-radio value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
<el-icon><Check /></el-icon>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 入库 Dialog -->
|
||||
<el-dialog
|
||||
v-model="stockInVisible"
|
||||
title="物料入库"
|
||||
width="480px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="stockInFormRef"
|
||||
:model="stockInForm"
|
||||
:rules="stockInRules"
|
||||
label-position="top"
|
||||
class="material-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<el-form-item label="物料名称">
|
||||
<el-input :model-value="stockInForm.materialName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="当前库存">
|
||||
<el-input :model-value="stockInForm.currentStock" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="入库数量" prop="qty">
|
||||
<el-input-number
|
||||
v-model="stockInForm.qty"
|
||||
:min="0.0001"
|
||||
:precision="4"
|
||||
style="width: 100%"
|
||||
placeholder="请输入入库数量"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="stockInForm.remark" type="textarea" :rows="3" placeholder="选填" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="stockInVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleStockInSubmit" :loading="stockInLoading">
|
||||
<el-icon><Check /></el-icon>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchQcMaterialList,
|
||||
fetchCreateQcMaterial,
|
||||
fetchUpdateQcMaterial,
|
||||
fetchDeleteQcMaterial,
|
||||
fetchStockInQcMaterial,
|
||||
} from '@/api/qc-material'
|
||||
import type { QcMaterial } from '@/api/qc-material'
|
||||
|
||||
defineOptions({ name: 'QualityInspectionQcMaterial' })
|
||||
|
||||
const searchForm = reactive({ keyword: '', status: '' })
|
||||
const tableData = ref<QcMaterial[]>([])
|
||||
const selectedRows = ref<QcMaterial[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
|
||||
// 新增/编辑
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增物料')
|
||||
const isEdit = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const editingId = ref<string | null>(null)
|
||||
|
||||
const formData = reactive({
|
||||
materialCode: '',
|
||||
materialName: '',
|
||||
specModel: '',
|
||||
unit: '',
|
||||
warningQty: 0,
|
||||
status: '1',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
materialCode: [{ required: true, message: '请输入物料编码', trigger: 'blur' }],
|
||||
materialName: [{ required: true, message: '请输入物料名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 入库
|
||||
const stockInVisible = ref(false)
|
||||
const stockInLoading = ref(false)
|
||||
const stockInFormRef = ref()
|
||||
const stockInTargetId = ref<string | null>(null)
|
||||
|
||||
const stockInForm = reactive({
|
||||
materialName: '',
|
||||
currentStock: 0,
|
||||
qty: undefined as number | undefined,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const stockInRules = {
|
||||
qty: [{ required: true, message: '请输入入库数量', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: QcMaterial[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchQcMaterialList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
status: searchForm.status || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.status = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增/编辑
|
||||
function resetForm() {
|
||||
formData.materialCode = ''
|
||||
formData.materialName = ''
|
||||
formData.specModel = ''
|
||||
formData.unit = ''
|
||||
formData.warningQty = 0
|
||||
formData.status = '1'
|
||||
formData.remark = ''
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增物料'
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
formRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
function handleEdit(row: QcMaterial) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑物料'
|
||||
editingId.value = row.id
|
||||
formData.materialCode = row.materialCode || ''
|
||||
formData.materialName = row.materialName || ''
|
||||
formData.specModel = row.specModel || ''
|
||||
formData.unit = row.unit || ''
|
||||
formData.warningQty = row.warningQty ?? 0
|
||||
formData.status = row.status || '1'
|
||||
formData.remark = row.remark || ''
|
||||
dialogVisible.value = true
|
||||
formRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate?.()
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
materialCode: formData.materialCode.trim(),
|
||||
materialName: formData.materialName.trim(),
|
||||
specModel: formData.specModel?.trim() || undefined,
|
||||
unit: formData.unit?.trim() || undefined,
|
||||
warningQty: formData.warningQty,
|
||||
status: formData.status,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
if (isEdit.value && editingId.value) {
|
||||
await fetchUpdateQcMaterial(editingId.value, payload)
|
||||
} else {
|
||||
await fetchCreateQcMaterial(payload)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
} catch {
|
||||
/* 校验或接口错误已由全局处理 */
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 入库
|
||||
function openStockInDialog(row: QcMaterial) {
|
||||
stockInTargetId.value = row.id
|
||||
stockInForm.materialName = row.materialName || ''
|
||||
stockInForm.currentStock = row.stockQty ?? 0
|
||||
stockInForm.qty = undefined
|
||||
stockInForm.remark = ''
|
||||
stockInVisible.value = true
|
||||
stockInFormRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
function handleStockIn(row: QcMaterial) {
|
||||
openStockInDialog(row)
|
||||
}
|
||||
|
||||
function handleStockInFromToolbar() {
|
||||
if (selectedRows.value.length === 0) return
|
||||
openStockInDialog(selectedRows.value[0])
|
||||
}
|
||||
|
||||
async function handleStockInSubmit() {
|
||||
await stockInFormRef.value?.validate?.()
|
||||
if (!stockInTargetId.value) return
|
||||
stockInLoading.value = true
|
||||
try {
|
||||
await fetchStockInQcMaterial(stockInTargetId.value, {
|
||||
qty: stockInForm.qty!,
|
||||
remark: stockInForm.remark?.trim() || undefined
|
||||
})
|
||||
ElMessage.success('入库成功')
|
||||
stockInVisible.value = false
|
||||
await loadData()
|
||||
} catch {
|
||||
/* 校验或接口错误已由全局处理 */
|
||||
} finally {
|
||||
stockInLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看变动记录
|
||||
function handleViewUsage(row: QcMaterial) {
|
||||
window.location.hash = `/sqxp/quality-inspection/qc-material-usage?materialId=${row.id}`
|
||||
}
|
||||
|
||||
// 删除
|
||||
async function handleDelete(row: QcMaterial) {
|
||||
if ((row.stockQty ?? 0) > 0) {
|
||||
ElMessage.warning('当前库存大于0,无法删除')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该物料吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await fetchDeleteQcMaterial(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.material-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">任务列表</h2>
|
||||
<h2>任务列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
@@ -178,6 +178,43 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-section material-usage-section">
|
||||
<div class="section-title">物料使用(选填)</div>
|
||||
<div v-for="(mu, idx) in materialUsages" :key="idx" class="material-usage-row">
|
||||
<el-form-item label="物料" class="mu-field-material">
|
||||
<el-select
|
||||
v-model="mu.materialId"
|
||||
filterable
|
||||
placeholder="选择物料"
|
||||
style="width: 100%"
|
||||
@change="(v: string) => onMaterialSelect(mu, v)"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in materialOptions"
|
||||
:key="opt.id"
|
||||
:label="opt.label"
|
||||
:value="opt.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="使用数量" class="mu-field-qty">
|
||||
<el-input-number
|
||||
v-model="mu.qty"
|
||||
:min="0.0001"
|
||||
:precision="4"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
placeholder="数量"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-button class="btn-ghost mu-remove-btn" @click="materialUsages.splice(idx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button class="btn-ghost" size="small" @click="materialUsages.push({ materialId: '', materialName: '', qty: undefined })">
|
||||
<el-icon><Plus /></el-icon>添加物料
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="form-section result-conclusion-section">
|
||||
<div class="section-title">检测结论</div>
|
||||
<el-form-item label="结论" required>
|
||||
@@ -186,21 +223,18 @@
|
||||
<el-radio value="btg">不通过</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-section result-remark-section">
|
||||
<div class="section-title">备注</div>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="resultRemark" type="textarea" :rows="3" placeholder="选填" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-section result-attachment-section">
|
||||
<ProtocolFinanceConfirmFields
|
||||
v-model:finance-remark="resultRemark"
|
||||
v-model:attachments="resultAttachments"
|
||||
:show-remark="false"
|
||||
:attachment-label="resultAttachmentLabel"
|
||||
:upload-folder="resultUploadFolder"
|
||||
/>
|
||||
<div class="result-attachment-block">
|
||||
<ProtocolFinanceConfirmFields
|
||||
v-model:finance-remark="resultRemark"
|
||||
v-model:attachments="resultAttachments"
|
||||
:show-remark="false"
|
||||
:attachment-label="resultAttachmentLabel"
|
||||
:upload-folder="resultUploadFolder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -270,6 +304,7 @@
|
||||
type QcTaskDetail
|
||||
} from '@/api/qc-task'
|
||||
import { fetchQcTaskResults, fetchCompleteTaskWithResult, type QcTaskResultItem } from '@/api/qc-task-result'
|
||||
import { fetchQcMaterialOptions, type QcMaterialOption } from '@/api/qc-material'
|
||||
import {
|
||||
taskTypeLabel,
|
||||
taskStatusLabel,
|
||||
@@ -298,6 +333,8 @@
|
||||
const resultRemark = ref('')
|
||||
const resultConclusion = ref('')
|
||||
const resultAttachments = ref<FinanceAttachmentItem[]>([])
|
||||
const materialOptions = ref<QcMaterialOption[]>([])
|
||||
const materialUsages = ref<Array<{ materialId: string; materialName: string; qty: number | undefined }>>([])
|
||||
|
||||
function canFillResult(row: QcTask) {
|
||||
if (row.taskType === 'sysz') {
|
||||
@@ -321,6 +358,11 @@
|
||||
resultTaskType.value === 'dsfjc' ? 'qc-third-party' : 'qc-lab'
|
||||
)
|
||||
|
||||
function onMaterialSelect(mu: { materialId: string; materialName: string; qty: number | undefined }, id: string) {
|
||||
const opt = materialOptions.value.find((o) => o.id === id)
|
||||
mu.materialName = opt ? opt.materialName : ''
|
||||
}
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewDetail = ref<QcTaskDetail | null>(null)
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
@@ -580,6 +622,7 @@
|
||||
resultRemark.value = row.remark || ''
|
||||
resultConclusion.value = ''
|
||||
resultAttachments.value = apiFilesToFinanceItems(row.attachmentFiles)
|
||||
materialUsages.value = []
|
||||
resultLoading.value = true
|
||||
try {
|
||||
const detail = await fetchQcTaskDetail(row.id)
|
||||
@@ -594,6 +637,11 @@
|
||||
} finally {
|
||||
resultLoading.value = false
|
||||
}
|
||||
try {
|
||||
materialOptions.value = await fetchQcMaterialOptions()
|
||||
} catch {
|
||||
materialOptions.value = []
|
||||
}
|
||||
resultVisible.value = true
|
||||
}
|
||||
|
||||
@@ -612,11 +660,15 @@
|
||||
}
|
||||
resultLoading.value = true
|
||||
try {
|
||||
const validMaterialUsages = materialUsages.value.filter(
|
||||
(mu) => mu.materialId && mu.qty && mu.qty > 0
|
||||
)
|
||||
const data: {
|
||||
conclusion: string
|
||||
results: QcTaskResultItem[]
|
||||
remark?: string
|
||||
thirdPartyAttachments?: string
|
||||
materialUsages?: Array<{ materialId: string; qty: number }>
|
||||
} = {
|
||||
conclusion: resultConclusion.value,
|
||||
remark: resultRemark.value,
|
||||
@@ -628,6 +680,12 @@
|
||||
}))
|
||||
}
|
||||
data.thirdPartyAttachments = financeAttachmentsToUrlString(resultAttachments.value)
|
||||
if (validMaterialUsages.length > 0) {
|
||||
data.materialUsages = validMaterialUsages.map((mu) => ({
|
||||
materialId: mu.materialId,
|
||||
qty: mu.qty!,
|
||||
}))
|
||||
}
|
||||
await fetchCompleteTaskWithResult(resultTaskId.value, data)
|
||||
ElMessage.success(resultTaskStatus.value === 'ywc' ? '已保存' : '已完成')
|
||||
resultVisible.value = false
|
||||
@@ -658,19 +716,61 @@
|
||||
border-left: 3px solid #10b981;
|
||||
}
|
||||
|
||||
.material-usage-section {
|
||||
margin-top: 12px;
|
||||
|
||||
.material-usage-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.mu-field-material {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
.mu-field-qty {
|
||||
flex: 1.2;
|
||||
}
|
||||
|
||||
.mu-remove-btn {
|
||||
margin-top: 28px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-conclusion-section {
|
||||
margin-top: 12px;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.result-remark-section {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.section-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.result-attachment-section {
|
||||
margin-top: 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.result-attachment-block {
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
|
||||
:deep(.protocol-finance-confirm-fields .el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.biz-dialog-form {
|
||||
|
||||
Reference in new issue
Block a user