feat:添加模具入库模块

master
黄伟杰 1 month ago
parent e3dec8568e
commit f45f518c2a

@ -0,0 +1,49 @@
import request from '@/utils/request'
export function getMoldReturnPage(params = {}) {
return request({
url: '/admin-api/erp/stock-in/page',
method: 'get',
params
})
}
export function getMoldReturnDetail(id) {
return request({
url: '/admin-api/erp/stock-in/get',
method: 'get',
params: { id }
})
}
export function createMoldReturn(data) {
return request({
url: '/admin-api/erp/stock-in/create',
method: 'post',
data
})
}
export function updateMoldReturn(data) {
return request({
url: '/admin-api/erp/stock-in/update',
method: 'put',
data
})
}
export function updateMoldReturnStatus(id, status) {
return request({
url: '/admin-api/erp/stock-in/update-status',
method: 'put',
params: { id, status }
})
}
export function deleteMoldReturn(ids = []) {
return request({
url: '/admin-api/erp/stock-in/delete',
method: 'delete',
params: { ids: ids.join(',') }
})
}

@ -144,6 +144,53 @@ const messages = {
confirmApprove: '确认审批出库单 {no} 吗?',
approveSuccess: '审批成功'
},
moldReturn: {
moduleName: '模具入库',
subTitle: '按入库单号与状态快速筛选',
detailTitle: '模具入库详情',
basicInfo: '基础信息',
inNo: '入库单号',
inType: '入库类型',
inTime: '入库时间',
inTimeSingle: '入库日期',
inTimePlaceholder: '请选择入库日期',
warehouse: '仓库',
allWarehouse: '全部仓库',
warehousePlaceholder: '请选择仓库',
creator: '创建人',
status: '状态',
allStatus: '全部状态',
remark: '备注',
itemRemark: '明细备注',
attachment: '附件',
fileUrlPlaceholder: '请输入附件地址',
remarkPlaceholder: '请输入备注',
moldName: '模具',
moldCode: '模具编码',
moldStatus: '模具状态',
moldUseTime: '使用次数',
searchNo: '请输入入库单号',
searchCode: '请输入模具编码',
searchName: '请输入模具名称',
itemListTitle: '入库明细',
selectMold: '选择模具',
noItems: '暂无入库明细',
count: '数量',
noAuto: '系统自动生成',
createTitle: '新增模具入库',
editTitle: '编辑模具入库',
approve: '审批',
empty: '暂无模具入库数据',
noMoldData: '暂无可选模具',
loadEditFailed: '加载编辑数据失败',
validatorInTimeRequired: '入库日期不能为空',
validatorWarehouseRequired: '仓库不能为空',
validatorItemRequired: '请至少选择一个模具',
validatorCountRequired: '数量必须大于0',
confirmDelete: '确认删除入库单 {no} 吗?',
confirmApprove: '确认审批入库单 {no} 吗?',
approveSuccess: '审批成功'
},
mine: {
clickLogin: '点击登录',
username: '用户名:{name}',
@ -372,6 +419,53 @@ const messages = {
confirmApprove: 'Approve stock-out {no}?',
approveSuccess: 'Approved successfully'
},
moldReturn: {
moduleName: 'Mold Stock-in',
subTitle: 'Filter quickly by no and status',
detailTitle: 'Mold Stock-in Detail',
basicInfo: 'Basic Info',
inNo: 'Stock-in No',
inType: 'Stock-in Type',
inTime: 'Stock-in Time',
inTimeSingle: 'Stock-in Date',
inTimePlaceholder: 'Select stock-in date',
warehouse: 'Warehouse',
allWarehouse: 'All Warehouses',
warehousePlaceholder: 'Select warehouse',
creator: 'Creator',
status: 'Status',
allStatus: 'All Status',
remark: 'Remark',
itemRemark: 'Item Remark',
attachment: 'Attachment',
fileUrlPlaceholder: 'Enter attachment URL',
remarkPlaceholder: 'Enter remark',
moldName: 'Mold',
moldCode: 'Mold Code',
moldStatus: 'Mold Status',
moldUseTime: 'Use Time',
searchNo: 'Enter stock-in no',
searchCode: 'Enter mold code',
searchName: 'Enter mold name',
itemListTitle: 'Item List',
selectMold: 'Select Mold',
noItems: 'No items',
count: 'Count',
noAuto: 'Generated automatically',
createTitle: 'Create Mold Stock-in',
editTitle: 'Edit Mold Stock-in',
approve: 'Approve',
empty: 'No mold stock-in data',
noMoldData: 'No mold options',
loadEditFailed: 'Failed to load edit data',
validatorInTimeRequired: 'Stock-in date is required',
validatorWarehouseRequired: 'Warehouse is required',
validatorItemRequired: 'Select at least one mold',
validatorCountRequired: 'Count must be greater than 0',
confirmDelete: 'Delete stock-in {no}?',
confirmApprove: 'Approve stock-in {no}?',
approveSuccess: 'Approved successfully'
},
mine: {
clickLogin: 'Tap to sign in',
username: 'Username: {name}',
@ -517,7 +611,9 @@ const literalMap = {
'否': 'functionCommon.no',
'暂无待办任务': 'dashboard.noTodo',
'模具出库': 'moldGet.moduleName',
'模具出库详情': 'moldGet.detailTitle'
'模具出库详情': 'moldGet.detailTitle',
'模具入库': 'moldReturn.moduleName',
'模具入库详情': 'moldReturn.detailTitle'
}
function applyTabBarLanguage() {

@ -462,6 +462,20 @@
"navigationBarTitleText": "模具出库详情",
"navigationStyle": "custom"
}
},
{
"path": "moldreturn/index",
"style": {
"navigationBarTitleText": "模具入库",
"navigationStyle": "custom"
}
},
{
"path": "moldreturn/detail",
"style": {
"navigationBarTitleText": "模具入库详情",
"navigationStyle": "custom"
}
}
]
}

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

