Files
ops2_uniapp/pages/workorder/add-workorder.vue
T
2026-04-24 20:52:31 +08:00

567 lines
12 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 class="header-right"></view>
</view>
<scroll-view scroll-y class="content">
<!-- 表单卡片 -->
<view class="card">
<!-- 工单标题 -->
<view class="form-item">
<text class="form-label">工单标题 <text class="required">*</text></text>
<input
v-model="form.title"
class="form-input"
type="text"
maxlength="200"
placeholder="请输入工单标题"
/>
</view>
<!-- 问题描述 -->
<view class="form-item">
<text class="form-label">问题描述</text>
<textarea
v-model="form.description"
class="form-textarea"
placeholder="请输入问题描述"
/>
</view>
<!-- 关联物品 -->
<view class="form-item">
<text class="form-label">关联物品</text>
<!-- 已选择物品 -->
<view v-if="selectedItem" class="selected-item">
<view class="selected-item-info">
<text class="selected-item-name">{{ selectedItem.Name }}</text>
<text v-if="selectedItem.SerialNumber" class="selected-item-serial">{{ selectedItem.SerialNumber }}</text>
</view>
<text class="clear-btn" @click="clearSelectedItem">清除</text>
</view>
<!-- 搜索框 -->
<view v-else class="search-box">
<input
v-model="searchQuery"
class="form-input"
type="text"
placeholder="搜索物品名称或编号"
@input="onSearchInput"
@focus="onSearchFocus"
/>
<!-- 搜索结果下拉 -->
<view v-if="showDropdown && searchResults.length > 0" class="dropdown">
<view
v-for="item in searchResults"
:key="item.ID"
class="dropdown-item"
@click="selectItem(item)"
>
<text class="dropdown-name">{{ item.Name }}</text>
<text v-if="item.SerialNumber" class="dropdown-serial">{{ item.SerialNumber }}</text>
</view>
</view>
<view v-if="showDropdown && searchLoading" class="dropdown">
<view class="dropdown-loading">搜索中...</view>
</view>
<view v-if="showDropdown && searchResults.length === 0 && !searchLoading && searchQuery.trim()" class="dropdown">
<view class="dropdown-empty">未找到匹配的物品</view>
</view>
</view>
</view>
<!-- 图片上传 -->
<view class="form-item">
<text class="form-label">图片</text>
<view class="photo-upload">
<view
v-for="(hash, index) in photos"
:key="index"
class="photo-item"
@click="previewImage(index)"
>
<image
class="photo-img"
:src="getPhotoUrl(hash)"
mode="aspectFill"
/>
<view class="photo-remove" @click.stop="removePhoto(index)">×</view>
</view>
<view v-if="photos.length < 9" class="photo-add" @click="chooseImage">
<text class="photo-add-icon">+</text>
<text class="photo-add-text">添加图片</text>
</view>
</view>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-bar">
<view class="submit-btn" :class="{ disabled: submitting }" @click="submitForm">
<text v-if="submitting">提交中...</text>
<text v-else>提交工单</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import api from '@/api/index.js'
import { workOrderApi } from '@/api/work_order.js'
import { warehouseApi } from '@/api/warehouse.js'
import { useConfigStore } from '@/stores/config.js'
import { onLoad } from '@dcloudio/uni-app'
const configStore = useConfigStore()
const goBack = () => uni.navigateBack()
// 预填物品ID(从物品详情跳转时)
const presetItemId = ref(null)
onLoad((options) => {
// 优先检查预填数据(从物品详情跳转时)
const prefillStr = uni.getStorageSync('prefill_work_order')
if (prefillStr) {
try {
const prefill = JSON.parse(prefillStr)
form.title = prefill.title || ''
form.description = prefill.description || ''
if (prefill.itemId) {
presetItemId.value = prefill.itemId
fetchPresetItem(prefill.itemId)
}
uni.removeStorageSync('prefill_work_order')
} catch (e) {
console.error('读取预填数据失败', e)
}
} else if (options && options.item_id) {
// 兼容旧方式:直接传 item_id 参数
presetItemId.value = parseInt(options.item_id)
fetchPresetItem(presetItemId.value)
}
})
async function fetchPresetItem(itemId) {
try {
const res = await warehouseApi.getItem(itemId)
if (res.errCode === 0 && res.data && res.data.item) {
selectedItem.value = res.data.item
linkedItemId.value = itemId
}
} catch (e) {
console.error('获取物品信息失败', e)
}
}
// 表单数据
const form = reactive({
title: '',
description: ''
})
// 关联物品
const selectedItem = ref(null)
const linkedItemId = ref(null)
const searchQuery = ref('')
const searchResults = ref([])
const showDropdown = ref(false)
const searchLoading = ref(false)
let searchTimer = null
// 图片
const photos = ref([])
// 提交状态
const submitting = ref(false)
function onSearchInput() {
clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
doSearch()
}, 300)
}
function onSearchFocus() {
if (searchQuery.value.trim()) {
showDropdown.value = true
}
}
async function doSearch() {
if (!searchQuery.value.trim()) {
searchResults.value = []
showDropdown.value = false
return
}
searchLoading.value = true
showDropdown.value = true
try {
const res = await warehouseApi.listItem({
search: searchQuery.value.trim()
})
if (res.errCode === 0 && res.data) {
searchResults.value = (res.data.items || []).slice(0, 10)
} else {
searchResults.value = []
}
} catch (e) {
console.error('搜索物品失败', e)
searchResults.value = []
} finally {
searchLoading.value = false
}
}
function selectItem(item) {
selectedItem.value = item
linkedItemId.value = item.ID
searchQuery.value = ''
searchResults.value = []
showDropdown.value = false
}
function clearSelectedItem() {
selectedItem.value = null
linkedItemId.value = null
}
function getPhotoUrl(hash) {
return configStore.getFileBaseUrl() + '/api/files/get/' + hash
}
function chooseImage() {
uni.chooseImage({
count: 9 - photos.value.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) {
photos.value.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) {
photos.value.splice(index, 1)
}
function previewImage(index) {
const urls = photos.value.map(hash => getPhotoUrl(hash))
uni.previewImage({
current: index,
urls: urls
})
}
async function submitForm() {
if (!form.title.trim()) {
uni.showToast({ title: '请输入工单标题', icon: 'none' })
return
}
if (submitting.value) return
submitting.value = true
try {
const data = {
title: form.title.trim(),
description: form.description.trim(),
photos: photos.value
}
if (linkedItemId.value) {
data.item_id = linkedItemId.value
}
const res = await workOrderApi.add(data)
if (res.errCode === 0) {
uni.showToast({ title: '提交成功', icon: 'success' })
uni.$emit('workorder-refresh')
setTimeout(() => {
uni.navigateBack()
}, 500)
} else {
uni.showToast({ title: res.errMsg || '提交失败', icon: 'none' })
}
} catch (e) {
console.error('提交工单失败', e)
uni.showToast({ title: '提交失败', icon: 'none' })
} finally {
submitting.value = false
}
}
</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;
}
.header-right {
width: 60rpx;
}
.content {
height: calc(100vh - 180rpx);
padding: 20rpx;
}
.card {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
}
.form-item {
margin-bottom: 30rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
}
.required {
color: #ff4d4f;
}
.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%;
min-height: 200rpx;
padding: 20rpx;
background-color: #f5f5f5;
border-radius: 10rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.selected-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
background-color: #e6f7ff;
border: 1rpx solid #91d5ff;
border-radius: 10rpx;
}
.selected-item-info {
flex: 1;
}
.selected-item-name {
display: block;
font-size: 28rpx;
color: #1890ff;
}
.selected-item-serial {
display: block;
font-size: 24rpx;
color: #8c8c8c;
margin-top: 5rpx;
}
.clear-btn {
font-size: 24rpx;
color: #ff4d4f;
padding: 10rpx;
}
.search-box {
position: relative;
}
.dropdown {
position: absolute;
top: 90rpx;
left: 0;
right: 0;
background-color: #fff;
border: 1rpx solid #e5e5e5;
border-radius: 10rpx;
max-height: 400rpx;
overflow-y: auto;
z-index: 100;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.dropdown-item {
padding: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.dropdown-item:last-child {
border-bottom: none;
}
.dropdown-name {
display: block;
font-size: 28rpx;
color: #333;
}
.dropdown-serial {
display: block;
font-size: 24rpx;
color: #999;
margin-top: 5rpx;
}
.dropdown-loading,
.dropdown-empty {
padding: 30rpx;
text-align: center;
font-size: 26rpx;
color: #999;
}
.photo-upload {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.photo-item {
width: 200rpx;
height: 200rpx;
border-radius: 10rpx;
overflow: hidden;
position: relative;
}
.photo-img {
width: 100%;
height: 100%;
}
.photo-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;
}
.photo-add {
width: 200rpx;
height: 200rpx;
background-color: #f5f5f5;
border: 2rpx dashed #d9d9d9;
border-radius: 10rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.photo-add-icon {
font-size: 60rpx;
color: #999;
line-height: 1;
}
.photo-add-text {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
.submit-bar {
margin-top: 40rpx;
padding: 0 20rpx;
}
.submit-btn {
background-color: #007AFF;
color: #fff;
font-size: 32rpx;
text-align: center;
padding: 30rpx;
border-radius: 12rpx;
}
.submit-btn.disabled {
background-color: #ccc;
}
</style>