index.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  1. <template>
  2. <div class="task-page" :style="{ height: `calc(100vh - ${tabBarHeight}px)` }">
  3. <div class="task-top">
  4. <div class="map-container" ref="mapContainer"></div>
  5. </div>
  6. <div class="calendar-container">
  7. <customCalendar ref="calendarRef" @dateSelect="handleDateSelect"></customCalendar>
  8. </div>
  9. <div class="task-list">
  10. <div class="list-filter">
  11. <div
  12. v-for="item in filterTabs"
  13. :key="item.value"
  14. class="filter-item"
  15. :class="{ active: activeIndex === item.value }"
  16. @click="handleActiveFilter(item.value)"
  17. >
  18. {{ t(item.labelKey) }}({{ taskCounts[item.value] || 0 }})
  19. </div>
  20. </div>
  21. <div class="list-box">
  22. <div class="list-header">
  23. <div class="select-group">
  24. <el-select class="select-item" v-model="selectParma.farmWorkTypeId" :placeholder="t('workExecute.farmWorkType')"
  25. @change="resetAndLoad">
  26. <el-option v-for="item in farmWorkTypeList" :key="item.id"
  27. :label="item.id === 0 ? t('workExecute.all') : item.name"
  28. :value="item.id" />
  29. </el-select>
  30. <el-select class="select-item" v-model="selectParma.districtCode" :placeholder="t('workExecute.farmFilter')"
  31. @change="resetAndLoad">
  32. <el-option v-for="item in districtList" :key="item.code" :label="item.name"
  33. :value="item.code" />
  34. </el-select>
  35. </div>
  36. <!-- <div class="action-btn">{{ t("workExecute.batchAccept") }}</div> -->
  37. </div>
  38. <!-- 任务列表 -->
  39. <div class="work-task-list" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.3)">
  40. <div class="task-item" v-for="(item, index) in taskList" :key="index"
  41. :class="[
  42. isAcceptOvertime(item) ? 'timeout-item' : '',
  43. locale === 'en' ? 'task-item--en' : '',
  44. ]" @click="handleItem(item, index)">
  45. <div class="item-title" :class="{ 'item-title--en': locale === 'en' }">
  46. <img class="task-icon" src="@/assets/img/home/task.png" alt="">
  47. <div class="item-title-main">
  48. <span class="title-text">{{ item?.operation?.name }}</span>
  49. <span class="task-status" :class="`task-status--${getWorkType(item.operation?.type)}`">{{
  50. item.operation?.type }}</span>
  51. </div>
  52. </div>
  53. <span class="task-tag timeout" v-if="isAcceptOvertime(item)">
  54. <el-icon>
  55. <WarningFilled />
  56. </el-icon>
  57. {{ t("workExecute.acceptTimeout") }}
  58. </span>
  59. <span class="task-tag" v-else :class="`task-tag--${getStatusTagClass()}`">
  60. <el-icon v-if="activeStatus === '已完成'" class="task-tag__icon">
  61. <CircleCheck />
  62. </el-icon>
  63. {{ getStatusLabel(activeStatus) }}
  64. </span>
  65. <div class="item-content">
  66. <div class="item-info">
  67. <div class="info-item">
  68. <div class="info-name">{{ t("workExecute.responsible") }}</div><span class="val-text">
  69. {{ item.operation?.executor }}</span>
  70. </div>
  71. <div class="info-item" v-if="mode !== 'review'">
  72. <div class="info-name">{{ t("workExecute.farmWorkDetail") }}</div><span class="val-text">
  73. {{ item.operation?.work_detail }}</span>
  74. </div>
  75. <div class="info-item">
  76. <div class="info-name">{{ t("workExecute.farmSituation") }}</div><span class="val-text">
  77. {{ item.operation?.work_reason }}</span>
  78. </div>
  79. </div>
  80. <div class="excutor-info">
  81. <div class="executor-stats">
  82. <div class="stat-cell">
  83. <div class="stat-value">
  84. {{ item.time ? formatGMTToYMD(item.time) : "--" }}
  85. </div>
  86. <div class="stat-label">{{ t("workExecute.executeTime") }}</div>
  87. </div>
  88. <div class="cell-line"></div>
  89. <div class="stat-cell">
  90. <div class="stat-value">{{ item.geohash_sample || "--" }}</div>
  91. <div class="stat-label">{{ t("workExecute.executeArea") }}</div>
  92. </div>
  93. <div class="cell-line"></div>
  94. <div class="stat-cell">
  95. <div class="stat-value">{{ item.operation?.machine_code || "--" }}</div>
  96. <div class="stat-label">{{ t("workExecute.executeMachine") }}</div>
  97. </div>
  98. </div>
  99. </div>
  100. </div>
  101. <div
  102. v-if="activeStatus === '已完成'"
  103. class="execute-photos"
  104. @click.stop
  105. >
  106. <div class="execute-photos__wrap">
  107. <span class="execute-photos__label">{{ t('workExecute.executePhotos') }}</span>
  108. <div class="execute-photos__list">
  109. <div
  110. v-for="(photo, photoIndex) in getExecutePhotoList(item)"
  111. :key="photoIndex"
  112. class="execute-photos__item"
  113. >
  114. <img :src="photo" alt="" />
  115. </div>
  116. </div>
  117. </div>
  118. </div>
  119. <div class="unqualified-reason" v-if="activeStatus === '未达标'">
  120. {{ t("workExecute.unqualifiedReason") }}{{ item.operation?.reason || t("workExecute.defaultUnqualifiedReason") }}
  121. </div>
  122. <div class="compare-imgs" v-if="activeStatus === '未达标' && (item.beforeImage || item.afterImage)"
  123. @click.stop>
  124. <div class="img-tag">{{ t("workExecute.before") }}</div>
  125. <div class="img-tag right-tag">{{ t("workExecute.after") }}</div>
  126. <div class="img-item" v-if="item.beforeImage">
  127. <img :src="getWorkImageUrl(item.beforeImage)" alt="" />
  128. </div>
  129. <div class="img-item" v-if="item.afterImage">
  130. <img :src="getWorkImageUrl(item.afterImage)" alt="" />
  131. </div>
  132. </div>
  133. <div class="task-footer">
  134. <div class="farm-info">{{ t("workExecute.fromFarm") }}</div>
  135. <div class="btn-group" v-if="getItemStatusButtons(item).length">
  136. <div v-for="btn in getItemStatusButtons(item)" :key="btn.label" class="edit-btn"
  137. :class="btn.type" @click.stop="handleStatusBtn(btn, item, index)">
  138. {{ btn.label }}
  139. </div>
  140. </div>
  141. </div>
  142. </div>
  143. <div class="empty-tip" v-if="!loading && taskList.length === 0">{{ t("workExecute.noData") }}</div>
  144. </div>
  145. </div>
  146. </div>
  147. </div>
  148. </template>
  149. <script setup>
  150. import { computed, nextTick, onActivated, onMounted, ref, watch } from "vue";
  151. import { useStore } from "vuex";
  152. import IndexMap from "./index";
  153. import { useRouter } from "vue-router";
  154. import { WarningFilled, CircleCheck } from "@element-plus/icons-vue";
  155. import { ElMessage } from "element-plus";
  156. import config from "@/api/config.js";
  157. import customCalendar from "./components/calendar.vue";
  158. import { useI18n } from "@/i18n";
  159. import executePhotoPlaceholder1 from "@/assets/img/common/sd-1.jpg";
  160. import executePhotoPlaceholder2 from "@/assets/img/common/sd-2.jpg";
  161. import executePhotoPlaceholder3 from "@/assets/img/common/sd-3.jpg";
  162. import executePhotoPlaceholder4 from "@/assets/img/common/sd-4.jpg";
  163. const EXECUTE_PHOTO_PLACEHOLDERS = [
  164. executePhotoPlaceholder1,
  165. executePhotoPlaceholder2,
  166. executePhotoPlaceholder3,
  167. executePhotoPlaceholder4,
  168. ];
  169. const { t, locale } = useI18n();
  170. const store = useStore();
  171. const router = useRouter();
  172. const indexMap = new IndexMap();
  173. const mapContainer = ref(null);
  174. const tabBarHeight = computed(() => store.state.home.tabBarHeight);
  175. const selectParma = ref({
  176. farmWorkTypeId: null,
  177. districtCode: null,
  178. });
  179. const taskList = ref([])
  180. const mode = "execute";
  181. const activeStatus = ref("待接受");
  182. const STATUS_BTN_MAP = {
  183. 待接受: [
  184. { labelKey: "workExecute.modifyInfo", type: "normal", action: "redispatch" },
  185. { labelKey: "workExecute.acceptTask", type: "primary", action: "remindAccept" },
  186. ],
  187. 已接受: [
  188. { labelKey: "workExecute.modifyInfo", type: "normal", action: "redispatch" },
  189. { labelKey: "workExecute.startExecute", type: "primary-blue", action: "startExecute" },
  190. ],
  191. 执行中: [{ labelKey: "workExecute.markComplete", type: "primary-green", action: "markComplete" }],
  192. 已超时: [
  193. { labelKey: "workExecute.redispatch", type: "normal", action: "redispatch" },
  194. { labelKey: "workExecute.remindAccept", type: "primary", action: "remindAccept" },
  195. ],
  196. 未达标: [{ labelKey: "workExecute.redispatchAgain", type: "primary", action: "redispatch" }],
  197. };
  198. const STATUS_LABEL_MAP = {
  199. 待接受: "workExecute.pending",
  200. 已接受: "workExecute.accepted",
  201. 执行中: "workExecute.executing",
  202. 已完成: "workExecute.completed",
  203. 已超时: "workExecute.timeout",
  204. 未达标: "workExecute.unqualified",
  205. };
  206. const getStatusLabel = (status) => t(STATUS_LABEL_MAP[status] || status);
  207. const STATUS_TAG_CLASS_MAP = {
  208. 待接受: "pending",
  209. 已接受: "accepted",
  210. 执行中: "executing",
  211. 已完成: "completed",
  212. };
  213. const getStatusTagClass = () => STATUS_TAG_CLASS_MAP[activeStatus.value] || "pending";
  214. const isAcceptOvertime = (item) => Number(item?.operation?.is_aovertime) === 1;
  215. const mapStatusButtons = (buttons) =>
  216. buttons.map((btn) => ({
  217. ...btn,
  218. label: t(btn.labelKey),
  219. }));
  220. const getItemStatusButtons = (item) => {
  221. if (isAcceptOvertime(item)) {
  222. return [];
  223. }
  224. return mapStatusButtons(STATUS_BTN_MAP[activeStatus.value] || []);
  225. };
  226. const formatGMTToYMD = (gmtString) => {
  227. const date = new Date(gmtString);
  228. return date.toISOString().split("T")[0];
  229. };
  230. const getWorkImageUrl = (image) => {
  231. if (!image?.cloud_filename) return "";
  232. return config.base_img_url3 + image.cloud_filename;
  233. };
  234. const resolveExecutePhotoUrl = (src) => {
  235. if (!src) return "";
  236. if (typeof src === "object" && src.cloud_filename) {
  237. return config.base_img_url3 + src.cloud_filename;
  238. }
  239. if (typeof src === "string") {
  240. if (src.startsWith("http")) return src;
  241. return config.base_img_url2 + src;
  242. }
  243. return "";
  244. };
  245. const getExecutePhotoList = (item) => {
  246. const operation = item?.operation || {};
  247. const raw =
  248. operation.execute_evidence ??
  249. operation.executeEvidence ??
  250. item.execute_evidence ??
  251. item.executeEvidence;
  252. let photos = [];
  253. if (raw) {
  254. if (Array.isArray(raw)) {
  255. photos = raw.map(resolveExecutePhotoUrl).filter(Boolean);
  256. } else if (typeof raw === "string") {
  257. try {
  258. const parsed = JSON.parse(raw);
  259. if (Array.isArray(parsed)) {
  260. photos = parsed.map(resolveExecutePhotoUrl).filter(Boolean);
  261. }
  262. } catch {
  263. const url = resolveExecutePhotoUrl(raw);
  264. if (url) photos = [url];
  265. }
  266. }
  267. }
  268. return (photos.length ? photos : EXECUTE_PHOTO_PLACEHOLDERS).slice(0, 4);
  269. };
  270. const handleItem = (item, index) => {
  271. const statusStr = item.operation?.is_aovertime ? '接受超时' : activeStatus.value;
  272. const status = item.operation?.is_aovertime ? 1 : activeIndex.value;
  273. router.push({
  274. path: "/agri_record_detail",
  275. query: {
  276. id: item.operation?.work_id,
  277. status,
  278. statusStr,
  279. },
  280. });
  281. };
  282. const handleStatusBtn = (btn, item, index) => {
  283. if (btn.action === "redispatch") {
  284. ElMessage.info(t("workExecute.redispatchMsg", { name: item.operation?.name }));
  285. } else {
  286. ElMessage.success(t("workExecute.remindSuccess"));
  287. }
  288. };
  289. const formatDateYMD = (date = new Date()) =>
  290. `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
  291. const calendarRef = ref(null);
  292. const filterDate = ref(formatDateYMD());
  293. const fullTaskList = ref([]);
  294. const getCalendarTaskList = (list) => {
  295. return list.map((item) => ({
  296. expectedExecuteDate: item.time,
  297. executeDeadlineDate: item.time,
  298. }));
  299. };
  300. const syncCalendarData = () => {
  301. nextTick(() => {
  302. calendarRef.value?.setSolarTerm(getCalendarTaskList(fullTaskList.value));
  303. });
  304. };
  305. const handleDateSelect = (date) => {
  306. filterDate.value = date ?? formatDateYMD();
  307. resetAndLoad();
  308. syncCalendarData();
  309. };
  310. // 各状态任务数量,key 为 work_operation_type:0待接受 1执行中 2已接受 3已完成
  311. const taskCounts = ref({ 0: 0, 1: 0, 2: 0, 3: 0 });
  312. const STATUS_TAB_VALUES = [0, 2, 1, 3];
  313. const filterTabs = [
  314. { value: 0, labelKey: "workExecute.pending" },
  315. { value: 2, labelKey: "workExecute.accepted" },
  316. { value: 1, labelKey: "workExecute.executing" },
  317. { value: 3, labelKey: "workExecute.completed" },
  318. ];
  319. // 当前选中的筛选索引
  320. const activeIndex = ref(0);
  321. const loading = ref(false);
  322. const cityCode = ref("");
  323. //根据城市的坐标返回区县列表
  324. const districtList = ref([]);
  325. //农事类型列表
  326. const farmWorkTypeList = ref([]);
  327. function getFarmWorkTypeList() {
  328. VE_API.z_farm_work_record.getFarmWorkTypeList().then(({ data }) => {
  329. farmWorkTypeList.value = data;
  330. farmWorkTypeList.value.unshift({ id: 0, name: "全部" });
  331. });
  332. }
  333. const mapPoint = ref(null);
  334. onMounted(() => {
  335. mapPoint.value = store.state.home.miniUserLocationPoint;
  336. getFarmWorkTypeList();
  337. syncCalendarData();
  338. nextTick(() => {
  339. indexMap.initMap(mapPoint.value, mapContainer.value, true);
  340. indexMap.initData(taskList.value, '', 'farmPoint');
  341. });
  342. });
  343. onActivated(() => {
  344. resetAndLoad();
  345. nextTick(() => {
  346. if (!indexMap.kmap) {
  347. if (mapContainer.value) {
  348. mapPoint.value = store.state.home.miniUserLocationPoint;
  349. indexMap.initMap(mapPoint.value, mapContainer.value, true);
  350. }
  351. return;
  352. }
  353. if (mapContainer.value && indexMap.kmap.map) {
  354. const checkAndUpdateSize = () => {
  355. const container = mapContainer.value;
  356. if (!container) return;
  357. const rect = container.getBoundingClientRect();
  358. if (rect.width > 0 && rect.height > 0) {
  359. indexMap.kmap.map.updateSize();
  360. } else {
  361. setTimeout(checkAndUpdateSize, 100);
  362. }
  363. };
  364. setTimeout(checkAndUpdateSize, 200);
  365. }
  366. });
  367. });
  368. const getRegionRecordList = (data) => {
  369. if (!Array.isArray(data)) return [];
  370. return data.filter((item) => item.operation !== null);
  371. };
  372. const getRegionRecordParams = (workOperationType) => ({
  373. farm_id: localStorage.getItem('selectedFarmId'),
  374. time: filterDate.value,
  375. work_operation_type: workOperationType,
  376. // farm_id: '320',
  377. // time: '2026-06-30',
  378. // work_operation_type: workOperationType,
  379. });
  380. const taskListsByType = ref({ 0: [], 1: [], 2: [], 3: [] });
  381. const loadPageData = async () => {
  382. loading.value = true;
  383. try {
  384. const results = await Promise.all(
  385. STATUS_TAB_VALUES.map((type) => VE_API.farm.getRegionRecord(getRegionRecordParams(type)))
  386. );
  387. const counts = { 0: 0, 1: 0, 2: 0, 3: 0 };
  388. const listsByType = { 0: [], 1: [], 2: [], 3: [] };
  389. STATUS_TAB_VALUES.forEach((type, index) => {
  390. const { code, data } = results[index] || {};
  391. const list = code === 200 ? getRegionRecordList(data) : [];
  392. listsByType[type] = list;
  393. counts[type] = list.length;
  394. });
  395. taskListsByType.value = listsByType;
  396. taskCounts.value = counts;
  397. taskList.value = listsByType[activeIndex.value] || [];
  398. } finally {
  399. loading.value = false;
  400. }
  401. };
  402. // 监听 activeIndex 变化,切换 Tab 时复用已请求的数据
  403. watch(activeIndex, () => {
  404. taskList.value = taskListsByType.value[activeIndex.value] || [];
  405. });
  406. // 重置并重新加载
  407. const resetAndLoad = () => {
  408. loadPageData();
  409. };
  410. function handleActiveFilter(i) {
  411. activeIndex.value = i;
  412. selectParma.value.districtCode = cityCode.value;
  413. selectParma.value.farmWorkTypeId = null;
  414. const statusMap = { 0: "待接受", 1: "执行中", 2: "已接受", 3: "已完成" };
  415. activeStatus.value = statusMap[i] || "待接受";
  416. syncCalendarData();
  417. }
  418. const getWorkType = (type) => {
  419. const map = {
  420. '标准防治类': 1,
  421. '标准施肥类': 2,
  422. '标准调节类': 3,
  423. }
  424. return map[type] || type;
  425. }
  426. </script>
  427. <style lang="scss" scoped>
  428. .task-page {
  429. width: 100%;
  430. height: calc(100vh - 50px);
  431. overflow: auto;
  432. box-sizing: border-box;
  433. background: #FFFFFF;
  434. .map-container {
  435. width: 100%;
  436. height: 162px;
  437. clip-path: inset(0px round 8px);
  438. }
  439. .list-box {
  440. background: #F1F3F4;
  441. padding: 12px 12px 8px 12px;
  442. .list-header {
  443. display: flex;
  444. align-items: center;
  445. justify-content: space-between;
  446. .action-btn {
  447. box-sizing: border-box;
  448. height: 32px;
  449. line-height: 30px;
  450. color: #2199F8;
  451. font-size: 14px;
  452. border-radius: 5px;
  453. padding: 0 10px;
  454. background: #FFFFFF;
  455. border: 1px solid #2199F8;
  456. color: #2199F8;
  457. }
  458. }
  459. }
  460. .select-group {
  461. display: flex;
  462. padding: 0px 12px 0 12px;
  463. .select-item {
  464. width: 100%;
  465. ::v-deep {
  466. .el-select__wrapper {
  467. text-align: center;
  468. gap: 2px;
  469. box-shadow: none;
  470. justify-content: center;
  471. background: none;
  472. }
  473. .el-select__selection {
  474. flex: none;
  475. width: fit-content;
  476. }
  477. .el-select__placeholder {
  478. position: static;
  479. transform: none;
  480. width: fit-content;
  481. color: rgba(0, 0, 0, 0.2);
  482. }
  483. .el-select__caret {
  484. color: rgba(0, 0, 0, 0.2);
  485. }
  486. }
  487. }
  488. }
  489. .calendar-container {
  490. padding: 4px 12px 8px 12px;
  491. }
  492. .task-top {
  493. padding: 10px 12px 0 12px;
  494. }
  495. .task-content-loading {
  496. height: 80px;
  497. border-radius: 8px;
  498. position: absolute;
  499. top: 60px;
  500. left: 0;
  501. width: 100%;
  502. }
  503. .task-content {
  504. min-height: 80px;
  505. }
  506. .empty-data {
  507. text-align: center;
  508. font-size: 14px;
  509. color: #6f7274;
  510. padding: 20px 0;
  511. }
  512. .task-list {
  513. position: relative;
  514. background: #fff;
  515. }
  516. .list-filter {
  517. display: flex;
  518. align-items: center;
  519. justify-content: space-around;
  520. margin-top: 10px;
  521. .filter-item {
  522. padding: 0 6px;
  523. height: 32px;
  524. color: #1D2129;
  525. font-size: 14px;
  526. border-radius: 20px;
  527. position: relative;
  528. &.active {
  529. color: #2199f8;
  530. &::after {
  531. content: "";
  532. position: absolute;
  533. bottom: 0;
  534. left: 0;
  535. width: 100%;
  536. height: 2px;
  537. background: #2199f8;
  538. }
  539. }
  540. }
  541. }
  542. .work-task-list {
  543. margin-top: 10px;
  544. .task-item {
  545. margin-top: 10px;
  546. background: #fff;
  547. border-radius: 8px;
  548. padding: 12px 16px;
  549. border: 1px solid rgba(0, 0, 0, 0.08);
  550. position: relative;
  551. &.timeout-item {
  552. border: 1px solid #f74e4e;
  553. }
  554. &--en {
  555. padding-top: 22px;
  556. }
  557. .item-title {
  558. display: flex;
  559. align-items: center;
  560. gap: 8px;
  561. padding-bottom: 10px;
  562. border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  563. color: #1d2129;
  564. font-size: 16px;
  565. .task-icon {
  566. width: 16px;
  567. flex-shrink: 0;
  568. }
  569. .item-title-main {
  570. flex: 1;
  571. min-width: 0;
  572. display: flex;
  573. align-items: center;
  574. gap: 8px;
  575. }
  576. &--en {
  577. align-items: flex-start;
  578. .task-icon {
  579. margin-top: 2px;
  580. }
  581. .item-title-main {
  582. flex-direction: column;
  583. align-items: flex-start;
  584. gap: 4px;
  585. }
  586. .task-status {
  587. margin-left: 0;
  588. }
  589. }
  590. .title-text {
  591. min-width: 0;
  592. }
  593. .task-status {
  594. margin-left: 0;
  595. border-radius: 2px;
  596. height: 20px;
  597. line-height: 20px;
  598. padding: 1px 6px;
  599. box-sizing: border-box;
  600. font-size: 12px;
  601. color: #ffb32f;
  602. background: rgba(255, 179, 47, 0.1);
  603. &.task-status--1 {
  604. background: rgba(33, 153, 248, 0.1);
  605. color: #2199f8;
  606. }
  607. &.task-status--2 {
  608. background: rgba(255, 179, 47, 0.1);
  609. color: #FFB32F;
  610. }
  611. &.task-status--3 {
  612. background: rgba(58, 173, 148, 0.1);
  613. color: #3AAD94;
  614. }
  615. }
  616. }
  617. .task-tag {
  618. position: absolute;
  619. right: 0;
  620. top: 0;
  621. background: rgba(255, 149, 61, 0.1);
  622. color: #F46E00;
  623. font-size: 12px;
  624. padding: 0 10px;
  625. height: 25px;
  626. line-height: 25px;
  627. border-radius: 0 8px 0 8px;
  628. &.timeout {
  629. background: rgba(255, 106, 106, 0.1);
  630. color: #FF6A6A;
  631. display: flex;
  632. align-items: center;
  633. gap: 2px;
  634. }
  635. &--pending {
  636. background: rgba(255, 149, 61, 0.1);
  637. color: #F46E00;
  638. }
  639. &--accepted {
  640. background: rgba(33, 153, 248, 0.1);
  641. color: #2199F8;
  642. }
  643. &--executing {
  644. background: rgba(33, 153, 248, 0.1);
  645. color: #2199F8;
  646. }
  647. &--completed {
  648. background: rgba(74, 191, 50, 0.1);
  649. color: #4ABF32;
  650. display: flex;
  651. align-items: center;
  652. gap: 2px;
  653. .task-tag__icon {
  654. font-size: 12px;
  655. }
  656. }
  657. }
  658. .execute-photos {
  659. margin: 8px 0 4px;
  660. &__wrap {
  661. background: #ebf5ff;
  662. border: 1px solid rgba(33, 153, 248, 0.15);
  663. border-radius: 12px;
  664. padding: 12px;
  665. }
  666. &__label {
  667. display: inline-block;
  668. padding: 4px 10px;
  669. background: #2199f8;
  670. color: #fff;
  671. font-size: 12px;
  672. line-height: 18px;
  673. border-radius: 4px;
  674. margin-bottom: 8px;
  675. }
  676. &__list {
  677. display: flex;
  678. gap: 8px;
  679. background: #fff;
  680. border-radius: 10px;
  681. padding: 10px;
  682. }
  683. &__item {
  684. flex: 1;
  685. aspect-ratio: 1;
  686. border-radius: 8px;
  687. overflow: hidden;
  688. img {
  689. display: block;
  690. width: 100%;
  691. height: 100%;
  692. object-fit: cover;
  693. }
  694. }
  695. }
  696. .item-content {
  697. padding: 12px 0;
  698. .excutor-info {
  699. margin-top: 12px;
  700. background: rgba(189, 189, 189, 0.1);
  701. border-radius: 6px;
  702. padding-bottom: 12px;
  703. .excutor-info-top {
  704. padding: 12px 12px 0 12px;
  705. }
  706. }
  707. .executor-stats {
  708. display: flex;
  709. align-items: center;
  710. padding-top: 10px;
  711. justify-content: space-around;
  712. }
  713. .cell-line {
  714. height: 20px;
  715. width: 1px;
  716. background: #e5e6eb;
  717. }
  718. .stat-value {
  719. font-size: 14px;
  720. font-weight: 500;
  721. color: #0B0B0B;
  722. margin-bottom: 4px;
  723. word-break: break-all;
  724. }
  725. .stat-label {
  726. font-size: 12px;
  727. line-height: 20px;
  728. color: rgba(107, 107, 107, 0.5);
  729. text-align: center;
  730. }
  731. }
  732. .item-info {
  733. color: rgba(111, 114, 116, 0.6);
  734. font-size: 14px;
  735. line-height: 21px;
  736. .info-item {
  737. display: flex;
  738. .info-name {
  739. flex: none;
  740. }
  741. }
  742. .val-text {
  743. // color: #1d2129;
  744. }
  745. .info-item+.info-item {
  746. padding-top: 6px;
  747. }
  748. }
  749. .task-footer {
  750. padding-top: 4px;
  751. display: flex;
  752. align-items: center;
  753. justify-content: space-between;
  754. .farm-info {
  755. font-size: 14px;
  756. color: rgba(32, 32, 32, 0.4);
  757. }
  758. }
  759. .btn-group {
  760. display: flex;
  761. align-items: center;
  762. justify-content: end;
  763. gap: 10px;
  764. }
  765. .edit-btn {
  766. height: 30px;
  767. padding: 0 16px;
  768. border-radius: 24px;
  769. text-align: center;
  770. line-height: 30px;
  771. font-size: 14px;
  772. &.normal {
  773. background: #F6F6F6;
  774. color: #000000;
  775. }
  776. &.primary {
  777. background: #FF953D;
  778. color: #fff;
  779. }
  780. &.primary-blue {
  781. background: #2199F8;
  782. color: #fff;
  783. }
  784. &.primary-green {
  785. background: #37C11B;
  786. color: #fff;
  787. }
  788. }
  789. .compare-imgs {
  790. display: flex;
  791. align-items: center;
  792. gap: 10px;
  793. position: relative;
  794. margin: 10px 0;
  795. .img-tag {
  796. position: absolute;
  797. z-index: 10;
  798. top: 0;
  799. left: 0;
  800. font-size: 12px;
  801. color: #fff;
  802. background: rgba(0, 0, 0, 0.7);
  803. border-radius: 5px 0 5px 0;
  804. padding: 0 8px;
  805. height: 15px;
  806. line-height: 15px;
  807. &.right-tag {
  808. right: 0;
  809. left: auto;
  810. border-radius: 0 5px 0 5px;
  811. }
  812. }
  813. .img-item {
  814. flex: 1;
  815. height: 120px;
  816. overflow: hidden;
  817. border: 0.5px solid rgba(0, 0, 0, 0.1);
  818. border-radius: 5px;
  819. img {
  820. width: 100%;
  821. height: 100%;
  822. border-radius: 5px;
  823. object-fit: cover;
  824. }
  825. }
  826. }
  827. }
  828. .unqualified-reason {
  829. color: #f74e4e;
  830. font-size: 12px;
  831. padding: 4px 10px;
  832. background: rgba(247, 78, 78, 0.08);
  833. border-radius: 4px;
  834. line-height: 18px;
  835. }
  836. .empty-tip {
  837. text-align: center;
  838. color: #86909c;
  839. font-size: 14px;
  840. padding: 20px 0;
  841. }
  842. }
  843. }
  844. </style>