dailyTask.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. <template>
  2. <view class="sub-page">
  3. <TopBar title="每日任务" show-back />
  4. <view class="px-4 pt-3">
  5. <!-- 今日任务概览 -->
  6. <view class="glass-card p-4">
  7. <view class="section-head-inner mb-3">
  8. <view class="section-bar mr-2"></view>
  9. <text class="section-title">今日概览</text>
  10. </view>
  11. <view class="grid grid-cols-2 gap-3">
  12. <view class="stat-card">
  13. <view class="stat-bar"></view>
  14. <text class="stat-value">{{ todayTasks.length }}</text>
  15. <text class="stat-label">今日任务</text>
  16. </view>
  17. <view class="stat-card">
  18. <view class="stat-bar"></view>
  19. <text class="stat-value">{{ completedTasks }}</text>
  20. <text class="stat-label">已完成</text>
  21. </view>
  22. </view>
  23. </view>
  24. <!-- 车辆检查 -->
  25. <view class="glass-card p-4">
  26. <view class="section-head-inner mb-3">
  27. <view class="section-bar mr-2"></view>
  28. <text class="section-title flex-1">车辆检查</text>
  29. <text v-if="vehicleCheckStatus" class="check-state check-state-done">已完成</text>
  30. <text v-else class="check-state check-state-pending">待检查</text>
  31. </view>
  32. <view v-if="!vehicleCheckStatus">
  33. <view class="check-group">
  34. <text class="check-group-title">常规检查</text>
  35. <view
  36. v-for="item in checkItems"
  37. :key="item.key"
  38. class="check-row"
  39. @click="toggleCheck(item.key)"
  40. >
  41. <text class="check-label">{{ item.label }}</text>
  42. <view class="check-circle" :class="{ 'check-circle-on': checks[item.key] }">
  43. <text v-if="checks[item.key]" class="uni-icons uniui-checkmarkempty check-tick"></text>
  44. </view>
  45. </view>
  46. </view>
  47. <view class="check-group">
  48. <text class="check-group-title">设备检查</text>
  49. <view
  50. v-for="item in equipmentItems"
  51. :key="item.key"
  52. class="check-row"
  53. @click="toggleCheck(item.key)"
  54. >
  55. <text class="check-label">{{ item.label }}</text>
  56. <view class="check-circle" :class="{ 'check-circle-on': checks[item.key] }">
  57. <text v-if="checks[item.key]" class="uni-icons uniui-checkmarkempty check-tick"></text>
  58. </view>
  59. </view>
  60. </view>
  61. <view class="check-group">
  62. <text class="check-group-title">三大系统检查</text>
  63. <view
  64. v-for="item in systemItems"
  65. :key="item.key"
  66. class="check-row"
  67. @click="toggleCheck(item.key)"
  68. >
  69. <text class="check-label">{{ item.label }}</text>
  70. <view class="check-circle" :class="{ 'check-circle-on': checks[item.key] }">
  71. <text v-if="checks[item.key]" class="uni-icons uniui-checkmarkempty check-tick"></text>
  72. </view>
  73. </view>
  74. </view>
  75. <view class="mb-3">
  76. <text class="check-group-title">备注</text>
  77. <textarea
  78. v-model="remark"
  79. class="form-textarea"
  80. placeholder="请输入检查备注..."
  81. :maxlength="200"
  82. />
  83. </view>
  84. <button
  85. class="submit-btn"
  86. hover-class="submit-btn-hover"
  87. :hover-start-time="0"
  88. :hover-stay-time="120"
  89. :disabled="!vehicleCheckStatus"
  90. @click="submitCheck"
  91. >
  92. 提交检查
  93. </button>
  94. </view>
  95. <view v-else class="done-wrap">
  96. <view class="done-icon">
  97. <text class="uni-icons uniui-checkmarkempty done-tick"></text>
  98. </view>
  99. <text class="done-text">今日车辆检查已完成</text>
  100. <text v-if="remark" class="done-meta">备注:{{ remark }}</text>
  101. <text class="done-meta">{{ today }}</text>
  102. </view>
  103. </view>
  104. <!-- 今日任务列表 -->
  105. <view class="glass-card">
  106. <view class="section-head">
  107. <view class="section-bar mr-2"></view>
  108. <text class="section-title flex-1">今日任务列表</text>
  109. </view>
  110. <view v-if="taskStore.loading" class="py-10 text-center">
  111. <text class="empty-text">加载中...</text>
  112. </view>
  113. <view v-else-if="todayTasks.length === 0" class="pb-3">
  114. <EmptyState message="今日无任务" />
  115. </view>
  116. <view v-else>
  117. <view
  118. v-for="task in todayTasks"
  119. :key="task.taskId || task.id"
  120. class="list-row"
  121. hover-class="row-hover"
  122. :hover-start-time="0"
  123. :hover-stay-time="120"
  124. @click="goToDetail(task.taskId || task.id)"
  125. >
  126. <view class="icon-chip mr-3">
  127. <text class="uni-icons uniui-flag chip-glyph"></text>
  128. </view>
  129. <view class="flex-1 min-w-0 mr-2">
  130. <text class="row-title truncate">{{ task.taskName || task.projectName }}</text>
  131. <text class="row-sub truncate">{{ task.address || '暂无地址' }}</text>
  132. </view>
  133. <StatusTag :status="task.status" />
  134. </view>
  135. </view>
  136. </view>
  137. </view>
  138. </view>
  139. </template>
  140. <script setup lang="ts">
  141. import { ref, computed, onMounted } from 'vue'
  142. import { useTaskStore } from '../../stores/task'
  143. import TopBar from '../../components/common/TopBar.vue'
  144. import StatusTag from '../../components/common/StatusTag.vue'
  145. import EmptyState from '../../components/common/EmptyState.vue'
  146. const taskStore = useTaskStore()
  147. const today = new Date().toISOString().split('T')[0]
  148. const storageKey = `vehicle_check_${today}`
  149. const remarkKey = `vehicle_check_remark_${today}`
  150. const checkItems = [
  151. { key: 'exterior', label: '车辆外观检查' },
  152. { key: 'tire', label: '轮胎气压检查' },
  153. { key: 'fluid', label: '油液检查' },
  154. { key: 'brake', label: '刹车系统检查' },
  155. { key: 'safety', label: '安全设备检查' },
  156. ]
  157. const equipmentItems = [
  158. { key: 'pump', label: '抽水泵检查' },
  159. { key: 'hose', label: '高压水管检查' },
  160. { key: 'tool', label: '施工工具检查' },
  161. { key: 'protective', label: '防护用品检查' },
  162. ]
  163. const systemItems = [
  164. { key: 'power', label: '动力系统' },
  165. { key: 'hydraulic', label: '液压系统' },
  166. { key: 'electrical', label: '电气系统' },
  167. ]
  168. const allItems = [...checkItems, ...equipmentItems, ...systemItems]
  169. const initialChecks: Record<string, boolean> = {}
  170. allItems.forEach((item) => {
  171. initialChecks[item.key] = false
  172. })
  173. const checks = ref<Record<string, boolean>>({ ...initialChecks })
  174. const remark = ref('')
  175. const vehicleCheckStatus = computed(() => {
  176. return allItems.every((item) => checks.value[item.key])
  177. })
  178. const completedTasks = computed(() => {
  179. return todayTasks.value.filter((t) => t.status === 'completed').length
  180. })
  181. const todayTasks = computed<any[]>(() => {
  182. return taskStore.tasks.filter(
  183. (t: any) => t.planDate === today || t.scheduleDate === today || t.serviceDate === today
  184. )
  185. })
  186. function toggleCheck(type: string) {
  187. checks.value[type] = !checks.value[type]
  188. uni.setStorageSync(storageKey, JSON.stringify(checks.value))
  189. }
  190. function submitCheck() {
  191. if (!vehicleCheckStatus.value) {
  192. uni.showToast({ title: '请完成所有检查项', icon: 'none' })
  193. return
  194. }
  195. uni.setStorageSync(storageKey, JSON.stringify(checks.value))
  196. uni.setStorageSync(remarkKey, remark.value)
  197. uni.showToast({ title: '提交成功', icon: 'success' })
  198. }
  199. function goToDetail(taskId: string | number) {
  200. if (!taskId) return
  201. uni.navigateTo({ url: `/subPackages/pages-construction/taskDetail?id=${taskId}` })
  202. }
  203. onMounted(() => {
  204. const saved = uni.getStorageSync(storageKey)
  205. if (saved) {
  206. try {
  207. checks.value = JSON.parse(saved)
  208. } catch {
  209. // 解析失败则使用默认值
  210. }
  211. }
  212. const savedRemark = uni.getStorageSync(remarkKey)
  213. if (savedRemark) {
  214. remark.value = savedRemark
  215. }
  216. taskStore.fetchMyTasks(today)
  217. })
  218. </script>
  219. <style scoped>
  220. .sub-page {
  221. max-width: 430px;
  222. margin: 0 auto;
  223. min-height: 100vh;
  224. background-color: #f6fbf9;
  225. padding-bottom: 96px;
  226. }
  227. .glass-card {
  228. background-color: rgba(255, 255, 255, 0.85);
  229. backdrop-filter: blur(14px);
  230. -webkit-backdrop-filter: blur(14px);
  231. border: 1px solid rgba(164, 216, 152, 0.35);
  232. border-radius: 24px;
  233. box-shadow: 0 18px 50px -12px rgba(54, 143, 111, 0.28);
  234. overflow: hidden;
  235. margin-bottom: 12px;
  236. }
  237. /* 区块标题 */
  238. .section-head {
  239. display: flex;
  240. align-items: center;
  241. padding: 16px 16px 12px;
  242. }
  243. .section-head-inner {
  244. display: flex;
  245. align-items: center;
  246. }
  247. .section-bar {
  248. width: 4px;
  249. height: 16px;
  250. border-radius: 2px;
  251. background: linear-gradient(180deg, #368f6f 0%, #5ab8d0 100%);
  252. }
  253. .section-title {
  254. font-size: 16px;
  255. font-weight: 700;
  256. color: #1f2937;
  257. }
  258. /* 统计卡 */
  259. .stat-card {
  260. position: relative;
  261. display: flex;
  262. flex-direction: column;
  263. align-items: center;
  264. justify-content: center;
  265. min-height: 88px;
  266. padding: 14px 6px 12px;
  267. border-radius: 16px;
  268. background-color: rgba(164, 216, 152, 0.14);
  269. border: 1px solid rgba(164, 216, 152, 0.3);
  270. overflow: hidden;
  271. }
  272. .stat-bar {
  273. position: absolute;
  274. top: 0;
  275. left: 50%;
  276. transform: translateX(-50%);
  277. width: 26px;
  278. height: 3px;
  279. border-radius: 0 0 3px 3px;
  280. background: linear-gradient(135deg, #368f6f 0%, #5ab8d0 100%);
  281. }
  282. .stat-value {
  283. font-size: 24px;
  284. font-weight: 800;
  285. line-height: 1.1;
  286. color: #368f6f;
  287. }
  288. .stat-label {
  289. margin-top: 6px;
  290. font-size: 12px;
  291. color: #6b7280;
  292. }
  293. /* 检查项 */
  294. .check-state {
  295. font-size: 12px;
  296. font-weight: 600;
  297. }
  298. .check-state-done {
  299. color: #368f6f;
  300. }
  301. .check-state-pending {
  302. color: #b7791f;
  303. }
  304. .check-group {
  305. margin-bottom: 12px;
  306. padding-top: 12px;
  307. border-top: 1px solid rgba(164, 216, 152, 0.18);
  308. }
  309. .check-group:first-of-type {
  310. border-top: none;
  311. padding-top: 0;
  312. }
  313. .check-group-title {
  314. display: block;
  315. margin-bottom: 8px;
  316. font-size: 13px;
  317. font-weight: 600;
  318. color: #6b7280;
  319. }
  320. .check-row {
  321. display: flex;
  322. align-items: center;
  323. justify-content: space-between;
  324. padding: 8px 0;
  325. }
  326. .check-label {
  327. font-size: 14px;
  328. color: #4b5563;
  329. }
  330. .check-circle {
  331. width: 24px;
  332. height: 24px;
  333. border-radius: 50%;
  334. border: 2px solid #d1d5db;
  335. display: flex;
  336. align-items: center;
  337. justify-content: center;
  338. }
  339. .check-circle-on {
  340. border-color: #368f6f;
  341. background: linear-gradient(135deg, #368f6f 0%, #4ba98a 100%);
  342. }
  343. .check-tick {
  344. font-size: 13px;
  345. color: #ffffff;
  346. }
  347. .form-textarea {
  348. width: 100%;
  349. min-height: 72px;
  350. padding: 10px 12px;
  351. border-radius: 12px;
  352. border: 1px solid #e5e7eb;
  353. background-color: #ffffff;
  354. font-size: 14px;
  355. color: #1f2937;
  356. }
  357. .submit-btn {
  358. width: 100%;
  359. padding: 12px 0;
  360. border-radius: 14px;
  361. font-size: 14px;
  362. font-weight: 600;
  363. color: #ffffff;
  364. background: linear-gradient(135deg, #368f6f 0%, #4ba98a 100%);
  365. box-shadow: 0 10px 24px -10px rgba(54, 143, 111, 0.7);
  366. line-height: 1.4;
  367. }
  368. .submit-btn-hover {
  369. opacity: 0.88;
  370. }
  371. .submit-btn[disabled] {
  372. opacity: 0.5;
  373. }
  374. /* 已完成态 */
  375. .done-wrap {
  376. display: flex;
  377. flex-direction: column;
  378. align-items: center;
  379. padding: 24px 0 12px;
  380. }
  381. .done-icon {
  382. width: 64px;
  383. height: 64px;
  384. border-radius: 50%;
  385. display: flex;
  386. align-items: center;
  387. justify-content: center;
  388. background-color: rgba(164, 216, 152, 0.24);
  389. margin-bottom: 12px;
  390. }
  391. .done-tick {
  392. font-size: 28px;
  393. color: #368f6f;
  394. }
  395. .done-text {
  396. font-size: 14px;
  397. font-weight: 600;
  398. color: #1f2937;
  399. }
  400. .done-meta {
  401. margin-top: 4px;
  402. font-size: 12px;
  403. color: #9ca3af;
  404. }
  405. /* 列表行 */
  406. .list-row {
  407. display: flex;
  408. align-items: center;
  409. padding: 13px 16px;
  410. border-top: 1px solid rgba(164, 216, 152, 0.18);
  411. }
  412. .row-title {
  413. display: block;
  414. font-size: 14px;
  415. font-weight: 500;
  416. color: #1f2937;
  417. }
  418. .row-sub {
  419. display: block;
  420. margin-top: 3px;
  421. font-size: 12px;
  422. color: #9ca3af;
  423. }
  424. .row-hover {
  425. background-color: rgba(164, 216, 152, 0.1);
  426. }
  427. .icon-chip {
  428. width: 42px;
  429. height: 42px;
  430. border-radius: 12px;
  431. flex-shrink: 0;
  432. display: flex;
  433. align-items: center;
  434. justify-content: center;
  435. background-color: rgba(164, 216, 152, 0.2);
  436. }
  437. .chip-glyph {
  438. font-size: 20px;
  439. color: #368f6f;
  440. }
  441. .empty-text {
  442. font-size: 13px;
  443. color: #9ca3af;
  444. }
  445. </style>