feat(qc): implement material library with CRUD, stock-in, usage tracking and task deduction

This commit is contained in:
zlwu committed 2026-05-20 06:36:50 +08:00
1 parent 4137c54588
commit e6072457fd
9 files changed
+1638 -22

No files matched your search

+39
View File
@@ -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
})
}
+81
View File
@@ -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
})
}
+35
View File
@@ -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 {