Files
hzdyjs/web/src/views/hq/pm/repairOrder/index.vue
T
2025-08-03 13:20:32 +08:00

3933 lines
129 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>
<div class="p-2 page-container">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<!-- 第一行:4个搜索条件 -->
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="工单编号" prop="orderNo">
<el-input v-model="queryParams.orderNo" placeholder="请输入工单编号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="报修标题" prop="title">
<el-input v-model="queryParams.title" placeholder="请输入报修标题" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="报修地点" prop="repairLocation">
<el-input v-model="queryParams.repairLocation" placeholder="请输入报修地点" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="维修类型" prop="categoryId">
<el-tree-select
v-model="queryParams.categoryId"
:data="categoryTreeOptions"
:props="{ value: 'id', label: 'categoryName', children: 'children' }"
value-key="id"
placeholder="请选择维修类型"
check-strictly
:render-after-expand="false"
clearable
filterable
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 第二行:4个搜索条件 + 操作按钮 -->
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="紧急程度" prop="priorityLevel">
<el-select v-model="queryParams.priorityLevel" placeholder="请选择紧急程度" clearable style="width: 100%">
<el-option v-for="dict in pm_priority_level" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="工单状态" prop="orderStatus">
<el-select v-model="queryParams.orderStatus" placeholder="请选择工单状态" clearable style="width: 100%">
<el-option v-for="dict in pm_order_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="报修人" prop="reporterName">
<el-input v-model="queryParams.reporterName" placeholder="请输入报修人姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="创建时间">
<el-date-picker
v-model="daterangeCreateTime"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleQuery"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<!-- 操作按钮行 -->
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 统计卡片区域 - 仅维护管理员和超级管理员可见 -->
<!-- <div v-if="canViewStatistics" class="mb-[10px]">
<el-row :gutter="16">
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-primary">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Files /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.totalOrders }}</div>
<div class="stat-label">总工单数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-warning">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.pendingAssign }}</div>
<div class="stat-label">待分派</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-info">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Loading /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.inProgress }}</div>
<div class="stat-label">进行中</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-success">
<div class="stat-content">
<div class="stat-icon">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.completed }}</div>
<div class="stat-label">已完成</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-info">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Timer /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.avgProcessTime }}</div>
<div class="stat-label">平均时长(小时)</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-primary">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Star /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.avgEvaluationScore }}</div>
<div class="stat-label">平均评分</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-success">
<div class="stat-content">
<div class="stat-icon">
<el-icon><TrendCharts /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.monthlyNewOrders }}</div>
<div class="stat-label">本月新增</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-warning">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Finished /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.monthlyCompletedOrders }}</div>
<div class="stat-label">本月完成</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div> -->
<!-- 表格区域 -->
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['pm:repairOrder:add']">新增报修</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['pm:repairOrder:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['pm:repairOrder:remove']">删除</el-button>
</el-col>
<!-- <el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['pm:repairOrder:export']">导出</el-button>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<div class="table-container">
<el-table
v-loading="loading"
border
:data="repairOrderList"
@selection-change="handleSelectionChange"
@row-dblclick="handleDetail"
style="width: 100%"
:scroll-x="true"
class="scroll-table"
>
<el-table-column type="selection" width="55" align="center" fixed="left" />
<el-table-column label="工单编号" align="center" prop="orderNo" width="140" show-overflow-tooltip fixed="left" />
<el-table-column label="报修标题" align="center" prop="title" min-width="200" show-overflow-tooltip />
<el-table-column label="报修地点" align="center" prop="repairLocation" min-width="180" show-overflow-tooltip />
<el-table-column label="维修类型" align="center" prop="categoryName" width="120" show-overflow-tooltip>
<template #default="scope">
{{ getCategoryNameById(scope.row.categoryId) }}
</template>
</el-table-column>
<el-table-column label="紧急程度" align="center" prop="priorityLevel" width="100">
<template #default="scope">
<el-tag :type="getPriorityTagType(scope.row.priorityLevel) as any" size="small">
{{ getPriorityText(scope.row.priorityLevel) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="报修人" align="center" prop="reporterName" width="100" show-overflow-tooltip />
<el-table-column label="报修人电话" align="center" prop="reporterPhone" width="120" show-overflow-tooltip />
<el-table-column label="工单状态" align="center" prop="orderStatus" width="100">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.orderStatus) as any" size="small">
{{ getStatusText(scope.row.orderStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="分派维修员" align="center" prop="assigneeName" width="120" show-overflow-tooltip />
<el-table-column label="期望完成时间" align="center" prop="expectedFinishTime" width="180">
<template #default="scope">
<span v-if="scope.row.expectedFinishTime">{{ proxy.parseTime(scope.row.expectedFinishTime, '{y}-{m}-{d} {h}:{i}') }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="实际完成时间" align="center" prop="finishTime" width="180">
<template #default="scope">
<span v-if="scope.row.finishTime">{{ proxy.parseTime(scope.row.finishTime, '{y}-{m}-{d} {h}:{i}') }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="处理时长" align="center" prop="totalDuration" width="100">
<template #default="scope">
<span v-if="scope.row.totalDuration">{{ formatDuration(scope.row.totalDuration) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="评价得分" align="center" prop="evaluationScore" width="200">
<template #default="scope">
<el-rate
v-if="scope.row.evaluationScore"
:model-value="scope.row.evaluationScore"
disabled
show-score
text-color="#ff9900"
score-template="{value}分"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="附件" align="center" prop="attachmentCount" width="80">
<template #default="scope">
<el-link v-if="scope.row.attachmentCount > 0" @click="handleDetail(scope.row)" type="primary" :underline="false">
<el-icon><Paperclip /></el-icon>{{ scope.row.attachmentCount }}
</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ proxy.parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="280" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<!-- 待分派状态 (1) - 可以编辑、分派、删除 -->
<template v-if="scope.row.orderStatus === '1' || scope.row.orderStatus === 1">
<el-tooltip content="编辑" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['pm:repairOrder:edit']"></el-button>
</el-tooltip>
<el-tooltip content="分派" placement="top" v-if="canAssign">
<el-button
link
type="success"
icon="UserFilled"
@click="handleAssign(scope.row)"
v-hasPermi="['pm:repairOrder:assign']"
></el-button>
</el-tooltip>
<el-tooltip content="查看详情" placement="top">
<el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['pm:repairOrder:remove']"></el-button>
</el-tooltip>
</template>
<!-- 已分派状态 (2) - 可以查看详情、查看进度 -->
<template v-else-if="scope.row.orderStatus === '2' || scope.row.orderStatus === 2">
<el-tooltip content="查看详情" placement="top">
<el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
</el-tooltip>
<el-tooltip content="查看进度" placement="top">
<el-button
link
type="primary"
icon="Histogram"
@click="handleProgress(scope.row)"
v-hasPermi="['pm:repairOrder:progress']"
></el-button>
</el-tooltip>
<el-tooltip content="更新进度" placement="top" v-if="canUpdateProgress(scope.row)">
<el-button
link
type="warning"
icon="collection"
@click="handleUpdateProgress(scope.row)"
v-hasPermi="['pm:repairOrder:updateProgress']"
></el-button>
</el-tooltip>
</template>
<!-- 进行中状态 (3) - 可以查看详情、查看进度、完成工单 -->
<template v-else-if="scope.row.orderStatus === '3' || scope.row.orderStatus === 3">
<el-tooltip content="查看详情" placement="top">
<el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
</el-tooltip>
<el-tooltip content="查看进度" placement="top">
<el-button
link
type="primary"
icon="Histogram"
@click="handleProgress(scope.row)"
v-hasPermi="['pm:repairOrder:progress']"
></el-button>
</el-tooltip>
<el-tooltip content="更新进度" placement="top" v-if="canUpdateProgress(scope.row)">
<el-button
link
type="warning"
icon="collection"
@click="handleUpdateProgress(scope.row)"
v-hasPermi="['pm:repairOrder:updateProgress']"
></el-button>
</el-tooltip>
<el-tooltip content="完成工单" placement="top" v-if="canComplete(scope.row)">
<el-button
link
type="success"
icon="CircleCheck"
@click="handleComplete(scope.row)"
v-hasPermi="['pm:repairOrder:complete']"
></el-button>
</el-tooltip>
</template>
<!-- 已完成状态 (4) - 可以查看详情、查看进度、服务评价 -->
<template v-else-if="scope.row.orderStatus === '4' || scope.row.orderStatus === 4">
<el-tooltip content="查看详情" placement="top">
<el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
</el-tooltip>
<el-tooltip content="查看进度" placement="top">
<el-button
link
type="primary"
icon="Histogram"
@click="handleProgress(scope.row)"
v-hasPermi="['pm:repairOrder:progress']"
></el-button>
</el-tooltip>
<el-tooltip content="服务评价" placement="top" v-if="canEvaluate(scope.row)">
<el-button link type="warning" icon="Star" @click="handleEvaluate(scope.row)" v-hasPermi="['pm:repairOrder:evaluate']"></el-button>
</el-tooltip>
</template>
<!-- 其他状态的默认操作 -->
<template v-else>
<el-tooltip content="查看详情" placement="top">
<el-button link type="info" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['pm:repairOrder:query']"></el-button>
</el-tooltip>
<el-tooltip content="查看进度" placement="top">
<el-button
link
type="primary"
icon="Histogram"
@click="handleProgress(scope.row)"
v-hasPermi="['pm:repairOrder:progress']"
></el-button>
</el-tooltip>
</template>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页组件 -->
<div class="pagination-container">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</el-card>
<!-- 添加或修改报修工单对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="900px" append-to-body>
<el-form ref="repairOrderFormRef" :model="form" :rules="rules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="报修标题" prop="title">
<el-input v-model="form.title" placeholder="请输入报修标题" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="维修类型" prop="categoryId">
<el-tree-select
v-model="form.categoryId"
:data="categoryTreeOptions"
:props="{ value: 'id', label: 'categoryName', children: 'children' }"
value-key="id"
placeholder="请选择维修类型"
check-strictly
:render-after-expand="false"
clearable
filterable
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="报修地点" prop="repairLocation">
<el-input v-model="form.repairLocation" placeholder="请输入报修地点" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="紧急程度" prop="priorityLevel">
<el-select v-model="form.priorityLevel" placeholder="请选择紧急程度" style="width: 100%">
<el-option v-for="dict in pm_priority_level" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="报修人姓名" prop="reporterName">
<el-input
v-model="form.reporterName"
placeholder="自动填充当前用户姓名"
readonly
disabled
style="background-color: #f5f7fa; color: #909399"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报修人电话" prop="reporterPhone">
<el-input v-model="form.reporterPhone" placeholder="请输入报修人电话" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="报修人部门" prop="reporterDept">
<el-input
v-model="form.reporterDept"
placeholder="自动填充当前用户部门"
readonly
disabled
style="background-color: #f5f7fa; color: #909399"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="期望完成时间" prop="expectedFinishTime">
<el-date-picker
v-model="form.expectedFinishTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择期望完成时间"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="问题描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="4" placeholder="请详细描述遇到的问题..." />
</el-form-item>
<el-form-item label="备注信息" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息..." />
</el-form-item>
<!-- 附件上传 -->
<el-form-item label="上传附件">
<div class="attachment-upload-container">
<el-upload
v-model:file-list="attachmentList"
class="upload-demo"
drag
list-type="picture-card"
:on-preview="handleFilePreview"
:action="uploadAction"
:headers="uploadHeaders"
:data="uploadData"
:auto-upload="true"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-remove="handleRemoveAttachment"
:on-change="handleFileChange"
multiple
:limit="15"
:show-file-list="true"
accept="image/*,video/*,.pdf,.doc,.docx,.txt"
>
<!-- <el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> -->
<template #tip>
<div class="el-upload__tip">
支持图片、视频、PDF、Word文档等格式,单个文件大小不超过10MB,最多上传15个文件。
<span class="el-upload__tip" v-if="!form.id" style="color: #ff0000">(新增时文件会立即上传,保存工单后自动关联)</span>
<span class="el-upload__tip" v-else style="color: #ff0000">(编辑时文件会立即上传并关联到当前工单)</span>
</div>
</template>
<!-- 调整拖拽上传图标与文字在同一行、居中对齐 -->
<div class="el-upload-dragger1">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</div>
<!-- 自定义文件卡片缩略图(含文件名) -->
<template #file="{ file }">
<div class="upload-file-card">
<!-- 缩略图区域 -->
<div class="upload-file-thumb">
<!-- {{ file.url }} -->
<image-preview v-if="isImageFile(file) && file.url" :src="file.url" class="upload-thumb-image" :isOssId="true" />
<!-- <el-image
v-if="isImageFile(file) && file.url"
:src="file.url"
fit="cover"
:preview-src-list="[file.url]"
class="upload-thumb-image"
/> -->
<div v-else class="upload-file-placeholder">
<el-icon><Document /></el-icon>
<span class="file-ext">{{ getFileExt(file.name) }}</span>
</div>
<!-- loading 遮罩只在上传中显示 -->
<div v-if="file.status === 'uploading'" class="upload-file-loading">
<el-icon class="is-loading"><Loading /></el-icon>
<span class="loading-text">上传中...</span>
</div>
</div>
<!-- 文件名称 -->
<div class="upload-file-name" :title="file.name">
{{ file.name }}
</div>
<!-- 操作按钮区域 (与预览界面样式一致) -->
<div class="upload-file-card-actions" v-if="file.status && file.status !== 'ready'">
<el-button-group size="small">
<el-button type="success" @click.stop="handleDownloadAttachment(file)" title="下载" :disabled="file.status !== 'success'">
<el-icon><Download /></el-icon>
</el-button>
<el-button type="danger" @click.stop="handleRemoveAttachmentConfirm(file)" title="删除">
<el-icon><Delete /></el-icon>
</el-button>
</el-button-group>
</div>
</div>
</template>
</el-upload>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer-right">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog v-model="detailDialog.visible" :title="detailDialog.title" width="900px" append-to-body>
<div v-loading="detailLoading" class="repair-order-detail">
<template v-if="orderDetail">
<!-- 基本信息 -->
<el-card class="detail-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title"
><el-icon><Document /></el-icon> 基本信息</span
>
</div>
</template>
<div class="detail-grid">
<div class="detail-item">
<label>工单编号:</label>
<span class="detail-value">{{ orderDetail.orderNo }}</span>
</div>
<div class="detail-item">
<label>工单状态:</label>
<el-tag :type="getStatusTagType(orderDetail.orderStatus) as 'success' | 'warning' | 'info' | 'primary' | 'danger'" size="small">
{{ getStatusText(orderDetail.orderStatus) }}
</el-tag>
</div>
<div class="detail-item">
<label>紧急程度:</label>
<el-tag :type="getPriorityTagType(orderDetail.priorityLevel) as 'success' | 'warning' | 'info' | 'primary' | 'danger'" size="small">
{{ getPriorityText(orderDetail.priorityLevel) }}
</el-tag>
</div>
<div class="detail-item">
<label>创建时间:</label>
<span class="detail-value">{{ orderDetail.createTime }}</span>
</div>
<div class="detail-item">
<label>维修类型:</label>
<span class="detail-value">{{ getCategoryNameById(orderDetail.categoryId) }}</span>
</div>
<div class="detail-item full-width">
<label>报修标题:</label>
<span class="detail-value">{{ orderDetail.title }}</span>
</div>
<div class="detail-item full-width">
<label>问题描述:</label>
<span class="detail-value">{{ orderDetail.description }}</span>
</div>
<div class="detail-item full-width" v-if="orderDetail.cancelReason">
<label>取消原因:</label>
<span class="detail-value description">{{ orderDetail.cancelReason }}</span>
</div>
<div class="detail-item full-width" v-if="orderDetail.remark">
<label>备注信息:</label>
<span class="detail-value">{{ orderDetail.remark }}</span>
</div>
</div>
</el-card>
<!-- 报修信息 -->
<el-card class="detail-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title"
><el-icon><User /></el-icon> 报修信息</span
>
</div>
</template>
<div class="detail-grid">
<div class="detail-item">
<label>报修人:</label>
<span class="detail-value">{{ orderDetail.reporterName }}</span>
</div>
<div class="detail-item">
<label>联系电话:</label>
<span class="detail-value">{{ orderDetail.reporterPhone }}</span>
</div>
<div class="detail-item">
<label>所属部门:</label>
<span class="detail-value">{{ orderDetail.reporterDept || '暂无' }}</span>
</div>
<div class="detail-item">
<label>期望完成时间:</label>
<span class="detail-value">{{ orderDetail.expectedFinishTime || '无要求' }}</span>
</div>
<div class="detail-item full-width">
<label>报修地点:</label>
<span class="detail-value">{{ orderDetail.repairLocation }}</span>
</div>
</div>
</el-card>
<!-- 维修信息 -->
<el-card class="detail-card" shadow="never" v-if="orderDetail.assigneeId">
<template #header>
<div class="card-header">
<span class="card-title"
><el-icon><Tools /></el-icon> 维修信息</span
>
</div>
</template>
<div class="detail-grid">
<div class="detail-item">
<label>维修员:</label>
<span class="detail-value">{{ orderDetail.assigneeName || '暂未分派' }}</span>
</div>
<div class="detail-item">
<label>分派时间:</label>
<span class="detail-value">{{ orderDetail.assignTime || '暂未分派' }}</span>
</div>
<div class="detail-item">
<label>开始时间:</label>
<span class="detail-value">{{ orderDetail.startTime || '暂未开始' }}</span>
</div>
<div class="detail-item">
<label>完成时间:</label>
<span class="detail-value">{{ orderDetail.finishTime || '暂未完成' }}</span>
</div>
<div class="detail-item">
<label>处理时长:</label>
<span class="detail-value">
{{ orderDetail.totalDuration ? formatDuration(orderDetail.totalDuration) : '暂无' }}
</span>
</div>
<div class="detail-item">
<label>进度记录:</label>
<span class="detail-value">{{ orderDetail.progressCount }}条</span>
</div>
<div class="detail-item full-width" v-if="orderDetail.assignRemark">
<label>分派备注:</label>
<span class="detail-value">{{ orderDetail.assignRemark }}</span>
</div>
</div>
</el-card>
<!-- 维修结果 -->
<el-card class="detail-card" shadow="never" v-if="orderDetail.repairResult || orderDetail.usedMaterials || orderDetail.followSuggestions">
<template #header>
<div class="card-header">
<span class="card-title"
><el-icon><Check /></el-icon> 维修结果</span
>
</div>
</template>
<div class="detail-grid">
<div class="detail-item full-width" v-if="orderDetail.repairResult">
<label>维修结果:</label>
<span class="detail-value">{{ orderDetail.repairResult }}</span>
</div>
<div class="detail-item full-width" v-if="orderDetail.usedMaterials">
<label>使用材料:</label>
<span class="detail-value">{{ orderDetail.usedMaterials }}</span>
</div>
<div class="detail-item full-width" v-if="orderDetail.followSuggestions">
<label>后续建议:</label>
<span class="detail-value">{{ orderDetail.followSuggestions }}</span>
</div>
</div>
</el-card>
<!-- 评价信息 -->
<el-card class="detail-card" shadow="never" v-if="orderDetail.evaluationScore || orderDetail.evaluationContent">
<template #header>
<div class="card-header">
<span class="card-title"
><el-icon><Star /></el-icon> 服务评价</span
>
</div>
</template>
<div class="detail-grid">
<div class="detail-item">
<label>综合评分:</label>
<span class="detail-value">
<el-rate v-model="orderDetail.evaluationScore" disabled show-score text-color="#ff9900" />
</span>
</div>
<div class="detail-item">
<label>评价时间:</label>
<span class="detail-value">{{ orderDetail.evaluationTime || '暂未评价' }}</span>
</div>
<div class="detail-item full-width" v-if="orderDetail.evaluationContent">
<label>评价内容:</label>
<span class="detail-value">{{ orderDetail.evaluationContent }}</span>
</div>
</div>
</el-card>
<!-- 附件信息 -->
<el-card class="detail-card" shadow="never" v-if="orderDetail.attachmentCount && orderDetail.attachmentCount > 0">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon><Paperclip /></el-icon> 附件信息 ({{ orderDetail.attachmentCount }}个文件)
</span>
<el-button
type="primary"
size="small"
@click="refreshAttachments(orderDetail.id)"
:loading="attachmentLoading"
style="margin-left: auto"
>
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</template>
<div v-loading="attachmentLoading" class="attachment-detail-container">
<div v-if="currentAttachments.length === 0" class="no-attachment-detail">
<el-empty description="暂无附件" :image-size="60" />
</div>
<div v-else class="attachment-grid-container">
<div v-for="attachment in currentAttachments" :key="attachment.id" class="attachment-card">
<!-- 附件缩略图 -->
<div class="attachment-thumbnail-wrapper">
<!-- <el-image
v-if="attachment.fileType === 'image'"
:src="attachment.filePath"
:preview-src-list="getImagePreviewList()"
fit="cover"
class="attachment-image"
lazy
/> -->
<image-preview v-if="attachment.fileType === 'image'" :src="attachment.filePath" class="attachment-image" :isOssId="true" />
<div v-else class="attachment-file-icon">
<el-icon :size="28">
<Document v-if="attachment.fileType === 'document'" />
<VideoPlay v-else-if="attachment.fileType === 'video'" />
<Files v-else />
</el-icon>
<span class="file-type-text">{{ getFileTypeLabel(attachment.fileType) }}</span>
</div>
</div>
<!-- 附件信息 -->
<div class="attachment-card-body">
<div class="attachment-title" :title="attachment.fileName">
{{ attachment.fileName }}
</div>
<div class="attachment-meta">
<span class="file-size">{{ formatFileSize(attachment.fileSize) }}</span>
<span class="upload-time">{{ formatUploadTime(attachment.createTime) }}</span>
</div>
<div v-if="attachment.description" class="attachment-desc" :title="attachment.description">
{{ attachment.description }}
</div>
</div>
<!-- 附件操作 -->
<div class="attachment-card-actions">
<el-button-group size="small">
<!-- <el-button
type="primary"
@click="previewAttachment(attachment)"
:disabled="!canPreview(attachment)"
:title="canPreview(attachment) ? '预览' : '不支持预览'"
>
<el-icon><View /></el-icon>
</el-button> -->
<el-button type="success" @click="downloadAttachment(attachment)" title="下载">
<el-icon><Download /></el-icon>
</el-button>
<el-button type="info" @click="showAttachmentDetail(attachment)" title="详情">
<el-icon><InfoFilled /></el-icon>
</el-button>
<!-- 在详情查看模式下不显示删除按钮 -->
<el-button
v-if="canDeleteAttachment(attachment) && !detailDialog.visible"
type="danger"
@click="confirmDeleteAttachment(attachment)"
title="删除"
>
<el-icon><Delete /></el-icon>
</el-button>
</el-button-group>
</div>
</div>
</div>
</div>
</el-card>
</template>
</div>
<template #footer>
<div class="dialog-footer-right">
<el-button @click="detailDialog.visible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 工单分派对话框 -->
<el-dialog title="工单分派" v-model="assignDialog.visible" width="600px" append-to-body>
<el-form ref="assignFormRef" :model="assignForm" :rules="assignRules" label-width="120px">
<el-form-item label="工单信息">
<div class="order-info">
<p><strong>工单编号:</strong>{{ currentOrder?.orderNo }}</p>
<p><strong>报修标题:</strong>{{ currentOrder?.title }}</p>
<p><strong>报修地点:</strong>{{ currentOrder?.repairLocation }}</p>
<p><strong>维修类型:</strong>{{ getCategoryNameById(currentOrder?.categoryId) }}</p>
<p><strong>问题描述:</strong>{{ currentOrder?.description }}</p>
<p><strong>备注信息:</strong>{{ currentOrder?.remark }}</p>
</div>
</el-form-item>
<el-form-item label="分派维修员" prop="assigneeId">
<el-select v-model="assignForm.assigneeId" placeholder="请选择维修员" style="width: 100%" @change="handleWorkerChange">
<el-option v-for="worker in workerOptions" :key="worker.value" :label="worker.label" :value="worker.value" />
</el-select>
</el-form-item>
<el-form-item label="分派说明" prop="assignRemark">
<el-input v-model="assignForm.assignRemark" type="textarea" :rows="4" placeholder="请输入分派说明,如特殊要求、注意事项等..." />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer-right">
<el-button :loading="buttonLoading" type="primary" @click="submitAssign">确认分派</el-button>
<el-button @click="cancelAssign">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 进度查看对话框 -->
<el-dialog title="进度查看" v-model="progressDialog.visible" width="700px" append-to-body>
<div v-loading="progressLoading" class="progress-container">
<!-- 空状态 -->
<div v-if="progressList.length === 0 && !progressLoading" class="no-progress">
<el-empty description="暂无进度记录" :image-size="80" />
</div>
<!-- 进度时间线 -->
<div v-else class="progress-timeline">
<div v-for="(progress, index) in progressList" :key="index" class="progress-item">
<!-- 时间线图标 -->
<div class="timeline-icon">
<el-icon :size="16" :color="getProgressIconColor(progress.progressType)">
<Clock v-if="progress.progressType === '1'" />
<UserFilled v-else-if="progress.progressType === '2'" />
<Location v-else-if="progress.progressType === '3'" />
<Tools v-else-if="progress.progressType === '4'" />
<Edit v-else-if="progress.progressType === '5'" />
<CircleCheck v-else-if="progress.progressType === '6'" />
<Star v-else-if="progress.progressType === '7'" />
<Files v-else-if="progress.progressType === '8'" />
<More v-else />
</el-icon>
</div>
<!-- 时间线连接线 -->
<div v-if="index < progressList.length - 1" class="timeline-line"></div>
<!-- 进度卡片 -->
<div class="progress-card">
<div class="progress-header">
<div class="progress-type-status">
<el-tag :type="getProgressTypeTag(progress.progressType)" size="small">
{{ getProgressTypeText(progress.progressType) }}
</el-tag>
<span class="progress-status">{{ progress.progressStatus }}</span>
</div>
<div class="progress-time">
<el-icon><Clock /></el-icon>
{{ progress.createTime }}
</div>
</div>
<div class="progress-content">
<div class="progress-detail">
<p class="detail-text">{{ progress.progressDetail }}</p>
</div>
<!-- 仅维修管理人员可见,且进度为完成维修 -->
<div v-if="progress.progressType === '6' && (isSuperAdmin || isMaintenanceAdmin)" class="progress-extra-info">
<div v-if="progress.usedMaterials">
<label>使用材料:</label>
<span>{{ progress.usedMaterials }}</span>
</div>
<div v-if="progress.followSuggestions">
<label>后续建议:</label>
<span>{{ progress.followSuggestions }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer-right">
<el-button @click="progressDialog.visible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 进度更新对话框 -->
<el-dialog title="进度更新" v-model="updateProgressDialog.visible" width="600px" append-to-body>
<el-form ref="progressFormRef" :model="progressForm" :rules="progressRules" label-width="120px">
<!-- 只读展示工单编号 -->
<el-form-item label="工单编号" prop="orderNo">
<el-input v-model="progressForm.orderNo" placeholder="工单编号" readonly disabled style="background-color: #f5f7fa; color: #909399" />
</el-form-item>
<!-- <el-form-item label="进度类型" prop="progressType">
<el-select v-model="progressForm.progressType" placeholder="请选择进度类型" style="width: 100%">
<el-option v-for="type in progressTypeOptions" :key="type.value" :label="type.label" :value="type.value" />
</el-select>
</el-form-item> -->
<el-form-item label="进度状态" prop="progressStatus">
<el-input v-model="progressForm.progressStatus" placeholder="请输入进度状态" />
</el-form-item>
<el-form-item label="进度详细描述" prop="progressDetail">
<el-input v-model="progressForm.progressDetail" type="textarea" placeholder="请输入进度详细描述" />
</el-form-item>
<!-- <el-form-item label="进度位置信息" prop="locationInfo">
<el-input v-model="progressForm.locationInfo" placeholder="请输入进度位置信息" />
</el-form-item>
<el-form-item label="进度估计时长" prop="estimatedDuration">
<el-input v-model="progressForm.estimatedDuration" placeholder="请输入进度估计时长" />
</el-form-item>
<el-form-item label="进度下一步" prop="nextStep">
<el-input v-model="progressForm.nextStep" placeholder="请输入进度下一步" />
</el-form-item>
<el-form-item label="遇到的问题" prop="issuesEncountered">
<el-input v-model="progressForm.issuesEncountered" placeholder="请输入遇到的问题" />
</el-form-item>
<el-form-item label="需要的材料" prop="materialsNeeded">
<el-input v-model="progressForm.materialsNeeded" placeholder="请输入需要的材料" />
</el-form-item>
<el-form-item label="进度备注" prop="remark">
<el-input v-model="progressForm.remark" placeholder="请输入进度备注" />
</el-form-item> -->
</el-form>
<template #footer>
<div class="dialog-footer-right">
<el-button :loading="buttonLoading" type="primary" @click="submitProgress">确认更新</el-button>
<el-button @click="cancelProgress">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 完成工单对话框 -->
<el-dialog title="完成工单" v-model="completeDialog.visible" width="600px" append-to-body>
<el-form ref="completeFormRef" :model="completeForm" :rules="completeRules" label-width="120px">
<el-form-item label="工单编号" prop="orderNo">
<el-input v-model="completeForm.orderNo" placeholder="请输入工单编号" readonly disabled />
</el-form-item>
<el-form-item label="维修结果" prop="repairResult">
<el-input v-model="completeForm.repairResult" placeholder="请输入维修结果" />
</el-form-item>
<el-form-item label="使用材料" prop="usedMaterials">
<el-input v-model="completeForm.usedMaterials" type="textarea" placeholder="请输入使用材料" />
</el-form-item>
<el-form-item label="后续建议" prop="followSuggestions">
<el-input v-model="completeForm.followSuggestions" type="textarea" placeholder="请输入后续建议" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer-right">
<el-button :loading="buttonLoading" type="primary" @click="submitComplete">确认完成</el-button>
<el-button @click="cancelComplete">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 服务评价对话框 -->
<el-dialog title="服务评价" v-model="evaluationDialog.visible" width="600px" append-to-body>
<el-form ref="evaluationFormRef" :model="evaluationForm" :rules="evaluationRules" label-width="120px">
<el-form-item label="工单编号" prop="orderId">
<el-input
v-model="evaluationForm.orderId"
placeholder="自动填充工单编号"
readonly
disabled
style="background-color: #f5f7fa; color: #909399"
/>
</el-form-item>
<el-form-item label="评价员工" prop="evaluatedWorkerId">
<el-select v-model="evaluationForm.evaluatedWorkerId" placeholder="请选择评价员工" style="width: 100%">
<el-option v-for="worker in workerOptions" :key="worker.value" :label="worker.label" :value="worker.value" />
</el-select>
</el-form-item>
<el-form-item label="综合评分" prop="overallScore">
<el-rate v-model="evaluationForm.overallScore" :max="5" show-score />
</el-form-item>
<el-form-item label="质量评分" prop="qualityScore">
<el-rate v-model="evaluationForm.qualityScore" :max="5" show-score />
</el-form-item>
<el-form-item label="态度评分" prop="attitudeScore">
<el-rate v-model="evaluationForm.attitudeScore" :max="5" show-score />
</el-form-item>
<el-form-item label="响应速度评分" prop="responseSpeedScore">
<el-rate v-model="evaluationForm.responseSpeedScore" :max="5" show-score />
</el-form-item>
<el-form-item label="沟通能力评分" prop="communicationScore">
<el-rate v-model="evaluationForm.communicationScore" :max="5" show-score />
</el-form-item>
<el-form-item label="评价内容" prop="evaluationContent">
<el-input v-model="evaluationForm.evaluationContent" type="textarea" placeholder="请输入评价内容" />
</el-form-item>
<el-form-item label="满意度等级" prop="satisfactionLevel">
<el-select v-model="evaluationForm.satisfactionLevel" placeholder="请选择满意度等级" style="width: 100%">
<el-option v-for="level in satisfactionLevelOptions" :key="level.value" :label="level.label" :value="level.value" />
</el-select>
</el-form-item>
<!-- <el-form-item label="是否匿名" prop="isAnonymous">
<el-switch v-model="evaluationForm.isAnonymous" />
</el-form-item>
<el-form-item label="是否公开" prop="isPublic">
<el-switch v-model="evaluationForm.isPublic" />
</el-form-item>
<el-form-item label="正面标签" prop="positiveTags">
<el-input v-model="evaluationForm.positiveTags" placeholder="请输入正面标签" />
</el-form-item>
<el-form-item label="负面标签" prop="negativeTags">
<el-input v-model="evaluationForm.negativeTags" placeholder="请输入负面标签" />
</el-form-item>
<el-form-item label="改进建议" prop="improvementSuggestions">
<el-input v-model="evaluationForm.improvementSuggestions" placeholder="请输入改进建议" />
</el-form-item>
<el-form-item label="备注信息" prop="remark">
<el-input v-model="evaluationForm.remark" placeholder="请输入备注信息" />
</el-form-item> -->
</el-form>
<template #footer>
<div class="dialog-footer-right">
<el-button :loading="buttonLoading" type="primary" @click="submitEvaluation">确认评价</el-button>
<el-button @click="cancelEvaluation">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 附件查看对话框 -->
<el-dialog title="附件查看" v-model="attachmentDialog.visible" width="800px" append-to-body>
<div v-loading="attachmentLoading" class="attachment-container">
<div v-if="currentAttachments.length === 0" class="no-attachment">
<el-empty description="暂无附件" />
</div>
<div v-else class="attachment-list">
<div v-for="attachment in currentAttachments" :key="attachment.id" class="attachment-item">
<div class="attachment-info">
<div class="attachment-preview">
<el-image
v-if="attachment.fileType === 'image'"
:src="attachment.filePath"
:preview-src-list="[attachment.filePath]"
fit="cover"
style="width: 80px; height: 80px"
lazy
/>
<el-icon v-else class="file-icon" :size="48">
<Document v-if="attachment.fileType === 'document'" />
<VideoPlay v-else-if="attachment.fileType === 'video'" />
<Folder v-else />
</el-icon>
</div>
<div class="attachment-details">
<div class="file-name">{{ attachment.fileName }}</div>
<div class="file-meta">
<span>{{ formatFileSize(attachment.fileSize) }}</span>
<span>{{ attachment.uploaderName }}</span>
<span>{{ attachment.createTime }}</span>
</div>
<div v-if="attachment.description" class="file-description">
{{ attachment.description }}
</div>
</div>
</div>
<div class="attachment-actions">
<el-button type="primary" size="small" @click="downloadAttachment(attachment)">
<el-icon><Download /></el-icon>
下载
</el-button>
<el-button type="success" size="small" @click="previewAttachment(attachment)">
<el-icon><View /></el-icon>
预览
</el-button>
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer-right">
<el-button @click="attachmentDialog.visible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="RepairOrder">
import {
listRepairOrder,
getRepairOrder,
delRepairOrder,
addRepairOrder,
updateRepairOrder,
exportRepairOrders,
assignRepairOrder,
getRepairStatistics,
getMaintenanceWorkers,
getRepairProgress,
updateRepairProgress,
finishRepairOrder,
createServiceEvaluation,
getServiceEvaluation,
getRepairAttachments,
uploadAttachment,
uploadAttachments,
deleteAttachment,
batchUpdateAttachmentOrderId
} from '@/api/hq/pm/repairOrder';
import { getRepairCategoryOptions, getRepairCategoryTree } from '@/api/hq/pm/repairCategory';
import {
RepairOrderVO,
RepairOrderForm,
RepairOrderQuery,
AssignOrderForm,
RepairStatisticsVO,
RepairProgressVO,
ProgressUpdateForm,
ServiceEvaluationForm,
ServiceEvaluationVO,
RepairCategoryVO
} from '@/api/hq/pm/types';
import { useUserStore } from '@/store/modules/user';
import { getInfo } from '@/api/login'; // 添加获取用户详细信息的API
import { getToken } from '@/utils/auth'; // 修复:添加getToken导入
import { vLoading, ElLoading } from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
import request from '@/utils/request';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 用户store
const userStore = useUserStore();
const repairOrderList = ref<RepairOrderVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const daterangeCreateTime = ref<[string, string]>(['', '']);
// 当前用户信息
const currentUser = ref<any>(null);
// 统计数据
const statistics = ref<RepairStatisticsVO>({
totalOrders: 0,
pendingAssign: 0,
inProgress: 0,
completed: 0,
cancelled: 0,
avgProcessTime: 0,
avgEvaluationScore: 0,
monthlyNewOrders: 0,
monthlyCompletedOrders: 0,
statusDistribution: [],
categoryDistribution: [],
priorityDistribution: []
});
// 下拉选项数据
const categoryOptions = ref<Array<{ value: string | number; label: string }>>([]);
const categoryTreeOptions = ref<RepairCategoryVO[]>([]);
const workerOptions = ref<Array<{ value: string | number; label: string; userName?: string; deptName?: string }>>([]);
// 附件相关数据
const attachmentList = ref<any[]>([]);
const uploadAction = ref(import.meta.env.VITE_APP_BASE_API + '/hq/pm/attachment/upload'); // 修复:使用正确的API路径
const uploadHeaders = ref({
'Authorization': `Bearer ${getToken() || ''}`,
'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e'
}); // 修复:添加认证头
const uploadData = ref<any>({});
// 新增:临时文件存储,用于新增时收集文件
const tempFileList = ref<File[]>([]);
// 新增:临时附件ID列表,用于新增时收集已上传附件的ID
const tempAttachmentIds = ref<string[]>([]);
// 待删除的附件ID列表 (保存时统一删除)
const pendingDeleteAttachmentIds = ref<string[]>([]);
// 字典数据
const pm_priority_level = ref([
{ value: '1', label: '非常紧急' },
{ value: '2', label: '紧急' },
{ value: '3', label: '一般' },
{ value: '4', label: '不紧急' },
{ value: '5', label: '可延后' }
]);
const pm_order_status = ref([
{ value: '1', label: '待分派' },
{ value: '2', label: '已分派' },
{ value: '3', label: '进行中' },
{ value: '4', label: '已完成' },
{ value: '5', label: '已评价' },
// { value: '6', label: '已归档' },
{ value: '9', label: '已取消' }
]);
const queryParams = ref<RepairOrderQuery>({
pageNum: 1,
pageSize: 10,
orderNo: '',
title: '',
repairLocation: '',
categoryId: '',
priorityLevel: undefined,
reporterName: '',
reporterPhone: '',
orderStatus: '',
assigneeId: '',
assigneeName: '',
createTimeRange: [],
finishTimeRange: [],
params: {}
});
const form = ref<RepairOrderForm>({});
const queryFormRef = ref<ElFormInstance>();
const repairOrderFormRef = ref<ElFormInstance>();
const rules = reactive<ElFormRules>({
title: [{ required: true, message: '报修标题不能为空', trigger: 'blur' }],
description: [{ required: true, message: '问题描述不能为空', trigger: 'blur' }],
repairLocation: [{ required: true, message: '报修地点不能为空', trigger: 'blur' }],
categoryId: [{ required: true, message: '维修类型不能为空', trigger: 'change' }],
priorityLevel: [{ required: true, message: '紧急程度不能为空', trigger: 'change' }],
reporterName: [{ required: true, message: '报修人姓名不能为空', trigger: 'blur' }],
reporterPhone: [
{ required: true, message: '报修人电话不能为空', trigger: 'blur' },
{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
]
});
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const buttonLoading = ref(false);
// 分派相关
const assignDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const assignForm = ref<AssignOrderForm>({
orderId: '',
assigneeId: '',
assigneeName: '',
assignRemark: ''
});
const assignFormRef = ref<ElFormInstance>();
const assignRules = reactive<ElFormRules>({
assigneeId: [{ required: true, message: '请选择维修员', trigger: 'change' }]
});
const currentOrder = ref<RepairOrderVO>();
// 详情对话框
const detailDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const detailLoading = ref(false);
// 扩展 RepairOrderVO 类型以补充评价内容字段,避免模板校验错误
type RepairOrderVOExt = RepairOrderVO & { evaluationContent?: string };
const orderDetail = ref<RepairOrderVOExt>();
// 进度查看对话框
const progressDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const progressLoading = ref(false);
const progressList = ref<RepairProgressVO[]>([]);
// 进度更新对话框
const updateProgressDialog = reactive<DialogOption>({
visible: false,
title: ''
});
// 增加 orderNo 字段,用于在进度更新弹窗中展示工单编号
const progressForm = ref<ProgressUpdateForm & { orderNo?: string }>({
orderNo: '',
orderId: '',
progressType: '5',
progressStatus: '',
progressDetail: '',
locationInfo: '',
estimatedDuration: undefined,
nextStep: '',
issuesEncountered: '',
materialsNeeded: '',
remark: ''
});
const progressFormRef = ref<ElFormInstance>();
const progressRules = reactive<ElFormRules>({
progressStatus: [{ required: true, message: '进度状态不能为空', trigger: 'blur' }],
progressDetail: [{ required: true, message: '进度详细描述不能为空', trigger: 'blur' }]
});
// 完成工单对话框
const completeDialog = reactive<DialogOption>({
visible: false,
title: ''
});
// 增加 orderNo 字段,用于完成工单弹窗展示工单编号
const completeForm = ref<{ orderId: string; orderNo?: string; repairResult: string; usedMaterials: string; followSuggestions: string }>({
orderId: '',
orderNo: '',
repairResult: '',
usedMaterials: '',
followSuggestions: ''
});
const completeFormRef = ref<ElFormInstance>();
const completeRules = reactive<ElFormRules>({
repairResult: [{ required: true, message: '维修结果不能为空', trigger: 'blur' }]
});
// 服务评价对话框
const evaluationDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const attachmentDialog = reactive<DialogOption>({
visible: false,
title: ''
});
// 附件相关数据
const attachmentLoading = ref(false);
const currentAttachments = ref<any[]>([]);
const evaluationForm = ref<ServiceEvaluationForm>({
orderId: '',
evaluatedWorkerId: '',
overallScore: 5,
qualityScore: 5,
attitudeScore: 5,
responseSpeedScore: 5,
communicationScore: 5,
evaluationContent: '',
satisfactionLevel: '1',
isAnonymous: 0,
isPublic: 1,
positiveTags: '',
negativeTags: '',
improvementSuggestions: '',
remark: ''
});
const evaluationFormRef = ref<ElFormInstance>();
const evaluationRules = reactive<ElFormRules>({
orderId: [{ required: true, message: '工单ID不能为空', trigger: 'blur' }],
evaluatedWorkerId: [{ required: true, message: '请选择要评价的维修员', trigger: 'change' }],
overallScore: [{ required: true, message: '请给出综合评分', trigger: 'change' }],
evaluationContent: [{ required: true, message: '请填写评价内容', trigger: 'blur' }],
satisfactionLevel: [{ required: true, message: '请选择满意度等级', trigger: 'change' }]
});
// 进度类型字典
const progressTypeOptions = ref([
{ value: '1', label: '工单分派' },
{ value: '2', label: '接收确认' },
{ value: '3', label: '到达现场' },
{ value: '4', label: '开始维修' },
{ value: '5', label: '进度更新' },
{ value: '6', label: '完成维修' },
{ value: '7', label: '用户评价' },
{ value: '8', label: '工单归档' },
{ value: '9', label: '其他' }
]);
// 满意度等级字典
const satisfactionLevelOptions = ref([
{ value: '1', label: '非常满意' },
{ value: '2', label: '满意' },
{ value: '3', label: '一般' },
{ value: '4', label: '不满意' },
{ value: '5', label: '非常不满意' }
]);
// 新增:过滤已删除(deleted = 1)及已停用(status = 0) 的维修类型
const filterActiveCategories = (categories: RepairCategoryVO[]): RepairCategoryVO[] => {
return (categories || [])
.filter((cat: any) => String(cat.deleted) !== '1' && String(cat.status) !== '0')
.map((cat) => ({
...cat,
children: cat.children ? filterActiveCategories(cat.children) : []
}));
};
/** 查询报修工单列表 */
const getList = async () => {
loading.value = true;
queryParams.value.createTimeRange = daterangeCreateTime.value;
// 设置创建时间范围
queryParams.value.createTimeRange = daterangeCreateTime.value;
// 将创建时间范围写入 params,供后台 between 过滤使用
if (daterangeCreateTime.value && daterangeCreateTime.value.length === 2 && daterangeCreateTime.value[0] && daterangeCreateTime.value[1]) {
queryParams.value.params = {
...queryParams.value.params,
beginCreateTime: daterangeCreateTime.value[0],
endCreateTime: daterangeCreateTime.value[1]
};
} else if (queryParams.value.params) {
delete queryParams.value.params.beginCreateTime;
delete queryParams.value.params.endCreateTime;
}
const res = await listRepairOrder(queryParams.value);
// 适配后端返回格式:支持新格式{total, rows, code, msg}和旧格式{code, msg, data: {total, rows}}
if ((res as any).rows && (res as any).total !== undefined) {
// 新格式:直接返回TableDataInfo
repairOrderList.value = (res as any).rows;
total.value = (res as any).total;
} else if ((res as any).data && (res as any).data.rows) {
// 旧格式:R类型包装的TableDataInfo
repairOrderList.value = (res as any).data.rows;
total.value = (res as any).data.total || 0;
} else {
// 兜底处理
repairOrderList.value = [];
total.value = 0;
}
loading.value = false;
};
/** 获取统计数据 */
const getStatistics = async () => {
try {
const res = await getRepairStatistics();
// 适配后端返回格式
statistics.value = res.data;
} catch (error) {
console.error('获取统计数据失败:', error);
}
};
/** 获取维修类型选项 */
const getCategoryOptions = async () => {
try {
const res = await getRepairCategoryOptions();
// 过滤掉已删除或停用的数据
categoryOptions.value = (res.data || []).filter((cat: any) => String(cat.deleted) !== '1' && String(cat.status) !== '0');
} catch (error) {
console.error('获取维修类型选项失败:', error);
}
};
/** 获取维修类型树形结构 */
const getCategoryTreeOptions = async () => {
try {
const res = await getRepairCategoryTree();
// 过滤掉已删除或停用的数据(递归)
categoryTreeOptions.value = filterActiveCategories(res.data || []);
} catch (error) {
console.error('获取维修类型树形结构失败:', error);
categoryTreeOptions.value = [];
}
};
/** 根据categoryId获取维修类型名称 */
const getCategoryNameById = (categoryId: string | number | undefined): string => {
if (!categoryId) {
return '未分类';
}
// 递归查找树形结构中的分类名称
const findCategoryName = (categories: RepairCategoryVO[], targetId: string | number): string | null => {
for (const category of categories) {
// 数据类型统一转换为字符串进行比较
if (String(category.id) === String(targetId)) {
return category.categoryName || '未命名分类';
}
// 如果有子分类,递归查找
if (category.children && category.children.length > 0) {
const found = findCategoryName(category.children, targetId);
if (found) {
return found;
}
}
}
return null;
};
// 先从树形结构中查找
const foundInTree = findCategoryName(categoryTreeOptions.value, categoryId);
if (foundInTree) {
return foundInTree;
}
// 如果树形结构中没找到,从普通选项中查找
const foundInOptions = categoryOptions.value.find((option) => String(option.value) === String(categoryId));
if (foundInOptions) {
return foundInOptions.label;
}
// 都没找到,返回ID
return `未知类型(${categoryId})`;
};
/** 获取维修人员选项 */
const getWorkerOptions = async () => {
try {
const res = await getMaintenanceWorkers();
workerOptions.value = res.data;
} catch (error) {
console.error('获取维修人员选项失败:', error);
}
};
/** 获取当前用户详细信息 */
const getCurrentUserDetailInfo = async () => {
try {
const res = await getInfo();
console.log('用户详细信息:', res.data);
return res.data.user;
} catch (error) {
console.error('获取用户详细信息失败:', error);
return null;
}
};
/** 获取当前用户信息 */
const getCurrentUserInfo = async () => {
try {
// 首先确保用户信息已加载
if (!userStore.userId) {
await userStore.getInfo();
}
// 构建当前用户信息对象,包含角色信息
currentUser.value = {
userId: userStore.userId,
userName: userStore.nickname, // 使用nickname作为用户名
nickName: userStore.nickname,
tenantId: userStore.tenantId,
avatar: userStore.avatar,
roles: userStore.roles?.map((roleKey: string) => ({ roleKey })) || [],
permissions: userStore.permissions || [],
admin: userStore.roles?.includes('admin') || userStore.roles?.includes('superadmin') || false
};
} catch (error) {
console.error('获取用户信息失败:', error);
// 设置默认值,避免后续计算属性报错
currentUser.value = {
userId: null,
userName: '',
nickName: '',
tenantId: '',
avatar: '',
roles: [],
permissions: [],
admin: false
};
}
};
/** 检查是否为维修管理员 */
const isMaintenanceAdmin = computed(() => {
if (!currentUser.value) return false;
const roles = currentUser.value.roles || [];
return roles.some((role: any) => role.roleKey === 'property_maintenance_admin');
});
/** 检查是否为维修人员 */
const isMaintenanceWorker = computed(() => {
if (!currentUser.value) return false;
const roles = currentUser.value.roles || [];
return roles.some((role: any) => role.roleKey === 'maintenance_worker');
});
/** 检查是否为超级管理员 */
const isSuperAdmin = computed(() => {
if (!currentUser.value) return false;
return currentUser.value.admin || false;
});
/** 检查是否为报修人 */
const isReporter = (reporterId: number | string) => {
if (!currentUser.value) return false;
return currentUser.value.userId === reporterId;
};
/** 检查是否为维修负责人 */
const isAssignee = (assigneeId: number | string) => {
if (!currentUser.value) return false;
return currentUser.value.userId === assigneeId;
};
/** 检查分派按钮权限 - 仅维修管理员 */
const canAssign = computed(() => {
return isSuperAdmin.value || isMaintenanceAdmin.value;
});
/** 检查更新进度按钮权限 - 仅维修人员且为负责人 */
const canUpdateProgress = (row: RepairOrderVO) => {
console.log(isSuperAdmin.value, isMaintenanceWorker.value, isAssignee(row.assigneeId));
if (isSuperAdmin.value) return true;
return isMaintenanceWorker.value && isAssignee(row.assigneeId);
};
/** 检查完成工单按钮权限 - 仅维修人员且为负责人 */
const canComplete = (row: RepairOrderVO) => {
if (isSuperAdmin.value) return true;
return isMaintenanceWorker.value && isAssignee(row.assigneeId);
};
/** 检查服务评价按钮权限 - 仅报修人 */
const canEvaluate = (row: RepairOrderVO) => {
if (isSuperAdmin.value) return true;
return isReporter(row.reporterId);
};
/** 检查统计卡片查看权限 - 仅维护管理员和超级管理员 */
const canViewStatistics = computed(() => {
return isSuperAdmin.value || isMaintenanceAdmin.value;
});
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
daterangeCreateTime.value = ['', ''];
queryFormRef.value?.resetFields();
queryParams.value.createTimeRange = [];
queryParams.value.pageNum = 1;
getList();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: RepairOrderVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = async () => {
await getCategoryTreeOptions(); // 新增:每次弹窗前获取最新维修类型
await reset(true); // 传入true表示是新增操作,需要自动填充用户信息
dialog.visible = true;
dialog.title = '添加报修工单';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: RepairOrderVO) => {
await getCategoryTreeOptions(); // 新增:每次弹窗前获取最新维修类型
await reset(); // 修改操作不自动填充用户信息
const id = row?.id || ids.value[0];
try {
// 加载工单基本信息
const res = await getRepairOrder(id);
Object.assign(form.value, res.data);
// 加载已有的附件信息
if (form.value.id) {
try {
const attachmentRes = await getRepairAttachments(form.value.id);
const existingAttachments = attachmentRes.data || [];
// 转换已有附件为el-upload组件需要的格式
attachmentList.value = existingAttachments.map((attachment: any) => ({
uid: attachment.id,
name: attachment.fileName,
status: 'success',
size: attachment.fileSize,
url: attachment.filePath,
response: {
code: 200,
data: {
id: attachment.id,
fileName: attachment.fileName,
filePath: attachment.filePath
}
},
// 保存原始附件信息
attachmentInfo: attachment
}));
console.log('已加载附件信息,数量:', attachmentList.value.length);
} catch (error) {
console.error('加载附件信息失败:', error);
attachmentList.value = [];
}
}
dialog.visible = true;
dialog.title = '修改报修工单';
} catch (error) {
console.error('加载工单信息失败:', error);
proxy?.$modal.msgError('加载工单信息失败');
}
};
/** 提交按钮 */
const submitForm = () => {
repairOrderFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
let orderId: string | number;
// todo form.attachementCount的数量等于附件栏中附件的数量
console.log('form.value', form.value);
console.log('attachmentList 长度:', attachmentList.value.length);
form.value.attachmentCount=attachmentList.value.length
if (form.value.id) {
await updateRepairOrder(form.value);
orderId = form.value.id;
proxy?.$modal.msgSuccess('修改成功');
} else {
const res = await addRepairOrder(form.value);
orderId = res.data?.id || res.data; // 修复:移除res.id,避免类型错误
console.log('新增工单响应:', res); // 调试信息
proxy?.$modal.msgSuccess('新增成功');
}
// 关闭对话框并刷新列表(确保这两步先执行)
dialog.visible = false;
await getList();
// 处理附件上传(移到最后,避免影响界面关闭)
if (orderId) {
try {
await handlePendingAttachments(orderId);
} catch (error) {
console.error('附件上传失败:', error);
proxy?.$modal.msgWarning('工单保存成功,但附件上传失败,请稍后重试');
}
}
} catch (error) {
console.error('保存工单失败:', error);
proxy?.$modal.msgError('保存失败,请重试');
} finally {
buttonLoading.value = false;
}
}
});
};
/** 处理待关联的附件 */
const handlePendingAttachments = async (orderId: string | number) => {
let hasOperations = false;
let successMessages: string[] = [];
let errorMessages: string[] = [];
// 处理待删除的附件(编辑模式)
if (pendingDeleteAttachmentIds.value.length > 0) {
try {
console.log('开始删除标记的附件,数量:', pendingDeleteAttachmentIds.value.length);
// 批量删除附件
for (const attachmentId of pendingDeleteAttachmentIds.value) {
await deleteAttachment(attachmentId);
}
console.log('成功删除附件IDs:', pendingDeleteAttachmentIds.value);
successMessages.push(`成功删除 ${pendingDeleteAttachmentIds.value.length} 个附件`);
// 清空待删除列表
pendingDeleteAttachmentIds.value = [];
hasOperations = true;
} catch (error) {
console.error('附件删除失败:', error);
errorMessages.push('部分附件删除失败');
}
}
// 处理新增模式下已上传的附件(更新order_id)
if (tempAttachmentIds.value.length > 0) {
try {
console.log('开始关联临时附件到工单,附件数量:', tempAttachmentIds.value.length);
// 调用API批量更新附件的order_id
await batchUpdateAttachmentOrderId(tempAttachmentIds.value, String(orderId));
console.log('成功关联附件IDs:', tempAttachmentIds.value, '到工单:', orderId);
successMessages.push(`成功关联 ${tempAttachmentIds.value.length} 个附件到工单`);
// 清空临时附件ID列表
tempAttachmentIds.value = [];
tempFileList.value = [];
attachmentList.value = [];
hasOperations = true;
} catch (error) {
console.error('附件关联失败:', error);
errorMessages.push('部分附件关联失败');
}
}
// 处理编辑模式下新添加的文件(attachmentList中status不为success的文件)
const newFiles = attachmentList.value.filter((file) => file.status !== 'success' && file.raw && !file.response);
if (newFiles.length > 0) {
try {
console.log('开始上传编辑模式下新添加的文件,文件数量:', newFiles.length);
for (const fileItem of newFiles) {
if (fileItem.raw) {
await uploadAttachment(String(orderId), '1', fileItem.raw, '');
console.log('成功上传编辑模式新文件:', fileItem.name);
// 更新文件状态
fileItem.status = 'success';
}
}
successMessages.push(`成功上传 ${newFiles.length} 个新附件`);
hasOperations = true;
} catch (error) {
console.error('新附件上传失败:', error);
errorMessages.push('部分新附件上传失败');
}
}
// 显示操作结果消息
if (hasOperations) {
if (successMessages.length > 0) {
proxy?.$modal.msgSuccess(successMessages.join(','));
}
if (errorMessages.length > 0) {
proxy?.$modal.msgWarning(`工单保存成功,但${errorMessages.join(',')}`);
}
}
};
/** 删除按钮操作 */
const handleDelete = async (row?: RepairOrderVO) => {
const orderIds = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除报修工单编号为"' + orderIds + '"的数据项?');
await delRepairOrder(orderIds);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.$download.zip('repair_order_export.zip', `repair_order_${new Date().getTime()}.xlsx`);
};
/** 分派按钮操作 */
const handleAssign = (row: RepairOrderVO) => {
currentOrder.value = row;
assignForm.value = {
orderId: row.id,
assigneeId: '',
assigneeName: '',
assignRemark: ''
};
assignDialog.visible = true;
};
/** 维修员选择变更 */
const handleWorkerChange = (value: string | number) => {
const worker = workerOptions.value.find((w) => w.value === value);
assignForm.value.assigneeName = worker?.label || '';
};
/** 提交分派 */
const submitAssign = () => {
assignFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
await assignRepairOrder(assignForm.value);
proxy?.$modal.msgSuccess('工单分派成功');
assignDialog.visible = false;
await getList();
} finally {
buttonLoading.value = false;
}
}
});
};
/** 取消分派 */
const cancelAssign = () => {
assignDialog.visible = false;
};
/** 查看详情 */
const handleDetail = async (row: RepairOrderVO) => {
detailLoading.value = true;
detailDialog.title = `工单详情 - ${row.orderNo}`;
detailDialog.visible = true;
try {
const res = await getRepairOrder(row.id);
// 适配不同的响应格式
orderDetail.value = (res as any).data || res;
// 自动加载附件信息
if (orderDetail.value.attachmentCount && orderDetail.value.attachmentCount > 0) {
await loadAttachments(orderDetail.value.id);
}
} catch (error) {
console.error('获取工单详情失败:', error);
proxy?.$modal.msgError('获取工单详情失败');
} finally {
detailLoading.value = false;
}
};
/** 加载附件信息 */
const loadAttachments = async (orderId: string | number) => {
try {
attachmentLoading.value = true;
const res = await getRepairAttachments(orderId);
currentAttachments.value = res.data || [];
} catch (error) {
console.error('获取附件失败:', error);
currentAttachments.value = [];
} finally {
attachmentLoading.value = false;
}
};
/** 刷新附件列表 */
const refreshAttachments = async (orderId: string | number) => {
await loadAttachments(orderId);
proxy?.$modal.msgSuccess('附件列表已刷新');
};
/** 获取图片预览列表 */
const getImagePreviewList = () => {
return currentAttachments.value.filter((att) => att.fileType === 'image').map((att) => att.filePath);
};
/** 获取文件类型标签 */
const getFileTypeLabel = (fileType: string) => {
const typeMap: Record<string, string> = {
'image': '图片',
'video': '视频',
'document': '文档',
'audio': '音频',
'archive': '压缩包'
};
return typeMap[fileType] || '文件';
};
/** 格式化上传时间 */
const formatUploadTime = (timeStr: string) => {
if (!timeStr) return '未知时间';
const time = new Date(timeStr);
const now = new Date();
const diff = now.getTime() - time.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) {
return '今天上传';
} else if (days === 1) {
return '昨天上传';
} else if (days < 7) {
return `${days}天前上传`;
} else {
return proxy.parseTime(timeStr, '{y}-{m}-{d}');
}
};
/** 获取上传阶段文本 */
const getUploadStageText = (stage: string | number) => {
const stageMap: Record<string, string> = {
'1': '报修申请',
'2': '维修过程',
'3': '维修完成',
'4': '服务评价'
};
return stageMap[String(stage)] || '未知阶段';
};
/** 获取文件存储信息 */
const getFileStorageInfo = (filePath: string) => {
if (!filePath) return '未知路径';
const parts = filePath.split('/');
return parts.length > 2 ? `.../${parts[parts.length - 2]}/${parts[parts.length - 1]}` : filePath;
};
/** 检查是否可以预览 */
const canPreview = (attachment: any) => {
const previewableTypes = ['image', 'document'];
const previewableExtensions = ['.pdf', '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
if (previewableTypes.includes(attachment.fileType)) {
return true;
}
if (attachment.fileExtension) {
return previewableExtensions.includes(attachment.fileExtension.toLowerCase());
}
return false;
};
/** 显示附件详情 */
const showAttachmentDetail = (attachment: any) => {
const details = `文件名称:${attachment.fileName}<br/>
文件大小:${formatFileSize(attachment.fileSize)}<br/>
文件类型:${getFileTypeLabel(attachment.fileType)}<br/>
上传者:${attachment.uploaderName || '未知'}<br/>
上传时间:${attachment.createTime}<br/>
上传阶段:${getUploadStageText(attachment.uploadStage)}<br/>
存储路径:${attachment.filePath}<br/>
文件描述:${attachment.description || '无'}`;
proxy?.$modal.alert(details);
};
/** 检查是否可以删除附件 */
const canDeleteAttachment = (attachment: any) => {
// 只有超级管理员、维修管理员或上传者本人可以删除附件
if (isSuperAdmin.value || isMaintenanceAdmin.value) {
return true;
}
// 检查是否为上传者本人
if (currentUser.value && attachment.uploaderId) {
return currentUser.value.userId === attachment.uploaderId;
}
return false;
};
/** 确认删除附件 */
const confirmDeleteAttachment = async (attachment: any) => {
try {
await proxy?.$modal.confirm(`确定要删除附件 "${attachment.fileName}" 吗?删除后无法恢复!`);
await deleteAttachment(attachment.id);
proxy?.$modal.msgSuccess('附件删除成功');
// 刷新附件列表和工单详情
if (orderDetail.value) {
await loadAttachments(orderDetail.value.id);
// 更新附件数量
if (orderDetail.value.attachmentCount > 0) {
orderDetail.value.attachmentCount--;
}
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除附件失败:', error);
proxy?.$modal.msgError('删除附件失败');
}
}
};
/** 查看图片集 */
const viewImageGallery = (attachment: any) => {
const imageList = currentAttachments.value.filter((att) => att.fileType === 'image');
const currentIndex = imageList.findIndex((img) => img.id === attachment.id);
// 使用Element Plus的图片预览功能
const previewList = imageList.map((img) => img.filePath);
const imageViewer = document.createElement('div');
imageViewer.innerHTML = `<el-image :src="'${attachment.filePath}'" :preview-src-list="previewList" style="display: none;"></el-image>`;
document.body.appendChild(imageViewer);
// 触发预览
setTimeout(() => {
const img = imageViewer.querySelector('img') as HTMLImageElement;
if (img) {
img.click();
}
// 清理临时元素
setTimeout(() => {
document.body.removeChild(imageViewer);
}, 100);
}, 50);
};
/** 在新窗口打开 */
const openInNewTab = (attachment: any) => {
window.open(attachment.filePath, '_blank');
};
/** 查看进度 */
const handleProgress = async (row: RepairOrderVO) => {
progressLoading.value = true;
progressDialog.title = `进度查看 - ${row.orderNo}`;
progressDialog.visible = true;
try {
const res = await getRepairProgress(row.id);
// 适配后端返回格式
progressList.value = (res as any).data || (res as any);
} catch (error) {
console.error('获取进度失败:', error);
proxy?.$modal.msgError('获取进度失败');
} finally {
progressLoading.value = false;
}
};
/** 更新进度 */
const handleUpdateProgress = (row: RepairOrderVO) => {
updateProgressDialog.title = `进度更新 - ${row.orderNo}`;
updateProgressDialog.visible = true;
progressForm.value = {
orderNo: row.orderNo,
orderId: row.id,
progressType: '5',
progressStatus: '',
progressDetail: '',
locationInfo: '',
estimatedDuration: undefined,
nextStep: '',
issuesEncountered: '',
materialsNeeded: '',
remark: ''
};
};
/** 完成工单 */
const handleComplete = (row: RepairOrderVO) => {
completeDialog.title = `完成工单 - ${row.orderNo}`;
completeDialog.visible = true;
completeForm.value = {
orderId: String(row.id),
orderNo: row.orderNo,
repairResult: '',
usedMaterials: '',
followSuggestions: ''
};
};
/** 服务评价 */
const handleEvaluate = (row: RepairOrderVO) => {
evaluationDialog.title = `服务评价 - ${row.orderNo}`;
evaluationDialog.visible = true;
// 验证工单状态,只有已完成的工单才能评价
if (String(row.orderStatus) !== '4') {
proxy?.$modal.msgWarning('只有已完成的工单才能进行评价');
return;
}
// 验证是否有分派的维修员
if (!row.assigneeId) {
proxy?.$modal.msgWarning('该工单没有分派维修员,无法进行评价');
return;
}
console.log('初始化评价表单,工单信息:', {
orderId: row.id,
orderNo: row.orderNo,
assigneeId: row.assigneeId,
assigneeName: row.assigneeName,
orderStatus: row.orderStatus
});
evaluationForm.value = {
orderId: row.id,
evaluatedWorkerId: row.assigneeId,
overallScore: 5,
qualityScore: 5,
attitudeScore: 5,
responseSpeedScore: 5,
communicationScore: 5,
evaluationContent: '',
satisfactionLevel: '1',
isAnonymous: 0,
isPublic: 1,
positiveTags: '',
negativeTags: '',
improvementSuggestions: '',
remark: ''
};
};
/** 上传前检查 */
const handleBeforeUpload = (file: File) => {
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
proxy?.$modal.msgError('上传文件大小不能超过 10MB!');
return false;
}
// 设置上传参数
const uploadParams: any = {
uploadStage: '1', // 报修申请阶段
description: `${form.value.title || '报修工单'}相关附件` // 添加描述信息
};
// 只有编辑模式时才传递orderId
if (form.value.id) {
uploadParams.orderId = form.value.id;
}
uploadData.value = uploadParams;
console.log('准备上传文件:', {
文件名: file.name,
文件大小: (file.size / 1024 / 1024).toFixed(2) + 'MB',
工单ID: uploadParams.orderId || '待关联',
上传阶段: uploadParams.uploadStage,
描述: uploadParams.description,
模式: form.value.id ? '编辑模式' : '新增模式'
});
file.uploading = true; // 新增:标记为上传中
return true;
};
/** 上传成功 */
const handleUploadSuccess = (response: any, file: any) => {
console.log('上传成功:', response, file);
if (response.code === 200) {
proxy?.$modal.msgSuccess(`文件 ${file.name} 上传成功`);
// todo
console.log('orderDetail', orderDetail.value);
// orderDetail.value.attachmentCount++;
// 立即记录附件数据
const attachmentData = response.data;
if (attachmentData && attachmentData.id) {
// 如果是新增模式,收集附件ID到临时列表
if (!form.value.id) {
tempAttachmentIds.value.push(attachmentData.id);
console.log('新增模式:已收集附件ID', attachmentData.id, '当前临时附件ID列表:', tempAttachmentIds.value);
}
// 更新文件对象的相关信息
file.attachmentId = attachmentData.id;
file.status = 'success';
file.url = attachmentData.filePath;
// 如果是编辑模式,附件已经自动关联到工单,直接更新UI
if (form.value.id) {
console.log('编辑模式:附件已关联到工单', form.value.id, '附件ID:', attachmentData.id);
// 可以选择刷新附件列表以确保数据同步
// loadAttachments(form.value.id);
}
console.log('附件记录成功:', {
附件ID: attachmentData.id,
文件名: attachmentData.fileName,
文件路径: attachmentData.filePath,
工单ID: form.value.id || '待关联',
模式: form.value.id ? '编辑' : '新增'
});
}
} else {
proxy?.$modal.msgError(`文件 ${file.name} 上传失败: ${response.msg || '未知错误'}`);
}
file.uploading = false; // 新增:上传成功后移除loading
};
/** 上传失败 */
const handleUploadError = (error: Error, file: any) => {
console.error('上传失败:', error, file);
proxy?.$modal.msgError(`文件 ${file.name} 上传失败`);
file.uploading = false; // 新增:上传失败后移除loading
};
/** 下载附件 */
const handleDownloadAttachment = async (file: any) => {
let downloadUrl = '';
let fileName = file.name || 'attachment';
// 如果文件已经上传成功,使用服务器返回的路径
if (file.response && file.response.data && file.response.data.filePath) {
downloadUrl = file.response.data.filePath;
fileName = file.response.data.fileName || fileName;
}
// 如果是已有的附件,使用文件的url
else if (file.url) {
downloadUrl = file.url;
}
// 如果是本地文件,直接使用raw文件
else if (file.raw) {
const link = document.createElement('a');
link.href = URL.createObjectURL(file.raw);
link.download = fileName;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
console.log('下载本地文件:', fileName);
return;
}
if (!downloadUrl) {
proxy?.$modal.msgError('无法获取文件下载地址');
return;
}
// 对于服务器文件,使用fetch获取内容强制下载
try {
const loading = ElLoading.service({
text: '正在下载文件...',
target: document.body
});
// const response = await fetch(downloadUrl);
// if (!response.ok) {
// throw new Error(`HTTP error! status: ${response.status}`);
// }
// 通过fetch获取文件内容,强制下载
// const response = await req('/resource/oss/download/' + attachment.filePath);
const response = await request({
url: '/resource/oss/download/' + downloadUrl,
method: 'get',
responseType: 'blob',
timeout: 30000 // 30秒超时
});
// 由于axios响应拦截器对blob类型响应直接返回blob数据
// 这里response直接就是blob对象,而不是包含data属性的响应对象
const blob = response;
// 验证响应数据
if (!blob) {
throw new Error('响应数据为空');
}
// const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
loading?.close();
console.log('下载服务器文件成功:', fileName);
} catch (error) {
console.error('下载文件失败:', error);
proxy?.$modal.msgError('下载文件失败,请稍后重试');
}
};
/** 删除附件确认 */
const handleRemoveAttachmentConfirm = async (file: any) => {
await proxy?.$modal.confirm('确定要删除该附件吗?删除操作将在保存时生效。');
handleRemoveAttachment(file);
};
/** 移除附件 (修改为延迟删除) */
const handleRemoveAttachment = (file: any) => {
// 如果是已上传的文件(编辑模式下的已有附件或新增模式下已上传的附件)
if (file.response && file.response.data && file.response.data.id) {
// 不立即删除数据库记录,而是标记为待删除
const attachmentId = file.response.data.id;
// 如果是新增模式,从临时附件ID列表中移除
if (!form.value.id) {
const attachmentIndex = tempAttachmentIds.value.indexOf(attachmentId);
if (attachmentIndex > -1) {
tempAttachmentIds.value.splice(attachmentIndex, 1);
console.log('新增模式:已从临时附件ID列表中移除:', attachmentId, '剩余附件数:', tempAttachmentIds.value.length);
}
} else {
// 编辑模式:添加到待删除列表
if (!pendingDeleteAttachmentIds.value.includes(attachmentId)) {
pendingDeleteAttachmentIds.value.push(attachmentId);
console.log('编辑模式:已标记附件为待删除:', attachmentId, '待删除列表:', pendingDeleteAttachmentIds.value);
}
}
// 从attachmentList中移除
const index = attachmentList.value.findIndex((item) => item.uid === file.uid);
if (index > -1) {
attachmentList.value.splice(index, 1);
}
proxy?.$modal.msgSuccess('附件已移除,将在保存时生效');
}
// 如果是临时文件(新增时添加但未上传的文件),从临时列表中删除
else if (file.raw && !form.value.id) {
const index = tempFileList.value.findIndex((f) => f.name === file.raw.name && f.size === file.raw.size);
if (index > -1) {
tempFileList.value.splice(index, 1);
console.log('已从临时列表中移除文件:', file.raw.name, '剩余文件数:', tempFileList.value.length);
}
}
// 如果是编辑模式下新添加但还未上传的文件
else if (file.raw && form.value.id) {
// 编辑模式下新添加的文件,直接从attachmentList中移除
const index = attachmentList.value.findIndex((item) => item.uid === file.uid);
if (index > -1) {
attachmentList.value.splice(index, 1);
console.log('已从附件列表中移除未上传文件:', file.name);
}
}
};
/** 表单重置 */
const reset = async (autoFillUserInfo: boolean = false) => {
form.value = {
id: undefined,
title: '',
description: '',
repairLocation: '',
categoryId: '',
priorityLevel: undefined,
expectedFinishTime: '',
reporterName: '',
reporterPhone: '',
reporterDept: '',
orderStatus: '',
assigneeId: '',
assignRemark: '',
repairResult: '',
usedMaterials: '',
followSuggestions: '',
cancelReason: '',
remark: ''
};
// 清空附件相关数据
attachmentList.value = [];
tempFileList.value = [];
tempAttachmentIds.value = [];
pendingDeleteAttachmentIds.value = [];
console.log('已清空附件列表、临时文件列表、临时附件ID列表和待删除附件列表');
// 如果是新增操作,自动填充当前用户信息
if (autoFillUserInfo) {
try {
const userInfo = await getCurrentUserDetailInfo();
if (userInfo) {
form.value.reporterName = userInfo.nickName || userInfo.userName || '';
form.value.reporterPhone = userInfo.phonenumber || '';
form.value.reporterDept = userInfo.deptName || '';
console.log('自动填充报修人信息:', {
姓名: form.value.reporterName,
电话: form.value.reporterPhone,
部门: form.value.reporterDept
});
// 显示提示信息
proxy?.$modal.msgSuccess('已自动填充报修人信息');
}
} catch (error) {
console.error('自动填充用户信息失败:', error);
proxy?.$modal.msgWarning('无法获取用户信息,请手动填写报修人信息');
}
}
repairOrderFormRef.value?.resetFields();
};
/** 取消按钮 */
const cancel = () => {
dialog.visible = false;
reset();
};
/** 获取紧急程度标签类型 */
const getPriorityTagType = (level: number) => {
const types: Record<number, string> = {
1: 'danger',
2: 'warning',
3: 'primary',
4: 'info',
5: 'success'
};
return types[level] || 'info';
};
/** 获取紧急程度文本 */
const getPriorityText = (level: number) => {
const dict = pm_priority_level.value.find((item) => parseInt(item.value) === level);
return dict?.label || level.toString();
};
/** 获取状态标签类型 */
const getStatusTagType = (status: string) => {
const types: Record<string, string> = {
'1': 'info', // 待分派
'2': 'warning', // 已分派
'3': 'primary', // 进行中
'4': 'success', // 已完成
'5': 'success', // 已评价
'6': 'info', // 已归档
'9': 'danger' // 已取消
};
return types[status] || 'info';
};
/** 获取状态文本 */
const getStatusText = (status: string) => {
const dict = pm_order_status.value.find((item) => item.value === status);
return dict?.label || status;
};
/** 格式化时长 */
const formatDuration = (minutes: number) => {
if (minutes < 60) {
return `${minutes}分钟`;
} else {
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return remainingMinutes > 0 ? `${hours}小时${remainingMinutes}分钟` : `${hours}小时`;
}
};
/** 提交进度更新 */
const submitProgress = () => {
progressFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
await updateRepairProgress(progressForm.value);
proxy?.$modal.msgSuccess('进度更新成功');
updateProgressDialog.visible = false;
await getList();
} finally {
buttonLoading.value = false;
}
}
});
};
/** 取消进度更新 */
const cancelProgress = () => {
updateProgressDialog.visible = false;
};
/** 提交完成工单 */
const submitComplete = () => {
completeFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
await finishRepairOrder(completeForm.value.orderId, completeForm.value);
proxy?.$modal.msgSuccess('工单完成成功');
completeDialog.visible = false;
await getList();
} finally {
buttonLoading.value = false;
}
}
});
};
/** 取消完成工单 */
const cancelComplete = () => {
completeDialog.visible = false;
};
/** 提交服务评价 */
const submitEvaluation = () => {
evaluationFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
console.log('提交评价数据:', evaluationForm.value);
// 验证必要字段
if (!evaluationForm.value.orderId) {
proxy?.$modal.msgError('工单ID不能为空');
return;
}
if (!evaluationForm.value.evaluatedWorkerId) {
proxy?.$modal.msgError('请选择要评价的维修员');
return;
}
const response = await createServiceEvaluation(evaluationForm.value);
console.log('评价提交响应:', response);
proxy?.$modal.msgSuccess('服务评价提交成功');
evaluationDialog.visible = false;
// 刷新列表,确保状态更新
await getList();
// 如果工单状态没有自动更新,可能需要手动调用状态更新接口
// 这里可以根据后端API设计添加相应的状态更新逻辑
} catch (error: any) {
console.error('评价提交失败:', error);
// 显示详细的错误信息
const errorMsg = error?.response?.data?.msg || error?.message || '评价提交失败,请重试';
proxy?.$modal.msgError(errorMsg);
} finally {
buttonLoading.value = false;
}
}
});
};
/** 取消服务评价 */
const cancelEvaluation = () => {
evaluationDialog.visible = false;
};
/** 下载附件 */
const downloadAttachment = async (attachment: any) => {
if (!attachment.filePath) {
proxy?.$modal.msgError('无法获取文件下载地址');
return;
}
try {
// 显示加载提示
const loading = ElLoading.service({
text: '正在下载文件...',
target: document.body
});
// 通过fetch获取文件内容,强制下载
// const response = await req('/resource/oss/download/' + attachment.filePath);
const response = await request({
url: '/resource/oss/download/' + attachment.filePath,
method: 'get',
responseType: 'blob',
timeout: 30000 // 30秒超时
});
// 由于axios响应拦截器对blob类型响应直接返回blob数据
// 这里response直接就是blob对象,而不是包含data属性的响应对象
const blob = response;
// 验证响应数据
if (!blob) {
throw new Error('响应数据为空');
}
// const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
// 创建下载链接
const link = document.createElement('a');
link.href = url;
link.download = attachment.fileName || '附件';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 释放blob URL
window.URL.revokeObjectURL(url);
loading?.close();
console.log('下载附件成功:', attachment.fileName || '附件');
} catch (error) {
console.error('下载附件失败:', error);
proxy?.$modal.msgError('下载文件失败,请稍后重试');
}
};
/** 预览附件 */
const previewAttachment = (attachment: any) => {
if (attachment.fileType === 'image') {
// 图片预览已内置在el-image组件中
return;
}
// 其他类型文件在新窗口打开
window.open(attachment.filePath, '_blank');
};
/** 格式化文件大小 */
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
/** 文件变更处理 */
const handleFileChange = (file: any, fileList: any[]) => {
console.log('文件变更:', file, fileList);
// 新增时收集文件到临时列表
if (!form.value.id && file.raw && file.status !== 'removed') {
// 检查文件大小
const isLt10M = file.raw.size / 1024 / 1024 < 10;
if (!isLt10M) {
proxy?.$modal.msgError(`文件 ${file.name} 大小超过 10MB,无法上传`);
// 从文件列表中移除这个文件
const index = fileList.findIndex((f) => f.uid === file.uid);
if (index > -1) {
fileList.splice(index, 1);
}
return;
}
// 如果是添加文件状态,添加到临时列表
if (file.status === 'ready' || !file.status) {
// 检查是否已存在(避免重复添加)
const existingFile = tempFileList.value.find(
(f) => f.name === file.raw.name && f.size === file.raw.size && f.lastModified === file.raw.lastModified
);
if (!existingFile) {
tempFileList.value.push(file.raw);
console.log('已添加文件到临时列表:', file.raw.name, '总文件数:', tempFileList.value.length);
}
}
}
// 编辑模式下新添加文件的处理
if (form.value.id && file.raw && file.status !== 'removed' && (file.status === 'ready' || !file.status)) {
// 检查文件大小
const isLt10M = file.raw.size / 1024 / 1024 < 10;
if (!isLt10M) {
proxy?.$modal.msgError(`文件 ${file.name} 大小超过 10MB,无法上传`);
// 从文件列表中移除这个文件
const index = fileList.findIndex((f) => f.uid === file.uid);
if (index > -1) {
fileList.splice(index, 1);
}
return;
}
console.log('编辑模式下添加新文件:', file.name);
}
// 如果是删除文件,相应处理
if (file.status === 'removed') {
if (!form.value.id && file.raw) {
// 新增模式下,从临时列表中移除
const index = tempFileList.value.findIndex(
(f) => f.name === file.raw.name && f.size === file.raw.size && f.lastModified === file.raw.lastModified
);
if (index > -1) {
tempFileList.value.splice(index, 1);
console.log('已从临时列表中移除文件:', file.raw.name, '剩余文件数:', tempFileList.value.length);
}
}
// 编辑模式下的删除在handleRemoveAttachment中处理
}
};
/** 获取进度类型图标颜色 */
const getProgressIconColor = (progressType: string) => {
const colorMap: Record<string, string> = {
'1': '#409eff', // 工单分派 - 蓝色
'2': '#67c23a', // 接收确认 - 绿色
'3': '#e6a23c', // 到达现场 - 橙色
'4': '#409eff', // 开始维修 - 蓝色
'5': '#909399', // 进度更新 - 灰色
'6': '#67c23a', // 完成维修 - 绿色
'7': '#f56c6c', // 用户评价 - 红色
'8': '#606266', // 工单归档 - 深灰色
'9': '#909399' // 其他 - 灰色
};
return colorMap[progressType] || '#909399';
};
/** 获取进度类型标签类型 */
const getProgressTypeTag = (progressType: string): 'success' | 'warning' | 'info' | 'primary' | 'danger' => {
const tagMap: Record<string, 'success' | 'warning' | 'info' | 'primary' | 'danger'> = {
'1': 'primary',
'2': 'success',
'3': 'warning',
'4': 'primary',
'5': 'info',
'6': 'success',
'7': 'danger',
'8': 'info',
'9': 'info'
};
return tagMap[progressType] || 'info';
};
/** 获取进度类型文本 */
const getProgressTypeText = (progressType: string) => {
const textMap: Record<string, string> = {
'1': '工单分派',
'2': '接收确认',
'3': '到达现场',
'4': '开始维修',
'5': '进度更新',
'6': '完成维修',
'7': '用户评价',
'8': '工单归档',
'9': '其他'
};
return textMap[progressType] || '未知类型';
};
/** 检查是否有额外信息 */
const hasAdditionalInfo = (progress: any) => {
return (
progress.locationInfo ||
progress.estimatedDuration ||
progress.nextStep ||
progress.issuesEncountered ||
progress.materialsNeeded ||
progress.remark
);
};
onMounted(() => {
getList();
getStatistics();
getCategoryOptions();
getCategoryTreeOptions();
getWorkerOptions();
getCurrentUserInfo();
});
/** 卡片预览 */
const handleFilePreview = (file: any) => {
// 如果文件已经上传成功,优先使用服务器返回的url
let url = file.url || (file.response && file.response.data && file.response.data.filePath);
// 若还未上传,则尝试使用本地预览地址
if (!url && file.raw) {
url = URL.createObjectURL(file.raw);
}
if (!url) return;
// 对图片使用图片预览,其余直接新窗口打开
const isImage = file.type ? file.type.startsWith('image') : file.raw && file.raw.type.startsWith('image');
if (isImage) {
// 利用 Element Plus ImageViewer 进行预览
const viewer = (proxy as any)?.$imageViewer;
viewer && viewer([url]);
} else {
window.open(url, '_blank');
}
};
/** 判断是否为图片文件 */
const isImageFile = (file: any): boolean => {
// 优先使用文件 MIME type 判断
const type = file.type || (file.raw && file.raw.type) || '';
if (type && type.startsWith('image')) {
return true;
}
// 编辑场景下从数据库加载的附件通常缺少 MIME type
// 通过文件名或 URL 后缀进行判断
const name: string = file.name || file.response?.data?.fileName || '';
const url: string = file.url || file.response?.data?.filePath || '';
const source = (name || url).toLowerCase();
// 常见图片扩展名
const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.tiff', '.ico'];
return IMAGE_EXTS.some((ext) => source.endsWith(ext));
};
/** 获取文件扩展名小写 */
const getFileExt = (fileName: string): string => {
if (!fileName) return '';
const idx = fileName.lastIndexOf('.');
return idx !== -1 ? fileName.substring(idx + 1).toLowerCase() : '';
};
</script>
<style lang="scss" scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.el-form-item {
margin-bottom: 0 !important;
}
}
/* 表格卡片布局 */
.table-card {
margin-top: 0;
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 表格主体 */
.table-container {
margin-bottom: 0;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
}
/* 统计卡片样式 */
.stat-card {
border-radius: 8px;
transition: all 0.3s ease;
height: 90px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.stat-content {
display: flex;
align-items: center;
padding: 5px 12px;
height: 100%;
box-sizing: border-box;
.stat-icon {
font-size: 24px;
margin-right: 12px;
opacity: 0.8;
}
.stat-info {
flex: 1;
.stat-value {
font-size: 20px;
font-weight: bold;
line-height: 1;
margin-bottom: 3px;
}
.stat-label {
font-size: 13px;
opacity: 0.7;
line-height: 1;
}
}
}
&.stat-card-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
&.stat-card-success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
&.stat-card-warning {
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 100%);
color: white;
}
&.stat-card-info {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #333;
}
}
/* 表格样式 */
.scroll-table {
:deep(.el-table__header-wrapper) {
.el-table__header {
th {
background-color: #f8f9fa;
color: #333;
font-weight: 600;
}
}
}
:deep(.el-table__body-wrapper) {
.el-table__row {
&:hover {
background-color: #f5f7fa;
}
}
}
}
/* 工单信息样式 */
.order-info {
width: 100%;
box-sizing: border-box; // 使内边距计入总宽度,避免超出表单内容区域
background-color: #f8f9fa;
padding: 16px;
border-radius: 8px;
border-left: 4px solid #409eff;
p {
margin: 8px 0;
line-height: 1.5;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
}
/* 对话框样式 */
.dialog-footer-right {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 附件上传容器样式 */
.attachment-upload-container {
.upload-demo {
.el-upload {
/* border: 2px dashed #d9d9d9; */
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s;
&:hover {
border-color: #409eff;
}
}
.el-upload__tip {
font-size: 12px;
color: #000000;
margin-top: 7px;
}
// 调整拖拽上传图标与文字在同一行、居中对齐
.el-upload-dragger1 {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15px;
padding: 0px !important;
}
.el-upload-dragger1 .el-icon--upload {
margin: 0;
padding: 0px !important;
}
/* 文件卡片占位封面样式 */
.upload-file-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #909399;
font-size: 14px;
.el-icon {
font-size: 28px;
margin-bottom: 4px;
}
.file-ext {
font-size: 12px;
text-transform: uppercase;
text-align: center;
}
}
}
/* 让附件卡片一行展示 4 个 */
:deep(.el-upload-list--picture-card) {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 9px;
margin-top: 12px;
/* 确保卡片宽度自适应 */
.el-upload-list__item {
margin: 0;
}
}
}
:deep(.el-upload-dragger) {
border: 0px !important;
padding: 0px !important;
.el-upload-dragger1 {
border: 0px !important;
padding: 0px !important;
}
}
/* 附件查看对话框样式 */
.attachment-container {
.no-attachment {
text-align: center;
padding: 40px 0;
}
.attachment-list {
.attachment-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
border: 1px solid #e8e8e8;
border-radius: 8px;
margin-bottom: 8px;
transition: all 0.3s;
&:last-child {
margin-bottom: 0;
}
&:hover {
border-color: #409eff;
background-color: #f0f9ff;
}
.attachment-info {
display: flex;
align-items: center;
flex: 1;
.attachment-preview {
margin-right: 12px;
.file-icon {
color: #909399;
}
}
.attachment-details {
flex: 1;
.file-name {
font-weight: 500;
color: #333;
margin-bottom: 4px;
word-break: break-all;
}
.file-meta {
font-size: 12px;
color: #909399;
display: flex;
gap: 12px;
}
.file-description {
font-size: 12px;
color: #666;
margin-top: 4px;
font-style: italic;
}
}
}
.attachment-actions {
display: flex;
gap: 8px;
}
}
}
}
/* 详情弹窗样式 */
.repair-order-detail {
.detail-card {
margin-bottom: 16px;
border: 1px solid #e8e8e8;
border-radius: 8px;
&:last-child {
margin-bottom: 0;
}
.card-header {
display: flex;
align-items: center;
font-weight: 600;
color: #333;
.card-title {
display: flex;
align-items: center;
font-size: 16px;
.el-icon {
margin-right: 8px;
font-size: 18px;
color: #409eff;
}
}
}
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px 24px;
.detail-item {
display: flex;
align-items: flex-start;
&.full-width {
grid-column: 1 / -1;
}
label {
flex-shrink: 0;
width: 120px;
font-weight: 500;
color: #666;
font-size: 14px;
line-height: 1.5;
}
.detail-value {
flex: 1;
color: #333;
font-size: 14px;
line-height: 1.5;
&.description {
line-height: 1.6;
word-break: break-word;
white-space: pre-wrap;
background-color: #f8f9fa;
padding: 8px 12px;
border-radius: 4px;
border-left: 3px solid #409eff;
}
}
.el-tag {
font-weight: 500;
}
.el-rate {
display: flex;
align-items: center;
}
}
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.detail-card .detail-grid {
grid-template-columns: 1fr;
gap: 12px;
.detail-item {
flex-direction: column;
align-items: flex-start;
label {
width: auto;
margin-bottom: 4px;
}
}
}
}
}
/* 附件网格卡片样式 */
.attachment-detail-container {
.no-attachment-detail {
text-align: center;
padding: 40px 0;
color: #909399;
}
.attachment-grid-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding: 8px 0;
.attachment-card {
background: #ffffff;
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
transform: translateY(-2px);
}
.attachment-thumbnail-wrapper {
position: relative;
height: 120px;
background: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.attachment-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.attachment-file-icon {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
color: #409eff;
.file-type-text {
font-size: 12px;
color: #666;
font-weight: 500;
}
}
}
.attachment-card-body {
padding: 12px;
height: 80px;
display: flex;
flex-direction: column;
justify-content: space-between;
.attachment-title {
font-size: 13px;
font-weight: 600;
color: #333;
line-height: 1.3;
margin-bottom: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attachment-meta {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #999;
margin-bottom: 4px;
.file-size {
font-weight: 500;
}
.upload-time {
font-style: italic;
}
}
.attachment-desc {
font-size: 11px;
color: #666;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: auto;
}
}
.attachment-card-actions {
padding: 8px 12px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: center;
.el-button-group {
.el-button {
padding: 4px 6px;
font-size: 12px;
.el-icon {
font-size: 14px;
}
}
}
}
}
}
}
/* 卡片头部样式增强 */
.detail-card .card-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.card-title {
flex: 1;
}
}
/* 进度查看样式 */
.progress-container {
.no-progress {
text-align: center;
padding: 40px 0;
color: #909399;
}
.progress-timeline {
position: relative;
padding: 16px 0;
.progress-item {
position: relative;
display: flex;
align-items: flex-start;
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
.timeline-line {
display: none;
}
}
.timeline-icon {
position: relative;
z-index: 2;
width: 32px;
height: 32px;
border-radius: 50%;
background: white;
border: 2px solid #e8e8e8;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
flex-shrink: 0;
margin-top: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.timeline-line {
position: absolute;
left: 15px;
top: 40px;
width: 2px;
height: calc(100% + 8px);
background: linear-gradient(to bottom, #e8e8e8 0%, #f0f0f0 100%);
z-index: 1;
}
.progress-card {
flex: 1;
background: white;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
transform: translateY(-1px);
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
.progress-type-status {
display: flex;
align-items: center;
gap: 8px;
.progress-status {
font-weight: 600;
color: #333;
font-size: 14px;
}
}
.progress-time {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #666;
.el-icon {
font-size: 14px;
}
}
}
.progress-content {
.progress-detail {
margin-bottom: 12px;
.detail-text {
margin: 0;
line-height: 1.6;
color: #333;
font-size: 14px;
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
border-left: 3px solid #409eff;
}
}
.progress-info-grid {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
.info-item {
display: flex;
align-items: flex-start;
font-size: 13px;
line-height: 1.5;
label {
display: flex;
align-items: center;
gap: 4px;
min-width: 80px;
color: #666;
font-weight: 500;
margin-right: 8px;
.el-icon {
font-size: 14px;
color: #409eff;
}
}
span {
flex: 1;
color: #333;
word-break: break-word;
}
}
}
}
}
}
}
}
/* 响应式设计 */
@media (max-width: 1200px) {
.attachment-detail-container .attachment-grid-container {
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
}
@media (max-width: 768px) {
.attachment-detail-container .attachment-grid-container {
grid-template-columns: 1fr;
gap: 12px;
.attachment-card {
.attachment-thumbnail-wrapper {
height: 100px;
}
.attachment-card-body {
height: 70px;
padding: 10px;
.attachment-title {
font-size: 12px;
}
.attachment-meta {
font-size: 10px;
}
}
.attachment-card-actions {
padding: 6px 10px;
.el-button-group .el-button {
padding: 3px 5px;
font-size: 11px;
.el-icon {
font-size: 12px;
}
}
}
}
}
/* 进度查看响应式 */
.progress-container .progress-timeline .progress-item {
.timeline-icon {
width: 28px;
height: 28px;
margin-right: 12px;
}
.timeline-line {
left: 13px;
}
.progress-card {
padding: 12px;
.progress-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
.progress-time {
font-size: 12px;
}
}
.progress-content {
.progress-detail .detail-text {
padding: 8px;
font-size: 13px;
}
.progress-info-grid .info-item {
font-size: 12px;
label {
min-width: 70px;
font-size: 12px;
}
}
}
}
}
}
:deep(.el-icon) {
display: flex !important;
}
/* Element Plus upload 组件样式覆盖 */
:deep(.el-upload-list--picture-card) {
.el-upload-list__item {
width: 166px !important;
height: auto !important;
min-height: 180px !important;
margin: 0 8px 8px 0 !important;
border-radius: 6px !important;
overflow: hidden !important;
}
}
/* 卡片整体布局包含文件名 */
.upload-file-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
min-height: 180px;
position: relative;
}
.upload-file-thumb {
width: 100%;
height: 120px;
position: relative;
overflow: hidden;
border-radius: 4px 4px 0 0;
}
.upload-file-thumb .el-image,
.upload-file-thumb .upload-file-placeholder {
width: 100%;
height: 100%;
}
.upload-file-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fafafa;
.el-icon {
font-size: 24px;
color: #c0c4cc;
margin-bottom: 8px;
}
.file-ext {
font-size: 12px;
color: #909399;
text-align: center;
word-break: break-all;
padding: 0 8px;
}
}
.upload-file-name {
width: 100%;
padding: 8px 4px 4px 4px;
font-size: 12px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #606266;
background-color: #fff;
flex-shrink: 0;
}
/* 上传文件卡片操作按钮样式 (与预览界面保持一致) */
.upload-file-card-actions {
width: 100%;
padding: 6px 8px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: center;
background-color: #fafafa;
margin-top: auto;
flex-shrink: 0;
.el-button-group {
.el-button {
padding: 4px 6px;
font-size: 12px;
.el-icon {
font-size: 14px;
}
}
}
}
.upload-file-loading {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
.is-loading {
font-size: 32px;
color: #409eff;
}
}
.upload-file-thumb {
position: relative;
}
.upload-file-loading {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.75);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
pointer-events: all;
.is-loading {
font-size: 38px;
color: var(--el-color-primary);
margin-bottom: 6px;
animation: spin 1s linear infinite;
}
.loading-text {
font-size: 14px;
color: var(--el-color-primary);
font-weight: bold;
text-shadow: 0 1px 2px #fff;
}
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
</style>