feat: add production task set maintenance page

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-18 16:16:12 +08:00
1 parent d60d4bd4b6
commit 11eb9874af
2 files changed
+591

No files matched your search

+11
View File
@@ -922,6 +922,17 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
isHide: true,
menuCode: 'production:process-route:list'
}
},
{
id: '95400000000000000000000000000001',
name: 'ProductionTaskSet',
path: 'production-task-set',
component: '/production/production-task-set/index',
meta: {
title: '生产任务集合',
keepAlive: true,
menuCode: 'production:task-set:list'
}
}
]
}
@@ -0,0 +1,580 @@
<!-- 生产基础信息 — 生产任务集合 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">生产任务集合列表</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">
<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">
<el-button v-if="canAdd" class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增集合
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="id">
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column
prop="setName"
label="集合名称"
min-width="180"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="itemCount"
label="任务条数"
min-width="110"
align="center"
header-align="center"
/>
<el-table-column
prop="status"
label="状态"
min-width="90"
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="160"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="createTime"
label="创建时间"
min-width="170"
align="center"
header-align="center"
>
<template #default="{ row }">{{ formatDisplayOrDash(row.createTime) }}</template>
</el-table-column>
<el-table-column
label="操作"
min-width="360"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
<el-button v-if="canEdit" class="btn-op-item primary" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button v-if="canEdit" class="btn-op-item" @click="handleToggleStatus(row)">
<el-icon><Switch /></el-icon>{{ row.status === '1' ? '停用' : '启用' }}
</el-button>
<el-button v-if="canRemove" class="btn-op-item" @click="handleDelete(row)">
<el-icon><Delete /></el-icon>删除
</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>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="860px"
class="dialog-form-layout"
align-center
append-to-body
modal-class="protocol-dialog-mask"
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="isView ? {} : formRules"
label-position="top"
class="biz-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="集合名称" prop="setName">
<el-input
v-model="formData.setName"
placeholder="请输入集合名称"
maxlength="200"
:disabled="isView"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status" :disabled="isView">
<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="2"
placeholder="可选"
maxlength="512"
:disabled="isView"
/>
</el-form-item>
<el-form-item class="form-col-span-2" prop="items">
<div class="set-items-toolbar">
<span class="set-items-title">任务条目</span>
<el-button v-if="!isView" class="btn-ghost" @click="addItem">
<el-icon><Plus /></el-icon>添加条目
</el-button>
</div>
<el-table class="list-table" :data="formData.items" stripe :row-key="itemRowKey">
<el-table-column label="任务名称" min-width="160">
<template #default="{ row }">
<el-input
v-model="row.taskName"
placeholder="请输入任务名称"
maxlength="200"
:disabled="isView"
/>
</template>
</el-table-column>
<el-table-column label="工艺路线" min-width="200">
<template #default="{ row }">
<el-select
v-model="row.routeId"
placeholder="输入路线名称检索(可选)"
filterable
remote
reserve-keyword
clearable
style="width: 100%"
:disabled="isView"
:loading="routeLoading"
:remote-method="remoteSearchRoutes"
@visible-change="onRouteSelectVisible"
>
<el-option
v-for="route in routeOptions"
:key="route.id"
:label="`${route.routeName}(${route.routeCode})`"
:value="route.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="描述" min-width="160">
<template #default="{ row }">
<el-input
v-model="row.description"
placeholder="可选"
maxlength="500"
:disabled="isView"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="140">
<template #default="{ row }">
<el-input
v-model="row.remark"
placeholder="可选"
maxlength="512"
:disabled="isView"
/>
</template>
</el-table-column>
<el-table-column v-if="!isView" label="操作" min-width="200" fixed="right">
<template #default="{ $index }">
<el-button
class="btn-op-item"
:disabled="$index === 0"
@click="moveItem($index, -1)"
>
<el-icon><Top /></el-icon>上移
</el-button>
<el-button
class="btn-op-item"
:disabled="$index === formData.items.length - 1"
@click="moveItem($index, 1)"
>
<el-icon><Bottom /></el-icon>下移
</el-button>
<el-button class="btn-op-item" @click="removeItem($index)">
<el-icon><Delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
</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
v-if="!isView"
class="btn-main"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
<el-icon><Check /></el-icon>确定
</el-button>
</template>
</el-dialog>
</section>
</div>
</template>
<script setup lang="ts">
import { formatDisplayOrDash } from '@/utils/datetime-display'
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Plus,
Close,
Check,
Search,
RefreshLeft,
Edit,
Delete,
Switch,
View,
Top,
Bottom
} from '@element-plus/icons-vue'
import { useMenuPerm } from '@/hooks/core/useMenuPerm'
import { fetchProcessRouteList } from '@/api/production'
import {
fetchProductionTaskSetList,
fetchProductionTaskSetDetail,
fetchCreateProductionTaskSet,
fetchUpdateProductionTaskSet,
fetchDeleteProductionTaskSet,
type ProductionTaskSet,
type ProductionTaskSetItem
} from '@/api/production-task-set'
defineOptions({ name: 'ProductionTaskSet' })
type FormItem = ProductionTaskSetItem & { _key: string }
const { hasMenuPerm } = useMenuPerm()
const canAdd = computed(() => hasMenuPerm('production:task-set:add'))
const canEdit = computed(() => hasMenuPerm('production:task-set:edit'))
const canRemove = computed(() => hasMenuPerm('production:task-set:remove'))
const searchForm = reactive({ keyword: '', status: '' as string })
const tableData = ref<ProductionTaskSet[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const dialogVisible = ref(false)
const dialogTitle = ref('新增集合')
const dialogMode = ref<'add' | 'edit' | 'view'>('add')
const submitLoading = ref(false)
const formRef = ref()
const editingId = ref<string | null>(null)
const routeOptions = ref<any[]>([])
const routeLoading = ref(false)
const isView = computed(() => dialogMode.value === 'view')
const formData = reactive({
setName: '',
status: '1',
remark: '',
items: [] as FormItem[]
})
const validateItems = (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => {
if (!formData.items.some((item) => String(item.taskName || '').trim())) {
callback(new Error('请至少添加一条任务名称非空的条目'))
return
}
callback()
}
const formRules = {
setName: [{ required: true, message: '请输入集合名称', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
items: [{ validator: validateItems, trigger: 'change' }]
}
let itemKeySeq = 0
function createEmptyItem(partial?: Partial<ProductionTaskSetItem>): FormItem {
itemKeySeq += 1
return {
_key: `item-${itemKeySeq}`,
taskName: partial?.taskName || '',
routeId: partial?.routeId || '',
routeName: partial?.routeName || '',
description: partial?.description || '',
remark: partial?.remark || ''
}
}
function itemRowKey(row: FormItem) {
return row._key
}
function addItem() {
formData.items.push(createEmptyItem())
}
function removeItem(index: number) {
formData.items.splice(index, 1)
if (!formData.items.length) {
formData.items.push(createEmptyItem())
}
}
function moveItem(index: number, delta: number) {
const target = index + delta
if (target < 0 || target >= formData.items.length) return
const next = [...formData.items]
const [row] = next.splice(index, 1)
next.splice(target, 0, row)
formData.items = next
}
async function remoteSearchRoutes(query: string) {
routeLoading.value = true
try {
const kw = query?.trim()
const res = await fetchProcessRouteList({
page: 1,
pageSize: 100,
routeName: kw || undefined
})
routeOptions.value = res.list || []
} catch {
routeOptions.value = []
} finally {
routeLoading.value = false
}
}
function onRouteSelectVisible(visible: boolean) {
if (visible) void remoteSearchRoutes('')
}
function seedRouteOptionsFromItems(items: ProductionTaskSetItem[]) {
const seen = new Set(routeOptions.value.map((r) => r.id))
for (const item of items) {
const id = String(item.routeId || '').trim()
if (!id || seen.has(id)) continue
seen.add(id)
routeOptions.value.push({
id,
routeName: item.routeName || id,
routeCode: ''
})
}
}
async function loadData() {
loading.value = true
try {
const res = await fetchProductionTaskSetList({
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.setName = ''
formData.status = '1'
formData.remark = ''
formData.items = [createEmptyItem()]
editingId.value = null
routeOptions.value = []
}
function applyDetail(detail: ProductionTaskSet) {
formData.setName = detail.setName || ''
formData.status = detail.status || '1'
formData.remark = detail.remark || ''
const items = (detail.items || []).map((item) => createEmptyItem(item))
formData.items = items.length ? items : [createEmptyItem()]
seedRouteOptionsFromItems(detail.items || [])
}
function handleAdd() {
dialogMode.value = 'add'
dialogTitle.value = '新增集合'
resetForm()
dialogVisible.value = true
void remoteSearchRoutes('')
}
async function openDetailDialog(row: ProductionTaskSet, mode: 'edit' | 'view') {
dialogMode.value = mode
dialogTitle.value = mode === 'edit' ? '编辑集合' : '查看集合'
resetForm()
editingId.value = row.id
const detail = await fetchProductionTaskSetDetail(row.id)
applyDetail(detail)
dialogVisible.value = true
void remoteSearchRoutes('')
}
function handleEdit(row: ProductionTaskSet) {
void openDetailDialog(row, 'edit')
}
function handleView(row: ProductionTaskSet) {
void openDetailDialog(row, 'view')
}
function buildPayload() {
return {
setName: formData.setName.trim(),
status: formData.status,
remark: formData.remark || undefined,
items: formData.items
.filter((item) => String(item.taskName || '').trim())
.map((item) => ({
taskName: String(item.taskName).trim(),
routeId: String(item.routeId || '').trim() || undefined,
description: String(item.description || '').trim() || undefined,
remark: String(item.remark || '').trim() || undefined
}))
}
}
async function handleSubmit() {
const valid = await formRef.value?.validate?.().catch(() => false)
if (!valid) return
submitLoading.value = true
try {
const payload = buildPayload()
if (dialogMode.value === 'edit' && editingId.value) {
await fetchUpdateProductionTaskSet(editingId.value, payload)
ElMessage.success('已保存')
} else {
await fetchCreateProductionTaskSet(payload)
ElMessage.success('已新增')
}
dialogVisible.value = false
loadData()
} finally {
submitLoading.value = false
}
}
async function handleToggleStatus(row: ProductionTaskSet) {
const next = row.status === '1' ? '0' : '1'
const tip = next === '1' ? '启用' : '停用'
await ElMessageBox.confirm(`确定${tip}集合「${row.setName}」?`, '提示', {
type: 'warning'
})
await fetchUpdateProductionTaskSet(row.id, { status: next })
ElMessage.success(`已${tip}`)
loadData()
}
async function handleDelete(row: ProductionTaskSet) {
await ElMessageBox.confirm(`确定删除集合「${row.setName}」?`, '提示', { type: 'warning' })
await fetchDeleteProductionTaskSet(row.id)
ElMessage.success('已删除')
loadData()
}
onMounted(() => loadData())
</script>
<style scoped lang="scss">
.set-items-toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.set-items-title {
font-size: 13px;
font-weight: 600;
color: #1e293b;
}
</style>