first commit

This commit is contained in:
2026-03-05 16:52:12 +08:00
commit 8ca2e6d52f
1899 changed files with 321565 additions and 0 deletions

View File

@@ -0,0 +1,515 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="110px"
v-loading="formLoading"
>
<el-divider>报修信息</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="报修单号" prop="repairNo">
<el-input v-model="formData.repairNo" placeholder="请输入报修单号" :readonly="formType === 'create'" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报修日期" prop="reportTime">
<el-date-picker v-model="formData.reportTime" type="datetime" placeholder="请选择报修日期" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="报修人" prop="reporter">
<el-input v-model="formData.reporter" placeholder="请输入报修人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系方式" prop="contact">
<el-input v-model="formData.contact" placeholder="请输入联系方式" />
</el-form-item>
</el-col>
</el-row>
<el-divider>产品信息</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="产品" prop="deviceId">
<el-select
v-model="formData.deviceId"
filterable
remote
clearable
placeholder="请输入关键词搜索产品"
:remote-method="searchProduct"
:loading="productLoading"
@change="onProductChange"
style="width: 100%"
>
<el-option v-for="item in productOptions" :key="item.id" :label="item.productName || item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="产品编码" prop="deviceCode">
<el-input v-model="formData.deviceCode" readonly />
</el-form-item>
</el-col>
</el-row>
<el-divider>故障信息</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="故障现象描述" prop="faultDesc">
<el-input v-model="formData.faultDesc" type="textarea" placeholder="请输入故障现象描述" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="故障发生日期" prop="faultTime">
<el-date-picker v-model="formData.faultTime" type="datetime" placeholder="请选择故障发生日期" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="故障严重程度" prop="faultLevel">
<el-select v-model="formData.faultLevel" placeholder="请选择严重程度" style="width: 100%">
<el-option label="紧急" value="urgent" />
<el-option label="高" value="high" />
<el-option label="中" value="medium" />
<el-option label="低" value="low" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否影响安全" prop="affectSafety">
<el-radio-group v-model="formData.affectSafety">
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-divider>其他</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="当前状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态" style="width: 100%">
<el-option label="待受理" value="pending" />
<el-option label="已派工" value="assigned" />
<el-option label="维修中" value="repairing" />
<el-option label="待验收" value="to_accept" />
<el-option label="已完成" value="finished" />
<el-option label="已关闭" value="closed" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="附件上传" prop="attachments">
<!-- <el-upload-->
<!-- v-model:file-list="fileList"-->
<!-- :auto-upload="true"-->
<!-- :multiple="true"-->
<!-- :http-request="customUpload"-->
<!-- :on-remove="handleUploadRemove"-->
<!-- :limit="5"-->
<!-- list-type="picture-card"-->
<!-- >-->
<!-- <el-icon><Plus /></el-icon>-->
<!-- </el-upload>-->
<el-upload
ref="uploadRef"
:file-list="fileList"
:http-request="customUpload"
:on-remove="handleUploadRemove"
list-type="picture-card"
:limit="6"
:multiple="true"
:show-file-list="true"
:auto-upload="true"
>
<!-- <i class="el-icon-plus"></i>-->
<el-icon><Plus /></el-icon>
</el-upload>
<!-- <div v-if="fileList.length > 0">-->
<!-- <p class="tip-text">已上传 {{fileList.length}} 个文件</p>-->
<!-- <p class="tip-text" style="color:#f56c6c" v-if="fileList.length >= 2">文件数量: {{fileList.length}}, IDs: {{fileList.map(f => f.uid).join(', ')}}</p>-->
<!-- </div>-->
<!-- -->
<!-- <div v-if="true" class="debug-info">-->
<!-- &lt;!&ndash; 调试信息只在需要时启用 v-if="true" &ndash;&gt;-->
<!-- <p>fileList: {{JSON.stringify(fileList)}}</p>-->
<!-- <p>formData.attachments: {{JSON.stringify(formData.attachments)}}</p>-->
<!-- </div>-->
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { RepairOrderApi, AttachmentVO } from '@/api/iot/maintain/repairOrder'
import { ProductApi } from '@/api/iot/product/product'
// 定义FormData类型
interface RepairFormData {
id: number
repairNo: string
reportTime: string
reporter: string
contact: string
deviceId: number
deviceName: string
deviceCode: string
faultDesc: string
faultTime: string
faultLevel: string
affectSafety: boolean
status: string
attachments: AttachmentVO[]
}
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const formData = ref<RepairFormData>({
id: 0,
repairNo: '',
reportTime: '',
reporter: '',
contact: '',
deviceId: 0,
deviceName: '',
deviceCode: '',
faultDesc: '',
faultTime: '',
faultLevel: '',
affectSafety: false,
status: '',
attachments: []
})
const formRules = reactive({
repairNo: [{ required: true, message: '报修单号不能为空', trigger: 'blur' }],
reportTime: [{ required: true, message: '报修日期不能为空', trigger: 'blur' }],
reporter: [{ required: true, message: '报修人不能为空', trigger: 'blur' }],
contact: [{ required: true, message: '联系方式不能为空', trigger: 'blur' }],
deviceId: [{ required: true, message: '请选择产品', trigger: 'change' }],
faultDesc: [{ required: true, message: '故障现象不能为空', trigger: 'blur' }],
faultTime: [{ required: true, message: '故障发生日期不能为空', trigger: 'blur' }],
faultLevel: [{ required: true, message: '请选择故障严重程度', trigger: 'change' }],
affectSafety: [{ required: true, message: '请选择是否影响安全', trigger: 'change' }],
status: [{ required: true, message: '请选择当前状态', trigger: 'change' }]
})
const formRef = ref()
// 产品下拉相关
const productOptions = ref<any[]>([])
const productLoading = ref(false)
async function searchProduct(query: string) {
productLoading.value = true
try {
const res = await ProductApi.getSimpleProductList()
productOptions.value = query
? res.filter(p => (p.productName || p.name).includes(query) || (p.productKey || p.code).includes(query))
: res
} finally {
productLoading.value = false
}
}
function onProductChange(val: number) {
const prod = productOptions.value.find(p => p.id === val)
if (prod) {
formData.value.deviceName = prod.productName || prod.name
formData.value.deviceCode = prod.productKey || prod.code
} else {
formData.value.deviceName = ''
formData.value.deviceCode = ''
}
}
// 附件上传相关
const fileList = ref<any[]>([])
// 自定义上传处理
const customUpload = async (options: any) => {
const { file, onSuccess, onError } = options;
try {
console.log('[DEBUG] 开始上传文件:', file.name);
// 使用RepairOrderApi.uploadImage上传图片
const response = await RepairOrderApi.uploadImage(file);
// 添加到文件列表
fileList.value.push({
name: response.name || file.name,
url: response.url || response,
status: 'success',
uid: file.uid
});
// 更新formData.attachments
formData.value.attachments = fileList.value.map(f => ({
url: f.url,
name: f.name || '未命名附件'
}));
console.log('[DEBUG] 上传成功,文件列表:', fileList.value);
console.log('[DEBUG] 附件数据:', formData.value.attachments);
ElMessage.success('上传成功');
onSuccess({ url: response.url || response, name: response.name || file.name });
} catch (error) {
console.error('[ERROR] 上传失败:', error);
ElMessage.error('上传失败');
onError({ status: 500, message: '上传失败' });
}
};
// 删除文件处理
function handleUploadRemove(file: any, fileListArr: any[]) {
console.log('[DEBUG] 删除文件:', file);
// 通过uid找到并删除文件
const idx = fileList.value.findIndex(f => f.uid === file.uid);
if (idx !== -1) {
fileList.value.splice(idx, 1);
// 更新formData中的attachments
formData.value.attachments = fileList.value.map(f => ({
url: f.url,
name: f.name || '未命名附件'
}));
console.log('[DEBUG] 删除后的文件列表:', fileList.value);
console.log('[DEBUG] 删除后的附件数据:', formData.value.attachments);
}
}
const emit = defineEmits(['success'])
const open = async (type: string, id?: number) => {
// 先确保表单和文件列表重置
resetForm();
// 设置对话框属性
dialogVisible.value = true;
dialogTitle.value = type === 'create' ? '新增报修单' : '编辑报修单';
formType.value = type;
// 如果是创建模式,自动生成报修单号
if (type === 'create') {
// 生成格式为 "BX" + 年月日 + 4位随机数
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const randomNum = Math.floor(1000 + Math.random() * 9000); // 生成1000-9999之间的随机数
formData.value.repairNo = `BX${year}${month}${day}${randomNum}`;
formData.value.reportTime = now.toISOString();
}
// 如果是编辑模式
if (type === 'update' && id) {
formLoading.value = true;
try {
// 获取报修单数据
const res = await RepairOrderApi.getRepairOrder(id);
console.log('[DEBUG] 获取到的数据:', res);
// 处理附件数据类型
let processedAttachments: Array<{url: string, name: string}> = [];
// 解析附件数据 - 确保是数组格式
if (typeof res.attachments === 'string' && res.attachments) {
try {
// 尝试解析JSON字符串
const parsed = JSON.parse(res.attachments);
console.log('[DEBUG] 解析JSON后的附件:', parsed);
if (Array.isArray(parsed)) {
processedAttachments = parsed.map(item => {
// 确保每个项都有url和name属性
if (typeof item === 'string') {
return { url: item, name: item.split('/').pop() || '附件' };
} else if (typeof item === 'object' && item !== null) {
return {
url: item.url || '',
name: item.name || item.url?.split('/').pop() || '附件'
};
}
return { url: '', name: '无效附件' };
}).filter(item => item.url); // 过滤掉没有url的项
}
} catch (e) {
console.error('[ERROR] 解析附件数据失败:', e);
// 如果解析失败,尝试处理逗号分隔的字符串
if (res.attachments.includes(',')) {
processedAttachments = res.attachments.split(',')
.filter(url => url.trim())
.map(url => ({
url,
name: url.split('/').pop() || '附件'
}));
} else if (res.attachments.trim()) {
// 单个URL字符串
processedAttachments = [{
url: res.attachments,
name: res.attachments.split('/').pop() || '附件'
}];
}
}
} else if (Array.isArray(res.attachments)) {
// 已经是数组格式
processedAttachments = res.attachments.map(item => {
if (typeof item === 'string') {
return { url: item, name: item.split('/').pop() || '附件' };
} else {
return {
url: item.url || '',
name: item.name || item.url?.split('/').pop() || '附件'
};
}
}).filter(item => item.url);
}
// 设置处理后的附件数据
res.attachments = processedAttachments;
console.log('[DEBUG] 处理后的附件数据:', processedAttachments);
// 将数据赋值给表单
Object.assign(formData.value, res);
// 设置文件列表
if (processedAttachments.length > 0) {
// 确保每个文件有唯一的uid
fileList.value = processedAttachments.map((item, idx) => ({
url: item.url,
name: item.name || `附件${idx+1}`,
status: 'success',
uid: Date.now() + idx // 使用时间戳+索引确保唯一性
}));
console.log('[DEBUG] 编辑模式文件列表:', JSON.stringify(fileList.value));
} else {
fileList.value = [];
formData.value.attachments = [];
}
} finally {
formLoading.value = false;
}
}
}
defineExpose({ open })
const resetForm = () => {
console.log('[DEBUG] 重置表单');
// 清空文件列表
fileList.value = [];
// 重置表单数据
formData.value = {
id: 0,
repairNo: '',
reportTime: '',
reporter: '',
contact: '',
deviceId: 0,
deviceName: '',
deviceCode: '',
faultDesc: '',
faultTime: '',
faultLevel: '',
affectSafety: false,
status: '',
attachments: []
};
// 重置表单字段
nextTick(() => {
if (formRef.value) {
formRef.value.resetFields();
}
});
}
const submitForm = async () => {
// 表单验证
try {
await formRef.value.validate();
} catch (e) {
console.error('[ERROR] 表单验证失败:', e);
return;
}
// 开始提交
formLoading.value = true;
try {
console.log('[DEBUG] 开始提交表单, 文件列表数量:', fileList.value.length);
// 创建一个新的表单数据对象避免直接修改formData
const submitData = { ...formData.value };
// 确保attachments中的内容与fileList同步
submitData.attachments = fileList.value
.filter(file => file && file.url && file.url.trim() !== '')
.map(file => ({
url: file.url,
name: file.name || file.url.split('/').pop() || '未命名附件'
}));
console.log('[DEBUG] 提交的附件数据:', JSON.stringify(submitData.attachments));
console.log('[DEBUG] 提交的附件数量:', submitData.attachments.length);
// 提交表单
if (formType.value === 'create') {
await RepairOrderApi.createRepairOrder(submitData);
ElMessage.success('新增成功');
} else {
await RepairOrderApi.updateRepairOrder(submitData);
ElMessage.success('编辑成功');
}
// 关闭对话框并刷新列表
dialogVisible.value = false;
emit('success');
} catch (error) {
console.error('[ERROR] 提交失败:', error);
ElMessage.error('提交失败: ' + (error instanceof Error ? error.message : String(error)));
} finally {
formLoading.value = false;
}
}
onMounted(() => {
searchProduct('')
})
</script>
<style scoped>
.tip-text {
margin-top: 5px;
font-size: 12px;
color: #409eff;
}
.debug-info {
margin-top: 10px;
font-size: 12px;
color: #999;
white-space: pre-wrap;
overflow-wrap: break-word;
}
</style>

