黄伟杰 3 days ago
commit 0bcd9bb579

@ -993,6 +993,13 @@
"navigationStyle": "custom"
}
},
{
"path": "moldLedger/childMoldForm",
"style": {
"navigationBarTitleText": "新增子模具",
"navigationStyle": "custom"
}
},
{
"path": "moldget/index",
"style": {

@ -0,0 +1,363 @@
<template>
<view class="page-container">
<NavBar title="新增子模具" />
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<view class="section-card">
<view class="section-header">
<view class="section-icon">
<uni-icons type="compose" size="24" color="#1f7cff"></uni-icons>
</view>
<text class="section-title">子模具信息</text>
</view>
<!-- 编码 -->
<view class="form-field">
<text class="form-label">编码</text>
<view class="code-row">
<view class="is-code-switch" @click="form.isCode = !form.isCode">
<text :class="['switch-label', form.isCode ? 'active' : '']">自动生成</text>
<view :class="['switch-track', form.isCode ? 'on' : 'off']">
<view class="switch-thumb"></view>
</view>
</view>
</view>
<input v-if="!form.isCode" v-model="form.code" class="form-input" placeholder="请输入编码" />
</view>
<!-- 名称 -->
<view class="form-field">
<text class="form-label">名称<text class="required-star">*</text></text>
<input v-model="form.name" class="form-input" placeholder="请输入名称" />
</view>
<!-- 类型 -->
<view class="form-field">
<text class="form-label">类型</text>
<view class="select-field" @click="openTypePicker">
<text :class="['select-text', form.type ? '' : 'placeholder']">{{ form.typeLabel || '请选择' }}</text>
<uni-icons type="right" size="18" color="#9ca3af"></uni-icons>
</view>
</view>
<!-- 安装位置 -->
<view class="form-field">
<text class="form-label">安装位置</text>
<input v-model="form.installLocation" class="form-input" placeholder="请输入安装位置" />
</view>
<!-- 材质 -->
<view class="form-field">
<text class="form-label">材质</text>
<input v-model="form.material" class="form-input" placeholder="请输入材质" />
</view>
<!-- 单位 -->
<view class="form-field">
<text class="form-label">单位<text class="required-star">*</text></text>
<view class="select-field" @click="openUnitPicker">
<text :class="['select-text', form.unitId ? '' : 'placeholder']">{{ form.unitLabel || '请选择' }}</text>
<uni-icons type="right" size="18" color="#9ca3af"></uni-icons>
</view>
</view>
<!-- 入库时间 -->
<view class="form-field">
<text class="form-label">入库时间<text class="required-star">*</text></text>
<picker mode="date" :value="form.inTimeDate" @change="handleInTimeChange">
<view class="select-field">
<text :class="['select-text', form.inTimeDate ? '' : 'placeholder']">{{ form.inTimeDate || '请选择' }}</text>
<uni-icons type="calendar" size="18" color="#9ca3af"></uni-icons>
</view>
</picker>
</view>
<!-- 图片 -->
<view class="form-field">
<text class="form-label">图片</text>
<view class="image-upload" @click="handleChooseImage">
<view v-if="form.images" class="image-preview-wrap">
<image :src="form.images" class="image-preview" mode="aspectFill" />
<view class="image-delete" @click.stop="form.images = ''"><uni-icons type="closeempty" size="18" color="#fff"></uni-icons></view>
</view>
<view v-else class="image-placeholder">
<uni-icons type="plusempty" size="28" color="#94a3b8"></uni-icons>
<text>上传图片</text>
</view>
</view>
</view>
<!-- 备注 -->
<view class="form-field">
<text class="form-label">备注</text>
<textarea v-model="form.remark" class="form-textarea" placeholder="请输入备注" placeholder-class="placeholder-text" maxlength="200" />
</view>
<!-- 是否启用 -->
<view class="form-field">
<text class="form-label">是否启用<text class="required-star">*</text></text>
<view class="enable-radio">
<view :class="['radio-option', form.isEnable === true ? 'active' : '']" @click="form.isEnable = true">启用</view>
<view :class="['radio-option', form.isEnable === false ? 'active' : '']" @click="form.isEnable = false">禁用</view>
</view>
</view>
<!-- 资料 -->
<view class="form-field">
<text class="form-label">资料</text>
<view class="file-upload" @click="handleChooseFile">
<view v-if="form.fileUrl" class="file-item">
<text class="file-name">{{ form.fileName || '已选文件' }}</text>
<view class="file-delete" @click.stop="form.fileUrl = ''; form.fileName = ''">
<uni-icons type="closeempty" size="16" color="#ef4444"></uni-icons>
</view>
</view>
<view v-else class="file-placeholder">
<uni-icons type="paperclip" size="20" color="#1f7cff"></uni-icons>
<text>选取文件</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="action-bar">
<view class="action-btn back-btn" @click="handleCancel"></view>
<view class="action-btn submit-btn" @click="handleSubmit"></view>
</view>
<!-- 类型选择弹窗 -->
<uni-popup ref="typePickerRef" type="bottom" background-color="#fff">
<view class="picker-panel">
<view class="picker-header">
<text class="picker-title">选择类型</text>
<text class="picker-clear" @click="form.type = ''; form.typeLabel = ''; typePickerRef.close()">清除</text>
</view>
<scroll-view scroll-y class="picker-list">
<view v-for="item in typeOptions" :key="item.value" class="picker-item" @click="selectType(item)">
<text class="picker-text">{{ item.label }}</text>
<text v-if="form.type === item.value" class="picker-check"></text>
</view>
</scroll-view>
</view>
</uni-popup>
<!-- 单位选择弹窗 -->
<uni-popup ref="unitPickerRef" type="bottom" background-color="#fff">
<view class="picker-panel">
<view class="picker-header">
<text class="picker-title">选择单位</text>
</view>
<scroll-view scroll-y class="picker-list">
<view v-for="item in unitOptions" :key="item.value" class="picker-item" @click="selectUnit(item)">
<text class="picker-text">{{ item.label }}</text>
<text v-if="form.unitId === item.value" class="picker-check"></text>
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import NavBar from '@/components/common/NavBar.vue'
import { createMold } from '@/api/mes/mold'
import { getProductUnitSimpleList } from '@/api/mes/product'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const brandId = ref('')
const typePickerRef = ref(null)
const unitPickerRef = ref(null)
const typeOptions = ref([])
const unitOptions = ref([])
const loading = ref(false)
const form = reactive({
code: '',
name: '',
type: '',
typeLabel: '',
installLocation: '',
material: '',
unitId: '',
unitLabel: '',
inTimeDate: formatToday(),
inTime: 0,
images: '',
remark: '',
isEnable: true,
isCode: true,
fileUrl: '',
fileName: ''
})
function formatToday() {
const d = new Date()
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
function handleInTimeChange(e) {
form.inTimeDate = e.detail.value
form.inTime = new Date(form.inTimeDate).getTime()
}
async function loadDictAndUnits() {
await initAllDict()
//
const types = []
for (let i = 0; i <= 20; i++) {
const label = getDictLabel(DICT_TYPE.SUBMOLD_TYPE, i, '')
if (label && label !== String(i)) types.push({ label, value: String(i) })
}
typeOptions.value = types
//
try {
const res = await getProductUnitSimpleList()
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
unitOptions.value = data.map(u => ({ value: String(u.id), label: u.name || String(u.id) }))
} catch (e) { console.error('load units error', e) }
}
function openTypePicker() { typePickerRef.value?.open() }
function selectType(item) {
form.type = item.value
form.typeLabel = item.label
typePickerRef.value?.close()
}
function openUnitPicker() { unitPickerRef.value?.open() }
function selectUnit(item) {
form.unitId = item.value
form.unitLabel = item.label
unitPickerRef.value?.close()
}
function handleChooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => { form.images = res.tempFilePaths?.[0] || '' }
})
}
function handleChooseFile() {
// #ifdef APP-PLUS
plus.gallery.pick((res) => {
const f = res?.files?.[0]
if (f) { form.fileUrl = f; form.fileName = f.name || '' }
}, () => {}, { filter: 'all', multiple: false })
// #endif
// #ifndef APP-PLUS
uni.chooseFile({
count: 1,
type: 'all',
success: (res) => { form.fileUrl = res.tempFiles?.[0]?.path || ''; form.fileName = res.tempFiles?.[0]?.name || '' },
fail: () => {}
})
// #endif
}
async function handleSubmit() {
if (!form.name.trim()) { uni.showToast({ title: '请输入名称', icon: 'none' }); return }
if (!form.unitId) { uni.showToast({ title: '请选择单位', icon: 'none' }); return }
if (!form.isCode && !form.code.trim()) { uni.showToast({ title: '请输入编码或启用自动生成', icon: 'none' }); return }
loading.value = true
try {
const data = {
brandId: Number(brandId.value),
name: form.name.trim(),
unitId: Number(form.unitId),
inTime: form.inTime || Date.now(),
isEnable: form.isEnable,
isCode: form.isCode,
code: form.isCode ? undefined : form.code.trim(),
type: form.type || undefined,
installLocation: form.installLocation.trim() || undefined,
material: form.material.trim() || undefined,
images: form.images || undefined,
remark: form.remark.trim() || undefined,
fileUrl: typeof form.fileUrl === 'string' ? form.fileUrl : undefined
}
await createMold(data)
getApp().globalData._moldChildMoldNeedRefresh = true
uni.showToast({ title: '新增成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1500)
} catch (e) {
const msg = e?.message || e?.data?.msg || '新增失败'
uni.showToast({ title: String(msg).substring(0, 50), icon: 'none' })
} finally {
loading.value = false
}
}
function handleCancel() { uni.navigateBack() }
onLoad((options) => { brandId.value = options?.brandId || '' })
onMounted(() => { loadDictAndUnits() })
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f5f7fb; }
.detail-scroll { height: calc(100vh - 172rpx); }
.content-section { padding: 20rpx 24rpx 28rpx; }
.section-card { background: #ffffff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 20rpx; border: 1rpx solid #eef2f7; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
.section-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 22rpx; padding-bottom: 18rpx; border-bottom: 1rpx solid #f1f5f9; }
.section-icon { width: 40rpx; height: 40rpx; border-radius: 10rpx; background: #eff6ff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.section-title { font-size: 32rpx; font-weight: 600; color: #1f2937; }
.form-field { display: flex; flex-direction: column; gap: 12rpx; }
.form-field + .form-field { margin-top: 24rpx; }
.form-label { font-size: 26rpx; color: #4b5563; font-weight: 500; }
.required-star { color: #ef4444; font-size: 28rpx; margin-left: 4rpx; }
.form-input { height: 76rpx; padding: 0 24rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; font-size: 28rpx; color: #374151; box-sizing: border-box; }
.form-textarea { width: 100%; min-height: 120rpx; background: #f8fafc; border-radius: 14rpx; padding: 18rpx 24rpx; font-size: 28rpx; color: #374151; box-sizing: border-box; }
.select-field { display: flex; align-items: center; justify-content: space-between; min-height: 76rpx; padding: 0 24rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; }
.select-text { flex: 1; min-width: 0; font-size: 28rpx; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.placeholder, .placeholder-text { color: #9ca3af; }
.code-row { display: flex; align-items: center; gap: 16rpx; }
.is-code-switch { display: flex; align-items: center; gap: 10rpx; }
.switch-label { font-size: 24rpx; color: #9ca3af; &.active { color: #1f4b79; font-weight: 600; } }
.switch-track { width: 64rpx; height: 34rpx; border-radius: 17rpx; display: flex; align-items: center; padding: 3rpx; box-sizing: border-box; transition: background .2s; }
.switch-track.on { background: #1f4b79; justify-content: flex-end; }
.switch-track.off { background: #d1d5db; justify-content: flex-start; }
.switch-thumb { width: 28rpx; height: 28rpx; border-radius: 14rpx; background: #fff; }
.enable-radio { display: flex; gap: 16rpx; }
.radio-option { flex: 1; height: 76rpx; border-radius: 14rpx; border: 1rpx solid #e5e7eb; background: #f8fafc; display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: #64748b; }
.radio-option.active { background: #1f4b79; color: #fff; border-color: #1f4b79; }
.image-upload { }
.image-placeholder { height: 160rpx; border: 2rpx dashed #d7dde8; border-radius: 14rpx; background: #f8fafc; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8rpx; color: #94a3b8; font-size: 24rpx; }
.image-preview-wrap { position: relative; width: 200rpx; height: 200rpx; border-radius: 14rpx; overflow: hidden; }
.image-preview { width: 100%; height: 100%; }
.image-delete { position: absolute; top: 6rpx; right: 6rpx; width: 40rpx; height: 40rpx; border-radius: 20rpx; background: rgba(0,0,0,.5); display: flex; align-items: center; justify-content: center; }
.file-upload { }
.file-placeholder { height: 76rpx; border-radius: 14rpx; border: 1rpx dashed #bfdbfe; background: #eff6ff; color: #1f7cff; display: flex; align-items: center; justify-content: center; gap: 10rpx; font-size: 26rpx; font-weight: 600; }
.file-item { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 18rpx; background: #f8fafc; border-radius: 12rpx; }
.file-name { flex: 1; font-size: 24rpx; color: #334155; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picker-panel { padding: 24rpx 24rpx 36rpx; border-radius: 28rpx 28rpx 0 0; }
.picker-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20rpx; }
.picker-title { color: #1f2d3d; font-size: 30rpx; font-weight: 700; }
.picker-clear { color: #1f61ff; font-size: 26rpx; }
.picker-list { max-height: 480rpx; }
.picker-item { display: flex; align-items: center; justify-content: space-between; padding: 26rpx 6rpx; border-bottom: 1rpx solid #edf1f6; }
.picker-text { color: #243447; font-size: 28rpx; }
.picker-check { color: #1f61ff; font-size: 30rpx; }
.action-bar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; gap: 18rpx; padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom)); background: #ffffff; box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06); z-index: 99; }
.action-btn { flex: 1; height: 84rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 600; }
.back-btn { background: #eef2f7; color: #475569; }
.submit-btn { background: #1f4b79; color: #ffffff; }
</style>

@ -64,11 +64,41 @@
</view>
<view v-if="currentTab === 0">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-row">
<view class="search-field">
<text class="search-label">编码</text>
<input v-model="childSearchCode" class="search-input" placeholder="请输入编码" confirm-type="search" @confirm="handleChildSearch" />
</view>
<view class="search-field">
<text class="search-label">名称</text>
<input v-model="childSearchName" class="search-input" placeholder="请输入名称" confirm-type="search" @confirm="handleChildSearch" />
</view>
</view>
<view class="search-btns">
<view class="search-btn primary" @click="handleChildSearch">
<uni-icons type="search" size="16" color="#fff"></uni-icons>
<text>查询</text>
</view>
<view class="search-btn" @click="resetChildSearch">
<uni-icons type="refresh" size="16" color="#fff"></uni-icons>
<text>重置</text>
</view>
<view class="search-btn add" @click="goAddChildMold">
<uni-icons type="plusempty" size="16" color="#fff"></uni-icons>
<text>新增</text>
</view>
</view>
</view>
<view v-if="childLoading" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!childMolds.length" class="hint">{{ t('moldLedger.noChildMold') }}</view>
<view v-for="item in childMolds" :key="item.id" class="record-card">
<view v-else-if="!filteredChildMolds.length" class="hint">{{ t('moldLedger.noChildMold') }}</view>
<view v-for="item in filteredChildMolds" :key="item.id" class="record-card">
<view class="record-head">
<text class="record-title">{{ detailValue(item.name) }}</text>
<view class="record-head-left">
<text class="record-title">{{ detailValue(item.name) }}</text>
<text class="record-code">{{ detailValue(item.code || item.moldCode) }}</text>
</view>
<view :class="['status-chip', statusClass(item.type)]">{{ moldTypeText(item.type) }}</view>
</view>
<view class="record-row"><text class="record-label">{{ t('moldLedger.installLocation') }}</text><text class="record-value">
@ -199,7 +229,7 @@
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import {
@ -224,6 +254,17 @@ const repairLoading = ref(false)
const maintainLoading = ref(false)
const installLoading = ref(false)
const childMolds = ref([])
const childSearchCode = ref('')
const childSearchName = ref('')
const filteredChildMolds = computed(() => {
if (!childSearchCode.value.trim() && !childSearchName.value.trim()) return childMolds.value
return childMolds.value.filter(item => {
const matchCode = !childSearchCode.value.trim() || ((item.code || item.moldCode || '').toLowerCase().includes(childSearchCode.value.trim().toLowerCase()))
const matchName = !childSearchName.value.trim() || ((item.name || item.moldName || '').toLowerCase().includes(childSearchName.value.trim().toLowerCase()))
return matchCode && matchName
})
})
const inspectionList = ref([])
const repairList = ref([])
const maintainList = ref([])
@ -290,6 +331,22 @@ const repairRecords = computed(() => {
})
})
function goAddChildMold() {
if (!brandId.value) return
uni.navigateTo({ url: `/pages_function/pages/moldLedger/childMoldForm?brandId=${encodeURIComponent(String(brandId.value))}` })
}
function handleChildSearch() { }
function resetChildSearch() {
childSearchCode.value = ''
childSearchName.value = ''
}
function refreshChildMoldsIfNeeded() {
loadedTabs.value.delete(0)
fetchChildMolds()
}
onLoad(async (query) => {
const id = query?.id ? decodeURIComponent(String(query.id)) : ''
brandId.value = id || undefined
@ -298,6 +355,14 @@ onLoad(async (query) => {
await loadTabData(0)
})
onShow(() => {
const needRefresh = getApp().globalData?._moldChildMoldNeedRefresh
if (needRefresh) {
getApp().globalData._moldChildMoldNeedRefresh = false
refreshChildMoldsIfNeeded()
}
})
async function fetchDetail() {
if (!brandId.value) {
uni.showToast({ title: t('moldLedger.missingBrandId'), icon: 'none' })
@ -710,6 +775,19 @@ function openRepairDetail(row) {
margin-bottom: 20rpx;
}
.tab-actions { display: flex; justify-content: flex-end; padding: 12rpx 24rpx 16rpx; position: relative; z-index: 10; }
.tab-action-btn { padding: 14rpx 32rpx; border-radius: 10rpx; background: #1f4b79; color: #fff; font-size: 26rpx; font-weight: 600; display: inline-flex; }
.tab-action-btn:active { opacity: 0.85; }
.search-bar { display: flex; flex-direction: column; gap: 12rpx; padding: 12rpx 0 16rpx; }
.search-row { display: flex; gap: 12rpx; }
.search-field { display: flex; align-items: center; gap: 8rpx; flex: 1; }
.search-label { font-size: 24rpx; color: #4b5563; white-space: nowrap; flex-shrink: 0; }
.search-input { flex: 1; height: 60rpx; padding: 0 14rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 8rpx; font-size: 24rpx; color: #374151; box-sizing: border-box; min-width: 0; }
.search-btns { display: flex; gap: 12rpx; justify-content: flex-end; }
.search-btn { flex: 1; height: 60rpx; border-radius: 8rpx; border: 1rpx solid #1f4b79; background: #1f4b79; display: flex; align-items: center; justify-content: center; gap: 4rpx; font-size: 24rpx; color: #fff; white-space: nowrap; }
.search-btn:active { opacity: 0.85; }
.hint {
padding: 40rpx 0;
text-align: center;
@ -733,12 +811,24 @@ function openRepairDetail(row) {
margin-bottom: 16rpx;
}
.record-head-left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4rpx; }
.record-title {
flex: 1;
min-width: 0;
font-size: 30rpx;
color: #1f2937;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.record-code {
font-size: 24rpx;
color: #9ca3af;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.record-status {

Loading…
Cancel
Save