Files
ops2_uniapp/pages/warehouse/item-detail.vue
T
2026-04-24 20:52:31 +08:00

671 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="container">
<view class="header">
<text class="back-btn" @click="goBack"> 返回</text>
<text class="title">物品详情</text>
</view>
<scroll-view scroll-y class="content" refresher-enabled @refresherrefresh="onRefresh" :refresher-triggered="refreshing">
<!-- 加载状态 -->
<view v-if="loading" class="loading">
<text>加载中...</text>
</view>
<!-- 详情内容 -->
<view v-else-if="item">
<!-- 基本信息卡片 -->
<view class="card">
<view class="card-header">
<text class="card-title">基本信息</text>
</view>
<view class="info-row">
<text class="info-label">名称</text>
<text class="info-value">{{ item.Name }}</text>
</view>
<view class="info-row" v-if="item.SerialNumber">
<text class="info-label">编号</text>
<text class="info-value">{{ item.SerialNumber }}</text>
</view>
<view class="info-row" v-if="item.Specification">
<text class="info-label">规格</text>
<text class="info-value">{{ item.Specification }}</text>
</view>
<view class="info-row" v-if="item.ContainerBreadcrumb">
<text class="info-label">位置</text>
<text class="info-value location">{{ item.ContainerBreadcrumb }}</text>
</view>
<view class="info-row" v-if="item.Remark">
<text class="info-label">备注</text>
<text class="info-value">{{ item.Remark }}</text>
</view>
<view class="info-row">
<text class="info-label">创建时间</text>
<text class="info-value">{{ formatDate(item.CreatedAt) }}</text>
</view>
<view class="info-row">
<text class="info-label">更新时间</text>
<text class="info-value">{{ formatDate(item.UpdatedAt) }}</text>
</view>
</view>
<!-- 图片卡片 -->
<view class="card" v-if="photos.length > 0">
<view class="card-header">
<text class="card-title">图片 ({{ photos.length }})</text>
</view>
<view class="photo-grid">
<view
class="photo-item"
v-for="photo in photos"
:key="photo.ID"
>
<image
class="photo-img"
:src="getImageUrl(photo.Sha256)"
mode="aspectFill"
@click="previewAllImages(photo.Sha256)"
/>
</view>
</view>
</view>
<!-- 移动历史 -->
<view class="card" v-if="commits.length > 0">
<view class="card-header">
<text class="card-title">移动历史</text>
</view>
<view class="timeline">
<view
class="timeline-item"
v-for="commit in commits"
:key="commit.ID"
>
<view class="timeline-dot"></view>
<view class="timeline-content">
<text class="timeline-text">
{{ commit.OldContainerBreadcrumb || '无' }} {{ commit.NewContainerBreadcrumb || '已移除' }}
</text>
<text class="timeline-date">{{ formatDate(commit.CreatedAt) }}</text>
</view>
</view>
</view>
</view>
<!-- 关联工单 -->
<view class="card" v-if="workOrders.length > 0">
<view class="card-header">
<text class="card-title">关联工单</text>
</view>
<view
class="workorder-item"
v-for="wo in workOrders"
:key="wo.ID"
@click="goToWorkOrder(wo.id)"
>
<text class="wo-id">#{{ wo.id }}</text>
<text class="wo-title">{{ wo.title }}</text>
<text class="wo-status" :class="wo.status">{{ getWorkOrderStatusText(wo.status) }}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else class="empty">
<text>未找到物品信息</text>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="action-bar" v-if="item">
<view class="action-btn workorder" @click="addWorkOrder">
<text>+ 工单</text>
</view>
<view class="action-btn edit" @click="editItem">
<text>编辑</text>
</view>
<view class="action-btn move" @click="openMoveModal">
<text>移动</text>
</view>
</view>
<!-- 移动弹窗 -->
<view class="modal" v-if="moveModalVisible" @click="closeMoveModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">移动物品</text>
<text class="modal-close" @click="closeMoveModal">×</text>
</view>
<view class="modal-body">
<view class="form-item">
<text class="form-label">选择容器</text>
<picker mode="selector" :range="containerOptions" range-key="Title" @change="onContainerChange">
<view class="picker">
{{ selectedContainer.Title || '请选择目标容器' }}
</view>
</picker>
</view>
</view>
<view class="modal-footer">
<text class="btn-cancel" @click="closeMoveModal">取消</text>
<text class="btn-confirm" @click="confirmMove">确定</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { warehouseApi } from '@/api/warehouse.js'
import { useConfigStore } from '@/stores/config.js'
const configStore = useConfigStore()
const itemId = ref(0)
const loading = ref(false)
const item = ref(null)
const photos = ref([])
const commits = ref([])
const workOrders = ref([])
const canModify = ref(false)
// 移动相关
const moveModalVisible = ref(false)
const containerOptions = ref([])
const selectedContainer = ref({})
const refreshing = ref(false)
const workOrderStatusOptions = [
{ value: 'pending', label: '待处理' },
{ value: 'checked', label: '已检查' },
{ value: 'parts_ordered', label: '已下单零件' },
{ value: 'repaired', label: '已维修' },
{ value: 'returned', label: '已送还' },
{ value: 'unrepairable', label: '无法维修' }
]
function getWorkOrderStatusText(status) {
const option = workOrderStatusOptions.find(s => s.value === status)
return option ? option.label : status
}
function formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
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')}`
}
function getImageUrl(sha256) {
const baseUrl = configStore.getFileBaseUrl()
return `${baseUrl}/api/files/get/${sha256}`
}
function previewAllImages(currentSha) {
const urls = photos.value.map(p => getImageUrl(p.Sha256))
const currentIndex = photos.value.findIndex(p => p.Sha256 === currentSha)
uni.previewImage({
urls: urls,
current: currentIndex >= 0 ? currentIndex : 0
})
}
async function fetchDetail() {
loading.value = true
try {
const res = await warehouseApi.getItem(itemId.value)
if (res.errCode === 0 && res.data) {
item.value = res.data.item
photos.value = res.data.photos || []
commits.value = res.data.commits || []
workOrders.value = res.data.work_orders || []
canModify.value = res.data.canModifyItem
} else {
uni.showToast({ title: '获取详情失败', icon: 'none' })
}
} catch (e) {
console.error('获取物品详情失败', e)
uni.showToast({ title: '获取详情失败', icon: 'none' })
} finally {
loading.value = false
}
}
async function fetchContainers() {
try {
const res = await warehouseApi.listContainer({
all_levels: true,
entries: 5000,
page: 1
})
if (res.errCode === 0 && res.data) {
// 添加根目录选项,并格式化容器数据
const containers = res.data.containers || []
containerOptions.value = [
{ ID: 0, Title: '根目录' }
].concat(containers.map(c => ({
ID: c.ID,
Title: c.Title
})))
}
} catch (e) {
console.error('获取容器列表失败', e)
}
}
function openMoveModal() {
moveModalVisible.value = true
fetchContainers()
}
function closeMoveModal() {
moveModalVisible.value = false
selectedContainer.value = {}
}
function onContainerChange(e) {
selectedContainer.value = containerOptions.value[e.detail.value]
}
async function confirmMove() {
try {
uni.showLoading({ title: '移动中...' })
// ID 为 0 表示移动到根目录
const targetId = selectedContainer.value.ID === 0 ? null : selectedContainer.value.ID
const res = await warehouseApi.moveItem(itemId.value, targetId)
if (res.errCode === 0) {
uni.showToast({ title: '移动成功', icon: 'success' })
moveModalVisible.value = false
selectedContainer.value = {}
fetchDetail()
} else {
uni.showToast({ title: res.errMsg || '移动失败', icon: 'none' })
}
} catch (e) {
console.error('移动失败', e)
uni.showToast({ title: '移动失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
function editItem() {
uni.navigateTo({
url: `/pages/warehouse/item-edit?id=${itemId.value}`
})
}
function addWorkOrder() {
// 存储预填数据到 storage(参考 web 端实现)
const prefillData = {
itemId: itemId.value,
title: item.value.SerialNumber
? `${item.value.Name}-${item.value.SerialNumber}`
: item.value.Name,
description: item.value.Remark || '',
}
uni.setStorageSync('prefill_work_order', JSON.stringify(prefillData))
uni.navigateTo({
url: '/pages/workorder/add-workorder'
})
}
function goToWorkOrder(id) {
uni.navigateTo({
url: `/pages/workorder/show-workorder?id=${id}`
})
}
function goBack() {
uni.navigateBack({ delta: 1 })
}
async function onRefresh() {
refreshing.value = true
await fetchDetail()
refreshing.value = false
}
onMounted(() => {
// 备用:在某些情况下 onLoad 可能未触发
})
// 使用 onLoad 获取页面参数(推荐方式)
import { onLoad, onShow } from '@dcloudio/uni-app'
onLoad((options) => {
console.log('item-detail onLoad options:', options)
if (options && options.id) {
itemId.value = parseInt(options.id)
fetchDetail()
}
})
// 每次显示页面时刷新数据(从编辑页返回时自动更新)
onShow(() => {
if (itemId.value > 0) {
fetchDetail()
}
})
</script>
<style scoped>
.container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.header {
background-color: #fff;
padding: 30rpx;
display: flex;
align-items: center;
}
.back-btn {
font-size: 32rpx;
color: #007AFF;
margin-right: 20rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
flex: 1;
text-align: center;
padding-right: 60rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.content {
height: calc(100vh - 180rpx);
}
.loading, .empty {
text-align: center;
padding: 100rpx 0;
color: #999;
}
.card {
background-color: #fff;
margin: 20rpx;
border-radius: 12rpx;
overflow: hidden;
}
.card-header {
padding: 25rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.card-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.info-row {
display: flex;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
width: 150rpx;
font-size: 28rpx;
color: #666;
}
.info-value {
flex: 1;
font-size: 28rpx;
color: #333;
}
.info-value.location {
color: #007AFF;
}
.photo-grid {
padding: 20rpx 30rpx;
display: flex;
flex-wrap: wrap;
gap: 15rpx;
}
.photo-item {
width: calc((100% - 30rpx) / 3);
height: 200rpx;
border-radius: 10rpx;
overflow: hidden;
}
.photo-img {
width: 100%;
height: 100%;
}
.timeline {
padding: 20rpx 30rpx;
}
.timeline-item {
display: flex;
padding: 15rpx 0;
}
.timeline-dot {
width: 16rpx;
height: 16rpx;
background-color: #007AFF;
border-radius: 50%;
margin-right: 20rpx;
margin-top: 8rpx;
}
.timeline-content {
flex: 1;
}
.timeline-text {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
}
.timeline-date {
display: block;
font-size: 24rpx;
color: #999;
}
.workorder-item {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.workorder-item:last-child {
border-bottom: none;
}
.wo-id {
font-size: 28rpx;
color: #007AFF;
margin-right: 15rpx;
}
.wo-title {
flex: 1;
font-size: 28rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wo-status {
font-size: 22rpx;
padding: 6rpx 16rpx;
border-radius: 20rpx;
background-color: #f0f0f0;
color: #666;
}
.wo-status.pending {
background-color: #fff3e0;
color: #ff9800;
}
.wo-status.checked {
background-color: #e3f2fd;
color: #2196f3;
}
.wo-status.parts_ordered {
background-color: #e8f5e9;
color: #4caf50;
}
.wo-status.repaired {
background-color: #f3e5f5;
color: #9c27b0;
}
.wo-status.returned {
background-color: #e0f2f1;
color: #009688;
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
background-color: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.action-btn {
flex: 1;
text-align: center;
padding: 25rpx;
border-radius: 10rpx;
font-size: 30rpx;
}
.action-btn.workorder {
background-color: #4CAF50;
color: #fff;
margin-right: 20rpx;
}
.action-btn.edit {
background-color: #007AFF;
color: #fff;
margin-right: 20rpx;
}
.action-btn.move {
background-color: #ff9800;
color: #fff;
}
/* 弹窗样式 */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
width: 85%;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.modal-close {
font-size: 50rpx;
color: #999;
}
.modal-body {
padding: 30rpx;
}
.form-item {
margin-bottom: 20rpx;
}
.form-label {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 15rpx;
}
.picker {
background-color: #f5f5f5;
padding: 25rpx;
border-radius: 10rpx;
font-size: 28rpx;
}
.modal-footer {
display: flex;
border-top: 1rpx solid #f0f0f0;
}
.btn-cancel, .btn-confirm {
flex: 1;
text-align: center;
padding: 30rpx;
font-size: 30rpx;
}
.btn-cancel {
color: #666;
border-right: 1rpx solid #f0f0f0;
}
.btn-confirm {
color: #007AFF;
}
</style>