Tasks.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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">
  96. <template slot="header">
  97. <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
  98. </template>
  99. </el-table-column>
  100. <el-table-column prop="title" label="任务名称" min-width="200">
  101. <template slot="header">
  102. <span class="sort-header" @click.stop="onSortClick('title', $event)">任务名称<span v-if="sortIndex('title') !== null" class="sort-index" :class="sortClass('title') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('title') }}</span>{{ sortIcon('title') }}</span>
  103. </template>
  104. </el-table-column>
  105. <el-table-column prop="points" label="积分" width="80">
  106. <template slot-scope="scope">
  107. <el-tag type="success">{{ scope.row.points }}</el-tag>
  108. </template>
  109. <template slot="header">
  110. <span class="sort-header" @click.stop="onSortClick('points', $event)">积分<span v-if="sortIndex('points') !== null" class="sort-index" :class="sortClass('points') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('points') }}</span>{{ sortIcon('points') }}</span>
  111. </template>
  112. </el-table-column>
  113. <el-table-column prop="category" label="分类" width="100">
  114. <template slot="header">
  115. <span class="sort-header" @click.stop="onSortClick('category', $event)">分类<span v-if="sortIndex('category') !== null" class="sort-index" :class="sortClass('category') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('category') }}</span>{{ sortIcon('category') }}</span>
  116. </template>
  117. </el-table-column>
  118. <el-table-column prop="deadline" label="截止时间" width="180">
  119. <template slot-scope="scope">
  120. {{ formatDate(scope.row.deadline) }}
  121. </template>
  122. <template slot="header">
  123. <span class="sort-header" @click.stop="onSortClick('deadline', $event)">截止时间<span v-if="sortIndex('deadline') !== null" class="sort-index" :class="sortClass('deadline') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('deadline') }}</span>{{ sortIcon('deadline') }}</span>
  124. </template>
  125. </el-table-column>
  126. <el-table-column prop="status" label="状态" width="100">
  127. <template slot-scope="scope">
  128. <el-tag :type="getStatusType(scope.row.status)" size="small">
  129. {{ getStatusText(scope.row.status) }}
  130. </el-tag>
  131. </template>
  132. <template slot="header">
  133. <span class="sort-header" @click.stop="onSortClick('status', $event)">状态<span v-if="sortIndex('status') !== null" class="sort-index" :class="sortClass('status') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('status') }}</span>{{ sortIcon('status') }}</span>
  134. </template>
  135. </el-table-column>
  136. <el-table-column prop="needReview" label="需审核" width="80">
  137. <template slot-scope="scope">
  138. <el-tag :type="scope.row.needReview ? 'warning' : 'info'" size="small">
  139. {{ scope.row.needReview ? '是' : '否' }}
  140. </el-tag>
  141. </template>
  142. <template slot="header">
  143. <span class="sort-header" @click.stop="onSortClick('needReview', $event)">需审核<span v-if="sortIndex('needReview') !== null" class="sort-index" :class="sortClass('needReview') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('needReview') }}</span>{{ sortIcon('needReview') }}</span>
  144. </template>
  145. </el-table-column>
  146. <el-table-column prop="executorName" label="用户" width="100">
  147. <template slot="header">
  148. <span class="sort-header" @click.stop="onSortClick('executorName', $event)">用户<span v-if="sortIndex('executorName') !== null" class="sort-index" :class="sortClass('executorName') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('executorName') }}</span>{{ sortIcon('executorName') }}</span>
  149. </template>
  150. </el-table-column>
  151. <el-table-column prop="createdAt" label="创建时间" width="180">
  152. <template slot-scope="scope">
  153. {{ formatDate(scope.row.createdAt) }}
  154. </template>
  155. <template slot="header">
  156. <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">创建时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
  157. </template>
  158. </el-table-column>
  159. <el-table-column label="操作" width="180" fixed="right">
  160. <template slot-scope="scope">
  161. <template v-if="scope.row.status === 'pending_review' && scope.row.needReview">
  162. <el-button size="mini" type="success" @click="handleReview(scope.row, true)">通过</el-button>
  163. <el-button size="mini" type="danger" @click="handleReview(scope.row, false)">拒绝</el-button>
  164. </template>
  165. <span v-else>-</span>
  166. </template>
  167. </el-table-column>
  168. </el-table>
  169. <!-- 分页 -->
  170. <el-pagination
  171. style="margin-top: 20px; text-align: right"
  172. @size-change="handleSizeChange"
  173. @current-change="handleCurrentChange"
  174. :current-page="pagination.current"
  175. :page-sizes="[10, 20, 50, 100]"
  176. :page-size="pagination.size"
  177. :total="pagination.total"
  178. layout="total, sizes, prev, pager, next, jumper"
  179. ></el-pagination>
  180. </el-card>
  181. <el-dialog title="审核任务" :visible.sync="reviewDialogVisible" width="400px">
  182. <el-form>
  183. <el-form-item label="任务标题">{{ currentTask.title }}</el-form-item>
  184. <el-form-item label="提交时间">{{ formatDate(currentTask.completedAt) }}</el-form-item>
  185. <el-form-item label="审核意见">
  186. <el-input type="textarea" v-model="reviewComment" rows="3" placeholder="请输入审核意见(选填)"></el-input>
  187. </el-form-item>
  188. </el-form>
  189. <div slot="footer">
  190. <el-button @click="reviewDialogVisible = false">取消</el-button>
  191. <el-button type="danger" @click="confirmReview(false)">拒绝</el-button>
  192. <el-button type="primary" @click="confirmReview(true)">通过</el-button>
  193. </div>
  194. </el-dialog>
  195. </div>
  196. </template>
  197. <script>
  198. import { getTasksList, getTaskStats, getFamilyList, reviewAdminTask } from '@/api/admin'
  199. import SortMixin from '@/mixins/SortMixin'
  200. export default {
  201. mixins: [SortMixin],
  202. name: 'Tasks',
  203. computed: {
  204. tableHeight() {
  205. return window.innerHeight - 340
  206. }
  207. },
  208. data() {
  209. return {
  210. loading: false,
  211. sortState: [],
  212. tableData: [],
  213. sortableColumns: { title: '任务名称', id: 'ID', points: '积分', category: '分类', deadline: '截止时间', status: '状态', needReview: '需审核', executorName: '用户', createdAt: '创建时间' },
  214. stats: {
  215. total: 0,
  216. today: 0
  217. },
  218. searchForm: {
  219. title: '',
  220. status: '',
  221. familyName: '',
  222. familyId: ''
  223. },
  224. pagination: {
  225. current: 1,
  226. size: 10,
  227. total: 0
  228. },
  229. // 家庭选择对话框
  230. familyDialogVisible: false,
  231. familyLoading: false,
  232. familyList: [],
  233. familyPagination: {
  234. current: 1,
  235. size: 10,
  236. total: 0
  237. },
  238. reviewDialogVisible: false,
  239. reviewAction: true,
  240. reviewComment: '',
  241. currentTask: {}
  242. }
  243. },
  244. mounted() {
  245. this.loadTasks()
  246. this.loadStats()
  247. },
  248. methods: {
  249. async loadTasks() {
  250. this.loading = true
  251. try {
  252. const params = {
  253. page: this.pagination.current,
  254. size: this.pagination.size,
  255. status: this.searchForm.status,
  256. familyId: this.searchForm.familyId || undefined
  257. }
  258. this.applySortToParams(params)
  259. const res = await getTasksList(params)
  260. this.tableData = res.data.records || res.data || []
  261. this.pagination.total = res.data.total || this.tableData.length
  262. } catch (error) {
  263. this.$message.error(error.message || '加载任务列表失败')
  264. } finally {
  265. this.loading = false
  266. }
  267. },
  268. async loadStats() {
  269. try {
  270. const res = await getTaskStats()
  271. this.stats = res.data || { total: 0, today: 0 }
  272. } catch (error) {
  273. console.error('加载统计失败:', error)
  274. }
  275. },
  276. // 显示家庭选择对话框
  277. async showFamilySelector() {
  278. this.familyDialogVisible = true
  279. this.loadFamilies()
  280. },
  281. async loadFamilies(page = 1) {
  282. this.familyLoading = true
  283. try {
  284. const res = await getFamilyList({
  285. page: page,
  286. size: this.familyPagination.size
  287. })
  288. this.familyList = res.data.records || res.data || []
  289. this.familyPagination.total = res.data.total || this.familyList.length
  290. this.familyPagination.current = page
  291. } catch (error) {
  292. this.$message.error(error.message || '加载家庭列表失败')
  293. } finally {
  294. this.familyLoading = false
  295. }
  296. },
  297. selectFamily(row) {
  298. this.searchForm.familyId = row.id
  299. this.searchForm.familyName = row.name
  300. this.familyDialogVisible = false
  301. this.handleSearch()
  302. },
  303. clearFamily() {
  304. this.searchForm.familyId = ''
  305. this.searchForm.familyName = ''
  306. this.handleSearch()
  307. },
  308. handleSearch() {
  309. this.pagination.current = 1
  310. this.loadTasks()
  311. },
  312. handleReset() {
  313. this.searchForm = {
  314. title: '',
  315. status: '',
  316. familyName: '',
  317. familyId: ''
  318. }
  319. this.handleSearch()
  320. },
  321. handleRefresh() {
  322. this.loadTasks()
  323. this.loadStats()
  324. },
  325. handleSizeChange(val) {
  326. this.pagination.size = val
  327. this.pagination.current = 1
  328. this.loadTasks()
  329. },
  330. handleCurrentChange(val) {
  331. this.pagination.current = val
  332. this.loadTasks()
  333. },
  334. getStatusType(status) {
  335. const types = {
  336. pending: 'warning',
  337. pending_review: 'warning',
  338. completed: 'success',
  339. overdue: 'danger',
  340. cancelled: 'info'
  341. }
  342. return types[status] || 'info'
  343. },
  344. getStatusText(status) {
  345. const texts = {
  346. pending: '进行中',
  347. pending_review: '待审核',
  348. completed: '已完成',
  349. overdue: '已逾期',
  350. cancelled: '已取消'
  351. }
  352. return texts[status] || status
  353. },
  354. handleReview(task, approved) {
  355. this.currentTask = task || {}
  356. this.reviewAction = approved
  357. this.reviewComment = ''
  358. this.reviewDialogVisible = true
  359. },
  360. async confirmReview(approved) {
  361. if (!this.currentTask || !this.currentTask.id) {
  362. return
  363. }
  364. try {
  365. await reviewAdminTask(this.currentTask.id, {
  366. approved: approved != null ? approved : this.reviewAction,
  367. comment: this.reviewComment
  368. })
  369. this.$message.success(approved ? '已通过,积分已发放' : '已拒绝')
  370. this.reviewDialogVisible = false
  371. this.loadTasks()
  372. } catch (e) {
  373. this.$message.error('操作失败')
  374. }
  375. },
  376. formatDate(dateStr) {
  377. if (!dateStr) return '-'
  378. const date = new Date(dateStr)
  379. return date.toLocaleString('zh-CN')
  380. }
  381. }
  382. }
  383. </script>
  384. <style scoped>
  385. .stat-card {
  386. text-align: center;
  387. }
  388. .stat-label {
  389. color: #909399;
  390. font-size: 14px;
  391. margin-bottom: 10px;
  392. }
  393. .stat-value {
  394. font-size: 28px;
  395. font-weight: bold;
  396. color: #409EFF;
  397. }
  398. .sort-header {
  399. cursor: pointer;
  400. user-select: none;
  401. font-weight: 500;
  402. font-size: 12px;
  403. white-space: nowrap;
  404. }
  405. .sort-header:hover {
  406. color: #409eff;
  407. }
  408. </style>