Tasks.vue 15 KB

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