| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614 |
- <template>
- <div class="task-page" :style="{ height: `calc(100vh - ${tabBarHeight}px)` }">
- <farm-header class="task-page__header" />
- <div class="task-top">
- <div class="map-container" ref="mapContainer"></div>
- </div>
- <div class="calendar-container">
- <customCalendar ref="calendarRef" @dateSelect="handleDateSelect"></customCalendar>
- </div>
- <div class="task-list">
- <div class="task-tabs">
- <div class="tab-item" :class="{ active: activeTab === 'pending' }" @click="handleTabChange('pending')">
- 待执行({{ pendingCount }})
- </div>
- <div class="tab-item" :class="{ active: activeTab === 'completed' }" @click="handleTabChange('completed')">
- 已完成({{ completedCount }})
- </div>
- </div>
- <div class="work-task-list">
- <div
- v-for="item in displayTaskList"
- :key="item.id"
- class="task-card"
- @click.stop="handleViewDetail(item)"
- :class="{ 'is-risk': item.isHighRisk }"
- >
- <div v-if="item.status === 'pending' && item.cornerTip" class="task-card__corner">{{ item.cornerTip }}</div>
- <div v-if="item.status === 'completed' && item.cornerTip" class="task-card__corner done-corner" :class="{ 'myself-corner': item.isByMyself }">
- <el-icon size="14" class="myself-corner__icon" :class="{ 'myself-corner__icon-success': item.isByMyself }"><SuccessFilled /></el-icon>
- {{ item.cornerTip }}
- </div>
- <div class="task-card__header">
- <!-- <img class="task-card__icon" src="@/assets/img/home/task.png" alt="" /> -->
- <span class="task-card__title van-ellipsis">{{ item.executeDate + ' ' + item.title }}</span>
- <span v-if="item.isNormal" class="badge-tag normal-badge">标准类</span>
- <span v-if="item.isAbnormal" class="badge-tag abnormal-badge">异常类</span>
- <span v-if="item.isWeather" class="badge-tag weather-badge">气象类</span>
- <span v-if="item.isHighRisk" class="badge-tag risk-badge">{{ t("workExecute.highRisk") }}</span>
- </div>
- <div class="task-card__body">
- <div class="info-line">
- 触发条件:{{ item.fw_trigger || '--' }}
- </div>
- <div class="info-line">
- {{ t("workExecute.farmSituation") }}{{ item.analysisSummary }}
- </div>
- <div class="info-line">
- {{ t("workExecute.executeAreaLabel") }}{{ item.areaCode }}
- </div>
- </div>
- <div class="task-card__footer">
- <div class="footer-l">
- <img class="footer-l__icon" src="@/assets/img/home/user-icon-fill.png" alt="">
- 外包农事
- </div>
- <div class="footer-r">
- <div class="card-btn card-btn--ghost" @click.stop="handleForward(item)">
- {{ t("workExecute.forward") }}
- </div>
- <div class="card-btn card-btn--primary" @click.stop="handleViewDetail(item)">
- 我已完成
- </div>
- </div>
- </div>
- </div>
- <div v-if="!displayTaskList.length" class="empty-tip">{{ t("workExecute.noData") }}</div>
- </div>
- </div>
- <farm-calendar-popup v-model:show="showFarmCalendarPopup" @confirm="goCompleteFarmInfo" />
- <survey-ask-popup v-model:show="showSurveyAskPopup" @confirm="handleSurveyConfirm" />
- <!-- 诊断报告弹窗:组件内接口判断 exists=2 且本地未展示过 -->
- <diagnosis-report-popup />
- <!-- 恢复种植弹窗 -->
- <restore-planting-popup />
- </div>
- </template>
- <script setup>
- import { computed, nextTick, onActivated, onMounted, ref } from "vue";
- import { useStore } from "vuex";
- import { useRouter } from "vue-router";
- import IndexMap from "./index";
- import customCalendar from "./components/calendar.vue";
- import farmHeader from "@/components/farmHeader.vue";
- import farmCalendarPopup from "@/components/popup/farmCalendarPopup.vue";
- import surveyAskPopup from "@/components/popup/surveyAskPopup.vue";
- import diagnosisReportPopup from "@/components/popup/diagnosisReportPopup.vue";
- import restorePlantingPopup from "@/components/popup/restorePlantingPopup.vue";
- import { useI18n } from "@/i18n";
- import wx from "weixin-js-sdk";
- // ---------- 常量 ----------
- /** zoneId / farmId / date 临时固定,后续改为当前分区 / 当天 */
- const QUERY = {
- zoneId: 41,
- farmId: 321,
- date: "2025-09-11",
- };
- /** 任务类型:fw_tasks 标准化农事 / weather_work 气象农事 */
- const TASK_KIND = {
- NORMAL: "normal",
- WEATHER: "weather",
- };
- const COMPLETED_STATUS_RE = /已完成|已执行|已认证|本人完成/;
- const { t } = useI18n();
- const store = useStore();
- const router = useRouter();
- const indexMap = new IndexMap();
- const mapContainer = ref(null);
- const calendarRef = ref(null);
- const showFarmCalendarPopup = ref(false);
- /** 调查问卷弹窗:后续由接口决定是否展示 */
- const showSurveyAskPopup = ref(false);
- const tabBarHeight = computed(() => store.state.home.tabBarHeight);
- // ---------- 页面状态 ----------
- const filterDate = ref(QUERY.date); // 当前选中日期
- const taskList = ref([]); // 已映射的农事卡片(归属接口 date)
- const mapCenterPoint = ref(null); // 接口返回的分区中心点 WKT
- const activeTab = ref("pending");
- // ---------- 计算属性 ----------
- const dayTaskList = computed(() =>
- taskList.value.filter((item) => item.executeDateFull === filterDate.value)
- );
- const pendingCount = computed(() => dayTaskList.value.filter((item) => item.status === "pending").length);
- const completedCount = computed(() => dayTaskList.value.filter((item) => item.status === "completed").length);
- const displayTaskList = computed(() => dayTaskList.value.filter((item) => item.status === activeTab.value));
- // ---------- 工具函数 ----------
- function getMiniUserId() {
- return store.state.home.miniUserId || localStorage.getItem("MINI_USER_ID");
- }
- /** 与图片一致:zone_id / farm_id / trigger_date(临时写死,后续改当前分区) */
- const resolveQueryParams = () => ({
- zone_id: QUERY.zoneId,
- farm_id: QUERY.farmId,
- trigger_date: filterDate.value || QUERY.date,
- });
- const isCompletedStatus = (status) => COMPLETED_STATUS_RE.test(String(status || ""));
- const formatExecuteDate = (dateStr) => {
- if (!dateStr) return "";
- return String(dateStr).replace(/^\d{4}-(\d{2})-(\d{2})$/, "$1/$2");
- };
- const buildPointWkt = (lng, lat, index = 0) => {
- if (lng == null || lat == null || Number.isNaN(Number(lng)) || Number.isNaN(Number(lat))) return "";
- const offset = (index % 6) * 0.00025;
- return `POINT(${Number(lng) + offset} ${Number(lat) + offset * 0.6})`;
- };
- /** 单条任务映射为卡片;kind 区分标准化 / 气象 */
- const mapTaskItem = (task, index, { dateStr, lat, lng, zoneId, kind }) => {
- const executeDateFull = task.date || dateStr;
- const completed = isCompletedStatus(task.fw_status);
- const isWeather = kind === TASK_KIND.WEATHER;
- const prefix = isWeather ? "weather" : "fw";
- return {
- id: `${prefix}_${task.manual_id || task.mechanical_id || index}_${executeDateFull}`,
- status: completed ? "completed" : "pending",
- title: task.fw_name || "",
- isHighRisk: false,
- isNormal: !isWeather,
- isAbnormal: false,
- isWeather,
- cornerTip: task.fw_status || (completed ? "已完成" : "待执行"),
- analysisSummary: task.fw_simple_explain || task.fw_explain || "",
- areaCode: zoneId ? `分区 ${zoneId}` : "",
- farmName: task.fw_name || "",
- executeDate: formatExecuteDate(executeDateFull),
- executeDateFull,
- zoneId,
- manualId: task.manual_id,
- mechanicalId: task.mechanical_id,
- latitude: lat,
- longitude: lng,
- wkt: buildPointWkt(lng, lat, index),
- };
- };
- /** 合并 fw_tasks(标准化)+ weather_work(气象) */
- const mapZoneTaskList = (data) => {
- const dateStr = resolveRangeStart(data);
- const lat = data.latitude ?? 22.970229;
- const lng = data.longitude ?? 113.746646;
- const zoneId = data.zone_id ?? QUERY.zoneId;
- const ctx = { dateStr, lat, lng, zoneId };
- const normalList = (Array.isArray(data.fw_tasks) ? data.fw_tasks : [])
- .filter(Boolean)
- .map((task, index) => mapTaskItem(task, index, { ...ctx, kind: TASK_KIND.NORMAL }));
- const weatherList = (Array.isArray(data.weather_work) ? data.weather_work : [])
- .filter(Boolean)
- .map((task, index) =>
- mapTaskItem(task, normalList.length + index, { ...ctx, kind: TASK_KIND.WEATHER })
- );
- return { list: [...normalList, ...weatherList], lat, lng, dateStr, zoneId };
- };
- const resolveRangeStart = (data) => {
- if (data?.start_date) return data.start_date;
- if (data?.date) return data.date;
- return QUERY.date;
- };
- // ---------- 业务逻辑 ----------
- /** 无果园信息时展示「完善农场信息 / 专属农事日历」弹窗 */
- async function checkFarmCalendarPopup() {
- const id = getMiniUserId();
- if (!id) {
- showFarmCalendarPopup.value = false;
- return;
- }
- try {
- const res = await VE_API.questionnaire.getFarms({ id });
- const list = Array.isArray(res?.data) ? res.data : [];
- showFarmCalendarPopup.value = list.length === 0;
- } catch (e) {
- console.warn("[work_execute] getFarms failed", e);
- showFarmCalendarPopup.value = false;
- }
- }
- const refreshMapMarkers = () => {
- if (!indexMap.kmap) return;
- indexMap.initData(getMapMarkerList(), "farmName", "wkt");
- };
- /** 地图展示全部日期的待执行农事 */
- const getMapMarkerList = () =>
- taskList.value.filter((item) => item.status === "pending" && item.wkt);
- const syncCalendar = (data) => {
- const start = resolveRangeStart(data);
- calendarRef.value?.setStartDate?.(start);
- nextTick(() => {
- const preferDate =
- (taskList.value.some((item) => item.executeDateFull === filterDate.value) && filterDate.value) ||
- start;
- calendarRef.value?.selectDateByValue?.(preferDate);
- calendarRef.value?.setSolarTerm?.(taskList.value);
- });
- };
- const fetchZoneFwTaskList = async () => {
- try {
- const params = resolveQueryParams();
- const res = await VE_API.questionnaire.getZoneFwTaskList(params);
- const data = res?.data;
- if (!data) {
- taskList.value = [];
- mapCenterPoint.value = null;
- calendarRef.value?.setStartDate?.(params.trigger_date);
- calendarRef.value?.selectDateByValue?.(params.trigger_date);
- calendarRef.value?.setSolarTerm?.([]);
- refreshMapMarkers();
- return;
- }
- const { list, lat, lng, dateStr } = mapZoneTaskList(data);
- mapCenterPoint.value = `POINT(${lng} ${lat})`;
- taskList.value = list;
- filterDate.value = dateStr;
- syncCalendar(data);
- nextTick(() => refreshMapMarkers());
- } catch (e) {
- console.error("获取农事任务失败", e);
- taskList.value = [];
- }
- };
- // ---------- 事件处理 ----------
- const handleTabChange = (key) => {
- if (activeTab.value === key) return;
- activeTab.value = key;
- };
- const handleDateSelect = (date) => {
- const next = date ?? QUERY.date;
- if (filterDate.value === next) return;
- filterDate.value = next;
- fetchZoneFwTaskList();
- };
- const handleForward = (item) => {
- const workName = item?.title || item?.farmName || "农事详情";
- const query = {
- askInfo: { title: "转发农事", content: "是否分享给好友" },
- shareText: `${workName} 已触发,请在最佳农时内及时执行`,
- targetUrl: "work_detail",
- paramsPage: JSON.stringify({
- ...buildFwTaskQuery(item),
- fromShare: 1,
- }),
- imageUrl: "https://birdseye-img.sysuimars.com/birdseye-look-mini/work_img.png",
- };
- wx.miniProgram.navigateTo({
- url: `/pages/subPages/share_page/index?pageParams=${JSON.stringify(query)}&type=sharePage`,
- });
- };
- const buildFwTaskQuery = (item) => ({
- zone_id: item?.zoneId ?? QUERY.zoneId,
- date: item?.executeDateFull || QUERY.date,
- manual_id: item?.manualId,
- mechanical_id: item?.mechanicalId,
- statusVal: item?.status,
- lat: item?.latitude,
- lng: item?.longitude,
- fw_name: item?.title || item?.farmName || "",
- isNormal: item?.isNormal ? 1 : 0,
- isAbnormal: item?.isAbnormal ? 1 : 0,
- isWeather: item?.isWeather ? 1 : 0,
- isHighRisk: item?.isHighRisk ? 1 : 0,
- });
- const handleViewDetail = (item) => {
- router.push({
- path: "/work_detail",
- query: buildFwTaskQuery(item),
- });
- };
- const goCompleteFarmInfo = () => {
- router.push("/entry_information");
- };
- const handleSurveyConfirm = () => {
- showSurveyAskPopup.value = false;
- router.push("/survey_entry");
- };
- const initMap = () => {
- if (!mapContainer.value) return;
- const fallback = store.state.home.miniUserLocationPoint || "POINT(113.746646 22.970229)";
- const point = mapCenterPoint.value || fallback;
- if (!indexMap.kmap) {
- indexMap.initMap(point, mapContainer.value, true);
- }
- refreshMapMarkers();
- };
- onMounted(() => {
- checkFarmCalendarPopup();
- nextTick(async () => {
- await fetchZoneFwTaskList();
- initMap();
- });
- });
- onActivated(() => {
- checkFarmCalendarPopup();
- nextTick(() => {
- if (!indexMap.kmap) {
- initMap();
- return;
- }
- if (mapContainer.value && indexMap.kmap.map) {
- const checkAndUpdateSize = () => {
- const container = mapContainer.value;
- if (!container) return;
- const rect = container.getBoundingClientRect();
- if (rect.width > 0 && rect.height > 0) {
- indexMap.kmap.map.updateSize();
- refreshMapMarkers();
- } else {
- setTimeout(checkAndUpdateSize, 100);
- }
- };
- setTimeout(checkAndUpdateSize, 200);
- }
- });
- });
- </script>
- <style lang="scss" scoped>
- .task-page {
- width: 100%;
- height: calc(100vh - 50px);
- overflow: auto;
- box-sizing: border-box;
- background: linear-gradient(180deg, #d1ebff 0%, #ffffff 100%);
- .map-container {
- width: 100%;
- height: 162px;
- clip-path: inset(0 round 8px);
- }
- .calendar-container {
- padding: 4px 12px 8px;
- }
- .task-top {
- padding: 10px 12px 0;
- }
- .task-list {
- padding: 0 12px 20px;
- }
- .task-tabs {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 50px;
- margin: 2px 0 26px 0;
- .tab-item {
- color: #1D2129;
- position: relative;
- &.active {
- color: #2199F8;
- font-weight: 500;
- &::after {
- content: '';
- position: absolute;
- bottom: -10px;
- left: 0;
- width: 100%;
- height: 2px;
- background: #2199F8;
- }
- }
- }
- }
- .work-task-list {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .task-card {
- position: relative;
- padding: 14px 12px 12px;
- border-radius: 8px;
- background: #fff;
- box-sizing: border-box;
- overflow: hidden;
- box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.04);
- &__corner {
- position: absolute;
- top: 0;
- right: 0;
- max-width: 78px;
- padding: 2px 8px;
- border-radius: 0 8px 0 8px;
- background: rgba(33, 153, 248, 0.1);
- color: #2199F8;
- // background: rgba(255, 106, 106, 0.1);
- // color: #FF6A6A;
- font-size: 12px;
- line-height: 16px;
- white-space: wrap;
- box-sizing: border-box;
- text-align: center;
- // overflow: hidden;
- // text-overflow: ellipsis;
- &.done-corner {
- max-width: 86px;
- height: 24px;
- background: rgba(118, 126, 116, 0.1);
- display: flex;
- align-items: center;
- gap: 4px;
- color: #959595;
- &.myself-corner {
- background: rgba(74, 191, 50, 0.1);
- color: #4ABF32;
- }
- .myself-corner__icon {
- color: #959595;
- flex-shrink: 0;
- }
- .myself-corner__icon-success {
- color: #4ABF32;
- }
- }
- }
- &__header {
- display: flex;
- align-items: center;
- gap: 8px;
- padding-right: 8px;
- min-width: 0;
- }
- &__icon {
- width: 16px;
- height: 16px;
- flex-shrink: 0;
- }
- &__title {
- max-width: 160px;
- font-size: 16px;
- font-weight: 500;
- color: #000;
- }
- .badge-tag {
- flex-shrink: 0;
- height: 18px;
- padding: 0 6px;
- border-radius: 2px;
- font-size: 12px;
- }
- .badge-tag + .badge-tag {
- margin-left: 4px;
- }
- .risk-badge {
- background: #FF6A6A;
- color: #FFFFFF;
- }
- .normal-badge {
- background: rgba(33, 153, 248, 0.1);
- color: #2199F8;
- }
- .abnormal-badge {
- background: rgba(255, 106, 106, 0.1);
- color: #FF6A6A;
- }
- .weather-badge {
- background: rgba(255, 149, 61, 0.1);
- color: #FDA202;
- }
- &__body {
- margin-top: 10px;
- padding-top: 10px;
- border-top: 0.5px solid rgba(0, 0, 0, 0.05);
- }
- .info-line {
- font-size: 14px;
- line-height: 20px;
- color: rgba(111, 114, 116, 0.6);
- & + .info-line {
- margin-top: 6px;
- }
- }
- &__footer {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-top: 14px;
- .footer-l {
- background: rgba(33, 153, 248, 0.1);
- height: 20px;
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 0 6px;
- border-radius: 2px;
- font-size: 12px;
- color: #2199F8;
- .footer-l__icon {
- width: 14px;
- height: 14px;
- flex-shrink: 0;
- }
- }
- .footer-r {
- display: flex;
- align-items: center;
- gap: 10px;
- }
- }
- .card-btn {
- height: 32px;
- border-radius: 20px;
- text-align: center;
- line-height: 32px;
- font-size: 12px;
- &--ghost {
- background: #F6F6F6;
- color: #000000;
- padding: 0 20px;
- }
- &--primary {
- background: #2199F8;
- color: #FFFFFF;
- padding: 0 16px;
- }
- }
- }
- .empty-tip {
- padding: 40px 0;
- text-align: center;
- color: #86909c;
- font-size: 14px;
- }
- }
- </style>
|