Index.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. <template>
  2. <section class="panel">
  3. <div class="toolbar">
  4. <el-select v-model="query.planId" placeholder="巡检计划" clearable filterable style="width: 180px;">
  5. <el-option v-for="p in plans" :key="p.planId" :label="p.planName" :value="p.planId" />
  6. </el-select>
  7. <el-select v-model="query.storeId" placeholder="所属门店" clearable filterable style="width: 180px;">
  8. <el-option v-for="s in stores" :key="s.storeId" :label="s.storeName" :value="s.storeId" />
  9. </el-select>
  10. <el-input v-model="query.inspectorId" placeholder="巡检员ID(我的任务)" clearable style="width:150px;" @keyup.enter.native="load()" />
  11. <el-select v-model="query.status" placeholder="任务状态" clearable>
  12. <el-option label="待执行" :value="1" />
  13. <el-option label="执行中" :value="2" />
  14. <el-option label="已提交" :value="3" />
  15. <el-option label="已取消" :value="4" />
  16. </el-select>
  17. <el-select v-model="query.passFlag" placeholder="考核结果" clearable>
  18. <el-option label="及格" :value="1" />
  19. <el-option label="不及格" :value="0" />
  20. </el-select>
  21. <el-date-picker v-model="dateRange" type="daterange" value-format="yyyy-MM-dd" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 240px;" />
  22. <el-button type="primary" @click="load()">查询</el-button>
  23. <el-button @click="reset">重置</el-button>
  24. <span class="grow"></span>
  25. <el-button type="primary" plain @click="loadAll()">刷新</el-button>
  26. </div>
  27. <!-- 任务统计 -->
  28. <div class="stat-cards">
  29. <div class="stat-card"><span>任务总数</span><b>{{ stats.total }}</b><em>条</em></div>
  30. <div class="stat-card"><span>已完成</span><b>{{ stats.finish }}</b><em>条 · 完成率 {{ stats.finishRate }}%</em></div>
  31. <div class="stat-card"><span>及格率</span><b>{{ stats.passRate }}</b><em>%</em></div>
  32. <div class="stat-card"><span>平均得分</span><b>{{ stats.avg }}</b><em>分</em></div>
  33. </div>
  34. <el-table :data="rows" border stripe v-loading="loading">
  35. <el-table-column prop="taskId" label="任务ID" width="80" />
  36. <el-table-column prop="planName" label="巡检计划" min-width="130" />
  37. <el-table-column prop="storeName" label="门店" min-width="140" />
  38. <el-table-column label="执行日期" width="110">
  39. <template slot-scope="s">{{ fmt(s.row.taskDate) }}</template>
  40. </el-table-column>
  41. <el-table-column prop="inspectorName" label="巡检员" width="90">
  42. <template slot-scope="s">{{ s.row.inspectorName || '-' }}</template>
  43. </el-table-column>
  44. <el-table-column label="得分" width="150">
  45. <template slot-scope="s">
  46. <div v-if="s.row.status === 3" class="score-cell">
  47. <el-progress :percentage="scorePct(s.row)" :status="scoreStatus(s.row)" :stroke-width="10" :show-text="false" />
  48. <span class="score-text">{{ fmtNum(s.row.totalScore) }}/{{ fmtNum(s.row.maxScore) }}</span>
  49. </div>
  50. <span v-else class="muted">-</span>
  51. </template>
  52. </el-table-column>
  53. <el-table-column label="结果" width="85">
  54. <template slot-scope="s">
  55. <el-tag v-if="s.row.status === 3" size="mini" :type="s.row.passFlag === 1 ? 'success' : 'danger'">{{ s.row.passFlag === 1 ? '及格' : '不及格' }}</el-tag>
  56. <span v-else class="muted">-</span>
  57. </template>
  58. </el-table-column>
  59. <el-table-column prop="unpassItemCount" label="不合格项" width="85" align="center">
  60. <template slot-scope="s">{{ s.row.unpassItemCount || 0 }}</template>
  61. </el-table-column>
  62. <el-table-column label="状态" width="90">
  63. <template slot-scope="s"><el-tag size="mini" :type="taskStatus(s.row.status).type">{{ taskStatus(s.row.status).text }}</el-tag></template>
  64. </el-table-column>
  65. <el-table-column label="操作" width="200" fixed="right">
  66. <template slot-scope="s">
  67. <el-button type="text" v-if="s.row.status === 1 || s.row.status === 2" @click="openExecute(s.row)">执行</el-button>
  68. <el-button type="text" v-if="s.row.status === 3" @click="openReport(s.row)">报告</el-button>
  69. <el-button type="text" v-if="s.row.status === 1 || s.row.status === 2" class="danger" @click="cancel(s.row)">取消</el-button>
  70. <el-button type="text" v-if="s.row.status === 1 || s.row.status === 4" class="danger" @click="remove(s.row)">删除</el-button>
  71. </template>
  72. </el-table-column>
  73. </el-table>
  74. <div class="pager">
  75. <el-pagination
  76. background
  77. layout="total, sizes, prev, pager, next, jumper"
  78. :total="total"
  79. :page-size="query.pageSize"
  80. :current-page="query.currentPage"
  81. @size-change="n => { query.pageSize = n; load() }"
  82. @current-change="n => { query.currentPage = n; load() }"
  83. />
  84. </div>
  85. <task-execute :visible.sync="executeVisible" :task-id="currentId" @saved="onSaved" />
  86. <task-report :visible.sync="reportVisible" :task-id="currentId" />
  87. </section>
  88. </template>
  89. <script>
  90. import { post, ok, msg, pageData, fmt, fmtNum, slashDate, cleanParams, TASK_STATUS } from '../common'
  91. import TaskExecute from './Execute.vue'
  92. import TaskReport from './Report.vue'
  93. export default {
  94. name: 'InspectTaskIndex',
  95. components: { TaskExecute, TaskReport },
  96. data() {
  97. return {
  98. query: { planId: '', storeId: '', inspectorId: '', status: '', passFlag: '', taskDateBegin: '', taskDateEnd: '', pageSize: 10, currentPage: 1 },
  99. dateRange: [],
  100. rows: [],
  101. total: 0,
  102. loading: false,
  103. stats: { total: 0, finish: 0, finishRate: 0, passRate: 0, avg: '0.0' },
  104. plans: [],
  105. stores: [],
  106. executeVisible: false,
  107. reportVisible: false,
  108. currentId: null
  109. }
  110. },
  111. created() {
  112. this.load()
  113. this.loadOptions()
  114. this.loadStats()
  115. },
  116. methods: {
  117. loadAll() {
  118. this.load()
  119. this.loadStats()
  120. },
  121. load() {
  122. const q = Object.assign({}, this.query)
  123. q.taskDateBegin = this.dateRange && this.dateRange[0] ? slashDate(this.dateRange[0]) : ''
  124. q.taskDateEnd = this.dateRange && this.dateRange[1] ? slashDate(this.dateRange[1]) : ''
  125. this.loading = true
  126. post(this.$axios, '/inspectTaskManage/getTaskPage', cleanParams(q)).then(res => {
  127. this.rows = (res && res.list) || []
  128. this.total = pageData(res).resultCount || this.rows.length
  129. }).finally(() => { this.loading = false })
  130. },
  131. loadStats() {
  132. post(this.$axios, '/inspectTaskManage/getTaskStatistics', {}).then(res => {
  133. const rows = (res && res.list) || []
  134. let total = 0
  135. let finish = 0
  136. let pass = 0
  137. let scoreSum = 0
  138. rows.forEach(r => {
  139. const arr = Array.isArray(r) ? r : Object.keys(r).map(k => r[k])
  140. const count = Number(arr[1] || 0)
  141. const finishCount = Number(arr[2] || 0)
  142. const passCount = Number(arr[3] || 0)
  143. const avgScore = Number(arr[4] || 0)
  144. total += count
  145. finish += finishCount
  146. pass += passCount
  147. scoreSum += avgScore * count
  148. })
  149. this.stats = {
  150. total,
  151. finish,
  152. finishRate: total ? Math.round(finish / total * 100) : 0,
  153. passRate: finish ? Math.round(pass / finish * 100) : 0,
  154. avg: total ? (scoreSum / total).toFixed(1) : '0.0'
  155. }
  156. })
  157. },
  158. loadOptions() {
  159. post(this.$axios, '/inspectPlanManage/getPlanPage', { pageSize: 200, currentPage: 1 }).then(res => {
  160. this.plans = (res && res.list) || []
  161. })
  162. post(this.$axios, '/storeManage/getStoreList', {}).then(res => {
  163. this.stores = (res && res.list) || []
  164. })
  165. },
  166. reset() {
  167. this.query = { planId: '', storeId: '', inspectorId: '', status: '', passFlag: '', taskDateBegin: '', taskDateEnd: '', pageSize: 10, currentPage: 1 }
  168. this.dateRange = []
  169. this.load()
  170. },
  171. openExecute(row) {
  172. this.currentId = row.taskId
  173. this.executeVisible = true
  174. },
  175. openReport(row) {
  176. this.currentId = row.taskId
  177. this.reportVisible = true
  178. },
  179. onSaved() {
  180. this.executeVisible = false
  181. this.load()
  182. },
  183. cancel(row) {
  184. this.$confirm('确认取消该任务?', '提示').then(() => {
  185. post(this.$axios, '/inspectTaskManage/cancelTask', { id: row.taskId }).then(res => {
  186. if (ok(res)) { this.load(); this.$message.success('取消完成') } else { this.$message.error(msg(res) || '取消失败') }
  187. })
  188. }).catch(() => {})
  189. },
  190. remove(row) {
  191. this.$confirm('确认删除该任务?仅待执行或已取消的任务可删除。', '提示').then(() => {
  192. post(this.$axios, '/inspectTaskManage/deleteTask', { id: row.taskId }).then(res => {
  193. if (ok(res)) { this.load(); this.$message.success('删除完成') } else { this.$message.error(msg(res) || '删除失败') }
  194. })
  195. }).catch(() => {})
  196. },
  197. scorePct(row) {
  198. const max = Number(row.maxScore || 0)
  199. return max > 0 ? Math.round(Number(row.totalScore || 0) / max * 100) : 0
  200. },
  201. scoreStatus(row) {
  202. return row.passFlag === 1 ? 'success' : 'exception'
  203. },
  204. taskStatus(v) { return TASK_STATUS[v] || { text: '-', type: 'info' } },
  205. fmt,
  206. fmtNum
  207. }
  208. }
  209. </script>
  210. <style scoped>
  211. .panel { background: #fff; border: 1px solid #e4e7ed; border-radius: 6px; padding: 20px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }
  212. .toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; }
  213. .toolbar .el-select, .toolbar .el-input { width: 160px; }
  214. .grow { flex: 1; }
  215. .danger { color: #f56c6c; }
  216. .muted { color: #909399; }
  217. .pager { margin-top: 16px; text-align: right; }
  218. .score-cell { display: flex; align-items: center; gap: 8px; }
  219. .score-cell .el-progress { flex: 1; }
  220. .score-text { width: 70px; font-size: 12px; color: #606266; white-space: nowrap; }
  221. .stat-cards { display: flex; gap: 16px; margin-bottom: 16px; }
  222. .stat-card { flex: 1; background: #f5f7fa; border-radius: 6px; padding: 14px 18px; display: flex; align-items: baseline; gap: 8px; }
  223. .stat-card span { color: #909399; font-size: 13px; }
  224. .stat-card b { font-size: 24px; color: #303133; }
  225. .stat-card em { color: #909399; font-style: normal; font-size: 12px; }
  226. </style>