surveyEntry.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. <template>
  2. <div class="survey-entry-page">
  3. <custom-header :name="t('workExecute.surveyEntryTitle')" bgColor="#fff" />
  4. <div class="survey-entry-body">
  5. <div class="hero">
  6. <div class="hero-l">
  7. <div class="hero__text">{{ t("workExecute.surveyEntryHint") }}</div>
  8. <div class="hero__text">{{ t("workExecute.surveyEntryHint2") }}<span class="text-tip">(可多选)</span></div>
  9. </div>
  10. <img class="hero__illust" src="@/assets/img/agricultural/enter.png" alt="" />
  11. </div>
  12. <div class="search-bar">
  13. <el-icon class="search-bar__icon"><Search /></el-icon>
  14. <input
  15. v-model="searchKeyword"
  16. class="search-bar__input"
  17. type="text"
  18. :placeholder="t('workExecute.surveySearchPlaceholder')"
  19. @keyup.enter="handleSearch"
  20. />
  21. <div class="search-bar__btn" @click="handleSearch">{{ t("workExecute.surveySearch") }}</div>
  22. </div>
  23. <div class="category-panel" v-loading="loading">
  24. <div v-for="group in displayGroups" :key="group.name" class="category-card">
  25. <div class="section-title">
  26. <span class="title-icon"></span>
  27. <span>{{ group.name }}</span>
  28. </div>
  29. <div class="tag-group">
  30. <div
  31. v-for="item in group.items"
  32. :key="item.id"
  33. class="tag-item"
  34. :class="{ selected: selectedIds.has(String(item.id)) }"
  35. @click="toggleSelect(item)"
  36. >
  37. <span class="text">{{ item.name }}</span>
  38. </div>
  39. </div>
  40. </div>
  41. <div v-if="!loading && !displayGroups.length" class="empty-tip">
  42. {{ t("workExecute.surveyNoMatch") }}
  43. </div>
  44. </div>
  45. </div>
  46. <div class="survey-entry-footer">
  47. <div class="submit-btn" @click="handleSubmit">{{ t("workExecute.surveySubmit") }}</div>
  48. </div>
  49. </div>
  50. </template>
  51. <script setup>
  52. import { computed, onMounted, reactive, ref } from "vue";
  53. import { useRouter } from "vue-router";
  54. import { ElMessage } from "element-plus";
  55. import { Search } from "@element-plus/icons-vue";
  56. import customHeader from "@/components/customHeader.vue";
  57. import { useI18n } from "@/i18n";
  58. const { t } = useI18n();
  59. const router = useRouter();
  60. const SESSION_KEY = "SURVEY_SELECTED_WORK_TYPES";
  61. const loading = ref(false);
  62. const categoryGroups = ref([]);
  63. const searchKeyword = ref("");
  64. const appliedKeyword = ref("");
  65. const selectedIds = reactive(new Set());
  66. const DEFAULT_GROUPS = [
  67. {
  68. name: "建园与土壤管理",
  69. items: [
  70. { id: 101, name: "整地类" },
  71. { id: 102, name: "除草" },
  72. { id: 103, name: "清园" },
  73. { id: 104, name: "清沟修渠" },
  74. ],
  75. },
  76. {
  77. name: "肥水管理",
  78. items: [
  79. { id: 201, name: "根部肥" },
  80. { id: 202, name: "根部灌水" },
  81. { id: 203, name: "根部灌药" },
  82. { id: 204, name: "飞防叶面肥" },
  83. { id: 205, name: "叶面喷水" },
  84. ],
  85. },
  86. {
  87. name: "打药管理",
  88. items: [
  89. { id: 301, name: "飞防打药" },
  90. { id: 302, name: "内膛打药" },
  91. ],
  92. },
  93. {
  94. name: "树体管理",
  95. items: [
  96. { id: 401, name: "整形修剪" },
  97. { id: 402, name: "嫁接" },
  98. { id: 403, name: "环割环剥" },
  99. ],
  100. },
  101. {
  102. name: "花果管理",
  103. items: [
  104. { id: 501, name: "疏花疏果" },
  105. { id: 502, name: "果实套袋" },
  106. ],
  107. },
  108. ];
  109. const displayGroups = computed(() => {
  110. const keyword = (appliedKeyword.value || "").trim();
  111. if (!keyword) return categoryGroups.value;
  112. return categoryGroups.value
  113. .map((group) => ({
  114. ...group,
  115. items: group.items.filter((item) => item.name.includes(keyword)),
  116. }))
  117. .filter((group) => group.items.length);
  118. });
  119. const allCategoryItems = computed(() =>
  120. categoryGroups.value.flatMap((group) => group.items)
  121. );
  122. const normalizeGroups = (list) => {
  123. if (!Array.isArray(list) || !list.length) return [];
  124. // 已是分组结构
  125. if (list[0]?.items) {
  126. return list
  127. .map((group, gIndex) => ({
  128. name: group.name || group.category_name || `分类${gIndex + 1}`,
  129. items: (group.items || group.list || [])
  130. .map((item, index) => ({
  131. id: item.id ?? item.type ?? item.code ?? `${gIndex}-${index + 1}`,
  132. name: item.name || item.type_name || item.label || "",
  133. }))
  134. .filter((item) => item.name),
  135. }))
  136. .filter((group) => group.items.length);
  137. }
  138. // 扁平列表兜底:按 category / group 字段分组
  139. const map = new Map();
  140. list.forEach((item, index) => {
  141. const name = item.name || item.type_name || item.label || "";
  142. if (!name) return;
  143. const groupName = item.category_name || item.group_name || item.category || "其他";
  144. if (!map.has(groupName)) map.set(groupName, []);
  145. map.get(groupName).push({
  146. id: item.id ?? item.type ?? item.code ?? index + 1,
  147. name,
  148. });
  149. });
  150. return Array.from(map.entries()).map(([name, items]) => ({ name, items }));
  151. };
  152. const restoreSelection = () => {
  153. try {
  154. const raw = sessionStorage.getItem(SESSION_KEY);
  155. if (!raw) return;
  156. const ids = JSON.parse(raw).map((item) => String(item.id));
  157. ids.forEach((id) => selectedIds.add(id));
  158. } catch {
  159. // ignore
  160. }
  161. };
  162. const fetchCategories = async () => {
  163. loading.value = true;
  164. try {
  165. categoryGroups.value = DEFAULT_GROUPS;
  166. // const res = await VE_API.z_farm_work_record.getFarmWorkTypeList();
  167. // const groups = normalizeGroups(res?.data);
  168. // categoryGroups.value = groups.length ? groups : DEFAULT_GROUPS;
  169. } catch {
  170. categoryGroups.value = DEFAULT_GROUPS;
  171. } finally {
  172. loading.value = false;
  173. }
  174. };
  175. const handleSearch = () => {
  176. appliedKeyword.value = searchKeyword.value;
  177. };
  178. const toggleSelect = (item) => {
  179. const id = String(item.id);
  180. if (selectedIds.has(id)) {
  181. selectedIds.delete(id);
  182. } else {
  183. selectedIds.add(id);
  184. }
  185. };
  186. const handleSubmit = () => {
  187. const selected = allCategoryItems.value.filter((item) => selectedIds.has(String(item.id)));
  188. if (!selected.length) {
  189. ElMessage.warning(t("workExecute.surveyPleaseSelect"));
  190. return;
  191. }
  192. sessionStorage.setItem(SESSION_KEY, JSON.stringify(selected));
  193. ElMessage.success(t("workExecute.surveySubmitSuccess"));
  194. router.back();
  195. };
  196. onMounted(() => {
  197. restoreSelection();
  198. fetchCategories();
  199. });
  200. </script>
  201. <style lang="scss" scoped>
  202. .survey-entry-page {
  203. height: 100vh;
  204. display: flex;
  205. flex-direction: column;
  206. // background: linear-gradient(90deg, #57B5FF 0%, #BBE1FF 11.5%, #F6F6F6 100%);
  207. background: linear-gradient(270deg, rgba(218, 195, 255, 0.59) 0%, #E6F2FF 0.01%, #8FC5FE 100%);
  208. // background: linear-gradient(180deg, #d7ecff 0%, #eaf5ff 42%, #f5f9fc 100%);
  209. overflow: hidden;
  210. }
  211. .survey-entry-body {
  212. padding: 0 10px;
  213. flex: 1;
  214. min-height: 0;
  215. display: flex;
  216. flex-direction: column;
  217. overflow: hidden;
  218. }
  219. .hero {
  220. padding: 20px 0px;
  221. position: relative;
  222. &__text {
  223. flex: 1;
  224. min-width: 0;
  225. font-size: 19px;
  226. line-height: 24px;
  227. color: #005599;
  228. font-family: "PangMenZhengDao";
  229. &.text-tip {
  230. font-size: 16px;
  231. }
  232. }
  233. &__illust {
  234. position: absolute;
  235. top: 4px;
  236. right: 0px;
  237. width: 106px;
  238. height: auto;
  239. flex-shrink: 0;
  240. }
  241. }
  242. .search-bar {
  243. display: flex;
  244. align-items: center;
  245. height: 36px;
  246. margin-bottom: 10px;
  247. padding: 0 12px;
  248. border-radius: 10px;
  249. background: rgba(255, 255, 255, 0.5);
  250. backdrop-filter: blur(4px);
  251. border: 1px solid rgba(33, 153, 248, 0.5);
  252. // box-shadow: 0 2px 8px rgba(33, 153, 248, 0.12);
  253. box-sizing: border-box;
  254. &__icon {
  255. color: rgba(0, 0, 0, 0.35);
  256. font-size: 16px;
  257. margin-right: 6px;
  258. flex-shrink: 0;
  259. }
  260. &__input {
  261. flex: 1;
  262. min-width: 0;
  263. border: none;
  264. outline: none;
  265. background: transparent;
  266. font-size: 14px;
  267. color: #333;
  268. &::placeholder {
  269. color: rgba(0, 0, 0, 0.3);
  270. }
  271. }
  272. &__btn {
  273. flex-shrink: 0;
  274. padding-left: 10px;
  275. color: #2199f8;
  276. font-size: 14px;
  277. cursor: pointer;
  278. }
  279. }
  280. .category-panel {
  281. flex: 1;
  282. min-height: 0;
  283. overflow-y: auto;
  284. -webkit-overflow-scrolling: touch;
  285. padding-bottom: 8px;
  286. }
  287. .category-card {
  288. background: #fff;
  289. border-radius: 12px;
  290. padding: 14px 12px 16px;
  291. margin-bottom: 12px;
  292. }
  293. .section-title {
  294. display: flex;
  295. align-items: center;
  296. gap: 8px;
  297. margin-bottom: 4px;
  298. font-size: 16px;
  299. font-weight: 600;
  300. color: #1a1a1a;
  301. .title-icon {
  302. position: relative;
  303. width: 14px;
  304. height: 14px;
  305. flex-shrink: 0;
  306. &::before,
  307. &::after {
  308. content: "";
  309. position: absolute;
  310. width: 10px;
  311. height: 10px;
  312. border-radius: 50%;
  313. }
  314. &::before {
  315. left: 0;
  316. top: 2px;
  317. background: #2199f8;
  318. opacity: 0.85;
  319. }
  320. &::after {
  321. right: 0;
  322. top: 0;
  323. background: #7ec8ff;
  324. opacity: 0.9;
  325. }
  326. }
  327. }
  328. .tag-group {
  329. display: grid;
  330. grid-template-columns: repeat(3, 1fr);
  331. gap: 0 8px;
  332. font-size: 14px;
  333. .tag-item {
  334. margin-top: 10px;
  335. position: relative;
  336. border-radius: 6px;
  337. box-sizing: border-box;
  338. height: 36px;
  339. text-align: center;
  340. line-height: 36px;
  341. cursor: pointer;
  342. color: #000;
  343. background: #f3f4f6;
  344. border: 1px solid transparent;
  345. .text {
  346. display: inline-block;
  347. max-width: 100%;
  348. overflow: hidden;
  349. text-overflow: ellipsis;
  350. white-space: nowrap;
  351. vertical-align: top;
  352. padding: 0 6px;
  353. }
  354. &.selected {
  355. border: 1px solid #2199f8;
  356. background: #e8f5ff;
  357. color: #2199f8;
  358. &::after {
  359. content: "";
  360. position: absolute;
  361. z-index: 9;
  362. top: -1px;
  363. right: -1px;
  364. width: 18px;
  365. height: 14px;
  366. background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
  367. }
  368. }
  369. }
  370. }
  371. .empty-tip {
  372. padding: 48px 0;
  373. text-align: center;
  374. color: rgba(0, 0, 0, 0.35);
  375. font-size: 14px;
  376. }
  377. .survey-entry-footer {
  378. flex-shrink: 0;
  379. padding: 10px 16px 24px;
  380. background: #fff;
  381. box-shadow: 2px 2px 4.5px 0px rgba(0, 0, 0, 0.4);
  382. .submit-btn {
  383. margin: 0 auto;
  384. width: fit-content;
  385. padding: 0 30px;
  386. height: 40px;
  387. line-height: 40px;
  388. text-align: center;
  389. border-radius: 25px;
  390. background: #2199f8;
  391. color: #fff;
  392. }
  393. }
  394. </style>