移动端功能基本完成
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
<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">
|
||||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<text class="form-label required">名称 *</text>
|
||||
<input class="form-input" v-model="form.name" placeholder="请输入物品名称" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">编号</text>
|
||||
<input class="form-input" v-model="form.serialNumber" placeholder="请输入物品编号(选填)" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">备注</text>
|
||||
<textarea class="form-textarea" v-model="form.remark" placeholder="请输入备注(选填)" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<text class="form-label">物品图片</text>
|
||||
<view class="image-list">
|
||||
<view
|
||||
v-for="(photo, index) in form.photos"
|
||||
:key="index"
|
||||
class="image-item"
|
||||
>
|
||||
<image class="preview-img" :src="getPhotoUrl(photo)" mode="aspectFill" />
|
||||
<view class="image-remove" @click="removePhoto(index)">×</view>
|
||||
</view>
|
||||
<view v-if="form.photos.length < 9" class="image-add" @click="chooseImage">
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit-section">
|
||||
<view class="submit-btn" @click="submitForm">
|
||||
<text>保存</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { warehouseApi } from '@/api/warehouse.js'
|
||||
import { useConfigStore } from '@/stores/config.js'
|
||||
import api from '@/api/index.js'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
|
||||
// 容器ID(从URL参数获取)
|
||||
const containerId = ref(0)
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
name: '',
|
||||
serialNumber: '',
|
||||
remark: '',
|
||||
photos: []
|
||||
})
|
||||
|
||||
function getPhotoUrl(sha256) {
|
||||
const baseUrl = configStore.getFileBaseUrl ? configStore.getFileBaseUrl() : ''
|
||||
return `${baseUrl}/api/files/get/${sha256}`
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function chooseImage() {
|
||||
uni.chooseImage({
|
||||
count: 9 - form.value.photos.length,
|
||||
success: (res) => {
|
||||
res.tempFilePaths.forEach(path => {
|
||||
uploadImage(path)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function uploadImage(filePath) {
|
||||
try {
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
const res = await api.upload('/files/upload/image', {
|
||||
uri: filePath,
|
||||
name: 'file'
|
||||
})
|
||||
|
||||
if (res.errCode === 0 && res.data && res.data.hash) {
|
||||
form.value.photos.push(res.data.hash)
|
||||
uni.showToast({ title: '上传成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: res.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('上传失败', e)
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
function removePhoto(index) {
|
||||
form.value.photos.splice(index, 1)
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!form.value.name.trim()) {
|
||||
uni.showToast({ title: '请输入名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
const data = {
|
||||
name: form.value.name,
|
||||
serial_number: form.value.serialNumber,
|
||||
remark: form.value.remark,
|
||||
container_id: containerId.value || null,
|
||||
photos: form.value.photos
|
||||
}
|
||||
|
||||
const res = await warehouseApi.addItem(data)
|
||||
|
||||
if (res.errCode === 0) {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
goBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({ title: res.errMsg || '保存失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('保存失败', e)
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取页面参数
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
onLoad((options) => {
|
||||
console.log('add-item onLoad options:', options)
|
||||
if (options && options.container_id) {
|
||||
containerId.value = parseInt(options.container_id)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: calc(100vh - 120rpx);
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background-color: #fff;
|
||||
margin: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.form-label.required::after {
|
||||
content: ' *';
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
min-height: 150rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 图片上传 */
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
position: relative;
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-remove {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0 0 0 12rpx;
|
||||
}
|
||||
|
||||
.image-add {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx dashed #ddd;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 60rpx;
|
||||
color: #999;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
padding: 40rpx 20rpx;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,670 @@
|
||||
<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>
|
||||
@@ -0,0 +1,351 @@
|
||||
<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">
|
||||
<view v-if="loading" class="loading">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<text class="form-label required">名称 *</text>
|
||||
<input class="form-input" v-model="form.name" placeholder="请输入物品名称" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">编号</text>
|
||||
<input class="form-input" v-model="form.serialNumber" placeholder="请输入物品编号(选填)" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">备注</text>
|
||||
<textarea class="form-textarea" v-model="form.remark" placeholder="请输入备注(选填)" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<text class="form-label">物品图片</text>
|
||||
<view class="image-list">
|
||||
<view
|
||||
v-for="(photo, index) in form.photos"
|
||||
:key="index"
|
||||
class="image-item"
|
||||
>
|
||||
<image class="preview-img" :src="getPhotoUrl(photo)" mode="aspectFill" />
|
||||
<view class="image-remove" @click="removePhoto(index)">×</view>
|
||||
</view>
|
||||
<view v-if="form.photos.length < 9" class="image-add" @click="chooseImage">
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit-section">
|
||||
<view class="submit-btn" @click="submitForm">
|
||||
<text>保存</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { warehouseApi } from '@/api/warehouse.js'
|
||||
import { useConfigStore } from '@/stores/config.js'
|
||||
import api from '@/api/index.js'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
|
||||
// 物品ID
|
||||
const itemId = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
name: '',
|
||||
serialNumber: '',
|
||||
remark: '',
|
||||
photos: []
|
||||
})
|
||||
|
||||
function getPhotoUrl(sha256) {
|
||||
const baseUrl = configStore.getFileBaseUrl ? configStore.getFileBaseUrl() : ''
|
||||
return `${baseUrl}/api/files/get/${sha256}`
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function chooseImage() {
|
||||
uni.chooseImage({
|
||||
count: 9 - form.value.photos.length,
|
||||
success: (res) => {
|
||||
res.tempFilePaths.forEach(path => {
|
||||
uploadImage(path)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function uploadImage(filePath) {
|
||||
try {
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
const res = await api.upload('/files/upload/image', {
|
||||
uri: filePath,
|
||||
name: 'file'
|
||||
})
|
||||
|
||||
if (res.errCode === 0 && res.data && res.data.hash) {
|
||||
form.value.photos.push(res.data.hash)
|
||||
uni.showToast({ title: '上传成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: res.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('上传失败', e)
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
function removePhoto(index) {
|
||||
form.value.photos.splice(index, 1)
|
||||
}
|
||||
|
||||
async function fetchDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await warehouseApi.getItem(itemId.value)
|
||||
if (res.errCode === 0 && res.data) {
|
||||
const item = res.data.item
|
||||
form.value = {
|
||||
name: item.Name || '',
|
||||
serialNumber: item.SerialNumber || '',
|
||||
remark: item.Remark || '',
|
||||
photos: res.data.photos ? res.data.photos.map(p => p.Sha256) : []
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: '获取详情失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取物品详情失败', e)
|
||||
uni.showToast({ title: '获取详情失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!form.value.name.trim()) {
|
||||
uni.showToast({ title: '请输入名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
const data = {
|
||||
id: itemId.value,
|
||||
name: form.value.name,
|
||||
serial_number: form.value.serialNumber,
|
||||
remark: form.value.remark,
|
||||
photos: form.value.photos
|
||||
}
|
||||
|
||||
const res = await warehouseApi.updateItem(data)
|
||||
|
||||
if (res.errCode === 0) {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
goBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({ title: res.errMsg || '保存失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('保存失败', e)
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取页面参数
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
onLoad((options) => {
|
||||
console.log('item-edit onLoad options:', options)
|
||||
if (options && options.id) {
|
||||
itemId.value = parseInt(options.id)
|
||||
fetchDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: calc(100vh - 120rpx);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 100rpx 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background-color: #fff;
|
||||
margin: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.form-label.required::after {
|
||||
content: ' *';
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
min-height: 150rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 图片上传 */
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
position: relative;
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-remove {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0 0 0 12rpx;
|
||||
}
|
||||
|
||||
.image-add {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx dashed #ddd;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 60rpx;
|
||||
color: #999;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
padding: 40rpx 20rpx;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user