detail-machine.vue 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599
  1. <template>
  2. <view class="container">
  3. <!-- 设备头部信息区域 -->
  4. <view class="device-header">
  5. <view class="device-info-row">
  6. <view class="device-name-container">
  7. <text class="device-name">{{ deviceInfo.machineName }}</text>
  8. <view
  9. class="status-tag"
  10. :class="deviceInfo.onlineStatus == 1 ? 'status-online' : 'status-offline'"
  11. >
  12. <view class="status-dot" :class="{'offline-dot': deviceInfo.onlineStatus == 0}"></view>
  13. {{ deviceInfo.onlineStatus == 1 ? '在线' : '离线' }}
  14. </view>
  15. </view>
  16. <view class="refresh-btn" :class="{'refreshing': isRefreshing}" @tap="refreshData">
  17. <image src="/static/icons/refresh_icon.png" mode="aspectFit" style="width: 22px; height: 22px;"></image>
  18. </view>
  19. </view>
  20. <view class="device-meta-row">
  21. <view class="device-meta-item">
  22. <view class="meta-icon">
  23. <image src="/static/icons/device_icon.png" mode="aspectFit" style="width: 36rpx; height: 36rpx;"></image>
  24. </view>
  25. <text class="meta-label">设备编号:</text>
  26. <text class="meta-value">{{ deviceInfo.machineCode }}</text>
  27. </view>
  28. <view class="device-meta-item">
  29. <view class="meta-icon">
  30. <image src="/static/icons/location_icon.png" mode="aspectFit" style="width: 36rpx; height: 36rpx;"></image>
  31. </view>
  32. <text class="meta-label">工作区域:</text>
  33. <text class="meta-value">{{ deviceInfo.location }}</text>
  34. </view>
  35. <view class="device-meta-item">
  36. <view class="meta-icon">
  37. <image src="/static/icons/clock_icon.png" mode="aspectFit" style="width: 36rpx; height: 36rpx;"></image>
  38. </view>
  39. <text class="meta-label">最近更新:</text>
  40. <text class="meta-value">{{ deviceInfo.updateTime }}</text>
  41. </view>
  42. </view>
  43. </view>
  44. <!-- 作业任务列表(竖向排列) -->
  45. <view class="task-section">
  46. <view class="section-title task-title-row">
  47. <view class="task-title-left">
  48. <text class="task-title-text">作业任务列表</text>
  49. <text class="task-title-sub">配置并选择当前设备的作业</text>
  50. </view>
  51. <view class="task-add-btn" @click.stop="goToCreateJob">
  52. <text class="task-add-plus">+</text>
  53. </view>
  54. </view>
  55. <view v-if="taskList.length" class="task-list">
  56. <view
  57. v-for="task in taskList"
  58. :key="task.id"
  59. class="task-card"
  60. :class="{ active: task.id === selectedTaskId }"
  61. @click="selectTask(task)"
  62. >
  63. <view class="task-header">
  64. <view class="task-icon">
  65. <image src="/static/icons/task_icon.png" mode="aspectFit" class="task-icon-img"></image>
  66. </view>
  67. <view class="task-title-wrap">
  68. <text class="task-title">{{ task.name }}</text>
  69. <text class="task-sub">{{ task.fieldArea }}</text>
  70. </view>
  71. <view class="task-area-type">{{ task.areaTypeText }}</view>
  72. <view class="task-radio">
  73. <view class="radio-outer" :class="{ checked: task.id === selectedTaskId }">
  74. <view class="radio-inner"></view>
  75. </view>
  76. </view>
  77. </view>
  78. <view class="task-meta">
  79. <text class="task-time">计划时间:{{ task.planTime }}</text>
  80. <view class="task-meta-right">
  81. <text class="task-status">{{ task.statusText }}</text>
  82. <button class="task-delete-btn" @click.stop="confirmDeleteTask(task)">删除</button>
  83. </view>
  84. </view>
  85. </view>
  86. </view>
  87. <view v-else class="task-empty">
  88. <text class="task-empty-text">当前设备暂无作业任务</text>
  89. </view>
  90. </view>
  91. <!-- 设备状态信息区域 -->
  92. <!-- <view class="status-section">
  93. <view class="section-title">
  94. <text>设备状态</text>
  95. </view>
  96. <view class="status-grid">
  97. <!-- 作业状态
  98. <view class="status-item" hover-class="status-item-hover">
  99. <text class="status-label">作业状态</text>
  100. <view class="status-value-container">
  101. <text class="status-value" :class="{
  102. 'status-working': deviceInfo.workStatus === 'working',
  103. 'status-idle': deviceInfo.workStatus === 'idle',
  104. 'status-error': deviceInfo.workStatus === 'error'
  105. }">{{ getWorkStatusText(deviceInfo.workStatus) }}</text>
  106. </view>
  107. <text class="status-time">{{ deviceInfo.statusUpdateTime || '1分钟前' }}</text>
  108. </view>
  109. <!-- 电量状态
  110. <view class="status-item" hover-class="status-item-hover">
  111. <text class="status-label">电量</text>
  112. <view class="status-value-container">
  113. <text class="status-value" :class="{
  114. 'value-warning': deviceInfo.battery < 30,
  115. 'value-alert': deviceInfo.battery < 15
  116. }">{{ deviceInfo.battery || 85 }}</text>
  117. <text class="status-unit">%</text>
  118. </view>
  119. <text class="status-time">{{ deviceInfo.batteryUpdateTime || '30秒前' }}</text>
  120. </view>
  121. <!-- GPS信号
  122. <view class="status-item" hover-class="status-item-hover">
  123. <text class="status-label">GPS信号</text>
  124. <view class="status-value-container">
  125. <text class="status-value" :class="{
  126. 'status-good': deviceInfo.gpsSignal >= 80,
  127. 'status-normal': deviceInfo.gpsSignal >= 60 && deviceInfo.gpsSignal < 80,
  128. 'status-weak': deviceInfo.gpsSignal < 60
  129. }">{{ getGpsSignalText(deviceInfo.gpsSignal) }}</text>
  130. </view>
  131. <text class="status-time">{{ deviceInfo.gpsUpdateTime || '15秒前' }}</text>
  132. </view>
  133. <!-- 工作时长
  134. <view class="status-item" hover-class="status-item-hover">
  135. <text class="status-label">今日工作</text>
  136. <view class="status-value-container">
  137. <text class="status-value">{{ deviceInfo.workDuration || '2.5' }}</text>
  138. <text class="status-unit">小时</text>
  139. </view>
  140. <text class="status-time">累计时长</text>
  141. </view>
  142. <!-- 作业面积
  143. <view class="status-item" hover-class="status-item-hover">
  144. <text class="status-label">今日面积</text>
  145. <view class="status-value-container">
  146. <text class="status-value">{{ deviceInfo.workArea || '15.6' }}</text>
  147. <text class="status-unit">亩</text>
  148. </view>
  149. <text class="status-time">已完成</text>
  150. </view>
  151. <!-- 当前速度
  152. <view class="status-item" hover-class="status-item-hover">
  153. <text class="status-label">当前速度</text>
  154. <view class="status-value-container">
  155. <text class="status-value">{{ deviceInfo.currentSpeed || '0' }}</text>
  156. <text class="status-unit">km/h</text>
  157. </view>
  158. <text class="status-time">{{ deviceInfo.speedUpdateTime || '实时' }}</text>
  159. </view>
  160. </view>
  161. </view> -->
  162. <!-- 设备控制区域
  163. <view class="control-section">
  164. <view class="section-title">
  165. <text>设备控制</text>
  166. <view class="engine-status" :class="{'engine-on': isEngineOn}">
  167. <text>{{ isEngineOn ? '引擎开启' : '引擎关闭' }}</text>
  168. </view>
  169. </view>
  170. <view class="control-container">
  171. <!-- 启动/停止按钮
  172. <view class="engine-control">
  173. <view
  174. class="engine-button"
  175. :class="{'engine-on': isEngineOn, 'disabled': deviceInfo.status === 'offline'}"
  176. @click="toggleEngine"
  177. >
  178. <view class="engine-icon">
  179. <image
  180. :src="isEngineOn ? '/static/icons/engine_stop.svg' : '/static/icons/engine_start.svg'"
  181. mode="aspectFit"
  182. style="width: 28px; height: 28px;"
  183. ></image>
  184. </view>
  185. <text class="engine-text">{{ isEngineOn ? '停止' : '启动' }}</text>
  186. </view>
  187. </view>
  188. <!-- 方向控制
  189. <view class="direction-control">
  190. <!-- 前进按钮
  191. <view class="control-btn control-forward"
  192. :class="{'disabled': !isEngineOn || deviceInfo.status === 'offline', 'active': activeControl === 'forward'}"
  193. @touchstart="startControl('forward')"
  194. @touchend="stopControl">
  195. <image src="/static/icons/arrow_up.svg" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  196. <text>前进</text>
  197. </view>
  198. <!-- 左右控制行
  199. <view class="control-row">
  200. <!-- 左转按钮
  201. <view class="control-btn control-left"
  202. :class="{'disabled': !isEngineOn || deviceInfo.status === 'offline', 'active': activeControl === 'left'}"
  203. @touchstart="startControl('left')"
  204. @touchend="stopControl">
  205. <image src="/static/icons/arrow_left.svg" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  206. <text>左转</text>
  207. </view>
  208. <!-- 中心状态显示
  209. <view class="control-center">
  210. <view class="speed-display">
  211. <text class="speed-value">{{ currentSpeed }}</text>
  212. <text class="speed-unit">km/h</text>
  213. </view>
  214. </view>
  215. <!-- 右转按钮
  216. <view class="control-btn control-right"
  217. :class="{'disabled': !isEngineOn || deviceInfo.status === 'offline', 'active': activeControl === 'right'}"
  218. @touchstart="startControl('right')"
  219. @touchend="stopControl">
  220. <image src="/static/icons/arrow_right.svg" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  221. <text>右转</text>
  222. </view>
  223. </view>
  224. <!-- 后退按钮
  225. <view class="control-btn control-backward"
  226. :class="{'disabled': !isEngineOn || deviceInfo.status === 'offline', 'active': activeControl === 'backward'}"
  227. @touchstart="startControl('backward')"
  228. @touchend="stopControl">
  229. <image src="/static/icons/arrow_down.svg" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  230. <text>后退</text>
  231. </view>
  232. </view>
  233. <!-- 快捷操作按钮
  234. <view class="quick-controls">
  235. <view class="quick-control-btn"
  236. :class="{'disabled': deviceInfo.status === 'offline'}"
  237. @click="emergencyStop">
  238. <view class="quick-icon emergency">
  239. <image src="/static/icons/emergency_stop.svg" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  240. </view>
  241. <text>紧急停止</text>
  242. </view>
  243. <view class="quick-control-btn"
  244. :class="{'disabled': deviceInfo.status === 'offline'}"
  245. @click="returnHome">
  246. <view class="quick-icon">
  247. <image src="/static/icons/home.svg" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  248. </view>
  249. <text>返回充电</text>
  250. </view>
  251. <view class="quick-control-btn"
  252. :class="{'disabled': deviceInfo.status === 'offline'}"
  253. @click="autoMode">
  254. <view class="quick-icon">
  255. <image src="/static/icons/auto_mode.svg" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  256. </view>
  257. <text>自动模式</text>
  258. </view>
  259. </view>
  260. </view>
  261. </view>-->
  262. <!-- 底部“开始作业”按钮 -->
  263. <view class="task-footer">
  264. <button
  265. class="start-btn"
  266. :class="{ disabled: !taskList.length || !selectedTaskId || deviceInfo.onlineStatus !== 1 }"
  267. @click="startWork"
  268. >
  269. 开始作业
  270. </button>
  271. </view>
  272. <!-- 告警信息列表 -->
  273. <view class="alerts-section">
  274. <view class="section-title">
  275. <text>告警信息</text>
  276. <view class="alert-badge" v-if="getUnhandledAlerts.length > 0">{{ getUnhandledAlerts.length }}</view>
  277. </view>
  278. <view class="alerts-list" v-if="getUnhandledAlerts.length > 0">
  279. <view
  280. v-for="(item, index) in getUnhandledAlerts"
  281. :key="index"
  282. class="alert-item"
  283. :class="{
  284. 'alert-urgent': item.level === 'high',
  285. 'alert-warning': item.level === 'medium',
  286. 'alert-info': item.level === 'low'
  287. }"
  288. @click="handleAlert(item)"
  289. >
  290. <view class="alert-item-icon">
  291. <image v-if="item.level === 'high'" src="/static/icons/warning_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  292. <image v-else-if="item.level === 'medium'" src="/static/icons/info_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  293. <image v-else src="/static/icons/success_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  294. </view>
  295. <view class="alert-item-info">
  296. <text class="alert-item-type">{{ item.title }}</text>
  297. <text class="alert-item-level">
  298. {{ item.level === 'high' ? '紧急' : item.level === 'medium' ? '警告' : '提示' }}
  299. </text>
  300. </view>
  301. <view class="alert-item-time">{{ item.time }}</view>
  302. </view>
  303. </view>
  304. <view v-else class="empty-alert">
  305. <text class="empty-text">暂无告警信息</text>
  306. </view>
  307. </view>
  308. </view>
  309. </template>
  310. <script setup>
  311. import { ref, reactive, computed, onMounted } from 'vue'
  312. import { onLoad } from '@dcloudio/uni-app'
  313. import { machineAlarmRecordsList } from '@/api/services/machineAlarmRecords'
  314. import { deviceTasksList, startTask, deleteTask } from '@/api/services/job'
  315. // 响应式数据
  316. const deviceInfo = reactive({
  317. id: '',
  318. machineCode: '',
  319. machineName: '',
  320. onlineStatus: 0,
  321. updateTime: '',
  322. location: ''
  323. })
  324. // 作业任务列表
  325. const taskList = ref([])
  326. const selectedTaskId = ref(null)
  327. // 控制状态
  328. const isEngineOn = ref(false)
  329. const activeControl = ref('') // forward, backward, left, right
  330. const currentSpeed = ref(0)
  331. // 页面状态
  332. const isRefreshing = ref(false)
  333. const alarmTypeMap = {
  334. 0: '其他',
  335. 1: '发动机',
  336. 2: '燃油',
  337. 3: '温度',
  338. 4: '压力',
  339. 5: '定位',
  340. }
  341. // 告警数据
  342. const alerts = ref([])
  343. // 计算属性
  344. // 未处理的告警
  345. const getUnhandledAlerts = computed(() => {
  346. return alerts.value.filter(alert => !alert.handled)
  347. })
  348. // 方法
  349. // 加载设备告警数据
  350. const loadDeviceAlarmData = () => {
  351. return machineAlarmRecordsList({
  352. machineId: deviceInfo.id
  353. }).then(res => {
  354. console.log('设备告警数据:', res)
  355. if (res.data.code === 200 && res.data.rows) {
  356. alerts.value = res.data.rows.map(item => ({
  357. id: item.id,
  358. title: item.alarmDesc,
  359. level: item.alarmLevel === 3 ? 'high' : item.alarmLevel === 2 ? 'medium' : 'low',
  360. time: item.alarmTime
  361. }))
  362. }
  363. })
  364. }
  365. const formatAlarmType = (level) => {
  366. return alarmTypeMap[level] || '其他'
  367. }
  368. // 加载设备数据
  369. const loadDeviceData = () => {
  370. console.log('加载设备数据:', deviceInfo.deviceId)
  371. }
  372. // 加载当前设备的作业任务列表
  373. const loadTaskList = () => {
  374. const params = {
  375. pageNum: 1,
  376. pageSize: 10,
  377. deviceId: deviceInfo.id
  378. }
  379. return deviceTasksList(params)
  380. .then((res) => {
  381. console.log("res任务作业列表",res)
  382. const { data } = res || {}
  383. if (data && data.code === 200) {
  384. const list = (data.rows) ? data.rows : []
  385. const areaTypeTextMap = {
  386. 1: '回字形',
  387. 2: '弓字形',
  388. 3: '自定义',
  389. 4: '垄沟'
  390. }
  391. taskList.value = list.map((item) => ({
  392. id: item.id,
  393. name: item.taskName,
  394. fieldArea: item.workArea ? item.workArea.areaName : '',
  395. planTime: item.createTime,
  396. status: item.taskStatus,
  397. statusText: item.taskStatusDesc,
  398. areaType: item.workAreas && item.workAreas.areaType,
  399. areaTypeText: areaTypeTextMap[item.workAreas && item.workAreas.areaType || 0] || '未知'
  400. }))
  401. console.log('taskList',taskList.value)
  402. if (taskList.value.length) {
  403. const exists = selectedTaskId.value && taskList.value.some(t => t.id === selectedTaskId.value)
  404. selectedTaskId.value = exists ? selectedTaskId.value : taskList.value[0].id
  405. } else {
  406. selectedTaskId.value = null
  407. }
  408. } else {
  409. taskList.value = []
  410. selectedTaskId.value = null
  411. uni.showToast({
  412. title: (data && data.msg) ? data.msg : '获取作业任务失败',
  413. icon: 'none'
  414. })
  415. }
  416. })
  417. .catch((err) => {
  418. console.error('获取作业任务列表失败:', err)
  419. taskList.value = []
  420. selectedTaskId.value = null
  421. uni.showToast({
  422. title: '获取作业任务失败',
  423. icon: 'none'
  424. })
  425. })
  426. }
  427. // 刷新数据
  428. const refreshData = () => {
  429. isRefreshing.value = true
  430. Promise.all([
  431. loadTaskList(),
  432. loadDeviceAlarmData()
  433. ]).finally(() => {
  434. isRefreshing.value = false
  435. deviceInfo.lastUpdate = '刚刚'
  436. uni.showToast({
  437. title: '刷新成功',
  438. icon: 'success',
  439. duration: 1500
  440. })
  441. })
  442. }
  443. // 获取工作状态文本
  444. const getWorkStatusText = (status) => {
  445. const statusMap = {
  446. working: '工作中',
  447. idle: '空闲',
  448. error: '故障'
  449. }
  450. return statusMap[status] || '未知'
  451. }
  452. // 获取GPS信号文本
  453. const getGpsSignalText = (signal) => {
  454. if (signal >= 80) return '强'
  455. if (signal >= 60) return '中等'
  456. if (signal >= 40) return '弱'
  457. return '无信号'
  458. }
  459. // 切换引擎状态
  460. const toggleEngine = () => {
  461. if (deviceInfo.status === 'offline') {
  462. uni.showToast({
  463. title: '设备离线,无法操作',
  464. icon: 'none'
  465. })
  466. return
  467. }
  468. isEngineOn.value = !isEngineOn.value
  469. if (isEngineOn.value) {
  470. deviceInfo.workStatus = 'idle'
  471. uni.showToast({
  472. title: '引擎已启动',
  473. icon: 'success'
  474. })
  475. } else {
  476. deviceInfo.workStatus = 'idle'
  477. currentSpeed.value = 0
  478. stopAllControls()
  479. uni.showToast({
  480. title: '引擎已关闭',
  481. icon: 'success'
  482. })
  483. }
  484. }
  485. // 开始控制
  486. const startControl = (direction) => {
  487. if (!isEngineOn.value || deviceInfo.status === 'offline') {
  488. return
  489. }
  490. activeControl.value = direction
  491. deviceInfo.workStatus = 'working'
  492. switch (direction) {
  493. case 'forward':
  494. currentSpeed.value = 5
  495. break
  496. case 'backward':
  497. currentSpeed.value = 2
  498. break
  499. case 'left':
  500. case 'right':
  501. currentSpeed.value = 3
  502. break
  503. }
  504. sendControlCommand(direction, true)
  505. }
  506. // 停止控制
  507. const stopControl = () => {
  508. if (activeControl.value) {
  509. sendControlCommand(activeControl.value, false)
  510. }
  511. activeControl.value = ''
  512. currentSpeed.value = 0
  513. deviceInfo.workStatus = 'idle'
  514. }
  515. // 停止所有控制
  516. const stopAllControls = () => {
  517. activeControl.value = ''
  518. currentSpeed.value = 0
  519. if (isEngineOn.value) {
  520. deviceInfo.workStatus = 'idle'
  521. }
  522. }
  523. // 发送控制指令
  524. const sendControlCommand = (direction, isStart) => {
  525. console.log(`发送控制指令: ${direction}, 开始: ${isStart}`)
  526. }
  527. // 紧急停止
  528. const emergencyStop = () => {
  529. if (deviceInfo.status === 'offline') {
  530. uni.showToast({
  531. title: '设备离线,无法操作',
  532. icon: 'none'
  533. })
  534. return
  535. }
  536. uni.showModal({
  537. title: '紧急停止',
  538. content: '确定要执行紧急停止吗?设备将立即停止所有操作。',
  539. success: (res) => {
  540. if (res.confirm) {
  541. isEngineOn.value = false
  542. stopAllControls()
  543. uni.showToast({
  544. title: '紧急停止已执行',
  545. icon: 'success'
  546. })
  547. }
  548. }
  549. })
  550. }
  551. // 返回充电
  552. const returnHome = () => {
  553. if (deviceInfo.status === 'offline') {
  554. uni.showToast({
  555. title: '设备离线,无法操作',
  556. icon: 'none'
  557. })
  558. return
  559. }
  560. uni.showToast({
  561. title: '设备正在返回充电站',
  562. icon: 'success'
  563. })
  564. }
  565. // 自动模式
  566. const autoMode = () => {
  567. if (deviceInfo.status === 'offline') {
  568. uni.showToast({
  569. title: '设备离线,无法操作',
  570. icon: 'none'
  571. })
  572. return
  573. }
  574. uni.showToast({
  575. title: '已切换到自动模式',
  576. icon: 'success'
  577. })
  578. }
  579. // 跳转到新增作业页面
  580. const goToCreateJob = () => {
  581. uni.navigateTo({
  582. url: `/pages/device/job-create/index?machineCode=${deviceInfo.machineCode}&id=${deviceInfo.id}`
  583. })
  584. }
  585. // 选择作业任务
  586. const selectTask = (task) => {
  587. selectedTaskId.value = task.id
  588. }
  589. // 开始作业
  590. const startWork = () => {
  591. if (!taskList.value.length) {
  592. uni.showToast({
  593. title: '暂无可执行的作业任务',
  594. icon: 'none'
  595. })
  596. return
  597. }
  598. if (!selectedTaskId.value) {
  599. uni.showToast({
  600. title: '请先选择一个作业任务',
  601. icon: 'none'
  602. })
  603. return
  604. }
  605. if (deviceInfo.onlineStatus !== 1) {
  606. uni.showToast({
  607. title: '设备未在线,无法开始作业',
  608. icon: 'none'
  609. })
  610. return
  611. }
  612. const task = taskList.value.find(t => t.id === selectedTaskId.value)
  613. uni.showModal({
  614. title: '开始作业',
  615. content: `确定开始执行「${task.name}」吗?`,
  616. success: async (res) => {
  617. if (res.confirm) {
  618. try {
  619. uni.showLoading({ title: '启动中...' })
  620. const resp = await startTask(selectedTaskId.value)
  621. const { data } = resp || {}
  622. if (data && data.code === 200) {
  623. uni.showToast({
  624. title: '作业已启动',
  625. icon: 'success'
  626. })
  627. isEngineOn.value = true
  628. deviceInfo.workStatus = 'working'
  629. setTimeout(() => {
  630. uni.navigateTo({
  631. url: `/pages/device/job-detail/index?id=${selectedTaskId.value}&deviceId=${deviceInfo.machineCode}&deviceName=${deviceInfo.machineName}`
  632. })
  633. }, 300)
  634. } else {
  635. uni.showToast({
  636. title: (data && data.msg) ? data.msg : '启动失败',
  637. icon: 'none'
  638. })
  639. }
  640. } catch (e) {
  641. console.error('开始作业失败:', e)
  642. uni.showToast({
  643. title: '网络异常,启动失败',
  644. icon: 'none'
  645. })
  646. } finally {
  647. uni.hideLoading()
  648. }
  649. }
  650. }
  651. })
  652. }
  653. // 删除作业
  654. const confirmDeleteTask = (task) => {
  655. if (!task || !task.id) return
  656. uni.showModal({
  657. title: '确认删除',
  658. content: `确定删除作业「${task.name}」吗?删除后不可恢复。`,
  659. confirmText: '删除',
  660. confirmColor: '#F56C6C',
  661. success: async (res) => {
  662. if (!res.confirm) return
  663. try {
  664. uni.showLoading({ title: '删除中...' })
  665. const resp = await deleteTask(task.id)
  666. const { data } = resp || {}
  667. if (data && data.code === 200) {
  668. uni.showToast({ title: '删除成功', icon: 'success' })
  669. if (selectedTaskId.value === task.id) {
  670. selectedTaskId.value = null
  671. }
  672. await loadTaskList()
  673. } else {
  674. uni.showToast({
  675. title: (data && data.msg) ? data.msg : '删除失败',
  676. icon: 'none'
  677. })
  678. }
  679. } catch (e) {
  680. console.error('删除作业失败:', e)
  681. uni.showToast({ title: '网络异常,删除失败', icon: 'none' })
  682. } finally {
  683. uni.hideLoading()
  684. }
  685. }
  686. })
  687. }
  688. // 处理告警
  689. const handleAlert = (alert) => {
  690. uni.showModal({
  691. title: alert.title,
  692. content: alert.description + '\n\n是否标记为已处理?',
  693. success: (res) => {
  694. if (res.confirm) {
  695. const index = alerts.value.findIndex(item => item.id === alert.id)
  696. if (index !== -1) {
  697. alerts.value[index].handled = true
  698. }
  699. uni.showToast({
  700. title: '已标记为处理',
  701. icon: 'success'
  702. })
  703. }
  704. }
  705. })
  706. }
  707. // 生命周期钩子
  708. onMounted(() => {
  709. // uni-app 生命周期
  710. uni.$once('agriculturalMachinesData', (data) => {
  711. if (data) {
  712. deviceInfo.machineCode = data.machineCode
  713. deviceInfo.machineName = data.machineName
  714. deviceInfo.onlineStatus = data.onlineStatus
  715. deviceInfo.updateTime = data.updateTime
  716. }
  717. })
  718. uni.setNavigationBarTitle({
  719. title: '农机设备详情'
  720. })
  721. loadDeviceData()
  722. loadTaskList()
  723. loadDeviceAlarmData()
  724. })
  725. onLoad((options)=>{
  726. deviceInfo.id = options.id;
  727. })
  728. </script>
  729. <style scoped>
  730. .container {
  731. display: flex;
  732. flex-direction: column;
  733. min-height: 100vh;
  734. background-color: #F8FCF9;
  735. padding-bottom: 30rpx;
  736. }
  737. /* 设备头部信息 */
  738. .device-header {
  739. background-color: #FFFFFF;
  740. border-radius: 20rpx;
  741. padding: 30rpx;
  742. margin: 30rpx 30rpx 30rpx;
  743. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  744. border: 1rpx solid rgba(210, 237, 217, 0.5);
  745. }
  746. .device-info-row {
  747. display: flex;
  748. justify-content: space-between;
  749. align-items: center;
  750. margin-bottom: 28rpx;
  751. }
  752. .device-name-container {
  753. display: flex;
  754. flex-direction: column;
  755. align-items: flex-start;
  756. }
  757. .device-name {
  758. font-size: 36rpx;
  759. color: #333333;
  760. font-weight: 600;
  761. margin-bottom: 12rpx;
  762. }
  763. .status-tag {
  764. padding: 6rpx 16rpx 6rpx 28rpx;
  765. border-radius: 30rpx;
  766. font-size: 24rpx;
  767. font-weight: 500;
  768. flex-shrink: 0;
  769. position: relative;
  770. overflow: hidden;
  771. }
  772. .status-online {
  773. background-color: rgba(76, 175, 80, 0.1);
  774. color: #3BB44A;
  775. }
  776. .status-offline {
  777. background-color: rgba(245, 108, 108, 0.1);
  778. color: #F56C6C;
  779. padding-left: 28rpx;
  780. }
  781. .status-dot {
  782. position: absolute;
  783. width: 6rpx;
  784. height: 6rpx;
  785. background-color: #3BB44A;
  786. border-radius: 50%;
  787. top: 50%;
  788. left: 12rpx;
  789. transform: translateY(-50%);
  790. box-shadow: 0 0 4rpx rgba(76, 175, 80, 0.8);
  791. animation: blink 1.5s infinite;
  792. display: inline-block;
  793. }
  794. .status-dot.offline-dot {
  795. background-color: #F56C6C;
  796. box-shadow: 0 0 4rpx rgba(245, 108, 108, 0.8);
  797. }
  798. .refresh-btn {
  799. width: 48rpx;
  800. height: 48rpx;
  801. background-color: rgba(76, 175, 80, 0.1);
  802. border-radius: 50%;
  803. display: flex;
  804. align-items: center;
  805. justify-content: center;
  806. padding: 12rpx;
  807. transition: transform 0.3s ease;
  808. }
  809. .refresh-btn:active {
  810. transform: rotate(180deg);
  811. }
  812. .refresh-btn.refreshing {
  813. animation: spin 1.2s linear infinite;
  814. }
  815. @keyframes spin {
  816. 0% {
  817. transform: rotate(0deg);
  818. }
  819. 100% {
  820. transform: rotate(360deg);
  821. }
  822. }
  823. @keyframes blink {
  824. 0% {
  825. opacity: 0.4;
  826. }
  827. 50% {
  828. opacity: 1;
  829. }
  830. 100% {
  831. opacity: 0.4;
  832. }
  833. }
  834. .device-meta-row {
  835. display: flex;
  836. flex-direction: column;
  837. background-color: #F9FCFA;
  838. padding: 20rpx 24rpx;
  839. border-radius: 16rpx;
  840. border: 1rpx solid rgba(210, 237, 217, 0.8);
  841. }
  842. .device-meta-item {
  843. display: flex;
  844. align-items: center;
  845. font-size: 28rpx;
  846. margin-top: 18rpx;
  847. }
  848. .device-meta-item:first-child {
  849. margin-top: 0;
  850. }
  851. .meta-icon {
  852. width: 36rpx;
  853. height: 36rpx;
  854. display: flex;
  855. align-items: center;
  856. justify-content: center;
  857. margin-right: 12rpx;
  858. }
  859. .meta-label {
  860. color: #777777;
  861. min-width: 140rpx;
  862. font-size: 28rpx;
  863. }
  864. .meta-value {
  865. color: #333333;
  866. flex: 1;
  867. font-size: 28rpx;
  868. font-weight: 500;
  869. }
  870. /* 设备状态区域 */
  871. .status-section {
  872. background: #ffffff;
  873. margin: 0 24rpx 20rpx;
  874. padding: 32rpx 24rpx;
  875. border-radius: 20rpx;
  876. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
  877. }
  878. .section-title {
  879. display: flex;
  880. justify-content: space-between;
  881. align-items: center;
  882. font-size: 32rpx;
  883. font-weight: 600;
  884. color: #333333;
  885. margin-bottom: 24rpx;
  886. padding-bottom: 16rpx;
  887. border-bottom: 2rpx solid #f0f0f0;
  888. }
  889. .status-grid {
  890. display: grid;
  891. grid-template-columns: repeat(2, 1fr);
  892. gap: 20rpx;
  893. }
  894. .status-item {
  895. background: #f8f9fa;
  896. padding: 24rpx;
  897. border-radius: 16rpx;
  898. border: 2rpx solid transparent;
  899. transition: all 0.3s ease;
  900. }
  901. .status-item-hover {
  902. border-color: #40a9ff;
  903. box-shadow: 0 4rpx 12rpx rgba(64, 169, 255, 0.15);
  904. }
  905. .status-label {
  906. font-size: 24rpx;
  907. color: #999999;
  908. margin-bottom: 8rpx;
  909. display: block;
  910. }
  911. .status-value-container {
  912. display: flex;
  913. align-items: baseline;
  914. margin-bottom: 8rpx;
  915. }
  916. .status-value {
  917. font-size: 36rpx;
  918. font-weight: 600;
  919. color: #333333;
  920. }
  921. .status-unit {
  922. font-size: 24rpx;
  923. color: #666666;
  924. margin-left: 4rpx;
  925. }
  926. .status-time {
  927. font-size: 20rpx;
  928. color: #cccccc;
  929. }
  930. /* 状态颜色 */
  931. .status-working {
  932. color: #52c41a;
  933. }
  934. .status-idle {
  935. color: #1890ff;
  936. }
  937. .status-error {
  938. color: #ff4d4f;
  939. }
  940. .status-good {
  941. color: #52c41a;
  942. }
  943. .status-normal {
  944. color: #faad14;
  945. }
  946. .status-weak {
  947. color: #ff4d4f;
  948. }
  949. .value-warning {
  950. color: #faad14;
  951. }
  952. .value-alert {
  953. color: #ff4d4f;
  954. }
  955. /* 控制区域 */
  956. .control-section {
  957. background: #ffffff;
  958. margin: 0 24rpx 20rpx;
  959. padding: 32rpx 24rpx;
  960. border-radius: 20rpx;
  961. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
  962. }
  963. .engine-status {
  964. padding: 8rpx 16rpx;
  965. border-radius: 20rpx;
  966. font-size: 24rpx;
  967. background-color: #fff1f0;
  968. color: #ff4d4f;
  969. }
  970. .engine-status.engine-on {
  971. background-color: #e8f5e8;
  972. color: #52c41a;
  973. }
  974. .control-container {
  975. display: flex;
  976. flex-direction: column;
  977. gap: 32rpx;
  978. }
  979. /* 引擎控制 */
  980. .engine-control {
  981. display: flex;
  982. justify-content: center;
  983. }
  984. .engine-button {
  985. display: flex;
  986. flex-direction: column;
  987. align-items: center;
  988. padding: 24rpx;
  989. background: #ffffff;
  990. border-radius: 20rpx;
  991. border: 3rpx solid #4CAF50;
  992. transition: all 0.3s ease;
  993. width: 160rpx;
  994. box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.2);
  995. }
  996. .engine-button.engine-on {
  997. background: #F44336;
  998. border-color: #F44336;
  999. color: white;
  1000. box-shadow: 0 4rpx 12rpx rgba(244, 67, 54, 0.3);
  1001. }
  1002. .engine-button:not(.engine-on) {
  1003. background: #4CAF50;
  1004. border-color: #4CAF50;
  1005. color: white;
  1006. }
  1007. .engine-button.disabled {
  1008. opacity: 0.3;
  1009. pointer-events: none;
  1010. background: #DDDDDD;
  1011. border-color: #DDDDDD;
  1012. }
  1013. .engine-button:active {
  1014. transform: scale(0.95);
  1015. }
  1016. .engine-icon {
  1017. margin-bottom: 12rpx;
  1018. }
  1019. .engine-text {
  1020. font-size: 28rpx;
  1021. font-weight: 600;
  1022. }
  1023. /* 方向控制 */
  1024. .direction-control {
  1025. display: flex;
  1026. flex-direction: column;
  1027. align-items: center;
  1028. gap: 20rpx;
  1029. }
  1030. .control-row {
  1031. display: flex;
  1032. align-items: center;
  1033. gap: 20rpx;
  1034. }
  1035. .control-btn {
  1036. display: flex;
  1037. flex-direction: column;
  1038. align-items: center;
  1039. justify-content: center;
  1040. width: 120rpx;
  1041. height: 120rpx;
  1042. background: #f8f9fa;
  1043. border-radius: 12rpx;
  1044. border: 2rpx solid #e0e0e0;
  1045. transition: all 0.3s ease;
  1046. font-size: 24rpx;
  1047. color: #999999;
  1048. gap: 8rpx;
  1049. }
  1050. .control-btn.active {
  1051. background: rgba(76, 175, 80, 0.1);
  1052. border-color: #4CAF50;
  1053. color: #4CAF50;
  1054. transform: scale(0.9);
  1055. box-shadow: 0 2rpx 8rpx rgba(76, 175, 80, 0.3);
  1056. }
  1057. .control-btn.disabled {
  1058. opacity: 0.3;
  1059. pointer-events: none;
  1060. background: #DDDDDD;
  1061. border-color: #DDDDDD;
  1062. }
  1063. .control-center {
  1064. width: 120rpx;
  1065. height: 120rpx;
  1066. display: flex;
  1067. flex-direction: column;
  1068. align-items: center;
  1069. justify-content: center;
  1070. background: #f0f0f0;
  1071. border-radius: 20rpx;
  1072. border: 2rpx solid #d0d0d0;
  1073. }
  1074. .speed-display {
  1075. display: flex;
  1076. flex-direction: column;
  1077. align-items: center;
  1078. }
  1079. .speed-value {
  1080. font-size: 32rpx;
  1081. font-weight: 600;
  1082. color: #333333;
  1083. }
  1084. .speed-unit {
  1085. font-size: 20rpx;
  1086. color: #999999;
  1087. }
  1088. /* 快捷控制 */
  1089. .quick-controls {
  1090. display: flex;
  1091. justify-content: space-around;
  1092. padding-top: 20rpx;
  1093. border-top: 2rpx solid #f0f0f0;
  1094. }
  1095. .quick-control-btn {
  1096. display: flex;
  1097. flex-direction: column;
  1098. align-items: center;
  1099. gap: 8rpx;
  1100. padding: 16rpx;
  1101. border-radius: 16rpx;
  1102. transition: all 0.3s ease;
  1103. }
  1104. .quick-control-btn.disabled {
  1105. opacity: 0.5;
  1106. pointer-events: none;
  1107. }
  1108. .quick-control-btn:active {
  1109. background: #f0f0f0;
  1110. }
  1111. .quick-icon {
  1112. width: 48rpx;
  1113. height: 48rpx;
  1114. background: #f8f9fa;
  1115. border-radius: 12rpx;
  1116. display: flex;
  1117. align-items: center;
  1118. justify-content: center;
  1119. border: 2rpx solid #e0e0e0;
  1120. }
  1121. .quick-icon.emergency {
  1122. background: rgba(245, 108, 108, 0.1);
  1123. border-color: #F56C6C;
  1124. }
  1125. .quick-control-btn text {
  1126. font-size: 24rpx;
  1127. color: #666666;
  1128. }
  1129. /* 告警信息 */
  1130. .alerts-section {
  1131. margin: 0 30rpx 30rpx;
  1132. background-color: #FFFFFF;
  1133. border-radius: 20rpx;
  1134. padding: 30rpx 24rpx;
  1135. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  1136. border: 1rpx solid rgba(210, 237, 217, 0.5);
  1137. }
  1138. .alert-badge {
  1139. background-color: #F56C6C;
  1140. color: #FFFFFF;
  1141. font-size: 22rpx;
  1142. border-radius: 30rpx;
  1143. padding: 2rpx 12rpx;
  1144. margin-left: 12rpx;
  1145. font-weight: normal;
  1146. min-width: 32rpx;
  1147. text-align: center;
  1148. }
  1149. .alerts-list {
  1150. display: flex;
  1151. flex-direction: column;
  1152. gap: 16rpx;
  1153. }
  1154. .alert-item {
  1155. display: flex;
  1156. align-items: center;
  1157. padding: 24rpx 20rpx;
  1158. border-radius: 12rpx;
  1159. margin-bottom: 16rpx;
  1160. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  1161. position: relative;
  1162. border: 1rpx solid transparent;
  1163. transition: transform 0.2s ease;
  1164. }
  1165. .alert-item:active {
  1166. transform: scale(0.98);
  1167. }
  1168. .alert-urgent {
  1169. background-color: #FEF3F3;
  1170. border-left: 4rpx solid #F56C6C;
  1171. border-color: rgba(245, 108, 108, 0.2);
  1172. }
  1173. .alert-warning {
  1174. background-color: #FFF8E6;
  1175. border-left: 4rpx solid #E6A23C;
  1176. border-color: rgba(230, 162, 60, 0.2);
  1177. }
  1178. .alert-info {
  1179. background-color: #F2FAF5;
  1180. border-left: 4rpx solid #67C23A;
  1181. border-color: rgba(103, 194, 58, 0.2);
  1182. }
  1183. .alert-item-icon {
  1184. width: 50rpx;
  1185. height: 50rpx;
  1186. display: flex;
  1187. align-items: center;
  1188. justify-content: center;
  1189. margin-right: 16rpx;
  1190. }
  1191. .alert-item-info {
  1192. flex: 1;
  1193. display: flex;
  1194. flex-direction: column;
  1195. gap: 8rpx;
  1196. }
  1197. .alert-item-type {
  1198. font-size: 28rpx;
  1199. color: #333333;
  1200. font-weight: 500;
  1201. margin-bottom: 8rpx;
  1202. }
  1203. .alert-item-level {
  1204. font-size: 24rpx;
  1205. color: #999999;
  1206. }
  1207. .alert-item-time {
  1208. font-size: 24rpx;
  1209. color: #999999;
  1210. margin-left: 16rpx;
  1211. min-width: 100rpx;
  1212. text-align: right;
  1213. }
  1214. .empty-alert {
  1215. padding: 60rpx 0;
  1216. display: flex;
  1217. justify-content: center;
  1218. align-items: center;
  1219. }
  1220. .empty-text {
  1221. font-size: 28rpx;
  1222. color: #999999;
  1223. }
  1224. /* 作业任务列表 */
  1225. .task-section {
  1226. background: #ffffff;
  1227. margin: 0 24rpx 20rpx;
  1228. padding: 28rpx 24rpx 20rpx;
  1229. border-radius: 20rpx;
  1230. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
  1231. }
  1232. .task-list {
  1233. margin-top: 8rpx;
  1234. }
  1235. .task-card {
  1236. background: #f9fbfa;
  1237. border-radius: 18rpx;
  1238. padding: 20rpx 18rpx;
  1239. margin-bottom: 16rpx;
  1240. border: 1rpx solid rgba(0, 0, 0, 0.03);
  1241. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
  1242. }
  1243. .task-card.active {
  1244. border-color: #3bb44a;
  1245. background: #f2fff4;
  1246. box-shadow: 0 4rpx 14rpx rgba(59, 180, 74, 0.12);
  1247. }
  1248. .task-header {
  1249. display: flex;
  1250. align-items: center;
  1251. }
  1252. .task-icon {
  1253. width: 60rpx;
  1254. height: 60rpx;
  1255. border-radius: 16rpx;
  1256. background: linear-gradient(135deg, #66cc6a, #3bb44a);
  1257. display: flex;
  1258. align-items: center;
  1259. justify-content: center;
  1260. margin-right: 16rpx;
  1261. }
  1262. .task-icon-img {
  1263. width: 34rpx;
  1264. height: 34rpx;
  1265. }
  1266. .task-area-type {
  1267. font-size: 22rpx;
  1268. color: #888;
  1269. margin-left: 8rpx;
  1270. flex-shrink: 0;
  1271. }
  1272. .task-title-wrap {
  1273. flex: 1;
  1274. display: flex;
  1275. flex-direction: column;
  1276. }
  1277. .task-title {
  1278. font-size: 30rpx;
  1279. font-weight: 600;
  1280. color: #2c3e50;
  1281. }
  1282. .task-sub {
  1283. margin-top: 6rpx;
  1284. font-size: 24rpx;
  1285. color: #8c9396;
  1286. }
  1287. .task-radio {
  1288. margin-left: 12rpx;
  1289. }
  1290. .radio-outer {
  1291. width: 32rpx;
  1292. height: 32rpx;
  1293. border-radius: 16rpx;
  1294. border: 2rpx solid #c0c4cc;
  1295. display: flex;
  1296. align-items: center;
  1297. justify-content: center;
  1298. }
  1299. .radio-outer.checked {
  1300. border-color: #3bb44a;
  1301. background: rgba(59, 180, 74, 0.1);
  1302. }
  1303. .radio-inner {
  1304. width: 18rpx;
  1305. height: 18rpx;
  1306. border-radius: 9rpx;
  1307. background: #3bb44a;
  1308. opacity: 0;
  1309. }
  1310. .radio-outer.checked .radio-inner {
  1311. opacity: 1;
  1312. }
  1313. .task-meta {
  1314. margin-top: 12rpx;
  1315. display: flex;
  1316. justify-content: space-between;
  1317. align-items: center;
  1318. font-size: 24rpx;
  1319. }
  1320. .task-time {
  1321. color: #909399;
  1322. }
  1323. .task-meta-right {
  1324. display: flex;
  1325. align-items: center;
  1326. gap: 12rpx;
  1327. }
  1328. .task-status {
  1329. color: #3bb44a;
  1330. }
  1331. .task-delete-btn {
  1332. height: 52rpx;
  1333. line-height: 52rpx;
  1334. padding: 0 16rpx;
  1335. border-radius: 26rpx;
  1336. background-color: rgba(245, 108, 108, 0.12);
  1337. color: #F56C6C;
  1338. font-size: 22rpx;
  1339. }
  1340. .task-empty {
  1341. padding: 40rpx 0 10rpx;
  1342. display: flex;
  1343. justify-content: center;
  1344. }
  1345. .task-empty-text {
  1346. font-size: 26rpx;
  1347. color: #999999;
  1348. }
  1349. .task-footer {
  1350. padding: 12rpx 30rpx 24rpx;
  1351. background: #f8fcf9;
  1352. }
  1353. .start-btn {
  1354. width: 100%;
  1355. height: 88rpx;
  1356. line-height: 88rpx;
  1357. border-radius: 44rpx;
  1358. background: linear-gradient(135deg, #3bb44a, #66cc6a);
  1359. color: #ffffff;
  1360. font-size: 30rpx;
  1361. font-weight: 600;
  1362. }
  1363. .start-btn.disabled {
  1364. background: #dcdfe6;
  1365. color: #ffffff;
  1366. }
  1367. /* 作业标题行加号按钮(与新增作业页保持一致) */
  1368. .task-title-row {
  1369. display: flex;
  1370. justify-content: space-between;
  1371. align-items: center;
  1372. }
  1373. .task-title-left {
  1374. display: flex;
  1375. flex-direction: column;
  1376. }
  1377. .task-title-text {
  1378. font-size: 32rpx;
  1379. font-weight: 600;
  1380. color: #2c3e50;
  1381. }
  1382. .task-title-sub {
  1383. margin-top: 4rpx;
  1384. font-size: 22rpx;
  1385. color: #8c9396;
  1386. }
  1387. .task-add-btn {
  1388. width: 60rpx;
  1389. height: 60rpx;
  1390. border-radius: 30rpx;
  1391. background: linear-gradient(135deg, #3bb44a, #66cc6a);
  1392. display: flex;
  1393. align-items: center;
  1394. justify-content: center;
  1395. box-shadow: 0 4rpx 12rpx rgba(59, 180, 74, 0.3);
  1396. }
  1397. .task-add-plus {
  1398. font-size: 40rpx;
  1399. color: #ffffff;
  1400. line-height: 1;
  1401. }
  1402. </style>