feat: add production task set maintenance page
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
d60d4bd4b6
commit
11eb9874af
2 files changed
+591
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user