View File

@@ -0,0 +1,189 @@
<template>
<ContentWrap>
<!-- 搜索栏 -->
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="90px">
<el-form-item label="报修单号" prop="repairNo">
<el-input v-model="queryParams.repairNo" placeholder="请输入报修单号" clearable @keyup.enter="handleQuery" class="!w-200px" />
</el-form-item>
<el-form-item label="报修人" prop="reporter">
<el-input v-model="queryParams.reporter" placeholder="请输入报修人" clearable class="!w-160px" />
</el-form-item>
<el-form-item label="设备编码" prop="deviceCode">
<el-input v-model="queryParams.deviceCode" placeholder="请输入设备编码" clearable class="!w-160px" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-140px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 查询</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" plain @click="openForm('create')">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="报修单号" align="center" prop="repairNo" />
<el-table-column label="报修日期" align="center" prop="reportTime">
<template #default="scope">
{{ formatDate(scope.row.reportTime) }}
</template>
</el-table-column>
<el-table-column label="报修人" align="center" prop="reporter" />
<el-table-column label="联系方式" align="center" prop="contact" />
<el-table-column label="设备编码" align="center" prop="deviceCode" />
<el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="故障现象" align="center" prop="faultDesc" />
<el-table-column label="故障发生日期" align="center" prop="faultTime">
<template #default="scope">
{{ formatDate(scope.row.faultTime) }}
</template>
</el-table-column>
<el-table-column label="故障严重程度" align="center" prop="faultLevel">
<template #default="scope">
{{ faultLevelLabel(scope.row.faultLevel) }}
</template>
</el-table-column>
<el-table-column label="是否影响安全" align="center" prop="affectSafety">
<template #default="scope">
<el-tag v-if="scope.row.affectSafety === true || scope.row.affectSafety === '是'" type="danger"></el-tag>
<el-tag v-else type="success"></el-tag>
</template>
</el-table-column>
<el-table-column label="当前状态" align="center" prop="status">
<template #default="scope">
<el-tag>{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" min-width="200px">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)" style="margin-right: 8px;">
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)" style="margin-right: 8px;">
删除
</el-button>
<el-dropdown trigger="click">
<el-button link type="info" style="padding: 0 8px;">
状态 <el-icon style="margin-left: 2px;"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="s in statusOptions"
:key="s.value"
@click="changeStatus(scope.row, s.value)"
>
<el-tag :type="scope.row.status === s.value ? 'primary' : 'info'" effect="plain" style="margin-right: 8px;">
{{ s.label }}
</el-tag>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<RepairOrder ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, Ref } from 'vue'
import RepairOrder from './RepairOrder.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { RepairOrderApi } from '@/api/iot/maintain/repairOrder'
import { ArrowDown } from '@element-plus/icons-vue'
const loading = ref(false)
const list: Ref<any[]> = ref([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
repairNo: '',
reporter: '',
deviceCode: '',
status: ''
})
const queryFormRef = ref()
const statusOptions = [
{ label: '待受理', value: 'pending' },
{ label: '已派工', value: 'assigned' },
{ label: '维修中', value: 'repairing' },
{ label: '待验收', value: 'to_accept' },
{ label: '已完成', value: 'finished' },
{ label: '已关闭', value: 'closed' }
]
function statusLabel(val: string) {
const found = statusOptions.find(s => s.value === val)
return found ? found.label : val
}
const formRef = ref()
const faultLevelMap = {
urgent: '紧急',
high: '高',
medium: '中',
low: '低'
}
function faultLevelLabel(val: string) {
return faultLevelMap[val] || val
}
const getList = async () => {
loading.value = true
try {
const res = await RepairOrderApi.getRepairOrderList(queryParams as any)
list.value = res.list || []
console.log(list)
total.value = res.total || 0
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
const handleDelete = async (id: number) => {
await ElMessageBox.confirm('确定删除该报修单吗?')
await RepairOrderApi.deleteRepairOrder(id)
ElMessage.success('删除成功')
getList()
}
const changeStatus = async (row: any, status: string) => {
await RepairOrderApi.updateRepairOrder({ ...row, status })
ElMessage.success('状态已更新')
getList()
}
function formatDate(ts: number) {
if (!ts) return ''
const date = new Date(ts)
return date.toLocaleString()
}
onMounted(() => {
getList()
})
</script>