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.

1390 lines
36 KiB
Vue

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden 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="模具台账" subTitle="按型号、状态、编码快速筛选" :showSubTitle="true" />
<view class="filter-card">
<view class="filter-row">
<view class="filter-item" @click="openBrandPicker">
<text class="filter-label">型号</text>
<text class="filter-value" :class="{ placeholder: !selectedBrandLabel }">
{{ selectedBrandLabel || '全部型号' }}
</text>
<text class="filter-arrow"></text>
</view>
<view class="filter-item" @click="openStatusPicker">
<text class="filter-label">状态</text>
<text class="filter-value" :class="{ placeholder: !selectedStatusLabel }">
{{ selectedStatusLabel || '全部状态' }}
</text>
<text class="filter-arrow"></text>
</view>
</view>
<view class="search-row">
<view class="search-wrapper">
<view class="search-icon">
<text class="iconfont icon-search"></text>
</view>
<input v-model="searchKeyword" class="search-input" type="text" placeholder="请输入模具编码或名称"
placeholder-class="input-placeholder" @confirm="handleSearch" />
<view v-if="searchKeyword" class="clear-btn" @click="clearSearch">
<text class="clear-icon">×</text>
</view>
</view>
<view class="search-btn" @click="handleSearch">
<text class="search-btn-text">查询</text>
</view>
</view>
</view>
<scroll-view scroll-y class="content-scroll" :scroll-top="scrollTop" @scroll="handleScroll"
@scrolltolower="loadMore" :lower-threshold="80">
<view class="list-wrap">
<view v-for="item in list" :key="item.id" class="ledger-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="name">{{ textValue(item.name) }}</text>
<view class="code-wrapper">
<text class="code">编码:{{ textValue(item.code) }}</text>
<CopyButton :content="item.code" />
</view>
</view>
<view class="header-right">
<view class="status-chip" :class="statusClass(item.status)">
{{ moldStatusText(item.status) }}
</view>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">使用次数</text>
<text class="value">{{ textValue(item.useTime) }}</text>
</view>
<view class="row">
<text class="label">创建时间</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="loading-text">加载中...</view>
<view v-else-if="!list.length" class="empty-text">暂无模具台账数据</view>
<view v-else-if="loadingMore" class="loading-text">正在加载更多...</view>
<view v-else-if="finished" class="finished-text">没有更多数据了</view>
</view>
</scroll-view>
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<text class="go-top-icon">↑</text>
</view>
<view class="add-btn" @click="openCreate">
<text class="add-icon">+</text>
</view>
<uni-popup ref="brandPickerRef" type="bottom" background-color="#fff">
<view class="picker-content">
<view class="picker-header">
<text class="picker-title">选择模具型号</text>
<view class="picker-clear" @click="resetBrand">
<text class="picker-clear-text">清空</text>
</view>
</view>
<scroll-view scroll-y class="picker-list">
<view v-for="option in brandOptions" :key="option.value" class="picker-item" @click="selectBrand(option)">
<text class="picker-text">{{ option.label }}</text>
<text v-if="selectedBrandId === option.value" class="picker-check">✓</text>
</view>
</scroll-view>
</view>
</uni-popup>
<uni-popup ref="statusPickerRef" type="bottom" background-color="#fff">
<view class="picker-content">
<view class="picker-header">
<text class="picker-title">选择模具状态</text>
<view class="picker-clear" @click="resetStatus">
<text class="picker-clear-text">清空</text>
</view>
</view>
<view class="picker-list">
<view v-for="option in statusOptions" :key="option.value" class="picker-item" @click="selectStatus(option)">
<text class="picker-text">{{ option.label }}</text>
<text v-if="selectedStatus === option.value" class="picker-check">✓</text>
</view>
</view>
</view>
</uni-popup>
<uni-popup ref="formPopupRef" type="center" background-color="#fff">
<view class="popup-content-center">
<view class="popup-header">
<text class="picker-title">{{ formMode === 'create' ? '新增模具台账' : '编辑模具台账' }}</text>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<view class="form-item">
<text class="form-label">模具型号 <text class="required-star">*</text></text>
<view class="form-picker" @click="openFormBrandPicker">
<text class="form-picker-text" :class="{ placeholder: !formBrandLabel }">
{{ formBrandLabel || '请选择模具型号' }}
</text>
<text class="filter-arrow"></text>
</view>
</view>
<view class="form-item">
<view class="form-label-row">
<text class="form-label">
模具编码
<text v-if="!formData.isCode" class="required-star">*</text>
</text>
<view class="switch-wrap">
<text class="switch-label">自动生成</text>
<switch :checked="formData.isCode" color="#2d5a87"
@change="(e) => formData.isCode = e.detail.value" />
</view>
</view>
<input v-model="formData.code" :disabled="formData.isCode || formMode === 'update'" class="form-input" type="text"
placeholder="请输入模具编码" />
</view>
<view class="form-item">
<text class="form-label">模具名称 <text class="required-star">*</text></text>
<input v-model="formData.name" class="form-input" type="text" placeholder="请输入模具名称" />
</view>
<view class="form-item">
<text class="form-label">单位 <text class="required-star">*</text></text>
<view class="form-picker" @click="openUnitPicker">
<text class="form-picker-text" :class="{ placeholder: !formUnitLabel }">
{{ formUnitLabel || '请选择单位' }}
</text>
<text class="filter-arrow"></text>
</view>
</view>
<view class="form-item">
<text class="form-label">入库时间 <text class="required-star">*</text></text>
<picker mode="date" :value="formData.inTimeDate" @change="handleInTimeChange">
<view class="form-picker">
<text class="form-picker-text" :class="{ placeholder: !formData.inTimeDate }">
{{ formData.inTimeDate || '请选择入库时间' }}
</text>
<text class="filter-arrow"></text>
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">状态</text>
<picker mode="selector" :range="formStatusRange" :value="formStatusIndex"
@change="handleFormStatusChange">
<view class="form-picker">
<text class="form-picker-text" :class="{ placeholder: formData.status === '' }">
{{ formStatusLabel || '请选择状态' }}
</text>
<text class="filter-arrow"></text>
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">使用次数</text>
<input v-model="formData.useTime" class="form-input" type="number" placeholder="请输入使用次数" />
</view>
<view class="form-item">
<text class="form-label">模具图片</text>
<view class="image-list">
<view v-for="(img, idx) in formData.images" :key="`${img}-${idx}`" class="image-item">
<image class="image-preview" :src="img" mode="aspectFill"
@click="previewImages(formData.images, img)" />
<view class="image-delete" @click.stop="removeImage(idx)">
<text class="image-delete-text">×</text>
</view>
</view>
<view v-if="formData.images.length < 6" class="image-add" @click="chooseAndUploadImages">
<text class="image-add-text">+</text>
</view>
</view>
</view>
<view class="form-item">
<text class="form-label">是否启用 <text class="required-star">*</text></text>
<view class="enable-group">
<view class="enable-item" :class="{ active: formData.isEnable === true }"
@click="formData.isEnable = true">
<text>是</text>
</view>
<view class="enable-item" :class="{ active: formData.isEnable === false }"
@click="formData.isEnable = false">
<text>否</text>
</view>
</view>
</view>
<view class="form-item">
<text class="form-label">备注</text>
<textarea v-model="formData.remark" class="form-textarea" placeholder="请输入备注" :maxlength="200" />
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeForm">
<text class="btn-text cancel-text">取消</text>
</view>
<view class="footer-btn confirm-btn" @click="submitForm">
<text class="btn-text">保存</text>
</view>
</view>
</view>
</uni-popup>
<uni-popup ref="formBrandPickerRef" type="bottom" background-color="#fff">
<view class="picker-content">
<view class="picker-header">
<text class="picker-title">选择模具型号</text>
</view>
<scroll-view scroll-y class="picker-list">
<view v-for="option in brandOptions" :key="`f-${option.value}`" class="picker-item"
@click="selectFormBrand(option)">
<text class="picker-text">{{ option.label }}</text>
<text v-if="formData.brandId === option.value" class="picker-check">✓</text>
</view>
</scroll-view>
</view>
</uni-popup>
<uni-popup ref="unitPickerRef" type="bottom" background-color="#fff">
<view class="picker-content">
<view class="picker-header">
<text class="picker-title">选择单位</text>
</view>
<scroll-view scroll-y class="picker-list">
<view v-for="option in unitOptions" :key="`u-${option.value}`" class="picker-item"
@click="selectUnit(option)">
<text class="picker-text">{{ option.label }}</text>
<text v-if="Number(formData.unitId) === Number(option.value)" class="picker-check"></text>
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AppTitleHeader from '@/components/common/AppTitleHeader.vue'
import CopyButton from '@/components/common/CopyButton.vue'
import { createMold, deleteMold, getMoldBrandTree, getMoldDetail, getMoldPage, updateMold, uploadMoldImage } from '@/api/mes/mold'
import { getProductUnitSimpleList, getUnitList } from '@/api/mes/product'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const brandPickerRef = ref(null)
const statusPickerRef = ref(null)
const formPopupRef = ref(null)
const formBrandPickerRef = ref(null)
const unitPickerRef = ref(null)
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const finished = ref(false)
const pageNo = ref(1)
const pageSize = ref(10)
const total = ref(0)
const searchKeyword = ref('')
const selectedBrandId = ref(undefined)
const selectedStatus = ref(undefined)
const brandOptions = ref([{ label: '全部', value: undefined }])
const scrollTop = ref(0)
const showGoTop = ref(false)
const formMode = ref('create')
const formData = ref({
id: undefined,
code: '',
isCode: true,
name: '',
unitId: '',
useTime: '',
inTimeDate: '',
status: '',
remark: '',
images: [],
brandId: undefined,
isEnable: true
})
const unitOptions = ref([])
const selectedBrandLabel = computed(() => {
const item = brandOptions.value.find((v) => v.value === selectedBrandId.value)
return item ? item.label : ''
})
const statusOptions = computed(() => {
const options = []
for (let i = 0; i <= 9; i += 1) {
const label = getDictLabel(DICT_TYPE.ERP_MOLD_STATUS, i, '')
if (label && label !== String(i)) {
options.push({ label, value: i })
}
}
return options
})
const selectedStatusLabel = computed(() => {
const item = statusOptions.value.find((v) => v.value === selectedStatus.value)
return item ? item.label : ''
})
const formStatusLabel = computed(() => {
if (formData.value.status === '' || formData.value.status === undefined || formData.value.status === null) return ''
return moldStatusText(formData.value.status)
})
const formStatusRange = computed(() => statusOptions.value.map((v) => v.label))
const formStatusIndex = computed(() => {
const idx = statusOptions.value.findIndex((v) => Number(v.value) === Number(formData.value.status))
return idx >= 0 ? idx : 0
})
const formBrandLabel = computed(() => {
const item = brandOptions.value.find((v) => v.value === formData.value.brandId)
return item ? item.label : ''
})
const formUnitLabel = computed(() => {
const item = unitOptions.value.find((v) => Number(v.value) === Number(formData.value.unitId))
return item ? item.label : ''
})
onLoad(async () => {
await initAllDict()
await loadUnitOptions()
await loadBrandTree()
await fetchList(true)
})
async function loadUnitOptions() {
try {
const res = await getProductUnitSimpleList()
const rows = normalizeListData(res)
unitOptions.value = rows
.map((item) => ({ label: textValue(item?.name), value: item?.id }))
.filter((item) => item.value !== undefined && item.value !== null)
} catch (e) {
try {
const fallbackRes = await getUnitList()
const rows = normalizeListData(fallbackRes)
unitOptions.value = rows
.map((item) => ({ label: textValue(item?.name), value: item?.id }))
.filter((item) => item.value !== undefined && item.value !== null)
} catch (error) {
unitOptions.value = []
}
}
}
async function loadBrandTree() {
try {
const res = await getMoldBrandTree()
const tree = normalizeListData(res)
const flatList = [{ label: '全部', value: undefined }, ...flattenTree(tree)]
brandOptions.value = flatList
} catch (e) {
brandOptions.value = [{ label: '全部', value: undefined }]
}
}
function flattenTree(nodes, level = 0) {
const result = []
const list = Array.isArray(nodes) ? nodes : []
for (const node of list) {
const id = node?.id
if (id === undefined || id === null) continue
const prefix = level > 0 ? `${' '.repeat(level)}` : ''
result.push({
label: `${prefix}${textValue(node?.name)}`,
value: id
})
if (Array.isArray(node?.children) && node.children.length) {
result.push(...flattenTree(node.children, level + 1))
}
}
return result
}
async function handleSearch() {
await fetchList(true)
}
async function clearSearch() {
searchKeyword.value = ''
await fetchList(true)
}
function openBrandPicker() {
brandPickerRef.value?.open()
}
function openStatusPicker() {
statusPickerRef.value?.open()
}
function openFormBrandPicker() {
formBrandPickerRef.value?.open()
}
function closeFormBrandPicker() {
formBrandPickerRef.value?.close()
}
function openUnitPicker() {
unitPickerRef.value?.open()
}
function closeUnitPicker() {
unitPickerRef.value?.close()
}
async function selectBrand(option) {
selectedBrandId.value = option.value
brandPickerRef.value?.close()
await fetchList(true)
}
async function selectStatus(option) {
selectedStatus.value = option.value
statusPickerRef.value?.close()
await fetchList(true)
}
async function resetBrand() {
selectedBrandId.value = undefined
brandPickerRef.value?.close()
await fetchList(true)
}
async function resetStatus() {
selectedStatus.value = undefined
statusPickerRef.value?.close()
await fetchList(true)
}
function selectFormBrand(option) {
formData.value.brandId = option.value
formBrandPickerRef.value?.close()
}
function selectUnit(option) {
formData.value.unitId = option.value
unitPickerRef.value?.close()
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
async function fetchList(reset) {
if (reset) {
pageNo.value = 1
finished.value = false
}
if (pageNo.value === 1) {
loading.value = true
} else {
loadingMore.value = true
}
try {
const keyword = searchKeyword.value.trim()
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
code: keyword || undefined,
name: keyword || undefined,
brandId: selectedBrandId.value,
status: selectedStatus.value
}
const res = await getMoldPage(params)
const page = normalizePageData(res)
total.value = page.total
if (reset) {
list.value = page.list
} else {
list.value = [...list.value, ...page.list]
}
finished.value = list.value.length >= total.value || page.list.length < pageSize.value
} catch (e) {
if (!reset) pageNo.value = Math.max(1, pageNo.value - 1)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
loadingMore.value = false
}
}
function handleScroll(e) {
const top = e?.detail?.scrollTop || 0
showGoTop.value = top > 600
}
function goTop() {
scrollTop.value = 0
}
function openDetail(item) {
const id = item?.id
if (id === undefined || id === null) {
uni.showToast({ title: '缺少ID无法查看详情', icon: 'none' })
return
}
uni.navigateTo({
url: `/pages_function/pages/moldLedger/detail?id=${encodeURIComponent(String(id))}`
})
}
function openCreate() {
formMode.value = 'create'
resetForm()
if (selectedBrandId.value !== undefined) {
formData.value.brandId = selectedBrandId.value
}
formPopupRef.value?.open()
}
async function openEdit(item) {
const id = item?.id
if (id === undefined || id === null) {
uni.showToast({ title: '缺少ID无法编辑', icon: 'none' })
return
}
try {
const res = await getMoldDetail(id)
const detail = normalizeDetailData(res)
formMode.value = 'update'
formData.value = {
id: detail?.id,
code: textValueForInput(detail?.code),
isCode: Boolean(detail?.isCode),
name: textValueForInput(detail?.name),
unitId: textValueForInput(detail?.unitId),
useTime: textValueForInput(detail?.useTime),
inTimeDate: normalizeDateInput(detail?.inTime),
status: detail?.status ?? '',
remark: textValueForInput(detail?.remark),
images: parseImages(detail?.images),
brandId: detail?.brandId,
isEnable: detail?.isEnable ?? true
}
formPopupRef.value?.open()
} catch (e) {
uni.showToast({ title: '加载编辑数据失败', icon: 'none' })
}
}
function closeForm() {
formPopupRef.value?.close()
}
async function submitForm() {
if (!formData.value.brandId && formData.value.brandId !== 0) {
uni.showToast({ title: '请选择模具型号', icon: 'none' })
return
}
if (!formData.value.isCode && !String(formData.value.code || '').trim()) {
uni.showToast({ title: '模具编码必填', icon: 'none' })
return
}
if (!String(formData.value.name || '').trim()) {
uni.showToast({ title: '模具名称必填', icon: 'none' })
return
}
if (!String(formData.value.unitId || '').trim()) {
uni.showToast({ title: '单位ID必填', icon: 'none' })
return
}
if (!String(formData.value.inTimeDate || '').trim()) {
uni.showToast({ title: '入库时间必填', icon: 'none' })
return
}
if (formData.value.isEnable === undefined || formData.value.isEnable === null) {
uni.showToast({ title: '请选择是否启用', icon: 'none' })
return
}
const payload = {
id: formMode.value === 'update' ? formData.value.id : undefined,
code: formData.value.isCode ? undefined : String(formData.value.code || '').trim(),
name: String(formData.value.name || '').trim(),
unitId: toNumberOrUndefined(formData.value.unitId),
brandId: formData.value.brandId,
useTime: toNumberOrUndefined(formData.value.useTime) ?? 0,
inTime: normalizeInTimeForSubmit(formData.value.inTimeDate),
status: toNumberOrUndefined(formData.value.status) ?? 3,
remark: String(formData.value.remark || '').trim() || undefined,
images: formData.value.images.join(','),
isEnable: formData.value.isEnable,
isCode: formData.value.isCode
}
try {
if (formMode.value === 'create') {
await createMold(payload)
uni.showToast({ title: '新增成功', icon: 'success' })
} else {
await updateMold(payload)
uni.showToast({ title: '更新成功', icon: 'success' })
}
closeForm()
await fetchList(true)
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' })
}
}
function confirmDelete(item) {
const id = item?.id
if (id === undefined || id === null) {
uni.showToast({ title: '缺少ID无法删除', icon: 'none' })
return
}
uni.showModal({
title: '确认删除',
content: `确认删除模具"${textValue(item?.name)}"吗?`,
success: async (res) => {
if (!res.confirm) return
try {
await deleteMold(id)
uni.showToast({ title: '删除成功', icon: 'success' })
await fetchList(true)
} catch (e) {
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
})
}
function resetForm() {
formData.value = {
id: undefined,
code: '',
isCode: true,
name: '',
unitId: '',
useTime: '',
inTimeDate: '',
status: '',
remark: '',
images: [],
brandId: undefined,
isEnable: true
}
}
function handleInTimeChange(e) {
formData.value.inTimeDate = e?.detail?.value || ''
}
function handleFormStatusChange(e) {
const idx = Number(e?.detail?.value)
const option = statusOptions.value[idx]
formData.value.status = option ? option.value : ''
}
function normalizePageData(res) {
const root = res && res.data !== undefined ? res.data : res
const candidateList = root?.list || root?.rows || root?.records || root?.data?.list || root?.data?.rows || []
const candidateTotal = root?.total ?? root?.data?.total ?? (Array.isArray(candidateList) ? candidateList.length : 0)
return {
list: Array.isArray(candidateList) ? candidateList : [],
total: Number(candidateTotal || 0)
}
}
function normalizeListData(res) {
const root = res && res.data !== undefined ? res.data : res
if (Array.isArray(root)) return root
if (Array.isArray(root?.data)) return root.data
if (Array.isArray(root?.list)) return root.list
return []
}
function normalizeDetailData(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
function moldStatusText(status) {
return getDictLabel(DICT_TYPE.ERP_MOLD_STATUS, status, textValue(status))
}
function statusClass(status) {
const label = moldStatusText(status)
if (label.includes('正常') || label.toLowerCase() === 'ok') return 'status-normal'
if (label.includes('停') || label.includes('坏') || label.includes('禁') || label.toLowerCase() === 'ng') return 'status-danger'
return 'status-warning'
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return '否'
if (value === true) return '是'
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
}
const text = String(value).trim()
if (!text) return '-'
const num = Number(text)
if (Number.isFinite(num)) {
const time = text.length === 10 ? num * 1000 : num
const date = new Date(time)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
}
const date = new Date(text)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
return text
}
function normalizeDateInput(value) {
const text = formatDateTime(value)
return text === '-' ? '' : text.split(' ')[0]
}
function toNumberOrUndefined(value) {
if (value === null || value === undefined || String(value).trim() === '') return undefined
const num = Number(value)
return Number.isFinite(num) ? num : undefined
}
function textValueForInput(value) {
if (value === null || value === undefined) return ''
return String(value)
}
function normalizeInTimeForSubmit(value) {
const text = String(value || '').trim()
if (!text) return undefined
const date = new Date(text)
if (Number.isNaN(date.getTime())) return text
return date.getTime()
}
function parseImages(value) {
if (!value) return []
if (Array.isArray(value)) return value.map((v) => String(v)).filter(Boolean)
return String(value).split(',').map((v) => v.trim()).filter(Boolean)
}
function previewImages(list, current) {
if (!list || !list.length) return
uni.previewImage({ urls: list, current })
}
function removeImage(index) {
formData.value.images.splice(index, 1)
}
async function chooseAndUploadImages() {
const remain = Math.max(0, 6 - formData.value.images.length)
if (remain <= 0) return
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const paths = Array.isArray(res?.tempFilePaths) ? res.tempFilePaths : []
if (!paths.length) return
uni.showLoading({ title: '上传中', mask: true })
try {
for (const filePath of paths) {
const uploadRes = await uploadMoldImage(filePath)
const url = resolveUploadUrl(uploadRes)
if (url) {
formData.value.images.push(url)
}
}
} catch (e) {
uni.showToast({ title: '图片上传失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
})
}
function resolveUploadUrl(res) {
const root = res && res.data !== undefined ? res.data : res
const data = root?.data !== undefined ? root.data : root
if (typeof data === 'string') return data
if (data && typeof data.url === 'string') return data.url
if (data && typeof data.fullUrl === 'string') return data.fullUrl
if (data && typeof data.path === 'string') return data.path
return ''
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.filter-card {
background: #ffffff;
margin: 20rpx 24rpx;
border-radius: 18rpx;
padding: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.filter-row {
display: flex;
gap: 16rpx;
}
.filter-item {
flex: 1;
background: #f7f9fc;
border-radius: 12rpx;
min-height: 84rpx;
display: flex;
align-items: center;
padding: 0 16rpx;
}
.filter-label {
font-size: 24rpx;
color: #8a9099;
margin-right: 12rpx;
}
.filter-value {
flex: 1;
font-size: 26rpx;
color: #2f353d;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.placeholder {
color: #a3a9b2;
}
.filter-arrow {
font-size: 34rpx;
color: #b1b7bf;
}
.search-row {
display: flex;
gap: 14rpx;
margin-top: 16rpx;
}
.search-wrapper {
flex: 1;
display: flex;
align-items: center;
background: #f5f7fa;
border-radius: 42rpx;
padding: 0 20rpx;
}
.search-icon {
margin-right: 14rpx;
.iconfont {
font-size: 34rpx;
color: #666666;
}
}
.search-input {
flex: 1;
height: 72rpx;
font-size: 28rpx;
color: #333333;
background: transparent;
}
.input-placeholder {
color: #a0a6ad;
}
.clear-btn {
width: 42rpx;
height: 42rpx;
display: flex;
align-items: center;
justify-content: center;
}
.clear-icon {
font-size: 34rpx;
color: #999999;
}
.search-btn {
width: 140rpx;
height: 72rpx;
border-radius: 36rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
display: flex;
align-items: center;
justify-content: center;
}
.search-btn-text {
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
}
.content-scroll {
height: calc(100vh - 380rpx);
}
.list-wrap {
padding: 0 24rpx 40rpx;
}
.ledger-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;
border-bottom: 1rpx solid #eef1f4;
padding-bottom: 16rpx;
}
.name {
display: block;
font-size: 32rpx;
color: #1a3a5c;
font-weight: 600;
}
.code {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #8c929a;
}
.code-wrapper {
display: flex;
align-items: center;
gap: 8rpx;
}
.header-right {
display: flex;
align-items: center;
gap: 12rpx;
}
.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;
}
.action-icon {
color: #ffffff;
font-size: 28rpx;
}
.edit-btn {
background: #1a3a5c;
}
.delete-btn {
background: #ff4d4f;
}
.status-chip {
padding: 8rpx 16rpx;
border-radius: 999rpx;
font-size: 22rpx;
}
.status-normal {
background: rgba(31, 178, 94, 0.12);
color: #0d9b4f;
}
.status-warning {
background: rgba(255, 153, 0, 0.14);
color: #db8400;
}
.status-danger {
background: rgba(255, 77, 79, 0.14);
color: #de3d40;
}
.card-body {
padding-top: 10rpx;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12rpx;
}
.label {
font-size: 26rpx;
color: #8a9099;
}
.value {
max-width: 62%;
text-align: right;
font-size: 27rpx;
color: #2f353d;
}
.loading-text,
.empty-text,
.finished-text {
text-align: center;
color: #9aa2ab;
font-size: 26rpx;
padding: 28rpx 0;
}
.go-top-btn {
position: fixed;
right: 28rpx;
bottom: 150rpx;
width: 88rpx;
height: 88rpx;
border-radius: 44rpx;
background: rgba(26, 58, 92, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 99;
box-shadow: 0 6rpx 16rpx rgba(26, 58, 92, 0.25);
}
.go-top-icon {
color: #ffffff;
font-size: 36rpx;
font-weight: 700;
}
.add-btn {
position: fixed;
right: 28rpx;
bottom: 258rpx;
width: 96rpx;
height: 96rpx;
border-radius: 48rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 99;
box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.32);
}
.add-icon {
color: #ffffff;
font-size: 56rpx;
line-height: 1;
}
.picker-content {
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
max-height: 70vh;
overflow: hidden;
}
.picker-header {
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1rpx solid #edf0f3;
}
.popup-header {
margin: 10rpx 0 0 20rpx;
}
.picker-title {
font-size: 30rpx;
font-weight: 600;
color: #1a3a5c;
}
.picker-clear-text {
font-size: 26rpx;
color: #2d5a87;
}
.picker-list {
max-height: 58vh;
}
.picker-item {
min-height: 84rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1rpx solid #f2f4f7;
}
.picker-text {
flex: 1;
font-size: 28rpx;
color: #2f353d;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.picker-check {
font-size: 30rpx;
color: #2d5a87;
margin-left: 20rpx;
}
.popup-content-center {
width: 680rpx;
max-height: 82vh;
overflow: hidden;
}
.form-scroll {
max-height: 56vh;
}
.form-content {
padding: 24rpx;
}
.form-item {
margin-bottom: 16rpx;
}
.form-label {
display: block;
margin-bottom: 8rpx;
font-size: 26rpx;
color: #8a9099;
}
.required-star {
color: #ff4d4f;
}
.form-label-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
}
.switch-wrap {
display: flex;
align-items: center;
gap: 10rpx;
}
.switch-label {
font-size: 24rpx;
color: #8a9099;
}
.form-input {
height: 76rpx;
padding: 0 20rpx;
border-radius: 12rpx;
background: #f5f7fa;
font-size: 28rpx;
color: #30363d;
}
.form-input[disabled] {
color: #a3a9b2;
}
.form-picker {
height: 76rpx;
border-radius: 12rpx;
background: #f5f7fa;
padding: 0 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.form-picker-text {
flex: 1;
font-size: 28rpx;
color: #30363d;
}
.form-textarea {
width: 100%;
min-height: 130rpx;
border-radius: 12rpx;
background: #f5f7fa;
padding: 16rpx 20rpx;
font-size: 28rpx;
color: #30363d;
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
}
.image-item,
.image-add {
width: 130rpx;
height: 130rpx;
border-radius: 12rpx;
overflow: hidden;
position: relative;
}
.image-preview {
width: 100%;
height: 100%;
background: #edf0f4;
}
.image-add {
background: #f5f7fa;
border: 1rpx dashed #c8ced6;
display: flex;
align-items: center;
justify-content: center;
}
.image-add-text {
font-size: 52rpx;
color: #8e95a0;
line-height: 1;
}
.image-delete {
position: absolute;
top: 0;
right: 0;
width: 36rpx;
height: 36rpx;
border-bottom-left-radius: 10rpx;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
}
.image-delete-text {
color: #ffffff;
font-size: 24rpx;
line-height: 1;
}
.enable-group {
display: flex;
gap: 14rpx;
}
.enable-item {
flex: 1;
height: 72rpx;
border-radius: 12rpx;
background: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #586070;
}
.enable-item.active {
background: rgba(45, 90, 135, 0.12);
color: #1a3a5c;
font-weight: 600;
}
.form-footer {
display: flex;
gap: 14rpx;
padding: 20rpx 24rpx 24rpx;
}
.footer-btn {
flex: 1;
height: 76rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
.cancel-btn {
background: #edf0f4;
}
.confirm-btn {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
}
.btn-text {
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
}
.cancel-text {
color: #586070;
}
</style>