3933 lines
129 KiB
Vue
3933 lines
129 KiB
Vue
<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>
|