selectEquipment.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  1. <template>
  2. <div class="select-equipment" :class="{ 'is-service-equipment': isService }">
  3. <div class="select-equipment__content">
  4. <div class="page-header">
  5. <div class="page-title">{{ pageTitle }}</div>
  6. <div class="page-subtitle">{{ pageSubtitle }}</div>
  7. </div>
  8. <div class="toolbar">
  9. <div class="search-bar">
  10. <el-icon class="search-icon"><Search /></el-icon>
  11. <input
  12. v-model="searchKeyword"
  13. class="search-input"
  14. type="text"
  15. placeholder="请输入农机名称"
  16. @keyup.enter="handleSearch"
  17. />
  18. <div class="search-btn" @click="handleSearch">搜索</div>
  19. </div>
  20. <div class="add-btn" @click="handleAddMachine">+ 添加农机</div>
  21. </div>
  22. <div class="equipment-list" v-loading="loading">
  23. <div v-for="group in displayGroups" :key="group.name" class="equipment-card">
  24. <div class="section-title">
  25. <img class="title-icon" src="@/assets/img/home/label-icon.png" alt="" />
  26. <span>{{ group.name }}</span>
  27. </div>
  28. <div v-if="group.desc" class="section-desc">{{ group.desc }}</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">暂无匹配农机</div>
  42. </div>
  43. </div>
  44. <div class="custom-bottom-fixed-btns">
  45. <div class="btns-l">
  46. <div class="bottom-btn secondary-btn" @click="emit('prev')">上一步</div>
  47. <div
  48. v-if="isLastStep && !isService"
  49. class="bottom-btn secondary-btn"
  50. :class="{ disabled: submitting }"
  51. @click="handleSkip"
  52. >
  53. 跳过
  54. </div>
  55. </div>
  56. <div class="bottom-btn primary-btn" :class="{ disabled: submitting }" @click="handleSubmit">
  57. {{ primaryBtnText }}
  58. </div>
  59. </div>
  60. <add-machine-popup
  61. v-model:show="showAddPopup"
  62. @confirm="handleAddConfirm"
  63. />
  64. <tip-popup
  65. v-model:show="showSuccessPopup"
  66. type="executeSuccess"
  67. :text="isService ? '您的信息已提交成功' : '您的信息已上传'"
  68. :text2="isService ? '' : '请等待诊断报告生成'"
  69. buttonText="完成"
  70. @confirm="handleComplete"
  71. />
  72. </div>
  73. </template>
  74. <script setup>
  75. import { computed, onMounted, reactive, ref } from "vue";
  76. import { ElMessage } from "element-plus";
  77. import { Search } from "@element-plus/icons-vue";
  78. import addMachinePopup from "./addMachinePopup.vue";
  79. import tipPopup from "@/components/popup/tipPopup.vue";
  80. const emit = defineEmits(["prev", "next", "confirm"]);
  81. const props = defineProps({
  82. isService: { type: Boolean, default: false },
  83. isLastStep: { type: Boolean, default: true },
  84. equipmentScope: { type: String, default: "" },
  85. totalSteps: { type: Number, default: 4 },
  86. });
  87. const EQUIPMENT_KEY = "ENTRY_SELECTED_EQUIPMENT";
  88. const TASK_KEY = "ENTRY_SELECTED_FARM_TASKS";
  89. const SELECTED_LIST_KEY = "ENTRY_SELECTED_VARIETY_LIST";
  90. const CATEGORY_SESSION_KEY = "ENTRY_SELECTED_CATEGORY";
  91. const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
  92. const EDIT_UID_KEY = "ENTRY_VARIETY_EDIT_UID";
  93. const STEP_KEY = "ENTRY_INFORMATION_STEP";
  94. const loading = ref(false);
  95. const submitting = ref(false);
  96. const showAddPopup = ref(false);
  97. const searchKeyword = ref("");
  98. const appliedKeyword = ref("");
  99. const selectedIds = reactive(new Set());
  100. const equipmentGroups = ref([]);
  101. let customMachineId = 9000;
  102. const DEFAULT_GROUPS = [
  103. {
  104. name: "耕地(整地、施肥、撒肥、中耕等)",
  105. items: [
  106. { id: 101, name: "滴灌系统" },
  107. { id: 102, name: "喷灌设备" },
  108. { id: 103, name: "水肥一体机" },
  109. { id: 104, name: "水泵机组" },
  110. { id: 105, name: "施肥机" },
  111. { id: 106, name: "过滤设备" },
  112. ],
  113. },
  114. {
  115. name: "种(播种、育秧、移栽、补苗等)",
  116. items: [
  117. { id: 201, name: "拖拉机" },
  118. { id: 202, name: "旋耕机" },
  119. { id: 203, name: "开沟机" },
  120. { id: 204, name: "植保机" },
  121. { id: 205, name: "无人机" },
  122. { id: 206, name: "运输车" },
  123. ],
  124. },
  125. {
  126. name: "收(收割、烘干、运输等)",
  127. items: [
  128. { id: 301, name: "修剪机" },
  129. { id: 302, name: "采收机" },
  130. { id: 303, name: "割草机" },
  131. { id: 304, name: "粉碎机" },
  132. { id: 305, name: "发电机" },
  133. { id: 306, name: "其他设备" },
  134. ],
  135. },
  136. ];
  137. const FIELD_GROUPS = [
  138. {
  139. name: "耕地(整地、施肥、撒肥、中耕等)",
  140. items: [
  141. { id: 1001, name: "大中型拖拉机" },
  142. { id: 1002, name: "手扶拖拉机" },
  143. { id: 1003, name: "旋耕机" },
  144. { id: 1004, name: "微耕机" },
  145. { id: 1005, name: "挖掘机" },
  146. { id: 1006, name: "培土机" },
  147. { id: 1007, name: "撒肥机" },
  148. { id: 1008, name: "秸秆还田机" },
  149. { id: 1009, name: "开沟机" },
  150. { id: 1010, name: "水田打浆机" },
  151. { id: 1011, name: "深松机" },
  152. { id: 1012, name: "起垄机" },
  153. { id: 1013, name: "甘蔗开行机" },
  154. { id: 1014, name: "棉花拔杆机" },
  155. ],
  156. },
  157. {
  158. name: "种(播种、育秧、移栽、补苗等)",
  159. items: [
  160. { id: 2001, name: "插秧机" },
  161. { id: 2002, name: "一体育秧机" },
  162. { id: 2003, name: "播种机" },
  163. { id: 2004, name: "移栽机" },
  164. { id: 2005, name: "无人机播种" },
  165. { id: 2006, name: "小麦/玉米精量播种机" },
  166. { id: 2007, name: "免耕播种机" },
  167. { id: 2008, name: "花生覆膜播种机" },
  168. { id: 2009, name: "马铃薯播种机" },
  169. ],
  170. },
  171. {
  172. name: "收(收割、烘干、运输等)",
  173. items: [
  174. { id: 3001, name: "联合收割机" },
  175. { id: 3002, name: "半喂入收割机" },
  176. { id: 3003, name: "玉米联合收割机" },
  177. { id: 3004, name: "水稻收割机" },
  178. { id: 3005, name: "烘干机" },
  179. { id: 3006, name: "运粮车" },
  180. { id: 3007, name: "挂车" },
  181. ],
  182. },
  183. ];
  184. const FRUIT_GROUPS = [
  185. {
  186. name: "土壤与园地管理",
  187. desc: "(整地、除草、清园、开沟修渠等)",
  188. items: [
  189. { id: 4001, name: "旋耕机" },
  190. { id: 4002, name: "微耕机" },
  191. { id: 4003, name: "挖掘机" },
  192. { id: 4004, name: "旋耕开沟机" },
  193. { id: 4005, name: "割草机" },
  194. { id: 4006, name: "清沟机" },
  195. { id: 4007, name: "运输车" },
  196. { id: 4008, name: "开沟施肥机" },
  197. { id: 4009, name: "树盘管理机" },
  198. { id: 4010, name: "行间除草机" },
  199. { id: 4011, name: "生草播种机" },
  200. { id: 4012, name: "压草机" },
  201. ],
  202. },
  203. {
  204. name: "肥水管理",
  205. desc: "(施肥、灌水、灌药等)",
  206. items: [
  207. { id: 5001, name: "水肥一体化" },
  208. { id: 5002, name: "滴灌系统" },
  209. { id: 5003, name: "注肥/注药泵" },
  210. { id: 5004, name: "施肥枪" },
  211. { id: 5005, name: "移动水车" },
  212. { id: 5006, name: "水泵" },
  213. ],
  214. },
  215. {
  216. name: "植保打药",
  217. desc: "(冠层打药、内膛打药等)",
  218. items: [
  219. { id: 6001, name: "植保无人机" },
  220. { id: 6002, name: "风送式喷雾机" },
  221. { id: 6003, name: "背负式喷雾器" },
  222. { id: 6004, name: "柴油喷药机" },
  223. { id: 6005, name: "烟雾机" },
  224. { id: 6006, name: "高架喷雾机" },
  225. ],
  226. },
  227. ];
  228. const displayGroups = computed(() => {
  229. const keyword = (appliedKeyword.value || "").trim();
  230. if (!keyword) return equipmentGroups.value;
  231. return equipmentGroups.value
  232. .map((group) => ({
  233. ...group,
  234. items: group.items.filter((item) => item.name.includes(keyword)),
  235. }))
  236. .filter((group) => group.items.length);
  237. });
  238. const allEquipmentItems = computed(() =>
  239. equipmentGroups.value.flatMap((group) => group.items)
  240. );
  241. const pageTitle = computed(() =>
  242. props.isService ? "完善设备信息" : "请填写您的农场设备"
  243. );
  244. const pageSubtitle = computed(() =>
  245. props.isService ? "让农机调度更精准(可多选)" : "完善设备信息,让农机调度更精准"
  246. );
  247. const primaryBtnText = computed(() => {
  248. if (submitting.value) return "提交中...";
  249. return props.isLastStep ? "提交信息" : `下一步 (3/${props.isService ? props.totalSteps : 4})`;
  250. });
  251. function readJson(key) {
  252. try {
  253. const raw = sessionStorage.getItem(key);
  254. return raw ? JSON.parse(raw) : null;
  255. } catch {
  256. return null;
  257. }
  258. }
  259. function cloneGroups(groups) {
  260. return groups.map((group) => ({
  261. name: group.name,
  262. desc: group.desc || "",
  263. items: group.items.map((item) => ({ ...item })),
  264. }));
  265. }
  266. function flattenEquipment(cached) {
  267. if (!cached) return [];
  268. if (Array.isArray(cached)) return cached;
  269. return [...(cached.field || []), ...(cached.fruit || [])];
  270. }
  271. function getScopeList(cached, scope) {
  272. if (!cached) return [];
  273. if (Array.isArray(cached)) return scope === "fruit" ? [] : cached;
  274. return cached[scope] || [];
  275. }
  276. function getCurrentScope() {
  277. if (props.equipmentScope === "fruit") return "fruit";
  278. if (props.equipmentScope === "field") return "field";
  279. return "";
  280. }
  281. function getSelectedNames(list) {
  282. return new Set((list || []).map((item) => item?.name).filter(Boolean));
  283. }
  284. function getFruitItemsByFieldNames(fieldItems) {
  285. const names = getSelectedNames(fieldItems);
  286. if (!names.size) return [];
  287. return FRUIT_GROUPS.flatMap((group) =>
  288. group.items
  289. .filter((item) => names.has(item.name))
  290. .map((item) => ({ ...item, scope: "fruit", groupName: group.name }))
  291. );
  292. }
  293. function mergeEquipmentById(existing, extra) {
  294. const map = new Map();
  295. [...(existing || []), ...(extra || [])].forEach((item) => {
  296. if (item?.id == null) return;
  297. map.set(String(item.id), item);
  298. });
  299. return Array.from(map.values());
  300. }
  301. function restoreSelection() {
  302. const cached = readJson(EQUIPMENT_KEY);
  303. const scope = getCurrentScope();
  304. const list = scope ? getScopeList(cached, scope) : flattenEquipment(cached);
  305. list.forEach((item) => {
  306. if (item?.id == null) return;
  307. selectedIds.add(String(item.id));
  308. if (!item.custom) return;
  309. const exists = allEquipmentItems.value.some((row) => String(row.id) === String(item.id));
  310. if (exists) return;
  311. let group = equipmentGroups.value.find((row) => row.name === item.groupName);
  312. if (!group) group = equipmentGroups.value[equipmentGroups.value.length - 1];
  313. if (group) {
  314. group.items.push({ id: item.id, name: item.name, custom: true });
  315. }
  316. if (Number(item.id) >= customMachineId) customMachineId = Number(item.id);
  317. });
  318. }
  319. function applyFieldOverlapToFruit() {
  320. if (!props.isService || getCurrentScope() !== "fruit") return;
  321. const fieldNames = getSelectedNames(getScopeList(readJson(EQUIPMENT_KEY), "field"));
  322. if (!fieldNames.size) return;
  323. let changed = false;
  324. allEquipmentItems.value.forEach((item) => {
  325. if (!fieldNames.has(item.name) || selectedIds.has(String(item.id))) return;
  326. selectedIds.add(String(item.id));
  327. changed = true;
  328. });
  329. if (changed) saveSelectionDraft();
  330. }
  331. function saveSelectionDraft() {
  332. const scope = getCurrentScope();
  333. const selected = allEquipmentItems.value
  334. .filter((item) => selectedIds.has(String(item.id)))
  335. .map((item) => {
  336. const group = equipmentGroups.value.find((row) =>
  337. row.items.some((rowItem) => String(rowItem.id) === String(item.id))
  338. );
  339. return { ...item, scope: scope || "farmer", groupName: group?.name };
  340. });
  341. if (!props.isService) {
  342. sessionStorage.setItem(EQUIPMENT_KEY, JSON.stringify(selected));
  343. return;
  344. }
  345. const cached = readJson(EQUIPMENT_KEY);
  346. const next = {
  347. field: Array.isArray(cached) ? cached : cached?.field || [],
  348. fruit: Array.isArray(cached) ? [] : cached?.fruit || [],
  349. };
  350. if (scope === "fruit") {
  351. next.fruit = selected;
  352. } else {
  353. next.field = selected;
  354. next.fruit = mergeEquipmentById(next.fruit, getFruitItemsByFieldNames(selected));
  355. }
  356. sessionStorage.setItem(EQUIPMENT_KEY, JSON.stringify(next));
  357. }
  358. function fetchEquipmentList() {
  359. loading.value = true;
  360. if (!props.isService) {
  361. equipmentGroups.value = cloneGroups(DEFAULT_GROUPS);
  362. } else if (props.equipmentScope === "fruit") {
  363. equipmentGroups.value = cloneGroups(FRUIT_GROUPS);
  364. } else {
  365. equipmentGroups.value = cloneGroups(FIELD_GROUPS);
  366. }
  367. loading.value = false;
  368. }
  369. const handleSearch = () => {
  370. appliedKeyword.value = searchKeyword.value;
  371. };
  372. const handleAddMachine = () => {
  373. showAddPopup.value = true;
  374. };
  375. const handleAddConfirm = ({ name }) => {
  376. const machineName = String(name || "").trim();
  377. if (!machineName) return;
  378. const CUSTOM_GROUP_NAME = "我的农机";
  379. // 已存在同名则直接选中
  380. const exists = allEquipmentItems.value.find((item) => item.name === machineName);
  381. if (exists) {
  382. selectedIds.add(String(exists.id));
  383. saveSelectionDraft();
  384. ElMessage.success("已选中该农机");
  385. return;
  386. }
  387. // 确保「自定义类」作为第一组存在
  388. let customGroup = equipmentGroups.value.find((group) => group.name === CUSTOM_GROUP_NAME);
  389. if (!customGroup) {
  390. customGroup = { name: CUSTOM_GROUP_NAME, items: [] };
  391. equipmentGroups.value.unshift(customGroup);
  392. } else {
  393. const index = equipmentGroups.value.indexOf(customGroup);
  394. if (index > 0) {
  395. equipmentGroups.value.splice(index, 1);
  396. equipmentGroups.value.unshift(customGroup);
  397. }
  398. }
  399. const newItem = {
  400. id: ++customMachineId,
  401. name: machineName,
  402. custom: true,
  403. };
  404. customGroup.items.push(newItem);
  405. selectedIds.add(String(newItem.id));
  406. saveSelectionDraft();
  407. ElMessage.success("添加成功");
  408. };
  409. const toggleSelect = (item) => {
  410. const id = String(item.id);
  411. if (selectedIds.has(id)) {
  412. selectedIds.delete(id);
  413. } else {
  414. selectedIds.add(id);
  415. }
  416. saveSelectionDraft();
  417. };
  418. function getVarietyDraftList() {
  419. const draft = readJson(SELECTED_LIST_KEY);
  420. if (Array.isArray(draft)) return draft;
  421. if (Array.isArray(draft?.list)) return draft.list;
  422. return [];
  423. }
  424. function getPhenophaseLabel(item) {
  425. return item.phenophase || item.phenologyName || String(item.phenologyId || "");
  426. }
  427. function buildPlotPayload(includeEquipment = true) {
  428. const baForm = readJson("ENTRY_BA_FORM") || {};
  429. const varietyList = getVarietyDraftList();
  430. const equipmentCache = readJson(EQUIPMENT_KEY);
  431. const equipmentList = includeEquipment ? flattenEquipment(equipmentCache) : [];
  432. const taskCache = readJson(TASK_KEY);
  433. const taskList = Array.isArray(taskCache)
  434. ? taskCache
  435. : [...(taskCache?.field || []), ...(taskCache?.fruit || [])];
  436. return {
  437. user_name: baForm.name,
  438. tel: baForm.phone,
  439. team_name: baForm.teamName || "",
  440. team_type: baForm.teamType || "",
  441. invite_type: baForm.inviteType || (props.isService ? "service" : "farmer"),
  442. crops: varietyList.map((item) => ({
  443. crop_type: item.categoryName,
  444. crop_id: Number(item.categoryId),
  445. variety_list: [Number(item.id)],
  446. phenophase: getPhenophaseLabel(item),
  447. start_time: item.startTime,
  448. plant_area: Number(item.area),
  449. point: item.location,
  450. })),
  451. farm_machines: equipmentList.map((item) => ({
  452. id: item.id,
  453. name: item.name,
  454. })),
  455. farm_tasks: taskList.map((item) => ({
  456. id: item.id,
  457. name: item.name,
  458. })),
  459. field_tasks: (taskCache?.field || []).map((item) => ({ id: item.id, name: item.name })),
  460. fruit_tasks: (taskCache?.fruit || []).map((item) => ({ id: item.id, name: item.name })),
  461. };
  462. }
  463. function clearEntrySession() {
  464. sessionStorage.removeItem(SELECTED_LIST_KEY);
  465. sessionStorage.removeItem(CATEGORY_SESSION_KEY);
  466. sessionStorage.removeItem("ENTRY_BA_FORM");
  467. sessionStorage.removeItem(LOCATION_KEY);
  468. sessionStorage.removeItem(EDIT_UID_KEY);
  469. sessionStorage.removeItem(EQUIPMENT_KEY);
  470. sessionStorage.removeItem(TASK_KEY);
  471. sessionStorage.removeItem("ENTRY_MANUAL_VIEW");
  472. sessionStorage.removeItem(STEP_KEY);
  473. sessionStorage.removeItem("ENTRY_INVITE_TYPE");
  474. }
  475. const showSuccessPopup = ref(false);
  476. async function submitEntry(includeEquipment = true) {
  477. if (submitting.value) return;
  478. const varietyList = getVarietyDraftList();
  479. if (!props.isService && !varietyList.length) {
  480. ElMessage.warning("请先完善种植品种信息");
  481. return;
  482. }
  483. const baForm = readJson("ENTRY_BA_FORM");
  484. if (!baForm?.name || !baForm?.phone) {
  485. ElMessage.warning("请先完善个人信息");
  486. return;
  487. }
  488. submitting.value = true;
  489. try {
  490. const params = buildPlotPayload(includeEquipment);
  491. console.log("entry submit params", params);
  492. // const res = await VE_API.entry.addPlotInfo(params);
  493. // if (res.code === 200) {
  494. // ElMessage.success(res.msg || "提交成功");
  495. // clearEntrySession();
  496. // emit("confirm", params);
  497. // } else {
  498. // ElMessage.error(res.msg || "提交失败,请稍后再试");
  499. // }
  500. showSuccessPopup.value = true;
  501. clearEntrySession();
  502. } catch (error) {
  503. console.error("entry submit failed", error);
  504. ElMessage.error("提交失败");
  505. } finally {
  506. submitting.value = false;
  507. }
  508. }
  509. const handleComplete = () => {
  510. showSuccessPopup.value = false;
  511. emit("confirm");
  512. };
  513. const handleSkip = () => {
  514. if (!props.isLastStep) {
  515. emit("next");
  516. return;
  517. }
  518. submitEntry(false);
  519. };
  520. const handleSubmit = () => {
  521. saveSelectionDraft();
  522. if (!props.isLastStep) {
  523. emit("next");
  524. return;
  525. }
  526. submitEntry(true);
  527. };
  528. onMounted(() => {
  529. fetchEquipmentList();
  530. restoreSelection();
  531. applyFieldOverlapToFruit();
  532. });
  533. </script>
  534. <style lang="scss" scoped>
  535. .select-equipment {
  536. flex: 1;
  537. min-height: 0;
  538. display: flex;
  539. flex-direction: column;
  540. overflow: hidden;
  541. &__content {
  542. flex: 1;
  543. min-height: 0;
  544. overflow-y: auto;
  545. -webkit-overflow-scrolling: touch;
  546. padding: 8px 16px 80px;
  547. }
  548. .page-header {
  549. padding: 8px 4px 16px;
  550. .page-title {
  551. font-size: 26px;
  552. color: #005599;
  553. font-family: "PangMenZhengDao";
  554. line-height: 36px;
  555. }
  556. .page-subtitle {
  557. margin-top: 4px;
  558. font-size: 14px;
  559. color: rgba(46, 46, 46, 0.4);
  560. line-height: 20px;
  561. }
  562. .text-tip {
  563. font-size: 20px;
  564. }
  565. }
  566. .toolbar {
  567. display: flex;
  568. align-items: center;
  569. gap: 10px;
  570. margin-bottom: 12px;
  571. }
  572. .search-bar {
  573. flex: 1;
  574. min-width: 0;
  575. display: flex;
  576. align-items: center;
  577. height: 36px;
  578. padding: 0 12px;
  579. background: rgba(255, 255, 255, 0.5);
  580. border: 1px solid rgba(33, 153, 248, 0.5);
  581. border-radius: 6px;
  582. box-sizing: border-box;
  583. .search-icon {
  584. color: rgba(0, 0, 0, 0.35);
  585. font-size: 16px;
  586. margin-right: 6px;
  587. flex-shrink: 0;
  588. }
  589. .search-input {
  590. flex: 1;
  591. min-width: 0;
  592. border: none;
  593. outline: none;
  594. background: transparent;
  595. font-size: 14px;
  596. color: #333;
  597. &::placeholder {
  598. color: rgba(0, 0, 0, 0.3);
  599. }
  600. }
  601. .search-btn {
  602. flex-shrink: 0;
  603. padding-left: 10px;
  604. color: #2199f8;
  605. font-size: 14px;
  606. cursor: pointer;
  607. }
  608. }
  609. .add-btn {
  610. flex-shrink: 0;
  611. height: 36px;
  612. padding: 0 12px;
  613. border-radius: 6px;
  614. background: #2199f8;
  615. color: #fff;
  616. font-size: 14px;
  617. line-height: 36px;
  618. white-space: nowrap;
  619. cursor: pointer;
  620. }
  621. .equipment-card {
  622. background: #fff;
  623. border-radius: 12px;
  624. padding: 14px 12px 16px;
  625. margin-bottom: 12px;
  626. }
  627. .section-title {
  628. display: flex;
  629. align-items: center;
  630. gap: 8px;
  631. margin-bottom: 4px;
  632. font-size: 16px;
  633. font-weight: 500;
  634. color: #1a1a1a;
  635. .title-icon {
  636. width: 15px;
  637. height: 15px;
  638. object-fit: contain;
  639. flex-shrink: 0;
  640. }
  641. }
  642. .section-desc {
  643. margin: -2px 0 2px 22px;
  644. font-size: 12px;
  645. color: rgba(0, 0, 0, 0.4);
  646. line-height: 18px;
  647. }
  648. .tag-group {
  649. display: grid;
  650. grid-template-columns: repeat(3, 1fr);
  651. gap: 0 8px;
  652. font-size: 14px;
  653. .tag-item {
  654. margin-top: 10px;
  655. position: relative;
  656. border-radius: 6px;
  657. box-sizing: border-box;
  658. height: 36px;
  659. text-align: center;
  660. line-height: 36px;
  661. cursor: pointer;
  662. color: #000;
  663. background: #f3f4f6;
  664. border: 1px solid transparent;
  665. .text {
  666. display: inline-block;
  667. max-width: 100%;
  668. overflow: hidden;
  669. text-overflow: ellipsis;
  670. white-space: nowrap;
  671. vertical-align: top;
  672. padding: 0 6px;
  673. }
  674. &.selected {
  675. border: 1px solid #2199f8;
  676. background: #e8f5ff;
  677. color: #2199f8;
  678. &::after {
  679. content: "";
  680. position: absolute;
  681. z-index: 9;
  682. top: -1px;
  683. right: -1px;
  684. width: 18px;
  685. height: 14px;
  686. background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
  687. }
  688. }
  689. }
  690. }
  691. .empty-tip {
  692. padding: 40px 0;
  693. text-align: center;
  694. color: rgba(0, 0, 0, 0.35);
  695. font-size: 14px;
  696. }
  697. .custom-bottom-fixed-btns {
  698. display: flex;
  699. justify-content: space-between;
  700. align-items: baseline;
  701. gap: 10px;
  702. padding: 12px 12px 0;
  703. height: 80px;
  704. background: #fff;
  705. box-sizing: border-box;
  706. box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.4);
  707. .btns-l {
  708. display: flex;
  709. align-items: center;
  710. gap: 10px;
  711. }
  712. .bottom-btn {
  713. padding: 0 30px;
  714. height: 40px;
  715. line-height: 40px;
  716. box-sizing: border-box;
  717. font-size: 14px;
  718. border-radius: 25px;
  719. text-align: center;
  720. white-space: nowrap;
  721. &.disabled {
  722. opacity: 0.6;
  723. pointer-events: none;
  724. }
  725. }
  726. .secondary-btn {
  727. padding: 0 26px;
  728. color: #2199f8;
  729. background: #fff;
  730. border: 1px solid #2199f8;
  731. }
  732. .primary-btn {
  733. background: #2199f8;
  734. color: #fff;
  735. }
  736. }
  737. &.is-service-equipment {
  738. .page-subtitle {
  739. font-size: 16px;
  740. font-weight: 600;
  741. color: #005599;
  742. line-height: 22px;
  743. }
  744. .tag-item {
  745. height: auto;
  746. min-height: 36px;
  747. padding: 6px 4px;
  748. display: flex;
  749. align-items: center;
  750. justify-content: center;
  751. line-height: 16px;
  752. .text {
  753. display: block;
  754. white-space: normal;
  755. overflow: visible;
  756. text-overflow: unset;
  757. font-size: 13px;
  758. line-height: 16px;
  759. padding: 0 4px;
  760. word-break: break-all;
  761. }
  762. }
  763. }
  764. }
  765. </style>