You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

487 lines
21 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="page-container">
<AppTitleHeader :title="t('moldInspectionPlan.moduleName')" :subTitle="t('moldInspectionPlan.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="searchKeyword" class="search-input" :placeholder="t('moldInspectionPlan.searchPlaceholder')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</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="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.planName) }}</text>
<view class="tag-row">
<text class="type-code">{{ planTypeLabelForRow(item.planType) }}</text>
</view>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('moldInspectionPlan.description') }}</text>
<text class="value">{{ textValue(item.description) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldInspectionPlan.creatorName') }}</text>
<text class="value">{{ textValue(item.creatorName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldInspectionPlan.createTime') }}</text>
<text class="value">{{ formatDateTime(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="#ffffff"></uni-icons>
</view>
<view class="action-btn delete-btn" @click.stop="confirmDelete(item)">
<uni-icons type="trash" size="18" color="#ffffff"></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('moldInspectionPlan.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="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ formMode === 'create' ? t('moldInspectionPlan.createTitle') : t('moldInspectionPlan.editTitle') }}</text>
<view class="popup-close" @click="closeForm">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<view class="form-item">
<text class="form-label">{{ t('moldInspectionPlan.planName') }} <text class="required-star">*</text></text>
<input v-model="formData.planName" class="form-input" type="text" :placeholder="t('moldInspectionPlan.placeholderPlanName')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('moldInspectionPlan.planType') }} <text class="required-star">*</text></text>
<picker mode="selector" :range="planTypeOptions" range-key="label" :value="planTypeIndex" @change="onPlanTypeChange">
<view class="picker-view">{{ planTypeLabel || t('moldInspectionPlan.placeholderPlanType') }}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldInspectionPlan.description') }}</text>
<textarea v-model="formData.description" class="form-textarea" :placeholder="t('moldInspectionPlan.placeholderDescription')" maxlength="500" />
</view>
<view class="form-item">
<text class="form-label">{{ t('moldInspectionPlan.subjectName') }}</text>
<view class="multi-select-wrap" @click="openSubjectPicker">
<text v-if="selectedSubjectLabels.length" class="selected-text">{{ selectedSubjectLabels.join(', ') }}</text>
<text v-else class="placeholder-text">{{ t('moldInspectionPlan.placeholderSubjectSelect') }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeForm"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="submitForm"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</view>
</view>
</uni-popup>
<!-- 点检项选择弹框 -->
<uni-popup ref="subjectPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ t('moldInspectionPlan.subjectSelectTitle') }}</text>
<view class="popup-close" @click="closeSubjectPicker">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="subject-scroll">
<view class="subject-list">
<view v-for="item in subjectOptions" :key="item.id" class="subject-item" @click="toggleSubject(item)">
<view class="checkbox-wrap">
<view :class="['checkbox-box', isSubjectSelected(item.id) ? 'checked' : '']">
<uni-icons v-if="isSubjectSelected(item.id)" type="checkmarkempty" size="14" color="#fff"></uni-icons>
</view>
</view>
<view class="subject-info">
<text class="subject-name">{{ textValue(item.subjectName) }}</text>
<text class="subject-code">{{ textValue(item.subjectCode) }}</text>
</view>
</view>
<view v-if="!subjectOptions.length" class="hint">{{ t('moldInspectionPlan.noSubjectData') }}</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeSubjectPicker"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="confirmSubjectPicker"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</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 { getMoldInspectionPlanPage, createMoldInspectionPlan, updateMoldInspectionPlan, deleteMoldInspectionPlan, getMoldSubjectAllList } from '@/api/mes/moldInspectionPlan'
const { t } = useI18n()
const formPopupRef = ref(null)
const subjectPopupRef = ref(null)
const searchKeyword = ref('')
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 formMode = ref('create')
const subjectOptions = ref([])
const tempSelectedSubjectIds = ref([])
const planTypeOptions = computed(() => [
{ value: 1, label: t('moldInspectionPlan.planTypeMaintain') },
{ value: 2, label: t('moldInspectionPlan.planTypeInspect') }
])
const formData = reactive({
id: undefined,
planName: '',
planType: undefined,
description: '',
subjectIds: []
})
const planTypeIndex = computed(() => {
const idx = planTypeOptions.value.findIndex((o) => o.value === formData.planType)
return idx < 0 ? 0 : idx
})
const planTypeLabel = computed(() => {
const hit = planTypeOptions.value.find((o) => o.value === formData.planType)
return hit ? hit.label : ''
})
const selectedSubjectLabels = computed(() => {
return formData.subjectIds
.map((id) => {
const found = subjectOptions.value.find((s) => s.id === id)
return found ? found.subjectName : ''
})
.filter(Boolean)
})
function planTypeLabelForRow(value) {
const v = value === '' || value === null || value === undefined ? undefined : Number(value)
if (v === 1) return t('moldInspectionPlan.planTypeMaintain')
if (v === 2) return t('moldInspectionPlan.planTypeInspect')
return textValue(value)
}
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function formatDateTime(v) {
if (!v) return '-'
if (Array.isArray(v) && v.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = v
const p = (n) => String(n).padStart(2, '0')
return `${y}-${p(m)}-${p(d)} ${p(hh)}:${p(mm)}:${p(ss)}`
}
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) }
}
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,
planName: searchKeyword.value.trim() || undefined
}
const res = await getMoldInspectionPlanPage(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 handleSearch() { await fetchList(true) }
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
function onScroll(e) {
const top = Number(e?.detail?.scrollTop || 0)
showGoTop.value = top > 600
}
function goTop() { scrollTop.value = 0 }
async function ensureSubjectOptionsLoaded() {
if (subjectOptions.value.length) return
try {
const res = await getMoldSubjectAllList()
const root = res && res.data !== undefined ? res.data : res
const l = Array.isArray(root) ? root : root?.list || root?.data || []
subjectOptions.value = Array.isArray(l) ? l : []
} catch (e) {
subjectOptions.value = []
}
}
function resetForm() {
formData.id = undefined
formData.planName = ''
formData.planType = undefined
formData.description = ''
formData.subjectIds = []
}
async function openCreate() {
formMode.value = 'create'
resetForm()
await ensureSubjectOptionsLoaded()
formPopupRef.value?.open()
}
async function openEdit(item) {
const id = item?.id
if (!id) {
uni.showToast({ title: t('functionCommon.noIdEdit'), icon: 'none' })
return
}
formMode.value = 'update'
resetForm()
await ensureSubjectOptionsLoaded()
formData.id = item.id
formData.planName = item.planName || ''
formData.planType = item.planType ?? undefined
formData.description = item.description || ''
formData.subjectIds = parseSubjectIds(item.subjectIdS || item.subjectIds)
formPopupRef.value?.open()
}
function parseSubjectIds(value) {
if (!value) return []
if (Array.isArray(value)) return value.map((v) => (typeof v === 'number' ? v : Number(v))).filter((v) => Number.isFinite(v))
return String(value)
.split(',')
.map((v) => v.trim())
.filter(Boolean)
.map((v) => Number(v))
.filter((v) => Number.isFinite(v))
}
async function openDetail(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
uni.setStorageSync('moldInspectionPlanDetail', JSON.stringify(item))
uni.navigateTo({
url: `/pages_function/pages/moldInspectionPlan/detail?id=${encodeURIComponent(String(item.id))}`
})
}
async function confirmDelete(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdDelete'), icon: 'none' })
return
}
uni.showModal({
title: t('functionCommon.confirmDelete'),
content: t('moldInspectionPlan.confirmDelete'),
success: async ({ confirm }) => {
if (!confirm) return
try {
await deleteMoldInspectionPlan([item.id])
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
function closeForm() { formPopupRef.value?.close() }
function onPlanTypeChange(e) {
const idx = Number(e?.detail?.value || 0)
formData.planType = planTypeOptions.value[idx]?.value
}
function openSubjectPicker() {
tempSelectedSubjectIds.value = [...formData.subjectIds]
subjectPopupRef.value?.open()
}
function closeSubjectPicker() {
subjectPopupRef.value?.close()
}
function confirmSubjectPicker() {
formData.subjectIds = [...tempSelectedSubjectIds.value]
subjectPopupRef.value?.close()
}
function isSubjectSelected(id) {
return tempSelectedSubjectIds.value.includes(id)
}
function toggleSubject(item) {
const id = item.id
const idx = tempSelectedSubjectIds.value.indexOf(id)
if (idx >= 0) {
tempSelectedSubjectIds.value.splice(idx, 1)
} else {
tempSelectedSubjectIds.value.push(id)
}
}
function validateForm() {
if (!String(formData.planName || '').trim()) {
uni.showToast({ title: t('moldInspectionPlan.validatorPlanNameRequired'), icon: 'none' })
return false
}
if (formData.planType === undefined || formData.planType === null || formData.planType === '') {
uni.showToast({ title: t('moldInspectionPlan.validatorPlanTypeRequired'), icon: 'none' })
return false
}
return true
}
async function submitForm() {
if (!validateForm()) return
const payload = {
id: formMode.value === 'update' ? formData.id : undefined,
planName: String(formData.planName).trim(),
planType: formData.planType,
description: String(formData.description || '').trim() || undefined,
subjectIdS: formData.subjectIds.length ? formData.subjectIds.join(',') : undefined
}
try {
if (formMode.value === 'create') {
await createMoldInspectionPlan(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
} else {
await updateMoldInspectionPlan(payload)
uni.showToast({ title: t('functionCommon.updateSuccess'), icon: 'success' })
}
closeForm()
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
onShow(async () => {
await fetchList(true)
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.search-card { margin: 16rpx 24rpx; display: flex; gap: 16rpx; background: #fff; border-radius: 14rpx; padding: 16rpx; }
.search-row { display: flex; gap: 12rpx; width: 100%; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 40rpx; padding: 0 20rpx; }
.search-icon { color: #909399; font-size: 30rpx; }
.search-input { flex: 1; height: 68rpx; margin-left: 12rpx; font-size: 26rpx; }
.search-btn { min-width: 120rpx; height: 68rpx; border-radius: 34rpx; background: #1a3a5c; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 26rpx; }
.list-scroll { height: calc(100vh - 280rpx); }
.list-wrap { padding: 0 24rpx 130rpx; }
.type-card { background: #ffffff; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18rpx; border-bottom: 1rpx solid #edf0f3; }
.header-left { display: flex; flex-direction: column; gap: 8rpx; flex: 1; }
.type-name { font-size: 32rpx; font-weight: 700; color: #1f2d3d; }
.tag-row { display: flex; align-items: center; }
.type-code { font-size: 24rpx; color: #1a3a5c; background: rgba(26, 58, 92, 0.08); padding: 4rpx 14rpx; border-radius: 8rpx; }
.card-actions { margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 14rpx; }
.action-btn { width: 60rpx; height: 60rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.edit-btn { background: #1a3a5c; }
.delete-btn { background: #e34d59; }
.card-body { padding-top: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.label { font-size: 26rpx; color: #8a9099; }
.value { font-size: 27rpx; color: #30363d; max-width: 62%; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.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; }
.popup-content { width: 680rpx; max-height: 80vh; background: #fff; border-radius: 24rpx; overflow: hidden; }
.popup-header { height: 92rpx; display: flex; align-items: center; justify-content: center; position: relative; border-bottom: 1rpx solid #edf0f3; }
.popup-title { font-size: 32rpx; font-weight: 700; color: #1f2d3d; }
.popup-close { position: absolute; right: 24rpx; top: 20rpx; width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; }
.close-icon { font-size: 42rpx; color: #9aa0a6; line-height: 1; }
.form-scroll { max-height: calc(80vh - 180rpx); }
.form-content { padding: 22rpx 28rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { display: block; font-size: 25rpx; color: #5f6b7a; margin-bottom: 10rpx; }
.required-star { color: #e34d59; }
.form-input { min-height: 72rpx; background: #f7f9fc; border-radius: 12rpx; padding: 0 18rpx; display: flex; align-items: center; font-size: 26rpx; color: #2f3a45; }
.form-textarea { width: 100%; min-height: 140rpx; background: #f7f9fc; border-radius: 12rpx; padding: 14rpx 18rpx; font-size: 26rpx; color: #2f3a45; box-sizing: border-box; }
.picker-view { min-height: 72rpx; background: #f7f9fc; border-radius: 12rpx; padding: 0 18rpx; display: flex; align-items: center; font-size: 26rpx; color: #2f3a45; }
.multi-select-wrap { min-height: 72rpx; background: #f7f9fc; border-radius: 12rpx; padding: 14rpx 18rpx; font-size: 26rpx; }
.selected-text { color: #2f3a45; word-break: break-all; }
.placeholder-text { color: #c0c4cc; }
.form-footer { height: 88rpx; display: flex; border-top: 1rpx solid #edf0f3; }
.footer-btn { flex: 1; display: flex; align-items: center; justify-content: center; }
.btn-text { font-size: 28rpx; }
.cancel-btn { background: #f4f6fa; color: #5f6b7a; }
.confirm-btn { background: #1a3a5c; color: #fff; }
.subject-scroll { max-height: 50vh; }
.subject-list { padding: 16rpx 24rpx; }
.subject-item { display: flex; align-items: center; padding: 18rpx 0; border-bottom: 1rpx solid #f0f2f5; }
.checkbox-wrap { margin-right: 20rpx; }
.checkbox-box { width: 40rpx; height: 40rpx; border-radius: 8rpx; border: 2rpx solid #d0d5dd; display: flex; align-items: center; justify-content: center; background: #fff; }
.checkbox-box.checked { background: #1a3a5c; border-color: #1a3a5c; }
.subject-info { display: flex; flex-direction: column; gap: 4rpx; }
.subject-name { font-size: 28rpx; color: #30363d; }
.subject-code { font-size: 24rpx; color: #8a9099; }
</style>