|
@@ -0,0 +1,1541 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="pwd-app">
|
|
|
|
|
+ <!-- 查询区 -->
|
|
|
|
|
+ <div class="qcard">
|
|
|
|
|
+ <div class="qrows">
|
|
|
|
|
+ <div class="qrow">
|
|
|
|
|
+ <div class="qf">
|
|
|
|
|
+ <label>关键字</label>
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="queryParams.keyword"
|
|
|
|
|
+ placeholder="工单编号 / 设备编码 / 故障描述"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ style="width: 240px"
|
|
|
|
|
+ @keyup.enter="doQuery"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="qf">
|
|
|
|
|
+ <label>优先级</label>
|
|
|
|
|
+ <el-select v-model="queryParams.orderLevel" placeholder="全部" clearable style="width: 120px">
|
|
|
|
|
+ <el-option label="紧急" :value="0" />
|
|
|
|
|
+ <el-option label="一般" :value="1" />
|
|
|
|
|
+ <el-option label="低" :value="2" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="qf">
|
|
|
|
|
+ <label>故障时间</label>
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="faultTimeRange"
|
|
|
|
|
+ type="datetimerange"
|
|
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
+ start-placeholder="开始时间"
|
|
|
|
|
+ end-placeholder="结束时间"
|
|
|
|
|
+ style="width: 360px"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="qbtns">
|
|
|
|
|
+ <el-button type="primary" icon="Search" @click="doQuery">查询</el-button>
|
|
|
|
|
+ <el-button icon="Refresh" @click="doReset">重置</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 状态筛选条 -->
|
|
|
|
|
+ <div class="strips">
|
|
|
|
|
+ <button class="chip" :class="{ on: !queryParams.orderStatus && queryParams.orderStatus !== 0 }" @click="pickStatus(undefined)">
|
|
|
|
|
+ <b>全部</b><span class="cnt">{{ total }}</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="s in STATUS_ORDER"
|
|
|
|
|
+ :key="s"
|
|
|
|
|
+ class="chip"
|
|
|
|
|
+ :class="{ on: queryParams.orderStatus === s }"
|
|
|
|
|
+ @click="pickStatus(s)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <b>{{ orderStatusText(s) }}</b><span class="cnt">{{ statusCount(s) }}</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="cols">
|
|
|
|
|
+ <!-- 左侧:工单列表 -->
|
|
|
|
|
+ <aside class="listcol">
|
|
|
|
|
+ <div class="lhd">
|
|
|
|
|
+ <b>工单列表</b>
|
|
|
|
|
+ <span v-loading="listLoading">共 {{ total }} 条</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-loading="listLoading" class="lscroll">
|
|
|
|
|
+ <div v-if="!list.length" class="lempty">暂无符合条件的工单</div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="o in list"
|
|
|
|
|
+ :key="o.orderId"
|
|
|
|
|
+ class="li"
|
|
|
|
|
+ :class="{ on: selectedOrderId === o.orderId }"
|
|
|
|
|
+ @click="handleSelectOrder(o)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="li-top">
|
|
|
|
|
+ <span class="ono">{{ o.orderNo }}</span>
|
|
|
|
|
+ <span class="bdg" :style="badgeStyle(o.orderStatus)">{{ orderStatusText(o.orderStatus) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="li-ft">{{ o.orderDesc || '-' }}</div>
|
|
|
|
|
+ <div class="li-dev">设备:{{ o.deviceCode || '-' }}</div>
|
|
|
|
|
+ <div class="li-tm">{{ timeText(o.faultTime) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="lpage">
|
|
|
|
|
+ <el-pagination
|
|
|
|
|
+ v-model:current-page="queryParams.pageNum"
|
|
|
|
|
+ v-model:page-size="queryParams.pageSize"
|
|
|
|
|
+ :total="total"
|
|
|
|
|
+ :page-sizes="[20, 50, 100]"
|
|
|
|
|
+ layout="total, prev, pager, next"
|
|
|
|
|
+ small
|
|
|
|
|
+ @current-change="handleSearch"
|
|
|
|
|
+ @size-change="handleSizeChange"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </aside>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右侧:工单详情 -->
|
|
|
|
|
+ <section class="detcol">
|
|
|
|
|
+ <template v-if="selectedOrder && selectedOrder.orderId">
|
|
|
|
|
+ <div class="dhd">
|
|
|
|
|
+ <b>工单详情</b>
|
|
|
|
|
+ <span class="no">{{ selectedOrder.orderNo }}</span>
|
|
|
|
|
+ <span class="bdg" :style="badgeStyle(selectedOrder.orderStatus)">{{ orderStatusText(selectedOrder.orderStatus) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 步骤条 -->
|
|
|
|
|
+ <div class="stepcard">
|
|
|
|
|
+ <div class="steps">
|
|
|
|
|
+ <template v-for="(n, i) in stepNodes" :key="i">
|
|
|
|
|
+ <div :class="n.cls">
|
|
|
|
|
+ <div class="st-ic">{{ n.icon }}</div>
|
|
|
|
|
+ <label>{{ n.label }}</label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="i < stepNodes.length - 1" class="sline" :class="{ dl: !isCancelled && i < stepIdxVal }"></div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 详情卡片 -->
|
|
|
|
|
+ <div class="tabcard">
|
|
|
|
|
+ <div class="tabs">
|
|
|
|
|
+ <button class="tab" :class="{ on: activeTab === 'info' }" @click="activeTab = 'info'">工单信息</button>
|
|
|
|
|
+ <button class="tab" :class="{ on: activeTab === 'hist' }" @click="activeTab = 'hist'">处理历史</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 工单信息 -->
|
|
|
|
|
+ <div v-show="activeTab === 'info'" class="pane">
|
|
|
|
|
+ <div class="sect">
|
|
|
|
|
+ <div class="sect-t">设备信息</div>
|
|
|
|
|
+ <div class="kv">
|
|
|
|
|
+ <div><div class="k">设备名称</div><div class="v">{{ selectedOrder.deviceName || '-' }}</div></div>
|
|
|
|
|
+ <div><div class="k">设备编码</div><div class="v mono">{{ selectedOrder.deviceCode || '-' }}</div></div>
|
|
|
|
|
+ <div><div class="k">设备类型</div><div class="v">{{ selectedOrder.deviceTypeName || '-' }}</div></div>
|
|
|
|
|
+ <div><div class="k">安装位置</div><div class="v">{{ selectedOrder.deviceLocation || '-' }}</div></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="sect">
|
|
|
|
|
+ <div class="sect-t">故障信息</div>
|
|
|
|
|
+ <div class="kv">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div class="k">优先级</div>
|
|
|
|
|
+ <div class="v"><span class="pri" :class="priClass(selectedOrder.orderLevel)"><i></i>{{ orderLevelText(selectedOrder.orderLevel) }}</span></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div><div class="k">故障发生时间</div><div class="v">{{ timeText(selectedOrder.faultTime) }}</div></div>
|
|
|
|
|
+ <div><div class="k">计划完成时间</div><div class="v">{{ timeText(selectedOrder.planFinishTime) }}</div></div>
|
|
|
|
|
+ <div><div class="k">责任人</div><div class="v">{{ selectedOrder.receiveUserName || '—' }}</div></div>
|
|
|
|
|
+ <div class="full"><div class="k">故障描述</div><div class="v">{{ selectedOrder.orderDesc || '-' }}</div></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="sect">
|
|
|
|
|
+ <div class="sect-t">附件</div>
|
|
|
|
|
+ <div class="zones">
|
|
|
|
|
+ <div class="zone z-pre">
|
|
|
|
|
+ <div class="zone-hd">维修前<span class="tip">{{ beforeAttach.length + ' 项' }}</span></div>
|
|
|
|
|
+ <div class="zbody">
|
|
|
|
|
+ <div v-for="(u, i) in beforeAttach" :key="'b' + i" class="att" :class="extCls(u) === 'img' ? 'att-img' : 'att-file'" @click="extCls(u) === 'img' && preview(u, beforeAttach)">
|
|
|
|
|
+ <img v-if="extCls(u) === 'img'" :src="u" alt="" />
|
|
|
|
|
+ <div v-else class="ext" :class="extCls(u)">{{ extText(u) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!beforeAttach.length" class="hint">暂无维修前附件</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="zone z-post">
|
|
|
|
|
+ <div class="zone-hd">维修后<span class="tip">{{ afterAttach.length + ' 项' }}</span></div>
|
|
|
|
|
+ <div class="zbody">
|
|
|
|
|
+ <div v-for="(u, i) in afterAttach" :key="'a' + i" class="att" :class="extCls(u) === 'img' ? 'att-img' : 'att-file'" @click="extCls(u) === 'img' && preview(u, afterAttach)">
|
|
|
|
|
+ <img v-if="extCls(u) === 'img'" :src="u" alt="" />
|
|
|
|
|
+ <div v-else class="ext" :class="extCls(u)">{{ extText(u) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!afterAttach.length" class="hint">维修完成后上传</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="sect nod">
|
|
|
|
|
+ <div class="sect-t">延期申请</div>
|
|
|
|
|
+ <div v-if="Number(selectedOrder.delayApplyStatus) > 0" class="delaybox">
|
|
|
|
|
+ <span class="dchip">{{ delayApplyStatusText(selectedOrder.delayApplyStatus) }}</span>
|
|
|
|
|
+ <div class="di">
|
|
|
|
|
+ <b>{{ selectedOrder.delayedReason || '-' }}</b>
|
|
|
|
|
+ <span>延期截止:{{ timeText(selectedOrder.delayedDeadline) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else class="nodelay">没有延期信息</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 处理历史 -->
|
|
|
|
|
+ <div v-show="activeTab === 'hist'" class="pane">
|
|
|
|
|
+ <div class="hhint">按时间倒序分组 · 滚动即可回溯</div>
|
|
|
|
|
+ <template v-if="logGroups.length">
|
|
|
|
|
+ <div v-for="g in logGroups" :key="g.day">
|
|
|
|
|
+ <div class="hday">{{ dayLabel(g.day) }}</div>
|
|
|
|
|
+ <div v-for="(l, i) in g.items" :key="i" class="hrow">
|
|
|
|
|
+ <span class="ht">{{ logTime(l.operTime) }}</span>
|
|
|
|
|
+ <div class="hb">
|
|
|
|
|
+ <div class="hl">
|
|
|
|
|
+ <b>{{ l.operUserName || '-' }}</b>
|
|
|
|
|
+ <span class="sep">·</span>{{ orderLogText(l.operType) }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="l.operDesc" class="hdesc">{{ l.operDesc }}</div>
|
|
|
|
|
+ <div v-if="splitAttach(l.attachUrl).length" class="hchips">
|
|
|
|
|
+ <template v-for="(u, k) in splitAttach(l.attachUrl)" :key="k">
|
|
|
|
|
+ <img
|
|
|
|
|
+ v-if="extCls(u) === 'img'"
|
|
|
|
|
+ :src="u"
|
|
|
|
|
+ class="hthumb"
|
|
|
|
|
+ alt=""
|
|
|
|
|
+ @click="preview(u, splitAttach(l.attachUrl))"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span v-else class="hchip" @click="preview(u, splitAttach(l.attachUrl))">{{ fileName(u) }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <div v-else class="hint" style="padding: 20px 0">暂无处理记录</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 操作栏 -->
|
|
|
|
|
+ <div class="abar">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-if="canOperate.delay(selectedOrder)"
|
|
|
|
|
+ v-hasPermi="['ops:pworkOrder:delay']"
|
|
|
|
|
+ @click="openDelay(selectedOrder)"
|
|
|
|
|
+ >申请延期</el-button>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-if="canOperate.delayAudit(selectedOrder)"
|
|
|
|
|
+ v-hasPermi="['ops:pworkOrder:delayAudit']"
|
|
|
|
|
+ @click="openDelayAudit(selectedOrder)"
|
|
|
|
|
+ >延期审核</el-button>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-if="canOperate.cancel(selectedOrder)"
|
|
|
|
|
+ v-hasPermi="['ops:pworkOrder:cancel']"
|
|
|
|
|
+ type="danger" plain
|
|
|
|
|
+ @click="openAction(selectedOrder, 'cancel')"
|
|
|
|
|
+ >取消</el-button>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-if="canOperate.remove(selectedOrder)"
|
|
|
|
|
+ v-hasPermi="['ops:pworkOrder:remove']"
|
|
|
|
|
+ type="danger" plain
|
|
|
|
|
+ @click="handleDelete(selectedOrder)"
|
|
|
|
|
+ >删除工单</el-button>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-if="canOperate.dispatch(selectedOrder)"
|
|
|
|
|
+ v-hasPermi="['ops:pworkOrder:dispatch']"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ @click="openDispatch(selectedOrder)"
|
|
|
|
|
+ >派单</el-button>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-if="canOperate.accept(selectedOrder)"
|
|
|
|
|
+ v-hasPermi="['ops:pworkOrder:accept']"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ @click="openAction(selectedOrder, 'accept')"
|
|
|
|
|
+ >核查接单</el-button>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-if="canOperate.process(selectedOrder)"
|
|
|
|
|
+ v-hasPermi="['ops:pworkOrder:process']"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ @click="openAction(selectedOrder, 'process')"
|
|
|
|
|
+ >开始处理</el-button>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-if="canOperate.submitVerify(selectedOrder)"
|
|
|
|
|
+ v-hasPermi="['ops:pworkOrder:submitVerify']"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ @click="openAction(selectedOrder, 'submitVerify')"
|
|
|
|
|
+ >提交验收</el-button>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-if="canOperate.verify(selectedOrder)"
|
|
|
|
|
+ v-hasPermi="['ops:pworkOrder:verify']"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ @click="openVerify(selectedOrder)"
|
|
|
|
|
+ >验收通过</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-else class="dempty">
|
|
|
|
|
+ <el-empty description="请从左侧选择工单" :image-size="150" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 附件预览 -->
|
|
|
|
|
+ <el-image-viewer v-if="viewerVisible" :url-list="viewerList" :initial-index="viewerIndex" @close="viewerVisible = false" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 派单弹窗 -->
|
|
|
|
|
+ <el-dialog v-model="dispatchVisible" title="工单派单" width="720px" append-to-body destroy-on-close>
|
|
|
|
|
+ <el-form label-position="top">
|
|
|
|
|
+ <el-form-item label="责任人" required>
|
|
|
|
|
+ <div class="handler-box">
|
|
|
|
|
+ <el-input :model-value="dispatchForm.handlerName" placeholder="请选择责任人" readonly style="width: 240px">
|
|
|
|
|
+ <template #append><el-button @click="userSelectVisible = true">选择</el-button></template>
|
|
|
|
|
+ </el-input>
|
|
|
|
|
+ <el-button v-if="dispatchForm.handlerId" link type="danger" @click="clearHandler">清除</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="派单备注">
|
|
|
|
|
+ <el-input v-model="dispatchForm.remarks" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="可填写故障要点、现场注意事项等派单说明" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="dispatchVisible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" :loading="submitting" @click="submitDispatch">确认派单</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 选择责任人 -->
|
|
|
|
|
+ <el-dialog v-model="userSelectVisible" title="选择责任人" width="760px" append-to-body>
|
|
|
|
|
+ <el-row :gutter="12">
|
|
|
|
|
+ <el-col :span="8">
|
|
|
|
|
+ <div class="dept-tree-box">
|
|
|
|
|
+ <el-tree
|
|
|
|
|
+ :data="deptTree" :props="{ label: 'label', children: 'children' }" node-key="id"
|
|
|
|
|
+ highlight-current default-expand-all :expand-on-click-node="false" @node-click="handleDeptNodeClick"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="16">
|
|
|
|
|
+ <el-table :data="deptUserList" height="320" border highlight-current-row @current-change="handleUserCurrentChange">
|
|
|
|
|
+ <el-table-column width="48" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-radio v-model="selectedUserId" :value="row.userId" @change="handleUserRadioChange(row)" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="用户昵称" prop="nickName" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="登录账号" prop="userName" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="部门" prop="dept.deptName" show-overflow-tooltip />
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="userSelectVisible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="confirmUserSelect">确定</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 通用操作:核查接单 / 处置 / 提交验收 / 取消 -->
|
|
|
|
|
+ <el-dialog v-model="actionVisible" :title="actionTitle" width="620px" append-to-body destroy-on-close>
|
|
|
|
|
+ <el-form label-position="top">
|
|
|
|
|
+ <el-form-item :label="actionContentLabel" required>
|
|
|
|
|
+ <el-input v-model="actionForm.content" type="textarea" :rows="4" maxlength="500" show-word-limit :placeholder="actionContentPlaceholder" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <template #label>
|
|
|
|
|
+ <span v-if="actionAttachRequired" class="req-mark">*</span>{{ actionAttachLabel }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <ImageUpload v-model="actionForm.attachUrl" :limit="9" :is-show-tip="true" :upload-url="imageUploadUrl" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="actionVisible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" :loading="submitting" @click="submitAction">确定</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 验收 -->
|
|
|
|
|
+ <el-dialog v-model="verifyVisible" title="工单验收" width="620px" append-to-body destroy-on-close>
|
|
|
|
|
+ <el-form label-position="top">
|
|
|
|
|
+ <el-form-item label="验收结果" required>
|
|
|
|
|
+ <el-radio-group v-model="verifyForm.passed">
|
|
|
|
|
+ <el-radio-button :value="true">验收通过(结案)</el-radio-button>
|
|
|
|
|
+ <el-radio-button :value="false">验收驳回(退回处理中)</el-radio-button>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="验收意见">
|
|
|
|
|
+ <el-input v-model="verifyForm.content" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入验收意见" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="验收附件(可选)">
|
|
|
|
|
+ <ImageUpload v-model="verifyForm.attachUrl" :limit="9" :is-show-tip="true" :upload-url="imageUploadUrl" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="verifyVisible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" :loading="submitting" @click="submitVerify">提交</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 延期申请 -->
|
|
|
|
|
+ <el-dialog v-model="delayVisible" title="申请延期" width="620px" append-to-body destroy-on-close>
|
|
|
|
|
+ <el-form label-position="top">
|
|
|
|
|
+ <el-form-item label="延期截止时间" required>
|
|
|
|
|
+ <el-date-picker v-model="delayForm.delayedDeadline" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择延期截止时间" style="width: 100%" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="延期原因" required>
|
|
|
|
|
+ <el-input v-model="delayForm.delayedReason" type="textarea" :rows="4" maxlength="300" show-word-limit placeholder="请填写延期原因" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="附件">
|
|
|
|
|
+ <ImageUpload v-model="delayForm.attachUrl" :limit="9" :is-show-tip="true" :upload-url="imageUploadUrl" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="delayVisible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" :loading="submitting" @click="submitDelay">提交申请</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 延期审核 -->
|
|
|
|
|
+ <el-dialog v-model="delayAuditVisible" title="延期审核" width="560px" append-to-body destroy-on-close>
|
|
|
|
|
+ <el-form label-position="top">
|
|
|
|
|
+ <el-form-item label="审核结果" required>
|
|
|
|
|
+ <el-radio-group v-model="delayAuditForm.approved">
|
|
|
|
|
+ <el-radio-button :value="true">审核通过</el-radio-button>
|
|
|
|
|
+ <el-radio-button :value="false">审核驳回</el-radio-button>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="审核意见">
|
|
|
|
|
+ <el-input v-model="delayAuditForm.auditOpinion" type="textarea" :rows="4" maxlength="300" show-word-limit placeholder="请输入审核意见" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="delayAuditVisible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" :loading="submitting" @click="submitDelayAudit">提交审核</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, reactive, computed, getCurrentInstance, onMounted } from 'vue'
|
|
|
|
|
+import {
|
|
|
|
|
+ listPworkOrder,
|
|
|
|
|
+ getPworkOrder,
|
|
|
|
|
+ dispatchPworkOrder,
|
|
|
|
|
+ acceptPworkOrder,
|
|
|
|
|
+ processPworkOrder,
|
|
|
|
|
+ submitVerifyPworkOrder,
|
|
|
|
|
+ verifyPworkOrder,
|
|
|
|
|
+ cancelPworkOrder,
|
|
|
|
|
+ applyDelayPworkOrder,
|
|
|
|
|
+ auditDelayPworkOrder,
|
|
|
|
|
+ delPworkOrder,
|
|
|
|
|
+ pworkOrderDeptTree,
|
|
|
|
|
+ pworkOrderUsers
|
|
|
|
|
+} from '@/api/pipeNetwork/ops/pworkOrder.js'
|
|
|
|
|
+import {
|
|
|
|
|
+ canOperate,
|
|
|
|
|
+ orderLevelText,
|
|
|
|
|
+ ORDER_STATUS_MAP,
|
|
|
|
|
+ orderStatusText,
|
|
|
|
|
+ delayApplyStatusText,
|
|
|
|
|
+ orderLogText,
|
|
|
|
|
+ timeText,
|
|
|
|
|
+ splitAttach
|
|
|
|
|
+} from './workOrderDict.js'
|
|
|
|
|
+import ImageUpload from '@/components/ImageUpload/index.vue'
|
|
|
|
|
+import { useRoute } from 'vue-router'
|
|
|
|
|
+
|
|
|
|
|
+defineOptions({ name: 'Pworkdisposition' })
|
|
|
|
|
+
|
|
|
|
|
+const { proxy } = getCurrentInstance()
|
|
|
|
|
+const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
|
|
|
|
|
+const route = useRoute()
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 本组件被多个菜单复用,不同菜单按业务口径查询不同范围的工单:
|
|
|
|
|
+ * - 运维-工单处置菜单:查询全部工单
|
|
|
|
|
+ * - 燃气-工单管理菜单:仅查询 parentTypeId 为 GAS_DEVICE_TYPE_PARENT_ID 的设备所产生的工单
|
|
|
|
|
+ *
|
|
|
|
|
+ * 判定方式:同一组件由后台 sys_menu.component 指向本文件时,菜单的 path 会体现在路由 path 中,
|
|
|
|
|
+ * 因此按路由 path 关键字区分来源。若后续调整菜单 path,同步维护 GAS_MENU_PATHS 即可。
|
|
|
|
|
+ */
|
|
|
|
|
+const GAS_MENU_PATHS = ['BusinessWorkOrder']
|
|
|
|
|
+const GAS_DEVICE_TYPE_PARENT_ID = '3'
|
|
|
|
|
+const gasMode = computed(() => GAS_MENU_PATHS.some((p) => String(route.path || '').includes(p)))
|
|
|
|
|
+
|
|
|
|
|
+/** 统一构造查询入参:燃气模式下固定附加设备父类别过滤,且不被重置操作清除 */
|
|
|
|
|
+function buildQueryParams() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...queryParams,
|
|
|
|
|
+ ...(gasMode.value ? { deviceTypeParentId: GAS_DEVICE_TYPE_PARENT_ID } : {})
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const STATUS_ORDER = [0, 1, 2, 3, 4, 5, 6, 7]
|
|
|
|
|
+const STEP_NODES = ['创建', '派单', '接单', '处置', '验收', '完成']
|
|
|
|
|
+const STEP_INDEX = { 0: 1, 1: 2, 2: 3, 3: 3, 4: 4, 5: 5, 6: 3 }
|
|
|
|
|
+const BADGE = {
|
|
|
|
|
+ 0: ['#FAEEDA', '#854F0B'], 1: ['#E6F1FB', '#185FA5'], 2: ['#E1F5EE', '#0F6E56'], 3: ['#ECEEF9', '#3F51B5'],
|
|
|
|
|
+ 4: ['#FAECE7', '#993C1D'], 5: ['#EAF3DE', '#3B6D11'], 6: ['#FBEAF0', '#993556'], 7: ['#F1EFE8', '#5F5E5A']
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const listLoading = ref(false)
|
|
|
|
|
+const detailLoading = ref(false)
|
|
|
|
|
+const submitting = ref(false)
|
|
|
|
|
+const list = ref([])
|
|
|
|
|
+const total = ref(0)
|
|
|
|
|
+const faultTimeRange = ref([])
|
|
|
|
|
+
|
|
|
|
|
+const queryParams = reactive({
|
|
|
|
|
+ pageNum: 1,
|
|
|
|
|
+ pageSize: 20,
|
|
|
|
|
+ keyword: '',
|
|
|
|
|
+ orderLevel: undefined,
|
|
|
|
|
+ orderStatus: undefined,
|
|
|
|
|
+ faultTimeStart: '',
|
|
|
|
|
+ faultTimeEnd: ''
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const selectedOrderId = ref(null)
|
|
|
|
|
+const selectedOrder = ref({})
|
|
|
|
|
+const currentOrderLogs = ref([])
|
|
|
|
|
+const activeTab = ref('info')
|
|
|
|
|
+
|
|
|
|
|
+const ACTION_CONFIG = {
|
|
|
|
|
+ accept: { title: '核查接单', label: '核查结论', placeholder: '请如实填写现场核查结论,注明故障是否属实、现场现状等内容', attachLabel: '上传现场情况', attachRequired: true, api: acceptPworkOrder },
|
|
|
|
|
+ process: { title: '工单处置', label: '处置内容', placeholder: '请记录故障原因、处置方案、实施措施及处理结果,支持多次处置填报', attachLabel: '上传附件(可选)', attachRequired: false, api: processPworkOrder },
|
|
|
|
|
+ submitVerify: { title: '提交验收', label: '验收说明', placeholder: '请填报维修处置完成情况,同步上传维修完成后的佐证材料', attachLabel: '上传维修后的情况', attachRequired: true, api: submitVerifyPworkOrder },
|
|
|
|
|
+ cancel: { title: '取消工单', label: '取消原因', placeholder: '请详细说明工单取消事由', attachLabel: '上传附件(可选)', attachRequired: false, api: cancelPworkOrder }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ============ 展示辅助 ============ */
|
|
|
|
|
+function badgeStyle(status) {
|
|
|
|
|
+ const c = BADGE[Number(status)] || ['#F1EFE8', '#5F5E5A']
|
|
|
|
|
+ return { background: c[0], color: c[1] }
|
|
|
|
|
+}
|
|
|
|
|
+function priClass(level) {
|
|
|
|
|
+ const n = Number(level)
|
|
|
|
|
+ return n === 0 ? 'h' : (n === 2 ? 'l' : 'm')
|
|
|
|
|
+}
|
|
|
|
|
+function fileName(url) {
|
|
|
|
|
+ if (!url) return '-'
|
|
|
|
|
+ return String(url).split('/').pop()
|
|
|
|
|
+}
|
|
|
|
|
+function extOf(url) {
|
|
|
|
|
+ const n = fileName(url)
|
|
|
|
|
+ const p = n.split('.')
|
|
|
|
|
+ return p.length > 1 ? p.pop().toLowerCase() : ''
|
|
|
|
|
+}
|
|
|
|
|
+function extCls(url) {
|
|
|
|
|
+ const e = extOf(url)
|
|
|
|
|
+ if (e === 'pdf') return 'pdf'
|
|
|
|
|
+ if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(e)) return 'img'
|
|
|
|
|
+ if (['doc', 'docx', 'xls', 'xlsx'].includes(e)) return 'doc'
|
|
|
|
|
+ return 'def'
|
|
|
|
|
+}
|
|
|
|
|
+function extText(url) {
|
|
|
|
|
+ const e = extOf(url).toUpperCase()
|
|
|
|
|
+ return e ? (e.length > 4 ? e.slice(0, 4) : e) : 'FILE'
|
|
|
|
|
+}
|
|
|
|
|
+function logTime(t) {
|
|
|
|
|
+ return t ? String(t).slice(11, 16) : '--:--'
|
|
|
|
|
+}
|
|
|
|
|
+function dayLabel(d) {
|
|
|
|
|
+ if (!d) return '-'
|
|
|
|
|
+ const today = new Date().toISOString().slice(0, 10)
|
|
|
|
|
+ if (d === today) return '今天 · ' + (+d.slice(5, 7)) + '月' + (+d.slice(8, 10)) + '日'
|
|
|
|
|
+ return (+d.slice(5, 7)) + '月' + (+d.slice(8, 10)) + '日'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const beforeAttach = computed(() => splitAttach(selectedOrder.value?.maintenanceBeforeImg))
|
|
|
|
|
+const afterAttach = computed(() => splitAttach(selectedOrder.value?.maintenanceAfterImg))
|
|
|
|
|
+const locked = computed(() => Number(selectedOrder.value?.orderStatus) === 5)
|
|
|
|
|
+const isCancelled = computed(() => Number(selectedOrder.value?.orderStatus) === 7)
|
|
|
|
|
+const stepIdxVal = computed(() => STEP_INDEX[Number(selectedOrder.value?.orderStatus)] ?? -1)
|
|
|
|
|
+
|
|
|
|
|
+const stepNodes = computed(() => {
|
|
|
|
|
+ if (!selectedOrder.value?.orderId) return []
|
|
|
|
|
+ if (isCancelled.value) {
|
|
|
|
|
+ let prog = 1
|
|
|
|
|
+ const logs = currentOrderLogs.value || []
|
|
|
|
|
+ if (logs.some((l) => Number(l.operType) === 1)) prog = 2
|
|
|
|
|
+ if (logs.some((l) => Number(l.operType) === 2)) prog = 3
|
|
|
|
|
+ return STEP_NODES.map((n, i) => {
|
|
|
|
|
+ if (i < prog) return { label: n, cls: 'stp donegray', icon: '✓' }
|
|
|
|
|
+ if (i === prog) return { label: '已终止', cls: 'stp offx', icon: '✕' }
|
|
|
|
|
+ return { label: n, cls: 'stp', icon: String(i + 1) }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ const idx = stepIdxVal.value
|
|
|
|
|
+ return STEP_NODES.map((n, i) => {
|
|
|
|
|
+ if (i < idx) return { label: n, cls: 'stp done', icon: '✓' }
|
|
|
|
|
+ if (i === idx) return { label: n, cls: 'stp cur', icon: String(i + 1) }
|
|
|
|
|
+ return { label: n, cls: 'stp', icon: String(i + 1) }
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const logGroups = computed(() => {
|
|
|
|
|
+ const days = {}
|
|
|
|
|
+ ;(currentOrderLogs.value || []).forEach((l) => {
|
|
|
|
|
+ const d = String(l.operTime || '').slice(0, 10) || '-'
|
|
|
|
|
+ if (!days[d]) days[d] = []
|
|
|
|
|
+ days[d].push(l)
|
|
|
|
|
+ })
|
|
|
|
|
+ return Object.keys(days)
|
|
|
|
|
+ .sort((a, b) => b.localeCompare(a))
|
|
|
|
|
+ .map((d) => ({ day: d, items: days[d].slice().sort((a, b) => logTime(b.operTime).localeCompare(logTime(a.operTime))) }))
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+function statusCount(s) {
|
|
|
|
|
+ return list.value.filter((o) => Number(o.orderStatus) === s).length
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ============ 附件预览 ============ */
|
|
|
|
|
+const viewerVisible = ref(false)
|
|
|
|
|
+const viewerList = ref([])
|
|
|
|
|
+const viewerIndex = ref(0)
|
|
|
|
|
+function preview(url, arr) {
|
|
|
|
|
+ const a = arr && arr.length ? arr : [url]
|
|
|
|
|
+ viewerList.value = a
|
|
|
|
|
+ viewerIndex.value = Math.max(0, a.indexOf(url))
|
|
|
|
|
+ viewerVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ============ 列表 ============ */
|
|
|
|
|
+async function handleSearch() {
|
|
|
|
|
+ listLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await listPworkOrder(buildQueryParams())
|
|
|
|
|
+ const data = res.data || {}
|
|
|
|
|
+ list.value = data.records || []
|
|
|
|
|
+ total.value = data.total || 0
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ listLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function doQuery() {
|
|
|
|
|
+ queryParams.pageNum = 1
|
|
|
|
|
+ if (faultTimeRange.value && faultTimeRange.value.length === 2) {
|
|
|
|
|
+ queryParams.faultTimeStart = faultTimeRange.value[0]
|
|
|
|
|
+ queryParams.faultTimeEnd = faultTimeRange.value[1]
|
|
|
|
|
+ } else {
|
|
|
|
|
+ queryParams.faultTimeStart = ''
|
|
|
|
|
+ queryParams.faultTimeEnd = ''
|
|
|
|
|
+ }
|
|
|
|
|
+ handleSearch()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function doReset() {
|
|
|
|
|
+ queryParams.pageNum = 1
|
|
|
|
|
+ queryParams.keyword = ''
|
|
|
|
|
+ queryParams.orderLevel = undefined
|
|
|
|
|
+ queryParams.orderStatus = undefined
|
|
|
|
|
+ queryParams.faultTimeStart = ''
|
|
|
|
|
+ queryParams.faultTimeEnd = ''
|
|
|
|
|
+ faultTimeRange.value = []
|
|
|
|
|
+ clearSelection()
|
|
|
|
|
+ handleSearch()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleSizeChange() {
|
|
|
|
|
+ queryParams.pageNum = 1
|
|
|
|
|
+ clearSelection()
|
|
|
|
|
+ handleSearch()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function pickStatus(s) {
|
|
|
|
|
+ queryParams.orderStatus = queryParams.orderStatus === s ? undefined : s
|
|
|
|
|
+ queryParams.pageNum = 1
|
|
|
|
|
+ clearSelection()
|
|
|
|
|
+ handleSearch()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function clearSelection() {
|
|
|
|
|
+ selectedOrderId.value = null
|
|
|
|
|
+ selectedOrder.value = {}
|
|
|
|
|
+ currentOrderLogs.value = []
|
|
|
|
|
+ activeTab.value = 'info'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleSelectOrder(o) {
|
|
|
|
|
+ selectedOrderId.value = o.orderId
|
|
|
|
|
+ selectedOrder.value = { ...o }
|
|
|
|
|
+ activeTab.value = 'info'
|
|
|
|
|
+ currentOrderLogs.value = []
|
|
|
|
|
+ detailLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getPworkOrder(o.orderId)
|
|
|
|
|
+ const data = res.data || {}
|
|
|
|
|
+ selectedOrder.value = { ...selectedOrder.value, ...(data.order || o) }
|
|
|
|
|
+ currentOrderLogs.value = data.logs || []
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ detailLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 操作后刷新列表并保留选中 */
|
|
|
|
|
+async function refresh(opOrderId) {
|
|
|
|
|
+ await handleSearch()
|
|
|
|
|
+ if (!opOrderId) return
|
|
|
|
|
+ const hit = list.value.find((x) => x.orderId === opOrderId)
|
|
|
|
|
+ if (hit) await handleSelectOrder(hit)
|
|
|
|
|
+ else clearSelection()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ============ 派单 ============ */
|
|
|
|
|
+const dispatchVisible = ref(false)
|
|
|
|
|
+const dispatchForm = reactive({ orderId: null, handlerId: null, handlerName: '', remarks: '' })
|
|
|
|
|
+
|
|
|
|
|
+function openDispatch(o) {
|
|
|
|
|
+ dispatchForm.orderId = o.orderId
|
|
|
|
|
+ dispatchForm.handlerId = o.receiveUser || null
|
|
|
|
|
+ dispatchForm.handlerName = o.receiveUserName || ''
|
|
|
|
|
+ dispatchForm.remarks = ''
|
|
|
|
|
+ dispatchVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitDispatch() {
|
|
|
|
|
+ if (!dispatchForm.handlerId) {
|
|
|
|
|
+ proxy.$modal.msgWarning('请选择责任人')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ submitting.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await dispatchPworkOrder({ orderId: dispatchForm.orderId, handlerId: dispatchForm.handlerId, remarks: dispatchForm.remarks })
|
|
|
|
|
+ proxy.$modal.msgSuccess('派单成功,工单状态变更为待接单')
|
|
|
|
|
+ dispatchVisible.value = false
|
|
|
|
|
+ await refresh(dispatchForm.orderId)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ submitting.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ============ 责任人选择 ============ */
|
|
|
|
|
+const userSelectVisible = ref(false)
|
|
|
|
|
+const deptTree = ref([])
|
|
|
|
|
+const deptUserList = ref([])
|
|
|
|
|
+const selectedUserId = ref(null)
|
|
|
|
|
+const selectedUser = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+function loadDeptTree() {
|
|
|
|
|
+ pworkOrderDeptTree().then((res) => { deptTree.value = res.data || [] })
|
|
|
|
|
+}
|
|
|
|
|
+function loadDeptUsers(deptId) {
|
|
|
|
|
+ pworkOrderUsers(deptId).then((res) => { deptUserList.value = res.data || [] })
|
|
|
|
|
+}
|
|
|
|
|
+function handleDeptNodeClick(node) { loadDeptUsers(node.id) }
|
|
|
|
|
+function handleUserCurrentChange(row) {
|
|
|
|
|
+ if (row) { selectedUser.value = row; selectedUserId.value = row.userId }
|
|
|
|
|
+}
|
|
|
|
|
+function handleUserRadioChange(row) { selectedUser.value = row; selectedUserId.value = row.userId }
|
|
|
|
|
+function confirmUserSelect() {
|
|
|
|
|
+ if (!selectedUser.value) { proxy.$modal.msgWarning('请选择责任人'); return }
|
|
|
|
|
+ dispatchForm.handlerId = selectedUser.value.userId
|
|
|
|
|
+ dispatchForm.handlerName = selectedUser.value.nickName || selectedUser.value.userName
|
|
|
|
|
+ userSelectVisible.value = false
|
|
|
|
|
+}
|
|
|
|
|
+function clearHandler() {
|
|
|
|
|
+ dispatchForm.handlerId = null
|
|
|
|
|
+ dispatchForm.handlerName = ''
|
|
|
|
|
+ selectedUser.value = null
|
|
|
|
|
+ selectedUserId.value = null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ============ 通用操作 ============ */
|
|
|
|
|
+const actionVisible = ref(false)
|
|
|
|
|
+const actionType = ref('')
|
|
|
|
|
+const actionForm = reactive({ orderId: null, content: '', attachUrl: '' })
|
|
|
|
|
+const actionTitle = ref('')
|
|
|
|
|
+const actionContentLabel = ref('')
|
|
|
|
|
+const actionContentPlaceholder = ref('')
|
|
|
|
|
+const actionAttachLabel = ref('上传附件')
|
|
|
|
|
+const actionAttachRequired = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+function openAction(o, type) {
|
|
|
|
|
+ const cfg = ACTION_CONFIG[type]
|
|
|
|
|
+ if (!cfg) return
|
|
|
|
|
+ actionType.value = type
|
|
|
|
|
+ actionForm.orderId = o.orderId
|
|
|
|
|
+ actionForm.content = ''
|
|
|
|
|
+ actionForm.attachUrl = ''
|
|
|
|
|
+ actionTitle.value = cfg.title
|
|
|
|
|
+ actionContentLabel.value = cfg.label
|
|
|
|
|
+ actionContentPlaceholder.value = cfg.placeholder
|
|
|
|
|
+ actionAttachLabel.value = cfg.attachLabel || '上传附件'
|
|
|
|
|
+ actionAttachRequired.value = !!cfg.attachRequired
|
|
|
|
|
+ actionVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitAction() {
|
|
|
|
|
+ const cfg = ACTION_CONFIG[actionType.value]
|
|
|
|
|
+ if (!actionForm.content || !actionForm.content.trim()) {
|
|
|
|
|
+ proxy.$modal.msgWarning(`请填写${actionContentLabel.value}`)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (actionAttachRequired.value && !(actionForm.attachUrl || '').trim()) {
|
|
|
|
|
+ proxy.$modal.msgWarning(`请${actionAttachLabel.value}`)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ submitting.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await cfg.api({ orderId: actionForm.orderId, content: actionForm.content.trim(), attachUrl: actionForm.attachUrl || '' })
|
|
|
|
|
+ proxy.$modal.msgSuccess(`${actionTitle.value}成功`)
|
|
|
|
|
+ actionVisible.value = false
|
|
|
|
|
+ await refresh(actionForm.orderId)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ submitting.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ============ 验收 ============ */
|
|
|
|
|
+const verifyVisible = ref(false)
|
|
|
|
|
+const verifyForm = reactive({ orderId: null, passed: true, content: '', attachUrl: '' })
|
|
|
|
|
+
|
|
|
|
|
+function openVerify(o) {
|
|
|
|
|
+ verifyForm.orderId = o.orderId
|
|
|
|
|
+ verifyForm.passed = true
|
|
|
|
|
+ verifyForm.content = ''
|
|
|
|
|
+ verifyForm.attachUrl = ''
|
|
|
|
|
+ verifyVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+async function submitVerify() {
|
|
|
|
|
+ submitting.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await verifyPworkOrder({ orderId: verifyForm.orderId, passed: verifyForm.passed, content: verifyForm.content, attachUrl: verifyForm.attachUrl || '' })
|
|
|
|
|
+ proxy.$modal.msgSuccess(verifyForm.passed ? '验收通过,工单已结案' : '验收驳回,工单退回处理中')
|
|
|
|
|
+ verifyVisible.value = false
|
|
|
|
|
+ await refresh(verifyForm.orderId)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ submitting.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ============ 延期申请 ============ */
|
|
|
|
|
+const delayVisible = ref(false)
|
|
|
|
|
+const delayForm = reactive({ orderId: null, delayedDeadline: '', delayedReason: '', attachUrl: '' })
|
|
|
|
|
+
|
|
|
|
|
+function openDelay(o) {
|
|
|
|
|
+ delayForm.orderId = o.orderId
|
|
|
|
|
+ delayForm.delayedDeadline = ''
|
|
|
|
|
+ delayForm.delayedReason = ''
|
|
|
|
|
+ delayForm.attachUrl = ''
|
|
|
|
|
+ delayVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+async function submitDelay() {
|
|
|
|
|
+ if (!delayForm.delayedDeadline) { proxy.$modal.msgWarning('请选择延期截止时间'); return }
|
|
|
|
|
+ if (!delayForm.delayedReason || !delayForm.delayedReason.trim()) { proxy.$modal.msgWarning('请填写延期原因'); return }
|
|
|
|
|
+ submitting.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await applyDelayPworkOrder({
|
|
|
|
|
+ orderId: delayForm.orderId,
|
|
|
|
|
+ delayedDeadline: delayForm.delayedDeadline,
|
|
|
|
|
+ delayedReason: delayForm.delayedReason.trim(),
|
|
|
|
|
+ attachUrl: delayForm.attachUrl || ''
|
|
|
|
|
+ })
|
|
|
|
|
+ proxy.$modal.msgSuccess('延期申请已提交,等待审核')
|
|
|
|
|
+ delayVisible.value = false
|
|
|
|
|
+ await refresh(delayForm.orderId)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ submitting.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ============ 延期审核 ============ */
|
|
|
|
|
+const delayAuditVisible = ref(false)
|
|
|
|
|
+const delayAuditForm = reactive({ orderId: null, approved: true, auditOpinion: '' })
|
|
|
|
|
+
|
|
|
|
|
+function openDelayAudit(o) {
|
|
|
|
|
+ delayAuditForm.orderId = o.orderId
|
|
|
|
|
+ delayAuditForm.approved = true
|
|
|
|
|
+ delayAuditForm.auditOpinion = ''
|
|
|
|
|
+ delayAuditVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+async function submitDelayAudit() {
|
|
|
|
|
+ submitting.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await auditDelayPworkOrder({ orderId: delayAuditForm.orderId, approved: delayAuditForm.approved, auditOpinion: delayAuditForm.auditOpinion })
|
|
|
|
|
+ proxy.$modal.msgSuccess(delayAuditForm.approved ? '延期审核通过' : '延期审核驳回')
|
|
|
|
|
+ delayAuditVisible.value = false
|
|
|
|
|
+ await refresh(delayAuditForm.orderId)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ submitting.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ============ 删除 ============ */
|
|
|
|
|
+function handleDelete(o) {
|
|
|
|
|
+ proxy.$modal
|
|
|
|
|
+ .confirm('确认删除该已取消工单吗?')
|
|
|
|
|
+ .then(() => delPworkOrder(o.orderId))
|
|
|
|
|
+ .then(() => {
|
|
|
|
|
+ proxy.$modal.msgSuccess('删除成功')
|
|
|
|
|
+ clearSelection()
|
|
|
|
|
+ handleSearch()
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(() => {})
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ handleSearch()
|
|
|
|
|
+ loadDeptTree()
|
|
|
|
|
+ loadDeptUsers()
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.pwd-app {
|
|
|
|
|
+ --line: #e5e8ec;
|
|
|
|
|
+ --line2: #d6dae0;
|
|
|
|
|
+ --txt: #1f2328;
|
|
|
|
|
+ --txt2: #57606a;
|
|
|
|
|
+ --txt3: #8b949e;
|
|
|
|
|
+ --indigo: #3f51b5;
|
|
|
|
|
+ --indigo-bg: #eceef9;
|
|
|
|
|
+ --sel-bg: #f0f2fb;
|
|
|
|
|
+ --r: 10px;
|
|
|
|
|
+ padding: 16px 20px 20px;
|
|
|
|
|
+ color: var(--txt);
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 查询区 */
|
|
|
|
|
+.qcard {
|
|
|
|
|
+ border: 1px solid var(--line);
|
|
|
|
|
+ border-radius: var(--r);
|
|
|
|
|
+ padding: 14px 18px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+
|
|
|
|
|
+ .qrows {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 560px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .qrow {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 22px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .qf {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ label {
|
|
|
|
|
+ color: var(--txt2);
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .range {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ i {
|
|
|
|
|
+ color: var(--txt3);
|
|
|
|
|
+ font-style: normal;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .qbtns {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ align-self: center;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 状态筛选条 */
|
|
|
|
|
+.strips {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ margin: 14px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chip {
|
|
|
|
|
+ padding: 5px 15px;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ border: 1px solid var(--line);
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ color: var(--txt2);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 12.5px;
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 7px;
|
|
|
|
|
+ font-family: inherit;
|
|
|
|
|
+ transition: 0.12s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover { border-color: #a8b0f0; color: var(--indigo); background: #f7f8fd; }
|
|
|
|
|
+ b { font-weight: 500; }
|
|
|
|
|
+ .cnt {
|
|
|
|
|
+ font-size: 11.5px;
|
|
|
|
|
+ color: var(--txt3);
|
|
|
|
|
+ background: #f2f4f7;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ padding: 0 6px;
|
|
|
|
|
+ min-width: 20px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }
|
|
|
|
|
+ &.on {
|
|
|
|
|
+ border: 1px solid var(--indigo);
|
|
|
|
|
+ background: var(--indigo);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ padding: 5px 15px;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(63, 81, 181, 0.32);
|
|
|
|
|
+
|
|
|
|
|
+ .cnt {
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.24);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 主体 */
|
|
|
|
|
+.cols {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 14px;
|
|
|
|
|
+ align-items: stretch;
|
|
|
|
|
+ height: calc(100vh - 240px);
|
|
|
|
|
+ min-height: 520px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.listcol {
|
|
|
|
|
+ width: 318px;
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ border: 1px solid var(--line);
|
|
|
|
|
+ border-radius: var(--r);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lhd {
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ padding: 12px 16px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ border-bottom: 1px solid var(--line);
|
|
|
|
|
+
|
|
|
|
|
+ b { font-size: 13.5px; font-weight: 500; }
|
|
|
|
|
+ span { font-size: 12px; color: var(--txt3); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lscroll {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+
|
|
|
|
|
+ &::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
|
|
|
+ &::-webkit-scrollbar-thumb { background: #d9dde3; border-radius: 4px; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lempty {
|
|
|
|
|
+ padding: 40px 16px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: var(--txt3);
|
|
|
|
|
+ font-size: 12.5px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.li {
|
|
|
|
|
+ padding: 11px 14px 11px 12px;
|
|
|
|
|
+ border-bottom: 1px solid #f0f2f4;
|
|
|
|
|
+ border-left: 3px solid transparent;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: 0.1s;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover { background: #f9fafb; }
|
|
|
|
|
+ &.on { background: var(--sel-bg); border-left-color: var(--indigo); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.li-top {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-bottom: 3px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ono {
|
|
|
|
|
+ font-family: ui-monospace, Consolas, "Courier New", monospace;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--txt2);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bdg {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ padding: 1.5px 9px;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.li-ft {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--txt2);
|
|
|
|
|
+ margin-bottom: 2px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.li-dev {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--txt2);
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.li-tm {
|
|
|
|
|
+ font-size: 11.5px;
|
|
|
|
|
+ color: var(--txt3);
|
|
|
|
|
+ margin-top: 3px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lpage {
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ border-top: 1px solid var(--line);
|
|
|
|
|
+ padding: 6px 8px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detcol {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dhd {
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 2px 2px 0;
|
|
|
|
|
+
|
|
|
|
|
+ b { font-size: 14.5px; font-weight: 500; }
|
|
|
|
|
+ .no { font-family: ui-monospace, Consolas, "Courier New", monospace; font-size: 12px; color: var(--txt3); }
|
|
|
|
|
+ .bdg { margin-left: auto; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 步骤条 */
|
|
|
|
|
+.stepcard {
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ border: 1px solid var(--line);
|
|
|
|
|
+ border-radius: var(--r);
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ padding: 18px 26px;
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.steps {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ min-width: 560px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stp {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 7px;
|
|
|
|
|
+ width: 64px;
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+
|
|
|
|
|
+ .st-ic {
|
|
|
|
|
+ width: 27px;
|
|
|
|
|
+ height: 27px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ border: 1.5px solid #c9ced6;
|
|
|
|
|
+ color: #98a1ac;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ label { font-size: 12px; color: #98a1ac; white-space: nowrap; cursor: default; }
|
|
|
|
|
+
|
|
|
|
|
+ &.done .st-ic { background: var(--indigo); border-color: var(--indigo); color: #fff; }
|
|
|
|
|
+ &.done label { color: #2a3142; font-weight: 500; }
|
|
|
|
|
+ &.cur .st-ic {
|
|
|
|
|
+ background: var(--indigo);
|
|
|
|
|
+ border-color: var(--indigo);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ box-shadow: 0 0 0 3px var(--indigo-bg);
|
|
|
|
|
+ }
|
|
|
|
|
+ &.cur label { color: #2a3142; font-weight: 500; }
|
|
|
|
|
+ &.donegray .st-ic { background: #9aa1ab; border-color: #9aa1ab; color: #fff; }
|
|
|
|
|
+ &.donegray { opacity: 0.55; }
|
|
|
|
|
+ &.offx .st-ic { background: #a32d2d; border-color: #a32d2d; color: #fff; }
|
|
|
|
|
+ &.offx label { color: #a32d2d; font-weight: 500; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sline {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ height: 1.5px;
|
|
|
|
|
+ background: var(--line);
|
|
|
|
|
+ margin-top: 13px;
|
|
|
|
|
+ min-width: 26px;
|
|
|
|
|
+
|
|
|
|
|
+ &.dl { background: var(--indigo); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 详情卡片 */
|
|
|
|
|
+.tabcard {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ border: 1px solid var(--line);
|
|
|
|
|
+ border-radius: var(--r);
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tabs {
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 26px;
|
|
|
|
|
+ padding: 0 20px;
|
|
|
|
|
+ border-bottom: 1px solid var(--line);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tab {
|
|
|
|
|
+ padding: 12px 2px;
|
|
|
|
|
+ font-size: 13.5px;
|
|
|
|
|
+ color: var(--txt2);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ background: none;
|
|
|
|
|
+ font-family: inherit;
|
|
|
|
|
+ border-bottom: 2px solid transparent;
|
|
|
|
|
+ margin-bottom: -1px;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover { color: var(--txt); }
|
|
|
|
|
+ &.on { color: #2a3142; font-weight: 500; border-bottom-color: var(--indigo); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.pane {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ padding: 16px 20px 20px;
|
|
|
|
|
+
|
|
|
|
|
+ &::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
|
|
|
+ &::-webkit-scrollbar-thumb { background: #d9dde3; border-radius: 4px; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sect {
|
|
|
|
|
+ padding: 2px 0 16px;
|
|
|
|
|
+
|
|
|
|
|
+ & + .sect { border-top: 1px solid #f0f2f4; padding-top: 16px; }
|
|
|
|
|
+ &.nod { padding-bottom: 4px; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sect-t {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ .sub { font-size: 11.5px; color: var(--txt3); font-weight: 400; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kv {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
+ gap: 12px 20px;
|
|
|
|
|
+
|
|
|
|
|
+ .full { grid-column: 1 / -1; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.k { font-size: 11.5px; color: var(--txt3); margin-bottom: 2px; }
|
|
|
|
|
+.v { font-size: 13px; word-break: break-all; }
|
|
|
|
|
+.v.mono { font-family: ui-monospace, Consolas, "Courier New", monospace; }
|
|
|
|
|
+
|
|
|
|
|
+.pri {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+
|
|
|
|
|
+ i { width: 7px; height: 7px; border-radius: 50%; }
|
|
|
|
|
+ &.h { color: #c0392b; i { background: #e24b4a; } }
|
|
|
|
|
+ &.m { color: #b0720f; i { background: #ef9f27; } }
|
|
|
|
|
+ &.l { color: var(--txt2); i { background: #b4b2a9; } }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 附件 */
|
|
|
|
|
+.zones {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.zone {
|
|
|
|
|
+ border: 1px solid var(--line);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.zone-hd {
|
|
|
|
|
+ padding: 8px 13px;
|
|
|
|
|
+ font-size: 12.5px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ .tip { margin-left: auto; font-size: 11px; font-weight: 400; opacity: 0.75; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.z-pre .zone-hd { background: #faece7; color: #993c1d; }
|
|
|
|
|
+.z-post .zone-hd { background: #eaf3de; color: #3b6d11; }
|
|
|
|
|
+
|
|
|
|
|
+.zbody {
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.att {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ border: 1px solid var(--line);
|
|
|
|
|
+ border-radius: 7px;
|
|
|
|
|
+ background: #fafbfc;
|
|
|
|
|
+ aspect-ratio: 1 / 1;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+
|
|
|
|
|
+ img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ext {
|
|
|
|
|
+ width: 30px;
|
|
|
|
|
+ height: 30px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 9.5px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ font-family: ui-monospace, Consolas, "Courier New", monospace;
|
|
|
|
|
+
|
|
|
|
|
+ &.img { background: #e6f1fb; color: #185fa5; }
|
|
|
|
|
+ &.pdf { background: #fcebeb; color: #a32d2d; }
|
|
|
|
|
+ &.doc { background: #eaf3de; color: #3b6d11; }
|
|
|
|
|
+ &.def { background: #f1efe8; color: #5f5e5a; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.att-img { cursor: pointer; }
|
|
|
|
|
+.att-img.locked { cursor: default; }
|
|
|
|
|
+.att-img:hover:not(.locked) { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18); }
|
|
|
|
|
+
|
|
|
|
|
+.att-file .ext {
|
|
|
|
|
+ width: 36px;
|
|
|
|
|
+ height: 36px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.locksm {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: 6px;
|
|
|
|
|
+ bottom: 6px;
|
|
|
|
|
+ font-size: 10.5px;
|
|
|
|
|
+ color: #3b6d11;
|
|
|
|
|
+ background: #eaf3de;
|
|
|
|
|
+ padding: 1px 7px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.hint { font-size: 11.5px; color: var(--txt3); }
|
|
|
|
|
+
|
|
|
|
|
+/* 延期 */
|
|
|
|
|
+.delaybox {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .dchip {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ padding: 3px 12px;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ background: #fbeaf0;
|
|
|
|
|
+ color: #993556;
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ .di b { display: block; font-size: 13px; font-weight: 500; }
|
|
|
|
|
+ .di span { font-size: 12px; color: var(--txt2); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nodelay { font-size: 12.5px; color: var(--txt3); }
|
|
|
|
|
+
|
|
|
|
|
+/* 处理历史 */
|
|
|
|
|
+.hhint {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--txt3);
|
|
|
|
|
+ padding-bottom: 10px;
|
|
|
|
|
+ border-bottom: 1px solid #f0f2f4;
|
|
|
|
|
+ margin-bottom: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.hday { font-size: 13px; font-weight: 500; color: #2a3142; margin: 16px 0 4px; }
|
|
|
|
|
+
|
|
|
|
|
+.hrow { display: flex; gap: 12px; padding: 8px 0 8px 4px; }
|
|
|
|
|
+
|
|
|
|
|
+.ht {
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ width: 40px;
|
|
|
|
|
+ font-family: ui-monospace, Consolas, "Courier New", monospace;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--txt3);
|
|
|
|
|
+ padding-top: 1px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.hb { flex: 1; min-width: 0; }
|
|
|
|
|
+
|
|
|
|
|
+.hl {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+
|
|
|
|
|
+ b { font-weight: 700; }
|
|
|
|
|
+ .sep { color: var(--txt3); margin: 0 4px; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.hdesc { font-size: 12.5px; color: var(--txt2); margin-top: 2px; }
|
|
|
|
|
+
|
|
|
|
|
+.hchips { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 7px; align-items: center; }
|
|
|
|
|
+
|
|
|
|
|
+.hthumb {
|
|
|
|
|
+ width: 50px;
|
|
|
|
|
+ height: 50px;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ border: 1px solid var(--line);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ transition: 0.15s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ transform: scale(1.06);
|
|
|
|
|
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.hchip {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ border: 1px solid var(--line);
|
|
|
|
|
+ background: #fafbfc;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ padding: 3px 10px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--txt);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover { border-color: var(--indigo); color: var(--indigo); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 操作栏 */
|
|
|
|
|
+.abar {
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ border: 1px solid var(--line);
|
|
|
|
|
+ border-radius: var(--r);
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ padding: 10px 16px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-button) {
|
|
|
|
|
+ height: 30px;
|
|
|
|
|
+ padding: 0 13px;
|
|
|
|
|
+ font-size: 12.5px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ font-weight: 400;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-button--primary) {
|
|
|
|
|
+ height: 30px;
|
|
|
|
|
+ padding: 0 16px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-button--danger.is-plain) {
|
|
|
|
|
+ color: #a32d2d;
|
|
|
|
|
+ border-color: #f0c9c9;
|
|
|
|
|
+ background: #fcebeb;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ border-color: #a32d2d;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ color: #a32d2d;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dempty {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dept-tree-box {
|
|
|
|
|
+ height: 320px;
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ padding: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.handler-box { display: flex; align-items: center; gap: 12px; }
|
|
|
|
|
+
|
|
|
|
|
+.req-mark {
|
|
|
|
|
+ color: #f56c6c;
|
|
|
|
|
+ margin-right: 4px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|