index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. <template>
  2. <div class="price-detail">
  3. <custom-header name="报价详情"></custom-header>
  4. <div class="price-content">
  5. <div class="box-wrap">
  6. <div class="price-info">
  7. <div class="info-title">执行时间</div>
  8. <div class="info-val">
  9. <el-date-picker
  10. class="item-input"
  11. style="width: 132px"
  12. value-format="YYYY-MM-DD"
  13. v-model="executeDate"
  14. type="date"
  15. :clearable="false"
  16. placeholder="选择日期"
  17. :editable="false"
  18. />
  19. </div>
  20. </div>
  21. <div class="price-info">
  22. <div class="info-title">施肥方式</div>
  23. <div class="info-val">{{ detailData?.usageMode }}</div>
  24. </div>
  25. </div>
  26. <div class="service-wrap">
  27. <div class="medicine-box">
  28. <div class="item-title">服务费用</div>
  29. <div class="box-wrap">
  30. <div class="medicine-item">
  31. <div class="item-name">执行方式</div>
  32. <div class="item-val" v-if="detailData?.usageMode === '叶面施'">
  33. <el-select
  34. class="select-item"
  35. v-model="executionMethod"
  36. placeholder="执行方式"
  37. style="width: 132px"
  38. @change="handleExecutionMethodChange"
  39. >
  40. <el-option
  41. v-for="(item, index) in modeList"
  42. :key="index"
  43. :label="item.name"
  44. :value="item.value"
  45. />
  46. </el-select>
  47. </div>
  48. <div class="item-val" v-else>人工</div>
  49. </div>
  50. <div class="medicine-item mt-8">
  51. <div class="item-name">亩单价</div>
  52. <div class="item-val">
  53. <el-input-number
  54. style="width: 132px"
  55. placeholder="服务单价"
  56. v-model="servicePricePerMu"
  57. :min="0.0001"
  58. />
  59. <!-- <span class="price-unit">元/亩</span> -->
  60. </div>
  61. </div>
  62. <div class="medicine-item">
  63. <div class="item-name">亩数</div>
  64. <div class="item-val">{{ formatArea(detailData?.area) }}亩</div>
  65. </div>
  66. <div class="medicine-item">
  67. <div class="item-total">总计:</div>
  68. <div class="item-price">{{ getServiceTotal() }}<span class="item-unit">元</span></div>
  69. </div>
  70. </div>
  71. </div>
  72. </div>
  73. <div class="medicine-wrap">
  74. <template v-for="(prescription, pIndex) in detailData?.prescriptionList" :key="pIndex">
  75. <div class="medicine-box" v-for="(pesticide, mIndex) in prescription.pesticideFertilizerList" :key="mIndex">
  76. <div class="item-title">肥药{{ mIndex + 1 }}</div>
  77. <div class="box-wrap">
  78. <div class="medicine-item">
  79. <div class="item-name">肥药名称</div>
  80. <div class="item-val">{{ pesticide.pesticideFertilizerName }}</div>
  81. </div>
  82. <div class="medicine-item">
  83. <div class="item-name">肥药品牌</div>
  84. <div class="item-val">
  85. <el-input v-model="pesticide.brand" placeholder="肥药品牌" style="width: 132px" />
  86. <!-- <el-select
  87. class="select-item"
  88. v-model="pesticide.brand"
  89. placeholder="肥药品牌"
  90. style="width: 132px"
  91. >
  92. <el-option
  93. v-for="(brand, bIndex) in brandList"
  94. :key="bIndex"
  95. :label="brand.name"
  96. :value="brand.value"
  97. />
  98. </el-select> -->
  99. </div>
  100. </div>
  101. <div class="medicine-item mt-8">
  102. <div class="item-name">肥药单价</div>
  103. <div class="item-val">
  104. <el-input-number
  105. style="width: 132px"
  106. placeholder="单价"
  107. v-model="pesticide.price"
  108. :min="0.0000000001"
  109. />
  110. <!-- <span class="price-unit">元/{{ pesticide.unit }}</span> -->
  111. </div>
  112. </div>
  113. <div class="medicine-item">
  114. <div class="item-name">单亩用量</div>
  115. <div class="item-val">{{ getMuUsage(pesticide) }}{{ pesticide.unit }}</div>
  116. </div>
  117. <div class="medicine-item">
  118. <div class="item-name">亩数</div>
  119. <div class="item-val">{{ formatArea(detailData?.area) }}亩</div>
  120. </div>
  121. <div class="medicine-item">
  122. <div class="item-total">总计:</div>
  123. <div class="item-price">{{ getPesticideTotal(pesticide) }}<span class="item-unit">元</span></div>
  124. </div>
  125. </div>
  126. </div>
  127. </template>
  128. </div>
  129. </div>
  130. <div class="bottom-btn">
  131. <div class="bottom-l">
  132. 合计:<span class="main-val">{{ getTotalCost() }}</span>元
  133. </div>
  134. <div class="bottom-r" @click="confirmPrice">确认报价</div>
  135. </div>
  136. </div>
  137. </template>
  138. <script setup>
  139. import customHeader from "@/components/customHeader.vue";
  140. import { ElMessage } from "element-plus";
  141. import { ref, onMounted, onActivated, onBeforeUnmount, onDeactivated } from "vue";
  142. import { useRoute, useRouter } from "vue-router";
  143. import { formatArea } from "@/common/commonFun";
  144. const router = useRouter();
  145. const executeDate = ref(null);
  146. let query = useRoute().query;
  147. const detailData = ref(JSON.parse(query.data));
  148. const priceData = ref({});
  149. const servicePricePerMu = ref(null);
  150. const executionMethod = ref(null);
  151. const modeList = ref([
  152. {name: "无人机", value: 1},
  153. {name: "人工", value: 2},
  154. ])
  155. // 初始化数据
  156. onActivated(() => {
  157. query = useRoute().query;
  158. detailData.value = JSON.parse(query.data);
  159. executeDate.value = detailData.value.executeDate;
  160. if (detailData.value.usageMode === "根部施") {
  161. executionMethod.value = 2;
  162. }
  163. priceData.value = JSON.parse(query?.priceData || '{}');
  164. // 从 priceData 中匹配价格和品牌到对应的药肥
  165. if (priceData.value?.itemsList && detailData.value?.prescriptionList) {
  166. executionMethod.value = priceData.value.executionMethod;
  167. if (detailData.value.usageMode === "根部施") {
  168. executionMethod.value = 2;
  169. }
  170. detailData.value.prescriptionList.forEach(prescription => {
  171. if (prescription.pesticideFertilizerList) {
  172. prescription.pesticideFertilizerList.forEach(pesticide => {
  173. const priceItem = priceData.value.itemsList.find(
  174. item => item.pesticideFertilizerId === pesticide.pesticideFertilizerId
  175. );
  176. if (priceItem) {
  177. pesticide.price = Number(priceItem.price) || null;
  178. pesticide.brand = priceItem.brand || '';
  179. } else {
  180. pesticide.price = null;
  181. pesticide.brand = '';
  182. }
  183. });
  184. }
  185. });
  186. }
  187. // 初始化服务费用
  188. if (priceData.value?.farmWorkServiceCost && detailData.value?.area) {
  189. servicePricePerMu.value = priceData.value.farmWorkServiceCost || null;
  190. }
  191. });
  192. // 清空数据
  193. const clearData = () => {
  194. executeDate.value = null;
  195. detailData.value = {};
  196. priceData.value = {};
  197. servicePricePerMu.value = null;
  198. executionMethod.value = null;
  199. };
  200. // 离开页面时清空数据
  201. onBeforeUnmount(() => {
  202. clearData();
  203. });
  204. onDeactivated(() => {
  205. clearData();
  206. });
  207. // 根据执行方式获取单亩用量:1=无人机用muUsage2,2=人工用muUsage
  208. const getMuUsage = (pesticide) => {
  209. if (!pesticide) return 0;
  210. // 如果是叶面施且有执行方式选择,根据执行方式判断
  211. if (detailData.value?.usageMode === '叶面施' && executionMethod.value !== null && executionMethod.value !== undefined) {
  212. // 1 = 无人机,使用 muUsage2
  213. if (executionMethod.value == 1) {
  214. return pesticide.muUsage2 || pesticide.muUsage || 0;
  215. }
  216. // 2 = 人工,使用 muUsage
  217. if (executionMethod.value == 2) {
  218. return pesticide.muUsage || 0;
  219. }
  220. }
  221. // 默认使用 muUsage(非叶面施的情况)
  222. return pesticide.muUsage || 0;
  223. }
  224. // 计算单个药肥的总计:单价 * 单亩用量 * 亩数
  225. const getPesticideTotal = (pesticide) => {
  226. const muUsage = getMuUsage(pesticide);
  227. if (!pesticide.price || !muUsage || !detailData.value.area) return '0.00';
  228. const total = (pesticide.price * muUsage * detailData.value.area).toFixed(2);
  229. return total;
  230. }
  231. // 计算服务费用总计:亩单价 * 亩数
  232. const getServiceTotal = () => {
  233. if (!servicePricePerMu.value || !detailData.value.area) return '0.00';
  234. const total = (servicePricePerMu.value * detailData.value.area).toFixed(2);
  235. return total;
  236. }
  237. // 计算总合计:所有药肥总计 + 服务费用总计
  238. const getTotalCost = () => {
  239. let pesticideTotal = 0;
  240. if (detailData.value.prescriptionList) {
  241. detailData.value.prescriptionList.forEach(prescription => {
  242. if (prescription.pesticideFertilizerList) {
  243. prescription.pesticideFertilizerList.forEach(pesticide => {
  244. const muUsage = getMuUsage(pesticide);
  245. if (pesticide.price && muUsage && detailData.value.area) {
  246. pesticideTotal += pesticide.price * muUsage * detailData.value.area;
  247. }
  248. });
  249. }
  250. });
  251. }
  252. const serviceTotal = servicePricePerMu.value && detailData.value.area
  253. ? servicePricePerMu.value * detailData.value.area
  254. : 0;
  255. return (pesticideTotal + serviceTotal).toFixed(2);
  256. }
  257. const confirmPrice = () => {
  258. if (!executionMethod.value) {
  259. ElMessage.error("请选择执行方式");
  260. return;
  261. }
  262. // 检查服务费用是否有价格
  263. if (!servicePricePerMu.value || servicePricePerMu.value <= 0) {
  264. ElMessage.error("请填写服务费用");
  265. return;
  266. }
  267. // 检查是否所有药肥都有价格和品牌
  268. if (detailData.value?.prescriptionList) {
  269. const emptyItems = [];
  270. detailData.value.prescriptionList.forEach(prescription => {
  271. (prescription.pesticideFertilizerList || []).forEach(pesticide => {
  272. const pesticideName = pesticide.pesticideFertilizerName || '药肥';
  273. const hasPrice = pesticide.price && pesticide.price > 0;
  274. const hasBrand = pesticide.brand && pesticide.brand.trim() !== '';
  275. if (!hasPrice || !hasBrand) {
  276. let errorMsg = '';
  277. if (!hasPrice && !hasBrand) {
  278. errorMsg = `${pesticideName}的品牌和单价`;
  279. } else if (!hasPrice) {
  280. errorMsg = `${pesticideName}的单价`;
  281. } else if (!hasBrand) {
  282. errorMsg = `${pesticideName}的品牌`;
  283. }
  284. emptyItems.push(errorMsg);
  285. }
  286. });
  287. });
  288. if (emptyItems.length > 0) {
  289. ElMessage.error(`请填写${emptyItems[0]}`);
  290. return;
  291. }
  292. }
  293. const pesticideFertilizerQuoteList = [];
  294. if (detailData.value?.prescriptionList) {
  295. detailData.value.prescriptionList.forEach(prescription => {
  296. (prescription.pesticideFertilizerList || []).forEach(pesticide => {
  297. pesticideFertilizerQuoteList.push({
  298. pesticideFertilizerId: pesticide.pesticideFertilizerId,
  299. price: Number(pesticide.price || 0),
  300. brand: pesticide.brand || ''
  301. });
  302. })
  303. })
  304. }
  305. const payload = {
  306. farmWorkRecordId: detailData.value?.id,
  307. pesticideFertilizerQuoteList,
  308. executeDate: executeDate.value,
  309. servicePrice: servicePricePerMu.value,
  310. executionMethod: executionMethod.value,
  311. };
  312. VE_API.z_farm_work_record.acceptFarmWorkRecord(payload).then(({ data, code, msg }) => {
  313. if (code === 0) {
  314. ElMessage.success("确认报价成功");
  315. // router.push({
  316. // path: "/task_condition",
  317. // });
  318. router.back();
  319. } else {
  320. ElMessage.error(msg);
  321. }
  322. })
  323. }
  324. const handleExecutionMethodChange = (val) => {
  325. if (val == 1) {
  326. servicePricePerMu.value = priceData.value.uavServicePrice
  327. } else {
  328. servicePricePerMu.value = priceData.value.manualServicePrice
  329. }
  330. }
  331. </script>
  332. <style lang="scss" scoped>
  333. .price-detail {
  334. height: 100vh;
  335. position: relative;
  336. font-size: 14px;
  337. background: #f2f3f5;
  338. color: #000;
  339. .price-content {
  340. padding: 12px;
  341. box-sizing: border-box;
  342. height: calc(100% - 40px - 74px);
  343. overflow: auto;
  344. ::v-deep {
  345. .el-input__wrapper, .el-select__wrapper {
  346. box-shadow: 0 0 0 1px rgba(33, 153, 248, 0.3) inset;
  347. }
  348. .el-input__inner {
  349. color: #2199f8;
  350. text-align: center;
  351. }
  352. .el-select__placeholder {
  353. color: #2199f8;
  354. &.is-transparent {
  355. color: rgba(33, 153, 248, 0.6);
  356. }
  357. text-align: center;
  358. }
  359. .el-input__prefix {
  360. color: #2199f8;
  361. }
  362. .el-select__caret {
  363. color: #2199f8;
  364. }
  365. .el-input {
  366. --el-input-placeholder-color: rgba(33, 153, 248, 0.6);
  367. }
  368. .el-input-number__decrease, .el-input-number__increase {
  369. display: none;
  370. }
  371. .el-input-number .el-input__wrapper {
  372. padding-left: 12px;
  373. padding-right: 12px;
  374. }
  375. }
  376. }
  377. .box-wrap {
  378. background: #fff;
  379. padding: 10px;
  380. border-radius: 8px;
  381. }
  382. .price-info {
  383. height: 32px;
  384. display: flex;
  385. align-items: center;
  386. justify-content: space-between;
  387. .info-val {
  388. color: #474747;
  389. width: 132px;
  390. text-align: center;
  391. }
  392. }
  393. .price-info + .price-info {
  394. padding-top: 6px;
  395. }
  396. .medicine-box {
  397. padding-top: 10px;
  398. .item-title {
  399. padding-left: 5px;
  400. font-size: 16px;
  401. color: #000000;
  402. font-weight: 500;
  403. padding-bottom: 10px;
  404. }
  405. .medicine-item {
  406. display: flex;
  407. align-items: center;
  408. justify-content: space-between;
  409. height: 32px;
  410. .item-name {
  411. color: rgba(0, 0, 0, 0.4);
  412. }
  413. .item-val {
  414. min-width: 142px;
  415. text-align: center;
  416. color: #474747;
  417. display: flex;
  418. align-items: center;
  419. justify-content: center;
  420. gap: 4px;
  421. .price-unit {
  422. font-size: 12px;
  423. color: rgba(0, 0, 0, 0.4);
  424. white-space: nowrap;
  425. }
  426. }
  427. .item-total {
  428. font-size: 16px;
  429. color: #000;
  430. }
  431. .item-price {
  432. color: #2199F8;
  433. font-size: 20px;
  434. font-weight: bold;
  435. .item-unit {
  436. font-size: 14px;
  437. font-weight: normal;
  438. padding-left: 2px;
  439. color: #474747;
  440. }
  441. }
  442. }
  443. .medicine-item + .medicine-item {
  444. padding-top: 2px;
  445. }
  446. .mt-8 {
  447. margin-top: 8px;
  448. }
  449. }
  450. .bottom-btn {
  451. height: 74px;
  452. display: flex;
  453. align-items: center;
  454. justify-content: space-between;
  455. background: #fff;
  456. padding: 12px;
  457. box-sizing: border-box;
  458. box-shadow: 0 -1px 11px rgba(0, 0, 0, 0.1);
  459. .bottom-l {
  460. color: #000000;
  461. font-size: 16px;
  462. .main-val {
  463. color: #2199F8;
  464. font-size: 24px;
  465. font-weight: bold;
  466. padding-right: 4px;
  467. }
  468. }
  469. .bottom-r {
  470. background: linear-gradient(136deg, #9FD5FF, #2199F8);
  471. padding: 8px 32px;
  472. border-radius: 20px;
  473. color: #fff;
  474. }
  475. }
  476. }
  477. </style>