@ -0,0 +1,145 @@
<template>
<view class="page-container">
<view class="fixed-header">
<AppTitleHeader :title="t('moldReturn.detailTitle')" />
</view>
<view class="content-section">
<!-- 基础信息卡片 -->
<view class="info-card">
<view class="card-title">{{ t('moldReturn.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('moldReturn.inNo') }}</text>
<text class="info-value">{{ textValue(detail.no) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldReturn.inType') }}</text>
<text class="info-value">{{ textValue(detail.inType) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldReturn.inTime') }}</text>
<text class="info-value">{{ dateTimeLabel(detail.inTime || detail.outTime) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldReturn.warehouse') }}</text>
<text class="info-value">{{ textValue(detail.warehouseName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldReturn.creator') }}</text>
<text class="info-value">{{ textValue(detail.creatorName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldReturn.status') }}</text>
<text class="info-value">{{ statusLabel(detail.status) }}</text>
</view>
<view class="info-row remark-row">
<text class="info-label">{{ t('moldReturn.remark') }}</text>
<text class="info-value remark-value">{{ textValue(detail.remark) }}</text>
</view>
</view>
</view>
<!-- 明细 tabs 卡片 -->
<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="!items.length" class="empty">{{ t('moldReturn.noItems') }}</view>
<view v-for="(item, index) in items" :key="index" class="item-card">
<view class="item-head">{{ textValue(item.productName) }}</view>
<view class="item-row">
<text class="item-label">{{ t('moldReturn.moldCode') }}</text>
<text class="item-value">{{ textValue(item.productCode || item.productBarCode) }}</text>
</view>
<view class="item-row">
<text class="item-label">{{ t('moldReturn.count') }}</text>
<text class="item-value">{{ textValue(item.count) }}</text>
</view>
<view class="item-row">
<text class="item-label">{{ t('moldReturn.itemRemark') }}</text>
<text class="item-value">{{ textValue(item.remark) }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import AppTitleHeader from '@/components/common/AppTitleHeader.vue'
import { getMoldReturnDetail } from '@/api/mes/moldreturn'
import { useDict } from '@/utils/dict'
const { t } = useI18n()
const { erp_audit_status } = useDict('erp_audit_status')
const detail = ref({})
const items = ref([])
const currentTab = ref(0)
const tabList = ref([{ name: t('moldReturn.itemListTitle') }])
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 statusLabel(status) {
const hit = (erp_audit_status.value || []).find((s) => String(s.value) === String(status))
return hit ? hit.label : textValue(status)
}
function onTabChange(e) {
const idx = e && typeof e === 'object' ? e.index : e
currentTab.value = Number(idx || 0)
}
async function loadDetail(id) {
const res = await getMoldReturnDetail(id)
const data = res?.data || {}
detail.value = data
items.value = Array.isArray(data.items) ? data.items : []
}
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,627 @@
<template>
<view class="page-container">
<AppTitleHeader :title="t('moldReturn.moduleName')" :subTitle="t('moldReturn.subTitle')" :showSubTitle="true" />
<!-- 列表查询区 -->
<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.no" class="search-input" :placeholder="t('moldReturn.searchNo')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</view>
<view class="filter-row">
<picker mode="selector" :range="warehouseOptions" range-key="name" :value="warehouseIndex" @change="onWarehouseFilterChange">
<view class="filter-item">{{ warehouseFilterLabel }}</view>
</picker>
<picker mode="selector" :range="statusOptions" range-key="label" :value="statusIndex" @change="onStatusFilterChange">
<view class="filter-item">{{ statusFilterLabel }}</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>
<view class="card-title">{{ textValue(item.no) }}</view>
<view class="card-sub">{{ t('moldReturn.moldName') }}: {{ textValue(item.productNames) }}</view>
</view>
<view class="status-chip" :class="statusClass(item.status)">{{ statusLabel(item.status) }}</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('moldReturn.inTime') }}</text>
<text class="value">{{ dateTimeLabel(item.inTime || item.outTime) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldReturn.warehouse') }}</text>
<text class="value">{{ textValue(item.warehouseName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldReturn.creator') }}</text>
<text class="value">{{ textValue(item.creatorName) }}</text>
</view>
</view>
<view class="card-actions">
<view v-if="String(item.status) === '10'" class="action-btn edit-btn" @click.stop="openEdit(item)">
<uni-icons type="compose" size="18" color="#096dd9"></uni-icons>
</view>
<view v-if="String(item.status) === '10'" class="action-btn approve-btn" @click.stop="approve(item)">
<uni-icons type="checkmarkempty" size="18" color="#389e0d"></uni-icons>
</view>
<view v-if="String(item.status) === '10'" 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('moldReturn.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('moldReturn.createTitle') : t('moldReturn.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('moldReturn.inType') }}</text>
<input class="form-input disabled" :value="t('moldReturn.moduleName')" disabled />
</view>
<view class="form-item">
<text class="form-label">{{ t('moldReturn.inNo') }}</text>
<input class="form-input disabled" :value="textValue(formData.no)" :placeholder="t('moldReturn.noAuto')" disabled />
</view>
<view class="form-item">
<text class="form-label"><text class="required">*</text>{{ t('moldReturn.inTimeSingle') }}</text>
<picker mode="date" :value="formInDate" @change="onFormDateChange">
<view class="picker-view">{{ formInDate || t('moldReturn.inTimePlaceholder') }}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label"><text class="required">*</text>{{ t('moldReturn.warehouse') }}</text>
<picker mode="selector" :range="warehouseOptions" range-key="name" :value="formWarehouseIndex" @change="onFormWarehouseChange">
<view class="picker-view">{{ formWarehouseLabel || t('moldReturn.warehousePlaceholder') }}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldReturn.attachment') }}</text>
<input v-model="formData.fileUrl" class="form-input" type="text" :placeholder="t('moldReturn.fileUrlPlaceholder')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('moldReturn.remark') }}</text>
<textarea v-model="formData.remark" class="form-textarea" :placeholder="t('moldReturn.remarkPlaceholder')" maxlength="200" />
</view>
<view class="sub-title-row">
<text class="sub-title"><text class="required">*</text>{{ t('moldReturn.itemListTitle') }}</text>
<view class="sub-action" @click="openMoldPicker">{{ t('moldReturn.selectMold') }}</view>
</view>
<view v-if="!formData.items.length" class="item-empty">{{ t('moldReturn.noItems') }}</view>
<view v-for="(item, index) in formData.items" :key="item.productId" class="item-card">
<view class="item-head">
<text class="item-name">{{ textValue(item.productName) }}</text>
<text class="item-remove" @click="removeFormItem(index)">×</text>
</view>
<view class="item-row">
<text class="item-label">{{ t('moldReturn.moldCode') }}</text>
<input class="item-input disabled" :value="textValue(item.productCode)" disabled />
</view>
<view class="item-row">
<text class="item-label"><text class="required">*</text>{{ t('moldReturn.count') }}</text>
<input v-model="item.count" class="item-input" type="number" />
</view>
<view class="item-row">
<text class="item-label">{{ t('moldReturn.itemRemark') }}</text>
<input v-model="item.remark" class="item-input" type="text" />
</view>
</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('moldReturn.selectMold') }}</text>
<text class="picker-close" @click="closeMoldPicker">×</text>
</view>
<view class="picker-search">
<view class="picker-search-row">
<input v-model="moldQuery.code" class="picker-search-input" :placeholder="t('moldReturn.searchCode')" @confirm="handleMoldSearch" />
</view>
<view class="picker-search-row">
<input v-model="moldQuery.name" class="picker-search-input" :placeholder="t('moldReturn.searchName')" @confirm="handleMoldSearch" />
</view>
<view class="picker-search-actions">
<view class="picker-action-btn" @click="handleMoldSearch">{{ t('functionCommon.search') }}</view>
<view class="picker-action-btn reset" @click="resetMoldSearch">{{ t('functionCommon.cancel') }}</view>
</view>
</view>
<scroll-view scroll-y class="picker-scroll">
<view v-for="m in moldPickerList" :key="m.id" class="picker-item" @click="toggleMold(m)">
<view>
<view class="picker-item-name">{{ textValue(m.name) }}</view>
<view class="picker-item-code">{{ textValue(m.code) }}</view>
<view class="picker-item-meta">
{{ t('moldReturn.moldStatus') }}: {{ moldStatusLabel(m.status) }} · {{ t('moldReturn.moldUseTime') }}: {{ textValue(m.useTime) }}
</view>
</view>
<text class="picker-check">{{ formItemIds.has(Number(m.id)) ? '✓' : '' }}</text>
</view>
<view v-if="moldLoading" class="picker-hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!moldPickerList.length" class="picker-hint">{{ t('moldReturn.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 } from '@/api/mes/mold'
import { getMoldReturnPage, getMoldReturnDetail, createMoldReturn, updateMoldReturn, updateMoldReturnStatus, deleteMoldReturn } from '@/api/mes/moldreturn'
import { getWarehouseSimpleList } from '@/api/mes/moldget'
import { useDict } from '@/utils/dict'
const { t } = useI18n()
const { erp_audit_status, erp_mold_status } = useDict('erp_audit_status', 'erp_mold_status')
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({
no: '',
warehouseId: undefined,
status: undefined
})
const warehouseOptions = ref([])
const warehouseIndex = ref(0)
const statusIndex = ref(0)
const statusOptions = computed(() => [{ label: t('moldReturn.allStatus'), value: undefined }, ...(erp_audit_status.value || [])])
const statusFilterLabel = computed(() => statusOptions.value[statusIndex.value]?.label || t('moldReturn.allStatus'))
const warehouseFilterLabel = computed(() => warehouseOptions.value[warehouseIndex.value]?.name || t('moldReturn.allWarehouse'))
const formPopupRef = ref(null)
const moldPickerRef = ref(null)
const formMode = ref('create')
const formData = reactive({
id: undefined,
no: '',
inType: '模具入库',
inTime: '',
warehouseId: undefined,
fileUrl: '',
remark: '',
items: []
})
const formItemIds = computed(() => new Set((formData.items || []).map((i) => Number(i.productId))))
const formWarehouseLabel = computed(() => warehouseOptions.value.find((w) => Number(w.id) === Number(formData.warehouseId))?.name || '')
const formWarehouseIndex = computed(() => {
const idx = warehouseOptions.value.findIndex((w) => Number(w.id) === Number(formData.warehouseId))
return idx < 0 ? 0 : idx
})
const formInDate = computed(() => formatDateOnly(formData.inTime))
const moldPickerList = ref([])
const moldLoading = ref(false)
const moldQuery = reactive({
code: '',
name: ''
})
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 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 formatDateOnly(v) {
if (!v) return ''
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return ''
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
}
function statusLabel(status) {
const hit = (erp_audit_status.value || []).find((s) => String(s.value) === String(status))
return hit ? hit.label : textValue(status)
}
function statusClass(status) {
if (String(status) === '10') return 'status-draft'
if (String(status) === '20') return 'status-approved'
return 'status-other'
}
function moldStatusLabel(status) {
const hit = (erp_mold_status.value || []).find((s) => String(s.value) === String(status))
return hit ? hit.label : textValue(status)
}
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,
no: query.no.trim() || undefined,
inType: '模具入库',
warehouseId: query.warehouseId,
status: query.status
}
const res = await getMoldReturnPage(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 loadOptions() {
const warehouseRes = await getWarehouseSimpleList()
const warehouseData = warehouseRes?.data || []
warehouseOptions.value = [{ id: undefined, name: t('moldReturn.allWarehouse') }, ...warehouseData]
}
async function loadMolds() {
moldLoading.value = true
try {
const res = await getMoldList({
code: moldQuery.code.trim() || undefined,
name: moldQuery.name.trim() || undefined,
statuss: [3, 0]
})
const root = res && res.data !== undefined ? res.data : res
const data = Array.isArray(root) ? root : Array.isArray(root?.list) ? root.list : []
moldPickerList.value = data
} finally {
moldLoading.value = false
}
}
function handleSearch() { fetchList(true) }
function onWarehouseFilterChange(e) {
const idx = Number(e?.detail?.value || 0)
warehouseIndex.value = idx
const option = warehouseOptions.value[idx]
query.warehouseId = option ? option.id : undefined
fetchList(true)
}
function onStatusFilterChange(e) {
const idx = Number(e?.detail?.value || 0)
statusIndex.value = idx
const option = statusOptions.value[idx]
query.status = option ? option.value : 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.no = ''
formData.inType = '模具入库'
formData.inTime = new Date().setHours(0, 0, 0, 0)
formData.warehouseId = warehouseOptions.value.find((w) => w.defaultStatus)?.id ?? warehouseOptions.value[1]?.id
formData.fileUrl = ''
formData.remark = ''
formData.items = []
}
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 getMoldReturnDetail(row.id)
const detail = res?.data || {}
formData.id = detail.id
formData.no = detail.no
formData.inType = detail.inType || '模具入库'
formData.inTime = detail.inTime || detail.outTime
formData.fileUrl = detail.fileUrl || ''
formData.remark = detail.remark || ''
formData.warehouseId = detail.items?.[0]?.warehouseId ?? detail.warehouseId ?? formData.warehouseId
formData.items = (detail.items || []).map((i) => ({
id: i.id,
productId: i.productId ?? i.id,
productName: i.productName,
productCode: i.productCode ?? i.productBarCode,
count: i.count ?? 1,
productPrice: i.productPrice ?? 0,
remark: i.remark || ''
}))
formPopupRef.value?.open()
} catch (e) {
uni.showToast({ title: t('moldReturn.loadEditFailed'), icon: 'none' })
}
}
function closeForm() { formPopupRef.value?.close() }
function onFormDateChange(e) {
const date = e?.detail?.value
if (!date) return
formData.inTime = new Date(`${date} 00:00:00`).getTime()
}
function onFormWarehouseChange(e) {
const idx = Number(e?.detail?.value || 0)
const option = warehouseOptions.value[idx]
formData.warehouseId = option ? option.id : undefined
}
function openMoldPicker() {
moldQuery.code = ''
moldQuery.name = ''
loadMolds()
moldPickerRef.value?.open()
}
function closeMoldPicker() { moldPickerRef.value?.close() }
function handleMoldSearch() { loadMolds() }
function resetMoldSearch() {
moldQuery.code = ''
moldQuery.name = ''
loadMolds()
}
function toggleMold(mold) {
const id = Number(mold.id)
const idx = formData.items.findIndex((i) => Number(i.productId) === id)
if (idx >= 0) {
formData.items.splice(idx, 1)
return
}
formData.items.push({
productId: id,
productName: mold.name,
productCode: mold.code,
count: 1,
productPrice: 0,
remark: '',
warehouseId: formData.warehouseId
})
}
function removeFormItem(index) { formData.items.splice(index, 1) }
function validForm() {
if (!formData.inTime) {
uni.showToast({ title: t('moldReturn.validatorInTimeRequired'), icon: 'none' })
return false
}
if (!formData.warehouseId) {
uni.showToast({ title: t('moldReturn.validatorWarehouseRequired'), icon: 'none' })
return false
}
if (!formData.items.length) {
uni.showToast({ title: t('moldReturn.validatorItemRequired'), icon: 'none' })
return false
}
const invalidItem = formData.items.some((i) => !i.count || Number(i.count) <= 0)
if (invalidItem) {
uni.showToast({ title: t('moldReturn.validatorCountRequired'), icon: 'none' })
return false
}
return true
}
async function submitForm() {
if (!validForm()) return
const payload = {
id: formData.id,
no: formData.no || undefined,
inType: '模具入库',
inTime: formData.inTime,
warehouseId: formData.warehouseId,
fileUrl: formData.fileUrl || undefined,
remark: formData.remark || undefined,
items: formData.items.map((i) => ({
id: i.id,
warehouseId: formData.warehouseId,
productId: i.productId,
productPrice: Number(i.productPrice || 0),
count: Number(i.count || 1),
remark: i.remark || undefined
}))
}
try {
if (formMode.value === 'create') {
await createMoldReturn(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
} else {
await updateMoldReturn(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/moldreturn/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('moldReturn.confirmDelete', { no: textValue(item.no) }),
success: async ({ confirm }) => {
if (!confirm) return
try {
await deleteMoldReturn([item.id])
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
function approve(item) {
uni.showModal({
title: t('moldReturn.approve'),
content: t('moldReturn.confirmApprove', { no: textValue(item.no) }),
success: async ({ confirm }) => {
if (!confirm) return
await updateMoldReturnStatus(item.id, 20)
uni.showToast({ title: t('moldReturn.approveSuccess'), icon: 'success' })
fetchList(true)
}
})
}
onShow(async () => {
await loadOptions()
await fetchList(true)
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f0f2f5; }
.search-card { margin: 18rpx 24rpx; 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 - 360rpx); }
.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; }
.card-title { font-size: 30rpx; font-weight: 700; color: #1a3a5c; }
.card-sub { margin-top: 8rpx; color: #606266; font-size: 24rpx; }
.status-chip { padding: 6rpx 16rpx; border-radius: 20rpx; font-size: 22rpx; }
.status-draft { background: rgba(250, 173, 20, 0.12); color: #ad6800; }
.status-approved { background: rgba(82, 196, 26, 0.12); color: #389e0d; }
.status-other { background: rgba(24, 144, 255, 0.12); color: #096dd9; }
.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); }
.approve-btn { background: rgba(82, 196, 26, 0.14); }
.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; }
.form-input,.picker-view { min-height: 72rpx; border-radius: 12rpx; background: #f5f7fa; padding: 0 18rpx; display: flex; align-items: center; }
.disabled { color: #909399; }
.form-textarea { width: 100%; min-height: 120rpx; border-radius: 12rpx; background: #f5f7fa; padding: 14rpx 18rpx; }
.sub-title-row { margin-top: 20rpx; display: flex; justify-content: space-between; align-items: center; }
.sub-title { font-size: 26rpx; font-weight: 600; }
.sub-action { color: #1a3a5c; font-size: 24rpx; }
.item-empty { padding: 20rpx; color: #909399; text-align: center; }
.item-card { margin-top: 14rpx; border-radius: 12rpx; background: #f8fafc; padding: 16rpx; }
.item-head { display: flex; justify-content: space-between; }
.item-name { color: #1a3a5c; font-size: 26rpx; font-weight: 600; }
.item-remove { color: #f56c6c; font-size: 34rpx; }
.item-row { margin-top: 10rpx; }
.item-label { color: #909399; font-size: 22rpx; }
.item-input { margin-top: 6rpx; min-height: 64rpx; border-radius: 10rpx; background: #fff; padding: 0 14rpx; }
.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: 72vh; }
.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-search { padding: 0 24rpx 12rpx; }
.picker-search-row { margin-bottom: 10rpx; }
.picker-search-input { height: 68rpx; border-radius: 10rpx; background: #f5f7fa; padding: 0 14rpx; }
.picker-search-actions { margin-top: 6rpx; display: flex; gap: 12rpx; }
.picker-action-btn { min-width: 110rpx; height: 56rpx; border-radius: 10rpx; background: #1a3a5c; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 22rpx; }
.picker-action-btn.reset { background: #eef1f4; color: #606266; }
.picker-scroll { height: calc(72vh - 150rpx); }
.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-item-meta { color: #909399; font-size: 20rpx; margin-top: 4rpx; }
.picker-check { color: #1a3a5c; font-size: 34rpx; }
.picker-hint { text-align: center; color: #909399; padding: 16rpx 0 22rpx; }
</style>
Loading…
Cancel
Save