feat:添加上下模模块

master
黄伟杰 2 months ago
parent f45f518c2a
commit 5aab6cad57

@ -72,6 +72,13 @@ export function getMoldList(params = {}) {
}) })
} }
export function getInTransitMoldAllList() {
return request({
url: '/admin-api/erp/mold/getInTransitMoldAllList',
method: 'get'
})
}
export function createMoldBrand(data) { export function createMoldBrand(data) {
return request({ return request({
url: '/admin-api/erp/mold-brand/create', url: '/admin-api/erp/mold-brand/create',

@ -0,0 +1,57 @@
import request from '@/utils/request'
export function getMoldOperatePage(params = {}) {
return request({
url: '/admin-api/mes/mold-operate/page',
method: 'get',
params
})
}
export function getMoldOperateDetail(id) {
return request({
url: '/admin-api/mes/mold-operate/get',
method: 'get',
params: { id }
})
}
export function createMoldOperate(data) {
return request({
url: '/admin-api/mes/mold-operate/create',
method: 'post',
data
})
}
export function updateMoldOperate(data) {
return request({
url: '/admin-api/mes/mold-operate/update',
method: 'put',
data
})
}
export function deleteMoldOperate(id) {
return request({
url: '/admin-api/mes/mold-operate/delete',
method: 'delete',
params: { id }
})
}
export function getLowerMoldList(id) {
return request({
url: '/admin-api/mes/mold-operate/getLowerMoldList',
method: 'get',
params: { id }
})
}
export function getDeviceLedgerList(params = {}) {
return request({
url: '/admin-api/mes/device-ledger/list',
method: 'get',
params
})
}

@ -191,6 +191,42 @@ const messages = {
confirmApprove: '确认审批入库单 {no} 吗?', confirmApprove: '确认审批入库单 {no} 吗?',
approveSuccess: '审批成功' approveSuccess: '审批成功'
}, },
moldOperate: {
moduleName: '上下模',
subTitle: '模具上模/下模操作记录',
detailTitle: '上下模详情',
basicInfo: '基础信息',
tabUp: '上模',
tabDown: '下模',
operateType: '操作类型',
mold: '模具',
lowerMold: '下模模具',
selectMold: '选择模具',
noSelectedMold: '暂无已选模具',
allMold: '全部模具',
searchRemark: '请输入备注关键字',
searchCode: '请输入模具编码',
searchName: '请输入模具名称',
moldCode: '模具编码',
moldName: '模具名称',
device: '设备',
deviceName: '设备名称',
creatorName: '创建人',
remark: '备注',
createTime: '创建时间',
createTitle: '新增上下模',
editTitle: '编辑上下模',
empty: '暂无上下模数据',
noMoldData: '暂无可选模具',
placeholderDevice: '请选择设备',
placeholderRemark: '请输入备注',
validatorOperateTypeRequired: '操作类型不能为空',
validatorDeviceRequired: '设备不能为空',
validatorMoldRequired: '请至少选择一个上模模具',
validatorLowerMoldRequired: '请至少选择一个下模模具',
loadEditFailed: '加载编辑数据失败',
confirmDelete: '确认删除该上下模记录吗?'
},
mine: { mine: {
clickLogin: '点击登录', clickLogin: '点击登录',
username: '用户名:{name}', username: '用户名:{name}',
@ -466,6 +502,42 @@ const messages = {
confirmApprove: 'Approve stock-in {no}?', confirmApprove: 'Approve stock-in {no}?',
approveSuccess: 'Approved successfully' approveSuccess: 'Approved successfully'
}, },
moldOperate: {
moduleName: 'Mold Operate',
subTitle: 'Mold mounting and dismounting records',
detailTitle: 'Mold Operate Detail',
basicInfo: 'Basic Info',
tabUp: 'Mount',
tabDown: 'Dismount',
operateType: 'Operation Type',
mold: 'Mold',
lowerMold: 'Dismount Mold',
selectMold: 'Select Mold',
noSelectedMold: 'No selected molds',
allMold: 'All Molds',
searchRemark: 'Enter remark keyword',
searchCode: 'Enter mold code',
searchName: 'Enter mold name',
moldCode: 'Mold Code',
moldName: 'Mold Name',
device: 'Device',
deviceName: 'Device Name',
creatorName: 'Creator',
remark: 'Remark',
createTime: 'Created At',
createTitle: 'Create Mold Operate',
editTitle: 'Edit Mold Operate',
empty: 'No mold operate data',
noMoldData: 'No mold options',
placeholderDevice: 'Select device',
placeholderRemark: 'Enter remark',
validatorOperateTypeRequired: 'Operation type is required',
validatorDeviceRequired: 'Device is required',
validatorMoldRequired: 'Select at least one mold for mounting',
validatorLowerMoldRequired: 'Select at least one mold for dismounting',
loadEditFailed: 'Failed to load edit data',
confirmDelete: 'Delete this mold operate record?'
},
mine: { mine: {
clickLogin: 'Tap to sign in', clickLogin: 'Tap to sign in',
username: 'Username: {name}', username: 'Username: {name}',
@ -613,7 +685,9 @@ const literalMap = {
'模具出库': 'moldGet.moduleName', '模具出库': 'moldGet.moduleName',
'模具出库详情': 'moldGet.detailTitle', '模具出库详情': 'moldGet.detailTitle',
'模具入库': 'moldReturn.moduleName', '模具入库': 'moldReturn.moduleName',
'模具入库详情': 'moldReturn.detailTitle' '模具入库详情': 'moldReturn.detailTitle',
'上下模': 'moldOperate.moduleName',
'上下模详情': 'moldOperate.detailTitle'
} }
function applyTabBarLanguage() { function applyTabBarLanguage() {

@ -476,6 +476,20 @@
"navigationBarTitleText": "模具入库详情", "navigationBarTitleText": "模具入库详情",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
},
{
"path": "moldoperate/index",
"style": {
"navigationBarTitleText": "上下模",
"navigationStyle": "custom"
}
},
{
"path": "moldoperate/detail",
"style": {
"navigationBarTitleText": "上下模详情",
"navigationStyle": "custom"
}
} }
] ]
} }

@ -245,11 +245,11 @@
</view> </view>
<text class="function-name">{{ t('moldReturn.moduleName') }}</text> <text class="function-name">{{ t('moldReturn.moduleName') }}</text>
</view> </view>
<view class="function-item" @click="handleClick('上下模')"> <view class="function-item" @click="handleClick('moldOperate')">
<view class="function-icon" style="background: rgba(156, 39, 176, 0.1);"> <view class="function-icon" style="background: rgba(156, 39, 176, 0.1);">
<text class="icon-inner">🔄</text> <text class="icon-inner">🔄</text>
</view> </view>
<text class="function-name">上下模</text> <text class="function-name">{{ t('moldOperate.moduleName') }}</text>
</view> </view>
</view> </view>
</view> </view>
@ -378,7 +378,7 @@ function handleClick(name) {
'模具台账': '/pages_function/pages/moldLedger/index', '模具台账': '/pages_function/pages/moldLedger/index',
moldGet: '/pages_function/pages/moldget/index', moldGet: '/pages_function/pages/moldget/index',
moldReturn: '/pages_function/pages/moldreturn/index', moldReturn: '/pages_function/pages/moldreturn/index',
'上下模': '', moldOperate: '/pages_function/pages/moldoperate/index',
'点检项库': '', '点检项库': '',
'点检模板': '', '点检模板': '',
'点检任务': '', '点检任务': '',

@ -0,0 +1,132 @@
<template>
<view class="page-container">
<view class="fixed-header">
<AppTitleHeader :title="t('moldOperate.detailTitle')" />
</view>
<view class="content-section">
<!-- 基础信息 -->
<view class="info-card">
<view class="card-title">{{ t('moldOperate.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('moldOperate.operateType') }}</text>
<text class="info-value">{{ typeLabel(detail.operateType) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldOperate.deviceName') }}</text>
<text class="info-value">{{ textValue(detail.deviceName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldOperate.creatorName') }}</text>
<text class="info-value">{{ textValue(detail.creatorName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldOperate.createTime') }}</text>
<text class="info-value">{{ dateTimeLabel(detail.createTime) }}</text>
</view>
<view class="info-row remark-row">
<text class="info-label">{{ t('moldOperate.remark') }}</text>
<text class="info-value remark-value">{{ textValue(detail.remark) }}</text>
</view>
</view>
</view>
<!-- 模具明细 -->
<view class="tabs-card">
<u-tabs :list="tabList" :current="currentTab" :is-scroll="false" activeColor="#1a3a5c" @change="onTabChange" />
<view v-if="currentTab === 0" class="item-wrap">
<view v-if="!moldRows.length" class="empty">{{ t('moldOperate.noMoldData') }}</view>
<view v-for="(m, index) in moldRows" :key="index" class="item-card">
<view class="item-head">{{ textValue(m.name) }}</view>
<view class="item-row">
<text class="item-label">{{ t('moldOperate.moldCode') }}</text>
<text class="item-value">{{ textValue(m.code) }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import AppTitleHeader from '@/components/common/AppTitleHeader.vue'
import { getMoldOperateDetail } from '@/api/mes/moldoperate'
const { t } = useI18n()
const detail = ref({})
const currentTab = ref(0)
const tabList = ref([{ name: t('moldOperate.mold') }])
const moldRows = computed(() => {
const names = String(detail.value?.moldName || '')
.split(/[,\n]/)
.map((s) => s.trim())
.filter(Boolean)
if (!names.length) return []
return names.map((name) => ({ name, code: '-' }))
})
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function typeLabel(type) {
return String(type) === '2' ? t('moldOperate.tabDown') : t('moldOperate.tabUp')
}
function dateTimeLabel(v) {
if (!v) return '-'
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
function onTabChange(e) {
const idx = e && typeof e === 'object' ? e.index : e
currentTab.value = Number(idx || 0)
}
async function loadDetail(id) {
const res = await getMoldOperateDetail(id)
detail.value = res?.data || {}
}
onLoad(async (query) => {
const id = query?.id
if (!id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
try {
await loadDetail(id)
} catch (e) {
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
}
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f0f2f5; }
.fixed-header { position: sticky; top: 0; z-index: 20; }
.content-section { padding: 0 24rpx 24rpx; }
.info-card,.tabs-card { margin-top: 20rpx; background: #fff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); }
.tabs-card { margin-bottom: 16rpx; }
.card-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; margin-bottom: 18rpx; }
.info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 18rpx 0; border-bottom: 1rpx solid #edf0f3; }
.info-label { font-size: 27rpx; color: #8a9099; width: 220rpx; }
.info-value { flex: 1; text-align: right; font-size: 28rpx; color: #303133; line-height: 1.45; }
.remark-row { border-bottom: none; }
.remark-value { white-space: pre-wrap; }
.item-wrap { margin-top: 18rpx; }
.item-card { background: #f8fafc; border-radius: 12rpx; padding: 16rpx; margin-bottom: 12rpx; }
.item-head { font-size: 26rpx; color: #1a3a5c; font-weight: 700; }
.item-row { margin-top: 10rpx; display: flex; justify-content: space-between; }
.item-label { color: #909399; font-size: 22rpx; }
.item-value { color: #303133; font-size: 22rpx; max-width: 65%; text-align: right; }
.empty { text-align: center; color: #909399; padding: 24rpx 0; }
</style>

@ -0,0 +1,544 @@
<template>
<view class="page-container">
<AppTitleHeader :title="t('moldOperate.moduleName')" :subTitle="t('moldOperate.subTitle')" :showSubTitle="true" />
<!-- 操作类型切换 -->
<view class="operate-tabs">
<view class="operate-tab" :class="{ active: query.operateType === '1' }" @click="switchOperateType('1')">{{ t('moldOperate.tabUp') }}</view>
<view class="operate-tab" :class="{ active: query.operateType === '2' }" @click="switchOperateType('2')">{{ t('moldOperate.tabDown') }}</view>
</view>
<!-- 列表查询区 -->
<view class="search-card">
<view class="search-row">
<view class="search-input-wrap">
<text class="iconfont icon-search search-icon"></text>
<input v-model="query.remark" class="search-input" :placeholder="t('moldOperate.searchRemark')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</view>
<view class="filter-row">
<picker mode="selector" :range="moldFilterOptions" range-key="name" :value="moldIndex" @change="onMoldFilterChange">
<view class="filter-item">{{ moldFilterLabel }}</view>
</picker>
</view>
</view>
<!-- 列表区 -->
<scroll-view scroll-y class="list-scroll" :scroll-top="scrollTop" @scroll="onScroll" @scrolltolower="loadMore" :lower-threshold="80">
<view class="list-wrap">
<view v-for="item in list" :key="item.id" class="card" @click="openDetail(item)">
<view class="card-head">
<view class="card-title">{{ textValue(item.moldName) }}</view>
<view class="type-chip">{{ typeLabel(item.operateType) }}</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('moldOperate.deviceName') }}</text>
<text class="value">{{ textValue(item.deviceName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldOperate.creatorName') }}</text>
<text class="value">{{ textValue(item.creatorName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldOperate.createTime') }}</text>
<text class="value">{{ dateTimeLabel(item.createTime) }}</text>
</view>
</view>
<view class="card-actions">
<view class="action-btn edit-btn" @click.stop="openEdit(item)">
<uni-icons type="compose" size="18" color="#096dd9"></uni-icons>
</view>
<view class="action-btn delete-btn" @click.stop="removeItem(item)">
<uni-icons type="trash" size="18" color="#cf1322"></uni-icons>
</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="hint">{{ t('moldOperate.empty') }}</view>
<view v-else-if="loadingMore" class="hint">{{ t('functionCommon.loadingMore') }}</view>
<view v-else-if="finished" class="hint">{{ t('functionCommon.noMoreData') }}</view>
</view>
</scroll-view>
<!-- 悬浮新增按钮 -->
<view class="fab-btn" @click="openCreate">
<uni-icons type="plusempty" size="30" color="#fff"></uni-icons>
</view>
<!-- 返回顶部按钮 -->
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<uni-icons type="arrow-up" size="20" color="#1a3a5c"></uni-icons>
</view>
<!-- 新增/编辑弹框 -->
<uni-popup ref="formPopupRef" type="bottom">
<view class="form-popup">
<view class="popup-header">
<text class="popup-title">{{ formMode === 'create' ? t('moldOperate.createTitle') : t('moldOperate.editTitle') }}</text>
<text class="popup-close" @click="closeForm">×</text>
</view>
<scroll-view scroll-y class="popup-scroll">
<view class="form-item">
<text class="form-label"><text class="required">*</text>{{ t('moldOperate.operateType') }}</text>
<view class="radio-row">
<view class="radio-item" :class="{ active: String(formData.operateType) === '1' }" @click="changeFormOperateType('1')">{{ t('moldOperate.tabUp') }}</view>
<view class="radio-item" :class="{ active: String(formData.operateType) === '2' }" @click="changeFormOperateType('2')">{{ t('moldOperate.tabDown') }}</view>
</view>
</view>
<view class="form-item">
<text class="form-label"><text class="required">*</text>{{ t('moldOperate.device') }}</text>
<picker mode="selector" :range="deviceOptions" range-key="label" :value="formDeviceIndex" @change="onFormDeviceChange">
<view class="picker-view">{{ formDeviceLabel || t('moldOperate.placeholderDevice') }}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label"><text class="required">*</text>{{ String(formData.operateType) === '2' ? t('moldOperate.lowerMold') : t('moldOperate.mold') }}</text>
<view class="sub-action block" @click="openMoldPicker">{{ t('moldOperate.selectMold') }}</view>
</view>
<view v-if="!selectedMolds.length" class="item-empty">{{ t('moldOperate.noSelectedMold') }}</view>
<view v-for="(mold, index) in selectedMolds" :key="mold.id" class="selected-item">
<view>
<view class="selected-name">{{ textValue(mold.name || mold.moldName) }}</view>
<view class="selected-code">{{ textValue(mold.code || mold.moldCode) }}</view>
</view>
<text class="item-remove" @click="removeSelectedMold(index)">×</text>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldOperate.remark') }}</text>
<textarea v-model="formData.remark" class="form-textarea" :placeholder="t('moldOperate.placeholderRemark')" maxlength="200" />
</view>
</scroll-view>
<view class="popup-footer">
<view class="footer-btn cancel" @click="closeForm">{{ t('functionCommon.cancel') }}</view>
<view class="footer-btn confirm" @click="submitForm">{{ t('functionCommon.save') }}</view>
</view>
</view>
</uni-popup>
<!-- 模具选择弹框 -->
<uni-popup ref="moldPickerRef" type="bottom">
<view class="picker-popup">
<view class="picker-header">
<text class="picker-title">{{ t('moldOperate.selectMold') }}</text>
<text class="picker-close" @click="closeMoldPicker">×</text>
</view>
<scroll-view scroll-y class="picker-scroll">
<view v-for="m in pickerSourceList" :key="m.id" class="picker-item" @click="toggleMold(m)">
<view>
<view class="picker-item-name">{{ textValue(m.name || m.moldName) }}</view>
<view class="picker-item-code">{{ textValue(m.code || m.moldCode) }}</view>
</view>
<text class="picker-check">{{ selectedMoldIdSet.has(String(m.id)) ? '✓' : '' }}</text>
</view>
<view v-if="pickerLoading" class="picker-hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!pickerSourceList.length" class="picker-hint">{{ t('moldOperate.noMoldData') }}</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import AppTitleHeader from '@/components/common/AppTitleHeader.vue'
import { getMoldList, getInTransitMoldAllList } from '@/api/mes/mold'
import { getMoldOperatePage, getMoldOperateDetail, createMoldOperate, updateMoldOperate, deleteMoldOperate, getLowerMoldList, getDeviceLedgerList } from '@/api/mes/moldoperate'
const { t } = useI18n()
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const finished = ref(false)
const pageNo = ref(1)
const pageSize = ref(10)
const scrollTop = ref(0)
const showGoTop = ref(false)
const query = reactive({
operateType: '1',
moldId: undefined,
remark: ''
})
const moldFilterOptions = ref([{ id: undefined, name: '' }])
const moldIndex = ref(0)
const moldFilterLabel = computed(() => moldFilterOptions.value[moldIndex.value]?.name || t('moldOperate.allMold'))
const formPopupRef = ref(null)
const moldPickerRef = ref(null)
const formMode = ref('create')
const formData = reactive({
id: undefined,
operateType: '1',
deviceId: undefined,
moldId: '',
lowerMoldId: '',
remark: ''
})
const selectedMolds = ref([])
const selectedMoldIdSet = computed(() => new Set(selectedMolds.value.map((m) => String(m.id))))
const deviceOptions = ref([])
const formDeviceIndex = computed(() => {
const idx = deviceOptions.value.findIndex((d) => Number(d.value) === Number(formData.deviceId))
return idx < 0 ? 0 : idx
})
const formDeviceLabel = computed(() => deviceOptions.value.find((d) => Number(d.value) === Number(formData.deviceId))?.label || '')
const upperMoldOptions = ref([])
const lowerMoldOptions = ref([])
const pickerLoading = ref(false)
const pickerSourceList = computed(() => (String(formData.operateType) === '2' ? lowerMoldOptions.value : upperMoldOptions.value))
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function dateTimeLabel(v) {
if (!v) return '-'
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
function normalizePageData(res) {
const root = res && res.data !== undefined ? res.data : res
const l = root?.list || root?.rows || root?.records || root?.data?.list || []
const total = root?.total ?? root?.data?.total ?? (Array.isArray(l) ? l.length : 0)
return { list: Array.isArray(l) ? l : [], total: Number(total || 0) }
}
function typeLabel(type) {
return String(type) === '2' ? t('moldOperate.tabDown') : t('moldOperate.tabUp')
}
async function fetchList(reset = true) {
if (reset) {
pageNo.value = 1
finished.value = false
}
if (pageNo.value === 1) loading.value = true
else loadingMore.value = true
try {
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
operateType: query.operateType,
moldId: query.moldId,
remark: query.remark || undefined
}
const res = await getMoldOperatePage(params)
const page = normalizePageData(res)
if (reset) list.value = page.list
else list.value = [...list.value, ...page.list]
finished.value = list.value.length >= page.total || page.list.length < pageSize.value
} catch (e) {
if (!reset) pageNo.value = Math.max(1, pageNo.value - 1)
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
loading.value = false
loadingMore.value = false
}
}
async function loadMoldFilter() {
const res = await getMoldList({})
const root = res && res.data !== undefined ? res.data : res
const data = Array.isArray(root) ? root : Array.isArray(root?.list) ? root.list : []
moldFilterOptions.value = [{ id: undefined, name: t('moldOperate.allMold') }, ...data]
}
async function loadDevices() {
const res = await getDeviceLedgerList({})
const root = res && res.data !== undefined ? res.data : res
const data = Array.isArray(root) ? root : Array.isArray(root?.list) ? root.list : []
deviceOptions.value = data.map((d) => ({
value: d.id,
label: `${d.deviceCode || ''} ${d.deviceName || ''}`.trim()
}))
}
async function loadUpperMoldOptions() {
const res = await getInTransitMoldAllList()
const root = res && res.data !== undefined ? res.data : res
upperMoldOptions.value = Array.isArray(root) ? root : []
}
async function loadLowerMoldOptions() {
if (!formData.deviceId) {
lowerMoldOptions.value = []
return
}
const res = await getLowerMoldList(formData.deviceId)
const root = res && res.data !== undefined ? res.data : res
lowerMoldOptions.value = Array.isArray(root) ? root : []
}
function switchOperateType(type) {
query.operateType = type
fetchList(true)
}
function handleSearch() { fetchList(true) }
function onMoldFilterChange(e) {
const idx = Number(e?.detail?.value || 0)
moldIndex.value = idx
const option = moldFilterOptions.value[idx]
query.moldId = option ? option.id : undefined
fetchList(true)
}
function onScroll(e) {
const top = Number(e?.detail?.scrollTop || 0)
showGoTop.value = top > 600
}
function goTop() { scrollTop.value = 0 }
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
function resetFormData() {
formData.id = undefined
formData.operateType = query.operateType || '1'
formData.deviceId = undefined
formData.moldId = ''
formData.lowerMoldId = ''
formData.remark = ''
selectedMolds.value = []
}
function openCreate() {
formMode.value = 'create'
resetFormData()
formPopupRef.value?.open()
}
async function openEdit(row) {
if (!row?.id) {
uni.showToast({ title: t('functionCommon.noIdEdit'), icon: 'none' })
return
}
formMode.value = 'update'
resetFormData()
try {
const res = await getMoldOperateDetail(row.id)
const detail = res?.data || {}
formData.id = detail.id
formData.operateType = String(detail.operateType || '1')
formData.deviceId = detail.deviceId
formData.remark = detail.remark || ''
await refreshPickerByType()
const selectedIds = String(formData.operateType) === '2' ? parseIdList(detail.lowerMoldId) : parseIdList(detail.moldId)
selectedMolds.value = pickerSourceList.value.filter((m) => selectedIds.includes(Number(m.id)))
formPopupRef.value?.open()
} catch (e) {
uni.showToast({ title: t('moldOperate.loadEditFailed'), icon: 'none' })
}
}
function parseIdList(raw) {
if (Array.isArray(raw)) return raw.map((x) => Number(x)).filter((x) => !Number.isNaN(x))
return String(raw || '').split(',').map((x) => Number(x)).filter((x) => !Number.isNaN(x))
}
function closeForm() { formPopupRef.value?.close() }
async function changeFormOperateType(type) {
formData.operateType = type
selectedMolds.value = []
await refreshPickerByType()
}
async function refreshPickerByType() {
pickerLoading.value = true
try {
if (String(formData.operateType) === '2') {
await loadLowerMoldOptions()
} else {
await loadUpperMoldOptions()
}
} finally {
pickerLoading.value = false
}
}
async function onFormDeviceChange(e) {
const idx = Number(e?.detail?.value || 0)
const option = deviceOptions.value[idx]
formData.deviceId = option ? option.value : undefined
if (String(formData.operateType) === '2') {
selectedMolds.value = []
await refreshPickerByType()
}
}
async function openMoldPicker() {
if (!formData.deviceId) {
uni.showToast({ title: t('moldOperate.validatorDeviceRequired'), icon: 'none' })
return
}
await refreshPickerByType()
moldPickerRef.value?.open()
}
function closeMoldPicker() { moldPickerRef.value?.close() }
function toggleMold(mold) {
const id = String(mold.id)
const idx = selectedMolds.value.findIndex((i) => String(i.id) === id)
if (idx >= 0) {
selectedMolds.value.splice(idx, 1)
return
}
selectedMolds.value.push({
id: mold.id,
name: mold.name || mold.moldName,
code: mold.code || mold.moldCode
})
}
function removeSelectedMold(index) { selectedMolds.value.splice(index, 1) }
function validForm() {
if (!formData.operateType) {
uni.showToast({ title: t('moldOperate.validatorOperateTypeRequired'), icon: 'none' })
return false
}
if (!formData.deviceId) {
uni.showToast({ title: t('moldOperate.validatorDeviceRequired'), icon: 'none' })
return false
}
if (!selectedMolds.value.length) {
uni.showToast({ title: String(formData.operateType) === '2' ? t('moldOperate.validatorLowerMoldRequired') : t('moldOperate.validatorMoldRequired'), icon: 'none' })
return false
}
return true
}
async function submitForm() {
if (!validForm()) return
const selectedIds = selectedMolds.value.map((m) => m.id)
const payload = {
id: formData.id,
operateType: Number(formData.operateType),
deviceId: formData.deviceId,
remark: formData.remark || undefined
}
if (String(formData.operateType) === '2') {
const allIds = lowerMoldOptions.value.map((m) => m.id)
const selectedSet = new Set(selectedIds.map((id) => String(id)))
payload.lowerMoldId = selectedIds.join(',')
payload.moldId = allIds.filter((id) => !selectedSet.has(String(id))).join(',')
} else {
payload.moldId = selectedIds.join(',')
}
try {
if (formMode.value === 'create') {
await createMoldOperate(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
} else {
await updateMoldOperate(payload)
uni.showToast({ title: t('functionCommon.updateSuccess'), icon: 'success' })
}
closeForm()
fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
function openDetail(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
uni.navigateTo({
url: `/pages_function/pages/moldoperate/detail?id=${encodeURIComponent(String(item.id))}`
})
}
function removeItem(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdDelete'), icon: 'none' })
return
}
uni.showModal({
title: t('functionCommon.confirmDelete'),
content: t('moldOperate.confirmDelete'),
success: async ({ confirm }) => {
if (!confirm) return
try {
await deleteMoldOperate(item.id)
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
onShow(async () => {
await Promise.allSettled([loadMoldFilter(), loadDevices(), loadUpperMoldOptions()])
await fetchList(true)
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f0f2f5; }
.operate-tabs { margin: 14rpx 24rpx 0; display: flex; background: #fff; border-radius: 12rpx; padding: 8rpx; }
.operate-tab { flex: 1; text-align: center; padding: 14rpx 0; border-radius: 8rpx; color: #606266; }
.operate-tab.active { background: #1a3a5c; color: #fff; font-weight: 600; }
.search-card { margin: 14rpx 24rpx 18rpx; padding: 20rpx; border-radius: 16rpx; background: #fff; }
.search-row { display: flex; gap: 16rpx; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 40rpx; padding: 0 20rpx; }
.search-icon { color: #909399; }
.search-input { flex: 1; height: 72rpx; margin-left: 12rpx; }
.search-btn { min-width: 120rpx; height: 72rpx; border-radius: 36rpx; background: #1a3a5c; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 26rpx; }
.filter-row { margin-top: 16rpx; display: flex; gap: 14rpx; }
.filter-item { min-width: 220rpx; padding: 12rpx 18rpx; border-radius: 10rpx; background: #f5f7fa; color: #303133; font-size: 24rpx; }
.list-scroll { height: calc(100vh - 420rpx); }
.list-wrap { padding: 0 24rpx 130rpx; }
.card { background: #fff; border-radius: 16rpx; padding: 20rpx; margin-bottom: 16rpx; }
.card-head { display: flex; justify-content: space-between; gap: 10rpx; align-items: center; }
.card-title { font-size: 30rpx; font-weight: 700; color: #1a3a5c; flex: 1; }
.type-chip { padding: 6rpx 16rpx; border-radius: 20rpx; font-size: 22rpx; color: #096dd9; background: rgba(24, 144, 255, 0.12); }
.card-body { margin-top: 14rpx; }
.row { display: flex; justify-content: space-between; margin-bottom: 10rpx; }
.label { color: #909399; font-size: 24rpx; }
.value { color: #303133; font-size: 24rpx; max-width: 66%; text-align: right; }
.card-actions { margin-top: 8rpx; display: flex; justify-content: flex-end; gap: 10rpx; }
.action-btn { width: 56rpx; height: 56rpx; border-radius: 10rpx; display: flex; align-items: center; justify-content: center; }
.edit-btn { background: rgba(24, 144, 255, 0.12); }
.delete-btn { background: rgba(245, 34, 45, 0.12); }
.hint { text-align: center; color: #909399; padding: 24rpx 0; }
.fab-btn { position: fixed; right: 34rpx; bottom: 120rpx; width: 96rpx; height: 96rpx; border-radius: 50%; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); display: flex; align-items: center; justify-content: center; box-shadow: 0 10rpx 30rpx rgba(26, 58, 92, 0.3); z-index: 20; }
.go-top-btn { position: fixed; right: 34rpx; bottom: 240rpx; width: 80rpx; height: 80rpx; border-radius: 50%; background: #fff; box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12); display: flex; align-items: center; justify-content: center; }
.form-popup { background: #fff; border-radius: 22rpx 22rpx 0 0; max-height: 88vh; }
.popup-header { height: 96rpx; display: flex; align-items: center; justify-content: center; position: relative; border-bottom: 1rpx solid #f0f0f0; }
.popup-title { font-size: 30rpx; font-weight: 700; }
.popup-close { position: absolute; right: 28rpx; top: 20rpx; font-size: 44rpx; color: #909399; }
.popup-scroll { max-height: calc(88vh - 180rpx); padding: 20rpx 26rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { font-size: 24rpx; color: #606266; margin-bottom: 8rpx; display: block; }
.required { color: #f56c6c; margin-right: 6rpx; }
.radio-row { display: flex; gap: 12rpx; }
.radio-item { flex: 1; text-align: center; padding: 18rpx 0; border-radius: 10rpx; background: #f5f7fa; color: #606266; }
.radio-item.active { background: rgba(26, 58, 92, 0.12); color: #1a3a5c; font-weight: 600; }
.picker-view { min-height: 72rpx; border-radius: 12rpx; background: #f5f7fa; padding: 0 18rpx; display: flex; align-items: center; }
.sub-action { color: #1a3a5c; font-size: 24rpx; }
.sub-action.block { background: #f5f7fa; border-radius: 10rpx; padding: 18rpx; text-align: center; }
.item-empty { padding: 20rpx; color: #909399; text-align: center; }
.selected-item { margin-top: 10rpx; border-radius: 12rpx; background: #f8fafc; padding: 14rpx; display: flex; justify-content: space-between; align-items: center; }
.selected-name { color: #303133; font-size: 24rpx; }
.selected-code { color: #909399; font-size: 22rpx; margin-top: 4rpx; }
.item-remove { color: #f56c6c; font-size: 34rpx; }
.form-textarea { width: 100%; min-height: 120rpx; border-radius: 12rpx; background: #f5f7fa; padding: 14rpx 18rpx; }
.popup-footer { height: 88rpx; display: flex; }
.footer-btn { flex: 1; display: flex; align-items: center; justify-content: center; font-size: 28rpx; }
.cancel { background: #f5f7fa; color: #606266; }
.confirm { background: #1a3a5c; color: #fff; }
.picker-popup { background: #fff; border-radius: 22rpx 22rpx 0 0; height: 70vh; }
.picker-header { height: 90rpx; display: flex; justify-content: center; align-items: center; position: relative; }
.picker-title { font-size: 28rpx; font-weight: 700; }
.picker-close { position: absolute; right: 24rpx; top: 18rpx; font-size: 42rpx; }
.picker-scroll { height: calc(70vh - 90rpx); }
.picker-item { margin: 0 24rpx 12rpx; padding: 16rpx; border-radius: 12rpx; background: #f8fafc; display: flex; justify-content: space-between; align-items: center; }
.picker-item-name { color: #303133; font-size: 26rpx; }
.picker-item-code { color: #909399; font-size: 22rpx; margin-top: 6rpx; }
.picker-check { color: #1a3a5c; font-size: 34rpx; }
.picker-hint { text-align: center; color: #909399; padding: 16rpx 0 22rpx; }
</style>
Loading…
Cancel
Save