Tasks.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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-pagination
  69. style="margin-top: 15px; text-align: right"
  70. @current-change="loadFamilies"
  71. :current-page="familyPagination.current"
  72. :page-size="familyPagination.size"
  73. :total="familyPagination.total"
  74. layout="total, prev, pager, next"
  75. ></el-pagination>
  76. </el-dialog>
  77. <!-- 统计卡片 -->
  78. <el-row :gutter="20" style="margin-bottom: 20px">
  79. <el-col :span="6">
  80. <el-card class="stat-card">
  81. <div class="stat-label">总任务数</div>
  82. <div class="stat-value">{{ stats.total || 0 }}</div>
  83. </el-card>
  84. </el-col>
  85. <el-col :span="6">
  86. <el-card class="stat-card">
  87. <div class="stat-label">今日任务</div>
  88. <div class="stat-value">{{ stats.today || 0 }}</div>
  89. </el-card>
  90. </el-col>
  91. </el-row>
  92. <!-- 任务列表 -->
  93. <div class="adm-table-wrap">
  94. <el-table :data="tableData" border v-loading="loading" style="width: 100%" height="100%">
  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. </div>
  170. <!-- 分页 -->
  171. <el-pagination
  172. style="margin-top: 20px; text-align: right"
  173. @size-change="handleSizeChange"
  174. @current-change="handleCurrentChange"
  175. :current-page="pagination.current"
  176. :page-sizes="[10, 20, 50, 100]"
  177. :page-size="pagination.size"
  178. :total="pagination.total"
  179. layout="total, sizes, prev, pager, next, jumper"
  180. ></el-pagination>
  181. </el-card>
  182. <el-dialog title="审核任务" :visible.sync="reviewDialogVisible" width="400px">
  183. <el-form>
  184. <el-form-item label="任务标题">{{ currentTask.title }}</el-form-item>
  185. <el-form-item label="提交时间">{{ formatDate(currentTask.completedAt) }}</el-form-item>
  186. <el-form-item label="审核意见">
  187. <el-input type="textarea" v-model="reviewComment" rows="3" placeholder="请输入审核意见(选填)"></el-input>
  188. </el-form-item>
  189. </el-form>
  190. <div slot="footer">
  191. <el-button @click="reviewDialogVisible = false">取消</el-button>
  192. <el-button type="danger" @click="confirmReview(false)">拒绝</el-button>
  193. <el-button type="primary" @click="confirmReview(true)">通过</el-button>
  194. </div>
  195. </el-dialog>
  196. </el-table>
  197. </div>
  198. </template>
  199. <script>
  200. import { getTasksList, getTaskStats, getFamilyList, reviewAdminTask } from '@/api/admin'
  201. import SortMixin from '@/mixins/SortMixin'
  202. export default {
  203. mixins: [SortMixin],
  204. name: 'Tasks',
  205. computed: {
  206. tableHeight() {
  207. return window.innerHeight - 340
  208. }
  209. },
  210. data() {
  211. return {
  212. loading: false,
  213. sortState: [],
  214. tableData: [],
  215. sortableColumns: { title: '任务名称', id: 'ID', points: '积分', category: '分类', deadline: '截止时间', status: '状态', needReview: '需审核', executorName: '用户', createdAt: '创建时间' },
  216. stats: {
  217. total: 0,
  218. today: 0
  219. },
  220. searchForm: {
  221. title: '',
  222. status: '',
  223. familyName: '',
  224. familyId: ''
  225. },
  226. pagination: {
  227. current: 1,
  228. size: 10,
  229. total: 0
  230. },
  231. // 家庭选择对话框
  232. familyDialogVisible: false,
  233. familyLoading: false,
  234. familyList: [],
  235. familyPagination: {
  236. current: 1,
  237. size: 10,
  238. total: 0
  239. },
  240. reviewDialogVisible: false,
  241. reviewAction: true,
  242. reviewComment: '',
  243. currentTask: {}
  244. }
  245. },
  246. mounted() {
  247. this.loadTasks()
  248. this.loadStats()
  249. },
  250. methods: {
  251. async loadTasks() {
  252. this.loading = true
  253. try {
  254. const params = {
  255. page: this.pagination.current,
  256. size: this.pagination.size,
  257. status: this.searchForm.status,
  258. familyId: this.searchForm.familyId || undefined
  259. }
  260. this.applySortToParams(params)
  261. const res = await getTasksList(params)
  262. this.tableData = res.data.records || res.data || []
  263. this.pagination.total = res.data.total || this.tableData.length
  264. } catch (error) {
  265. this.$message.error(error.message || '加载任务列表失败')
  266. } finally {
  267. this.loading = false
  268. }
  269. },
  270. async loadStats() {
  271. try {
  272. const res = await getTaskStats()
  273. this.stats = res.data || { total: 0, today: 0 }
  274. } catch (error) {
  275. console.error('加载统计失败:', error)
  276. }
  277. },
  278. // 显示家庭选择对话框
  279. async showFamilySelector() {
  280. this.familyDialogVisible = true
  281. this.loadFamilies()
  282. },
  283. async loadFamilies(page = 1) {
  284. this.familyLoading = true
  285. try {
  286. const res = await getFamilyList({
  287. page: page,
  288. size: this.familyPagination.size
  289. })
  290. this.familyList = res.data.records || res.data || []
  291. this.familyPagination.total = res.data.total || this.familyList.length
  292. this.familyPagination.current = page
  293. } catch (error) {
  294. this.$message.error(error.message || '加载家庭列表失败')
  295. } finally {
  296. this.familyLoading = false
  297. }
  298. },
  299. selectFamily(row) {
  300. this.searchForm.familyId = row.id
  301. this.searchForm.familyName = row.name
  302. this.familyDialogVisible = false
  303. this.handleSearch()
  304. },
  305. clearFamily() {
  306. this.searchForm.familyId = ''
  307. this.searchForm.familyName = ''
  308. this.handleSearch()
  309. },
  310. handleSearch() {
  311. this.pagination.current = 1
  312. this.loadTasks()
  313. },
  314. handleReset() {
  315. this.searchForm = {
  316. title: '',
  317. status: '',
  318. familyName: '',
  319. familyId: ''
  320. }
  321. this.handleSearch()
  322. },
  323. handleRefresh() {
  324. this.loadTasks()
  325. this.loadStats()
  326. },
  327. handleSizeChange(val) {
  328. this.pagination.size = val
  329. this.pagination.current = 1
  330. this.loadTasks()
  331. },
  332. handleCurrentChange(val) {
  333. this.pagination.current = val
  334. this.loadTasks()
  335. },
  336. getStatusType(status) {
  337. const types = {
  338. pending: 'warning',
  339. pending_review: 'warning',
  340. completed: 'success',
  341. overdue: 'danger',
  342. cancelled: 'info'
  343. }
  344. return types[status] || 'info'
  345. },
  346. getStatusText(status) {
  347. const texts = {
  348. pending: '进行中',
  349. pending_review: '待审核',
  350. completed: '已完成',
  351. overdue: '已逾期',
  352. cancelled: '已取消'
  353. }
  354. return texts[status] || status
  355. },
  356. handleReview(task, approved) {
  357. this.currentTask = task || {}
  358. this.reviewAction = approved
  359. this.reviewComment = ''
  360. this.reviewDialogVisible = true
  361. },
  362. async confirmReview(approved) {
  363. if (!this.currentTask || !this.currentTask.id) {
  364. return
  365. }
  366. try {
  367. await reviewAdminTask(this.currentTask.id, {
  368. approved: approved != null ? approved : this.reviewAction,
  369. comment: this.reviewComment
  370. })
  371. this.$message.success(approved ? '已通过,积分已发放' : '已拒绝')
  372. this.reviewDialogVisible = false
  373. this.loadTasks()
  374. } catch (e) {
  375. this.$message.error('操作失败')
  376. }
  377. },
  378. }
  379. }
  380. </script>
  381. <style scoped>
  382. .stat-card {
  383. text-align: center;
  384. }
  385. .stat-label {
  386. color: #909399;
  387. font-size: 14px;
  388. margin-bottom: 10px;
  389. }
  390. .stat-value {
  391. font-size: 28px;
  392. font-weight: bold;
  393. color: #409EFF;
  394. }
  395. .sort-header {
  396. cursor: pointer;
  397. user-select: none;
  398. font-weight: 500;
  399. font-size: 12px;
  400. white-space: nowrap;
  401. }
  402. .sort-header:hover {
  403. color: #409eff;
  404. }
  405. </style>