Tasks.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. <template>
  2. <div class="page-container">
  3. <el-card>
  4. <div slot="header">
  5. <span>任务管理</span>
  6. <el-button
  7. type="primary"
  8. size="small"
  9. style="float: right"
  10. @click="handleRefresh"
  11. :loading="loading"
  12. >
  13. 刷新
  14. </el-button>
  15. </div>
  16. <!-- 搜索筛选 -->
  17. <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
  18. <el-form-item label="任务标题">
  19. <el-input
  20. v-model="searchForm.title"
  21. placeholder="请输入任务标题"
  22. clearable
  23. @keyup.enter.native="handleSearch"
  24. ></el-input>
  25. </el-form-item>
  26. <el-form-item label="状态">
  27. <el-select v-model="searchForm.status" placeholder="全部状态" clearable @change="handleSearch">
  28. <el-option label="进行中" value="pending"></el-option>
  29. <el-option label="待审核" value="pending_review"></el-option>
  30. <el-option label="已完成" value="completed"></el-option>
  31. <el-option label="已逾期" value="overdue"></el-option>
  32. <el-option label="已取消" value="cancelled"></el-option>
  33. </el-select>
  34. </el-form-item>
  35. <el-form-item label="家庭">
  36. <el-input
  37. v-model="searchForm.familyName"
  38. placeholder="点击选择家庭"
  39. readonly
  40. @click.native="showFamilySelector"
  41. >
  42. <el-button
  43. slot="append"
  44. icon="el-icon-close"
  45. v-if="searchForm.familyId"
  46. @click.stop="clearFamily"
  47. style="padding: 8px"
  48. ></el-button>
  49. <i slot="suffix" class="el-input__icon el-icon-arrow-down" @click.stop="showFamilySelector"></i>
  50. </el-input>
  51. </el-form-item>
  52. <el-form-item>
  53. <el-button type="primary" @click="handleSearch">搜索</el-button>
  54. <el-button @click="handleReset">重置</el-button>
  55. </el-form-item>
  56. </el-form>
  57. <!-- 家庭选择对话框 -->
  58. <el-dialog title="选择家庭" :visible.sync="familyDialogVisible" width="600px">
  59. <el-table :data="familyList" border v-loading="familyLoading" @row-click="selectFamily" style="cursor: pointer" :max-height="300">
  60. <el-table-column prop="id" label="ID" width="80"></el-table-column>
  61. <el-table-column prop="name" label="家庭名称" min-width="150"></el-table-column>
  62. <el-table-column prop="inviteCode" label="邀请码" width="120"></el-table-column>
  63. <el-table-column prop="createdAt" label="创建时间" width="180">
  64. <template slot-scope="scope">
  65. {{ formatDate(scope.row.createdAt) }}
  66. </template>
  67. </el-table-column>
  68. </el-table>
  69. <el-pagination
  70. style="margin-top: 15px; text-align: right"
  71. @current-change="loadFamilies"
  72. :current-page="familyPagination.current"
  73. :page-size="familyPagination.size"
  74. :total="familyPagination.total"
  75. layout="total, prev, pager, next"
  76. ></el-pagination>
  77. </el-dialog>
  78. <!-- 统计卡片 -->
  79. <el-row :gutter="20" style="margin-bottom: 20px">
  80. <el-col :span="6">
  81. <el-card class="stat-card">
  82. <div class="stat-label">总任务数</div>
  83. <div class="stat-value">{{ stats.total || 0 }}</div>
  84. </el-card>
  85. </el-col>
  86. <el-col :span="6">
  87. <el-card class="stat-card">
  88. <div class="stat-label">今日任务</div>
  89. <div class="stat-value">{{ stats.today || 0 }}</div>
  90. </el-card>
  91. </el-col>
  92. </el-row>
  93. <!-- 任务列表 -->
  94. <el-table :data="tableData" border v-loading="loading" style="width: 100%" :max-height="tableHeight">
  95. <el-table-column prop="id" label="ID" width="80"></el-table-column>
  96. <el-table-column prop="title" label="任务名称" min-width="200" :class-name="sortClass('title')" @click.native="onSortClick('title')"></el-table-column>
  97. <el-table-column prop="points" label="积分" width="80">
  98. <template slot-scope="scope">
  99. <el-tag type="success">{{ scope.row.points }}</el-tag>
  100. </template>
  101. </el-table-column>
  102. <el-table-column prop="category" label="分类" width="100"></el-table-column>
  103. <el-table-column prop="deadline" label="截止时间" width="180">
  104. <template slot-scope="scope">
  105. {{ formatDate(scope.row.deadline) }}
  106. </template>
  107. </el-table-column>
  108. <el-table-column prop="status" label="状态" width="100">
  109. <template slot-scope="scope">
  110. <el-tag :type="getStatusType(scope.row.status)" size="small">
  111. {{ getStatusText(scope.row.status) }}
  112. </el-tag>
  113. </template>
  114. </el-table-column>
  115. <el-table-column prop="needReview" label="需审核" width="80">
  116. <template slot-scope="scope">
  117. <el-tag :type="scope.row.needReview ? 'warning' : 'info'" size="small">
  118. {{ scope.row.needReview ? '是' : '否' }}
  119. </el-tag>
  120. </template>
  121. </el-table-column>
  122. <el-table-column prop="executorName" label="用户" width="100"></el-table-column>
  123. <el-table-column prop="createdAt" label="创建时间" width="180">
  124. <template slot-scope="scope">
  125. {{ formatDate(scope.row.createdAt) }}
  126. </template>
  127. </el-table-column>
  128. <el-table-column label="操作" width="180" fixed="right">
  129. <template slot-scope="scope">
  130. <template v-if="scope.row.status === 'pending_review' && scope.row.needReview">
  131. <el-button size="mini" type="success" @click="handleReview(scope.row, true)">通过</el-button>
  132. <el-button size="mini" type="danger" @click="handleReview(scope.row, false)">拒绝</el-button>
  133. </template>
  134. <span v-else>-</span>
  135. </template>
  136. </el-table-column>
  137. </el-table>
  138. <!-- 分页 -->
  139. <el-pagination
  140. style="margin-top: 20px; text-align: right"
  141. @size-change="handleSizeChange"
  142. @current-change="handleCurrentChange"
  143. :current-page="pagination.current"
  144. :page-sizes="[10, 20, 50, 100]"
  145. :page-size="pagination.size"
  146. :total="pagination.total"
  147. layout="total, sizes, prev, pager, next, jumper"
  148. ></el-pagination>
  149. </el-card>
  150. <el-dialog title="审核任务" :visible.sync="reviewDialogVisible" width="400px">
  151. <el-form>
  152. <el-form-item label="任务标题">{{ currentTask.title }}</el-form-item>
  153. <el-form-item label="提交时间">{{ formatDate(currentTask.completedAt) }}</el-form-item>
  154. <el-form-item label="审核意见">
  155. <el-input type="textarea" v-model="reviewComment" rows="3" placeholder="请输入审核意见(选填)"></el-input>
  156. </el-form-item>
  157. </el-form>
  158. <div slot="footer">
  159. <el-button @click="reviewDialogVisible = false">取消</el-button>
  160. <el-button type="danger" @click="confirmReview(false)">拒绝</el-button>
  161. <el-button type="primary" @click="confirmReview(true)">通过</el-button>
  162. </div>
  163. </el-dialog>
  164. </div>
  165. </template>
  166. <script>
  167. import { getTasksList, getTaskStats, getFamilyList, reviewAdminTask } from '@/api/admin'
  168. import SortMixin from '@/mixins/SortMixin'
  169. export default {
  170. mixins: [SortMixin],
  171. name: 'Tasks',
  172. sortableColumns: { title: '任务名称' },
  173. computed: {
  174. tableHeight() {
  175. return window.innerHeight - 340
  176. }
  177. },
  178. data() {
  179. return {
  180. loading: false,
  181. sortState: [],
  182. tableData: [],
  183. stats: {
  184. total: 0,
  185. today: 0
  186. },
  187. searchForm: {
  188. title: '',
  189. status: '',
  190. familyName: '',
  191. familyId: ''
  192. },
  193. pagination: {
  194. current: 1,
  195. size: 10,
  196. total: 0
  197. },
  198. // 家庭选择对话框
  199. familyDialogVisible: false,
  200. familyLoading: false,
  201. familyList: [],
  202. familyPagination: {
  203. current: 1,
  204. size: 10,
  205. total: 0
  206. },
  207. reviewDialogVisible: false,
  208. reviewAction: true,
  209. reviewComment: '',
  210. currentTask: {}
  211. }
  212. },
  213. mounted() {
  214. this.loadTasks()
  215. this.loadStats()
  216. },
  217. methods: {
  218. async loadTasks() {
  219. this.loading = true
  220. try {
  221. const params = {
  222. page: this.pagination.current,
  223. size: this.pagination.size,
  224. status: this.searchForm.status,
  225. familyId: this.searchForm.familyId || undefined
  226. }
  227. this.applySortToParams(params)
  228. const res = await getTasksList(params)
  229. this.tableData = res.data.records || res.data || []
  230. this.pagination.total = res.data.total || this.tableData.length
  231. } catch (error) {
  232. this.$message.error(error.message || '加载任务列表失败')
  233. } finally {
  234. this.loading = false
  235. }
  236. },
  237. async loadStats() {
  238. try {
  239. const res = await getTaskStats()
  240. this.stats = res.data || { total: 0, today: 0 }
  241. } catch (error) {
  242. console.error('加载统计失败:', error)
  243. }
  244. },
  245. // 显示家庭选择对话框
  246. async showFamilySelector() {
  247. this.familyDialogVisible = true
  248. this.loadFamilies()
  249. },
  250. async loadFamilies(page = 1) {
  251. this.familyLoading = true
  252. try {
  253. const res = await getFamilyList({
  254. page: page,
  255. size: this.familyPagination.size
  256. })
  257. this.familyList = res.data.records || res.data || []
  258. this.familyPagination.total = res.data.total || this.familyList.length
  259. this.familyPagination.current = page
  260. } catch (error) {
  261. this.$message.error(error.message || '加载家庭列表失败')
  262. } finally {
  263. this.familyLoading = false
  264. }
  265. },
  266. selectFamily(row) {
  267. this.searchForm.familyId = row.id
  268. this.searchForm.familyName = row.name
  269. this.familyDialogVisible = false
  270. this.handleSearch()
  271. },
  272. clearFamily() {
  273. this.searchForm.familyId = ''
  274. this.searchForm.familyName = ''
  275. this.handleSearch()
  276. },
  277. handleSearch() {
  278. this.pagination.current = 1
  279. this.loadTasks()
  280. },
  281. handleReset() {
  282. this.searchForm = {
  283. title: '',
  284. status: '',
  285. familyName: '',
  286. familyId: ''
  287. }
  288. this.handleSearch()
  289. },
  290. handleRefresh() {
  291. this.loadTasks()
  292. this.loadStats()
  293. },
  294. handleSizeChange(val) {
  295. this.pagination.size = val
  296. this.pagination.current = 1
  297. this.loadTasks()
  298. },
  299. handleCurrentChange(val) {
  300. this.pagination.current = val
  301. this.loadTasks()
  302. },
  303. getStatusType(status) {
  304. const types = {
  305. pending: 'warning',
  306. pending_review: 'warning',
  307. completed: 'success',
  308. overdue: 'danger',
  309. cancelled: 'info'
  310. }
  311. return types[status] || 'info'
  312. },
  313. getStatusText(status) {
  314. const texts = {
  315. pending: '进行中',
  316. pending_review: '待审核',
  317. completed: '已完成',
  318. overdue: '已逾期',
  319. cancelled: '已取消'
  320. }
  321. return texts[status] || status
  322. },
  323. handleReview(task, approved) {
  324. this.currentTask = task || {}
  325. this.reviewAction = approved
  326. this.reviewComment = ''
  327. this.reviewDialogVisible = true
  328. },
  329. async confirmReview(approved) {
  330. if (!this.currentTask || !this.currentTask.id) {
  331. return
  332. }
  333. try {
  334. await reviewAdminTask(this.currentTask.id, {
  335. approved: approved != null ? approved : this.reviewAction,
  336. comment: this.reviewComment
  337. })
  338. this.$message.success(approved ? '已通过,积分已发放' : '已拒绝')
  339. this.reviewDialogVisible = false
  340. this.loadTasks()
  341. } catch (e) {
  342. this.$message.error('操作失败')
  343. }
  344. },
  345. formatDate(dateStr) {
  346. if (!dateStr) return '-'
  347. const date = new Date(dateStr)
  348. return date.toLocaleString('zh-CN')
  349. }
  350. }
  351. }
  352. </script>
  353. <style scoped>
  354. .stat-card {
  355. text-align: center;
  356. }
  357. .stat-label {
  358. color: #909399;
  359. font-size: 14px;
  360. margin-bottom: 10px;
  361. }
  362. .stat-value {
  363. font-size: 28px;
  364. font-weight: bold;
  365. color: #409EFF;
  366. }
  367. </style>