activity-detail.vue 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798
  1. <template>
  2. <view class="page-container">
  3. <!-- 页面滚动区域 -->
  4. <scroll-view class="page-scroll" scroll-y>
  5. <!-- 地块信息卡片 -->
  6. <view class="info-card">
  7. <view class="card-title">
  8. <text>地块信息</text>
  9. </view>
  10. <view class="info-item">
  11. <text class="info-label">地块名称</text>
  12. <text class="info-value">{{ formData.fieldName || '未知' }}</text>
  13. </view>
  14. <view class="info-item">
  15. <text class="info-label">作物名称</text>
  16. <text class="info-value">{{ formData.growCrops || '未知' }}</text>
  17. </view>
  18. <view class="info-item">
  19. <text class="info-label">负责人</text>
  20. <text class="info-value">{{ formData.manager || '未知' }}</text>
  21. </view>
  22. </view>
  23. <!-- 任务填写表单 -->
  24. <view class="form-card">
  25. <view class="card-title">
  26. <text>任务信息</text>
  27. </view>
  28. <!-- 任务名称 -->
  29. <view class="form-item">
  30. <view class="form-label required">任务名称</view>
  31. <input
  32. v-model="formData.taskName"
  33. placeholder="请输入任务名称,例如:水稻田施肥"
  34. :disabled="pageMode === 'view'"
  35. class="form-input"
  36. />
  37. </view>
  38. <!-- 任务类型 -->
  39. <view class="form-item" @click="pageMode !== 'view' && showTaskTypeSelector()">
  40. <view class="form-label required">任务类型</view>
  41. <view class="select-wrapper">
  42. <!-- <input
  43. :value="formData.typeName"
  44. :placeholder="dictLoading ? '加载中...' : '请选择任务类型'"
  45. readonly
  46. class="form-input select-input"
  47. /> -->
  48. <view class="form-input select-input">
  49. {{ formData.typeName || (dictLoading ? '加载中...' : '请选择任务类型') }}
  50. </view>
  51. <view v-if="pageMode !== 'view'" class="select-arrow">
  52. <text>▼</text>
  53. </view>
  54. </view>
  55. </view>
  56. <!-- 执行时间 -->
  57. <view class="form-item" @click="pageMode !== 'view' && selectExecuteTime()">
  58. <view class="form-label required">执行时间</view>
  59. <view class="select-wrapper">
  60. <!-- <input
  61. :value="formattedExecuteTime"
  62. placeholder="请选择任务计划时间"
  63. readonly
  64. class="form-input select-input"
  65. readonly
  66. /> -->
  67. <view class="form-input select-input">
  68. {{ formattedExecuteTime || '请选择任务计划时间' }}
  69. </view>
  70. <view v-if="pageMode !== 'view'" class="select-arrow">
  71. <text>选择</text>
  72. </view>
  73. </view>
  74. </view>
  75. <!-- 负责人选择 -->
  76. <view class="form-item" @click="pageMode !== 'view' && showUserSelector()">
  77. <view class="form-label required">负责人</view>
  78. <view class="select-wrapper">
  79. <!-- <input
  80. :value="formData.assigneeName"
  81. placeholder="请选择负责人"
  82. readonly
  83. class="form-input select-input"
  84. /> -->
  85. <view class="form-input select-input">
  86. {{ formData.assigneeName || '请选择负责人' }}
  87. </view>
  88. <view v-if="pageMode !== 'view'" class="select-arrow">
  89. <text>选择</text>
  90. </view>
  91. </view>
  92. </view>
  93. <!-- 任务说明 -->
  94. <view class="form-item">
  95. <view class="form-label">任务说明</view>
  96. <textarea
  97. v-model="formData.remark"
  98. placeholder="请输入任务要点,例如:每亩用肥20kg"
  99. :disabled="pageMode === 'view'"
  100. class="form-textarea"
  101. maxlength="200"
  102. ></textarea>
  103. <view class="char-count">{{ (formData.remark || '').length }}/200</view>
  104. </view>
  105. <!-- 任务完成情况 -->
  106. <view class="section-divider"></view>
  107. <view class="section-title">
  108. <text>任务完成情况</text>
  109. </view>
  110. <!-- 完成状态选择 - 新建和编辑模式 -->
  111. <view class="form-item" v-if="pageMode === 'create' || pageMode === 'edit'">
  112. <view class="form-label">完成状态</view>
  113. <view class="radio-group">
  114. <view
  115. class="radio-item"
  116. v-for="(item, index) in completionStatusOptions"
  117. :key="index"
  118. @click="selectCompletionStatus(item.value)"
  119. >
  120. <view class="radio-circle" :class="{'radio-checked': formData.completionStatus === item.value}">
  121. <view v-if="formData.completionStatus === item.value" class="radio-dot"></view>
  122. </view>
  123. <text class="radio-label">{{ item.label }}</text>
  124. </view>
  125. </view>
  126. </view>
  127. <!-- 查看模式显示完成状态 -->
  128. <view class="form-item" v-if="pageMode === 'view'">
  129. <view class="form-label">完成状态</view>
  130. <view class="status-completed">
  131. <text class="status-icon">✓</text>
  132. <text>已完成</text>
  133. </view>
  134. </view>
  135. <!-- 完成时间 -->
  136. <view class="form-item" v-if="formData.completionStatus === '1'">
  137. <view class="form-label" :class="{'required': (pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1'}">完成时间</view>
  138. <view class="select-wrapper" @click="(pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1' && selectCompletionTime()">
  139. <!-- <input
  140. :value="formattedCompletionTime"
  141. placeholder="请选择实际完成时间"
  142. readonly
  143. class="form-input select-input"
  144. /> -->
  145. <view class="form-input select-input">
  146. {{ formattedCompletionTime || '请选择实际完成时间' }}
  147. </view>
  148. <view v-if="(pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1'" class="select-arrow">
  149. <text>选择</text>
  150. </view>
  151. </view>
  152. </view>
  153. <!-- 完成说明 -->
  154. <view class="form-item" v-if="formData.completionStatus === '1'">
  155. <view class="form-label" :class="{'required': (pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1'}">完成说明</view>
  156. <textarea
  157. v-model="formData.completionDesc"
  158. placeholder="请输入完成说明,例如:已完成并拍照记录"
  159. :disabled="pageMode === 'view'"
  160. class="form-textarea"
  161. maxlength="300"
  162. ></textarea>
  163. <view class="char-count">{{ (formData.completionDesc || '').length }}/300</view>
  164. <view class="form-error" v-if="formErrors.completionDesc">
  165. {{ formErrors.completionDesc }}
  166. </view>
  167. </view>
  168. <!-- 现场图片 -->
  169. <view class="form-item" v-if="formData.completionStatus === '1'">
  170. <view class="form-label">现场图片</view>
  171. <!-- 新建和编辑模式 -->
  172. <view v-if="pageMode === 'create' || pageMode === 'edit'" class="image-upload">
  173. <view class="image-list">
  174. <view
  175. class="image-preview"
  176. v-for="(item, index) in formData.images"
  177. :key="index"
  178. @click="previewImage(item, index)"
  179. >
  180. <!-- <image :src="getImageUrl(item)" mode="aspectFill"/> -->
  181. <image :src="item.url" mode="aspectFill"/>
  182. <view class="delete-btn" @click.stop="deletePic(index)">
  183. <text>×</text>
  184. </view>
  185. </view>
  186. <view
  187. v-if="formData.images.length < 6"
  188. class="upload-btn"
  189. @click="chooseImage"
  190. >
  191. <text class="upload-icon">+</text>
  192. <text class="upload-text">添加图片</text>
  193. </view>
  194. </view>
  195. <view class="upload-tip">最多可上传6张图片</view>
  196. </view>
  197. <!-- 查看模式 -->
  198. <view v-else-if="pageMode === 'view' && formData.images && formData.images.length > 0" class="image-view">
  199. <view class="image-list">
  200. <view
  201. class="image-preview"
  202. v-for="(item, index) in formData.images"
  203. :key="index"
  204. @click="previewImage(item, index)"
  205. >
  206. <!-- <image :src="getImageUrl(item)" mode="aspectFill"/> -->
  207. <image :src="item.url" mode="aspectFill"/>
  208. </view>
  209. </view>
  210. </view>
  211. <!-- 无图片提示 -->
  212. <view v-else class="no-images">
  213. <text>暂无图片</text>
  214. </view>
  215. </view>
  216. </view>
  217. <!-- 底部占位 -->
  218. <view class="bottom-safe"></view>
  219. </scroll-view>
  220. <!-- 底部提交按钮 -->
  221. <view class="footer-safe" v-if="pageMode !== 'view'">
  222. <view class="footer-content">
  223. <button
  224. class="submit-button"
  225. :class="{'loading': isSubmitting}"
  226. @click="submitForm"
  227. :disabled="isSubmitting"
  228. >
  229. {{ isSubmitting ? '提交中...' : submitButtonText }}
  230. </button>
  231. </view>
  232. </view>
  233. <!-- 遮罩层 -->
  234. <view v-if="showTaskTypePicker || showDateTimeSelector || showUserPicker" class="picker-mask" @click="closePickers"></view>
  235. <!-- 任务类型选择弹窗 -->
  236. <view v-if="showTaskTypePicker" class="picker-popup">
  237. <view class="picker-header">
  238. <text class="picker-cancel" @click="showTaskTypePicker = false">取消</text>
  239. <text class="picker-title">选择任务类型</text>
  240. <text class="picker-confirm" @click="confirmTaskType">确定</text>
  241. </view>
  242. <view class="picker-content">
  243. <view v-if="dictLoading" class="picker-loading">
  244. <text>加载中...</text>
  245. </view>
  246. <view
  247. v-else
  248. class="picker-item"
  249. v-for="(item, index) in taskTypeOptions"
  250. :key="index"
  251. :class="{'selected': tempTaskTypeIndex === index}"
  252. @click="tempTaskTypeIndex = index"
  253. >
  254. <text>{{ item.dictLabel }}</text>
  255. <text v-if="tempTaskTypeIndex === index" class="check-mark">✓</text>
  256. </view>
  257. </view>
  258. </view>
  259. <!-- 用户选择弹窗 -->
  260. <view v-if="showUserPicker" class="picker-popup">
  261. <view class="picker-header">
  262. <text class="picker-cancel" @click="showUserPicker = false">取消</text>
  263. <text class="picker-title">选择负责人</text>
  264. <text class="picker-confirm" @click="confirmUser">确定</text>
  265. </view>
  266. <view class="picker-content">
  267. <view v-if="usersLoading" class="picker-loading">
  268. <text>加载中...</text>
  269. </view>
  270. <view v-else-if="userList.length === 0" class="picker-empty">
  271. <text>暂无可选负责人</text>
  272. </view>
  273. <view
  274. v-else
  275. class="picker-item"
  276. v-for="(user, index) in userList"
  277. :key="user.userId"
  278. :class="{'selected': tempUserIndex === index}"
  279. @click="tempUserIndex = index"
  280. >
  281. <text>{{ user.userName }}</text>
  282. <text v-if="tempUserIndex === index" class="check-mark">✓</text>
  283. </view>
  284. </view>
  285. </view>
  286. <!-- 日期时间选择弹窗 -->
  287. <view v-if="showDateTimeSelector" class="picker-popup datetime-picker">
  288. <view class="picker-header">
  289. <text class="picker-cancel" @click="cancelDateTime">取消</text>
  290. <text class="picker-title">选择时间</text>
  291. <text class="picker-confirm" @click="confirmDateTime">确定</text>
  292. </view>
  293. <view class="datetime-picker-content">
  294. <picker-view
  295. class="datetime-picker-view"
  296. :value="dateTimePickerValue"
  297. @change="onDateTimePickerChange"
  298. :indicator-style="'height: 80rpx;'"
  299. :mask-style="'background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6)), linear-gradient(0deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6));'"
  300. >
  301. <picker-view-column>
  302. <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[0]" :key="'year-'+index">
  303. <text class="picker-item-text">{{ item }}</text>
  304. </view>
  305. </picker-view-column>
  306. <picker-view-column>
  307. <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[1]" :key="'month-'+index">
  308. <text class="picker-item-text">{{ item }}</text>
  309. </view>
  310. </picker-view-column>
  311. <picker-view-column>
  312. <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[2]" :key="'day-'+index">
  313. <text class="picker-item-text">{{ item }}</text>
  314. </view>
  315. </picker-view-column>
  316. <picker-view-column>
  317. <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[3]" :key="'hour-'+index">
  318. <text class="picker-item-text">{{ item }}</text>
  319. </view>
  320. </picker-view-column>
  321. <picker-view-column>
  322. <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[4]" :key="'min-'+index">
  323. <text class="picker-item-text">{{ item }}</text>
  324. </view>
  325. </picker-view-column>
  326. </picker-view>
  327. </view>
  328. </view>
  329. </view>
  330. </template>
  331. <script>
  332. import api from "@/config/api.js";
  333. import { getAgriculturalTasksById, addAgriculturalTask, updateAgriculturalTask } from '@/api/services/activity.js';
  334. import { getUsersByPlotId, getUserInfo } from '@/api/services/user.js';
  335. import dictMixin from '@/utils/mixins/dictMixin';
  336. import storage from "@/utils/storage.js";
  337. export default {
  338. mixins: [dictMixin],
  339. data() {
  340. return {
  341. // 需要加载的字典类型
  342. dictTypeList: ['task_type','task_status'],
  343. // 页面模式:create-新建, edit-编辑, view-查看
  344. pageMode: 'create',
  345. // 选择器显示状态
  346. showTaskTypePicker: false,
  347. showDateTimeSelector: false,
  348. showUserPicker: false, // 新增用户选择器显示状态
  349. // 临时选择的任务类型索引
  350. tempTaskTypeIndex: 0,
  351. tempUserIndex: 0, // 新增临时用户选择索引
  352. // 时间选择器相关
  353. currentTimeType: '', // 当前选择的时间类型
  354. dateTimePickerValue: [0, 0, 0, 0, 0], // 年月日时分的选择值
  355. dateTimePickerRange: [], // 选择器的范围数据
  356. // 表单数据
  357. formData: {
  358. // 任务ID,新建任务时为空
  359. id: '',
  360. // 地块基础信息
  361. plotId: '',
  362. fieldName: '',
  363. growCrops: '',
  364. manager: '',
  365. // 任务信息
  366. taskName: '',
  367. typeName: '', // 对应后端的typeName字段
  368. typeNameId: '', // 存储任务类型的实际值(dictValue)
  369. executeTime: new Date(),
  370. remark: '', // 对应后端的remark字段
  371. // 完成情况
  372. taskStatus: '0',
  373. completionStatus: '0',
  374. completionTime: new Date(),
  375. completionDesc: '',
  376. images: [],
  377. taskImages: '', // 对应后端的taskImages字段,存储图片URL字符串
  378. // 负责人信息
  379. assigneeId: '',
  380. assigneeName: '',
  381. // 创建人信息
  382. create_by: ''
  383. },
  384. // 完成状态选项
  385. completionStatusOptions: [
  386. { label: '待完成', value: '0' },
  387. { label: '已完成', value: '1' }
  388. ],
  389. // 是否正在提交
  390. isSubmitting: false,
  391. // 表单验证错误
  392. formErrors: {
  393. completionDesc: ''
  394. },
  395. // 用户列表
  396. userList: [],
  397. usersLoading: false,
  398. // 任务类型列表
  399. taskStatusList:[]
  400. }
  401. },
  402. created() {
  403. // 组件创建时,如果有定义dictTypeList,则自动加载字典数据
  404. if (this.dictTypeList && this.dictTypeList.length > 0) {
  405. this.loadDict().then(() => {
  406. // 字典加载完成后,如果没有设置任务类型,则默认选择第一个
  407. if (!this.formData.typeName && this.dictData.task_type && this.dictData.task_type.length > 0) {
  408. const firstType = this.dictData.task_type[0];
  409. this.taskStatusList = this.dictData.task_type;
  410. console.log("this.dictData",this.dictData.task_status[0].dictLabel);
  411. this.formData.typeName = firstType.dictLabel;
  412. this.formData.typeNameId = firstType.dictValue; // 设置默认的typeNameId
  413. }
  414. });
  415. }
  416. },
  417. computed: {
  418. // 页面标题
  419. pageTitle() {
  420. switch (this.pageMode) {
  421. case 'create':
  422. return '新建农事任务';
  423. case 'edit':
  424. return '编辑农事任务';
  425. default:
  426. return '农事任务详情';
  427. }
  428. },
  429. // 提交按钮文字
  430. submitButtonText() {
  431. switch (this.pageMode) {
  432. case 'create':
  433. return '创建任务';
  434. case 'edit':
  435. return '保存修改';
  436. default:
  437. return '';
  438. }
  439. },
  440. // 格式化后的执行时间
  441. formattedExecuteTime() {
  442. return this.formatDateTime(this.formData.executeTime);
  443. },
  444. // 格式化后的完成时间
  445. formattedCompletionTime() {
  446. return this.formatDateTime(this.formData.completionTime);
  447. },
  448. // 任务类型选项
  449. taskTypeOptions() {
  450. console.log("this.dictData:",this.dictData.task_type);
  451. return this.dictData.task_type;
  452. },
  453. // 当前任务类型索引
  454. taskTypeIndex() {
  455. console.log("taskTypeOptions",this.taskTypeOptions);
  456. const index = this.taskTypeOptions.findIndex(item => item.dictValue === this.formData.typeNameId);
  457. console.log("查找:",index);
  458. return index >= 0 ? index : 0;
  459. }
  460. },
  461. onLoad(options) {
  462. console.log('页面加载,接收参数:', options);
  463. // 设置页面模式
  464. if (options.mode) {
  465. this.pageMode = options.mode;
  466. }
  467. // 设置地块基础信息
  468. this.formData.fieldName = decodeURIComponent(options.fieldName === 'undefined' ? '未选择地块' : options.fieldName);
  469. this.formData.growCrops = decodeURIComponent(options.growCrops === 'undefined' ? '未选择地块' : options.growCrops);
  470. this.formData.manager = decodeURIComponent(options.manager === 'undefined' ? '未选择地块' : options.manager);
  471. this.formData.plotId = parseInt(options.plotId || '1');
  472. this.formData.farmId = parseInt(options.farmId || '1');
  473. // 加载用户列表
  474. this.loadUserList(this.formData.farmId);
  475. // 如果是编辑或查看模式,获取任务详情
  476. if (options.id && options.id !== 'new') {
  477. this.formData.id = options.id;
  478. this.loadTaskDetail(options.id);
  479. } else if (options.id === 'new') {
  480. // 加载当前登录用户
  481. this.loadAssigneeInfo()
  482. // 创建新任务时,加载字典数据并设置默认任务类型
  483. // this.loadDict().then(() => {
  484. // // 确保字典数据加载完成后设置默认任务类型
  485. // if (this.dictData.task_type && this.dictData.task_type.length > 0) {
  486. // const firstType = this.dictData.task_type[0];
  487. // this.formData.typeName = firstType.dictLabel;
  488. // this.formData.typeName = firstType.dictLabel;
  489. // console.log('已设置默认任务类型:', this.formData.typeName);
  490. // }
  491. // });
  492. }
  493. // 设置导航栏标题
  494. uni.setNavigationBarTitle({
  495. title: this.pageTitle
  496. });
  497. // 初始化时间选择器数据
  498. this.initDateTimeRange();
  499. },
  500. methods: {
  501. // 跨端安全解析日期
  502. normalizeToDate(input) {
  503. if (!input) return new Date();
  504. let dateObj = input;
  505. if (typeof input === 'string') {
  506. // 兼容 iOS:将 2025-08-12 12:30:00 转为 2025/08/12 12:30:00
  507. dateObj = input.replace(/-/g, '/');
  508. }
  509. if (typeof input === 'number') {
  510. // 秒级时间戳转毫秒
  511. if (input.toString().length === 10) {
  512. dateObj = input * 1000;
  513. }
  514. }
  515. const d = new Date(dateObj);
  516. return isNaN(d.getTime()) ? new Date() : d;
  517. },
  518. // 加载任务详情
  519. loadTaskDetail(taskId) {
  520. uni.showLoading({
  521. title: '加载中...',
  522. mask: true
  523. });
  524. getAgriculturalTasksById(taskId).then(res => {
  525. if (res.data.code === 200) {
  526. const taskDetail = res.data.data;
  527. // 设置表单数据
  528. this.formData = {
  529. // ...this.formData, // 保留原有的地块信息
  530. ...taskDetail, // 合并后端返回的任务信息
  531. manager: this.formData.manager,
  532. completionStatus: taskDetail.taskStatus, // 将后端的taskStatus映射为前端的completionStatus
  533. };
  534. // 转换任务类型值
  535. // 找到对应的字典项
  536. if (this.taskStatusList && this.taskStatusList.length > 0) {
  537. const typeDict = this.taskStatusList.find(item => item.dictValue === taskDetail.typeName.toString());
  538. if (typeDict) {
  539. this.formData.typeName = typeDict.dictLabel;
  540. this.formData.typeNameId = typeDict.dictValue;
  541. }
  542. }
  543. // 处理图片数据
  544. if (this.formData.taskImages) {
  545. try {
  546. // 尝试解析图片数据字符串
  547. const imageUrls = this.formData.taskImages.split(',');
  548. this.formData.images = imageUrls.filter(url => url && url.trim()).map(url => ({
  549. url: url.trim(), // 保存原始URL,显示时会通过getImageUrl方法处理
  550. status: 'success'
  551. }));
  552. console.log('解析后的图片数据:', this.formData.images);
  553. } catch (e) {
  554. console.error('解析图片数据失败:', e);
  555. this.formData.images = [];
  556. }
  557. } else {
  558. this.formData.images = [];
  559. }
  560. console.log("this.formData.assigneeId",this.formData.assigneeId);
  561. console.log("!this.formData.assigneeName",this.formData.assigneeName);
  562. // 如果有负责人ID但没有负责人名称,获取负责人信息
  563. if (this.formData.assigneeId && !this.formData.assigneeName) {
  564. this.loadAssigneeInfo(this.formData.assigneeId);
  565. }
  566. uni.hideLoading();
  567. } else {
  568. uni.hideLoading();
  569. uni.showToast({
  570. title: res.data.msg || '获取任务详情失败',
  571. icon: 'none'
  572. });
  573. // 失败后返回上一页
  574. setTimeout(() => {
  575. uni.navigateBack();
  576. }, 1500);
  577. }
  578. }).catch(err => {
  579. console.error('获取任务详情失败:', err);
  580. uni.hideLoading();
  581. uni.showToast({
  582. title: '获取任务详情失败',
  583. icon: 'none'
  584. });
  585. // 失败后返回上一页
  586. setTimeout(() => {
  587. uni.navigateBack();
  588. }, 1500);
  589. });
  590. },
  591. // 加载负责人信息
  592. loadAssigneeInfo(userId) {
  593. getUserInfo(userId).then(res => {
  594. console.log("你是谁:",res);
  595. if (res.data.code === 200) {
  596. const userData = res.data.data;
  597. this.formData.assigneeName = userData.userName || '未知用户';
  598. this.formData.assigneeId = userData.userId;
  599. }
  600. }).catch(err => {
  601. console.error('获取负责人信息失败:', err);
  602. this.formData.assigneeName = '未知用户';
  603. });
  604. },
  605. // 加载用户列表
  606. loadUserList(farmId) {
  607. this.usersLoading = true;
  608. const params = {
  609. pageNum: 1,
  610. pageSize: 10,
  611. deptId: farmId
  612. }
  613. getUsersByPlotId(params).then(res => {
  614. console.log("加载用户:",res);
  615. if (res.data.code === 200) {
  616. this.userList = res.data.rows || [];
  617. // 设置默认选中第一个用户
  618. if (this.userList.length > 0 && !this.formData.assigneeId) {
  619. this.formData.assigneeId = this.userList[0].userId;
  620. this.formData.assigneeName = this.userList[0].userName;
  621. }
  622. } else {
  623. console.error('获取用户列表失败:', res.data.msg);
  624. uni.showToast({
  625. title: '获取用户列表失败',
  626. icon: 'none'
  627. });
  628. }
  629. }).catch(err => {
  630. console.error('获取用户列表失败:', err);
  631. uni.showToast({
  632. title: '获取用户列表失败',
  633. icon: 'none'
  634. });
  635. }).finally(() => {
  636. this.usersLoading = false;
  637. });
  638. },
  639. // 初始化时间选择器范围数据
  640. initDateTimeRange() {
  641. // 年份:从2020年开始显示10年
  642. const startYear = 2020;
  643. const years = [];
  644. for (let i = 0; i < 10; i++) {
  645. years.push((startYear + i) + '年');
  646. }
  647. // 月份:1-12月
  648. const months = [];
  649. for (let i = 1; i <= 12; i++) {
  650. months.push(i + '月');
  651. }
  652. // 日期:1-31日
  653. const days = [];
  654. for (let i = 1; i <= 31; i++) {
  655. days.push(i + '日');
  656. }
  657. // 小时:0-23时
  658. const hours = [];
  659. for (let i = 0; i <= 23; i++) {
  660. hours.push(i.toString().padStart(2, '0') + '时');
  661. }
  662. // 分钟:0-59分
  663. const minutes = [];
  664. for (let i = 0; i <= 59; i++) {
  665. minutes.push(i.toString().padStart(2, '0') + '分');
  666. }
  667. this.dateTimePickerRange = [years, months, days, hours, minutes];
  668. console.log('日期选择器范围:', {
  669. years: years.length,
  670. months: months.length,
  671. days: days.length,
  672. hours: hours.length,
  673. minutes: minutes.length
  674. });
  675. },
  676. // 初始化日期时间选择器
  677. initDateTimePicker(timestamp) {
  678. const date = this.normalizeToDate(timestamp);
  679. const currentYear = date.getFullYear();
  680. const currentMonth = date.getMonth() + 1; // 1-12
  681. const currentDate = date.getDate(); // 1-31
  682. const currentHour = date.getHours();
  683. const currentMinute = date.getMinutes();
  684. // 基准年份,用于计算索引
  685. const startYear = 2020;
  686. // 如果当前年份小于起始年份或大于结束年份,调整为合法范围内
  687. let yearIndex = currentYear - startYear;
  688. if (yearIndex < 0) yearIndex = 0;
  689. if (yearIndex >= 10) yearIndex = 9;
  690. console.log('初始化日期选择器:', {
  691. timestamp,
  692. year: currentYear,
  693. month: currentMonth,
  694. date: currentDate,
  695. hour: currentHour,
  696. minute: currentMinute,
  697. yearIndex
  698. });
  699. // 设置选择器的初始值
  700. this.dateTimePickerValue = [
  701. yearIndex, // 年份索引
  702. currentMonth - 1, // 月份索引(0-11)
  703. currentDate - 1, // 日期索引(0-30)
  704. currentHour, // 小时索引
  705. currentMinute // 分钟索引
  706. ];
  707. console.log('选择器初始值:', this.dateTimePickerValue);
  708. // 更新日期范围
  709. this.updateDaysRange();
  710. },
  711. // 更新日期范围(根据选择的年月)
  712. updateDaysRange() {
  713. const yearIndex = this.dateTimePickerValue[0];
  714. const monthIndex = this.dateTimePickerValue[1];
  715. // 计算实际年月
  716. const year = 2020 + yearIndex;
  717. const month = monthIndex + 1; // 索引为0-11,实际月份为1-12
  718. // 获取该月的天数
  719. const daysInMonth = new Date(year, month, 0).getDate();
  720. // 更新天数范围
  721. const days = [];
  722. for (let i = 1; i <= daysInMonth; i++) {
  723. days.push(i + '日');
  724. }
  725. // 更新日期列
  726. this.dateTimePickerRange[2] = days;
  727. // 如果当前选择的日期超过了该月的天数,调整为该月最后一天
  728. if (this.dateTimePickerValue[2] >= daysInMonth) {
  729. this.dateTimePickerValue[2] = daysInMonth - 1;
  730. }
  731. console.log('更新日期范围:', {
  732. year,
  733. month,
  734. daysInMonth,
  735. daysLength: days.length
  736. });
  737. },
  738. // 显示任务类型选择器
  739. showTaskTypeSelector() {
  740. this.tempTaskTypeIndex = this.taskTypeIndex;
  741. this.showTaskTypePicker = true;
  742. },
  743. // 确认任务类型选择
  744. confirmTaskType() {
  745. const selectedType = this.taskTypeOptions[this.tempTaskTypeIndex];
  746. console.log("选中:",selectedType);
  747. this.formData.typeName = selectedType.dictLabel;
  748. this.formData.typeNameId = selectedType.dictValue; // 设置typeNameId为dictValue值
  749. this.showTaskTypePicker = false;
  750. },
  751. // 选择执行时间
  752. selectExecuteTime() {
  753. this.currentTimeType = 'executeTime';
  754. this.initDateTimePicker(this.formData.executeTime);
  755. this.showDateTimeSelector = true;
  756. },
  757. // 选择完成时间
  758. selectCompletionTime() {
  759. this.currentTimeType = 'completionTime';
  760. this.initDateTimePicker(this.formData.completionTime);
  761. this.showDateTimeSelector = true;
  762. },
  763. // 日期时间选择器变更
  764. onDateTimePickerChange(e) {
  765. this.dateTimePickerValue = e.detail.value;
  766. // 当年月变更时,更新日期范围
  767. this.updateDaysRange();
  768. },
  769. // 确认时间选择
  770. confirmDateTime() {
  771. const values = this.dateTimePickerValue;
  772. // 计算实际日期时间
  773. const year = 2020 + values[0];
  774. const month = values[1] + 1; // 索引为0-11,实际月份为1-12
  775. const date = values[2] + 1; // 索引为0-30,实际日期为1-31
  776. const hour = values[3];
  777. const minute = values[4];
  778. console.log('确认日期时间:', {
  779. selectedValues: values,
  780. convertedDate: `${year}-${month}-${date} ${hour}:${minute}`
  781. });
  782. // 创建日期对象
  783. const selectedTime = new Date(year, month - 1, date, hour, minute).getTime();
  784. // 更新相应的表单字段
  785. if (this.currentTimeType === 'executeTime') {
  786. this.formData.executeTime = selectedTime;
  787. } else if (this.currentTimeType === 'completionTime') {
  788. this.formData.completionTime = selectedTime;
  789. }
  790. // 关闭选择器
  791. this.showDateTimeSelector = false;
  792. uni.showToast({
  793. title: '时间已设置',
  794. icon: 'success',
  795. duration: 1500
  796. });
  797. },
  798. // 取消时间选择
  799. cancelDateTime() {
  800. this.showDateTimeSelector = false;
  801. },
  802. // 显示用户选择器
  803. showUserSelector() {
  804. this.tempUserIndex = this.userList.findIndex(user => user.userId === this.formData.assigneeId);
  805. this.showUserPicker = true;
  806. },
  807. // 确认用户选择
  808. confirmUser() {
  809. const selectedUser = this.userList[this.tempUserIndex];
  810. this.formData.assigneeId = selectedUser.userId;
  811. this.formData.assigneeName = selectedUser.userName;
  812. this.showUserPicker = false;
  813. },
  814. // 关闭所有选择器
  815. closePickers() {
  816. this.showTaskTypePicker = false;
  817. this.showDateTimeSelector = false;
  818. this.showUserPicker = false;
  819. },
  820. // 选择完成状态
  821. selectCompletionStatus(value) {
  822. this.formData.completionStatus = value;
  823. this.formData.taskStatus = value; // 同时设置taskStatus字段
  824. if (value === '1') {
  825. this.formData.completionTime = new Date();
  826. }
  827. },
  828. // 格式化日期时间
  829. /**
  830. * 日期格式化工具(兼容 iOS 和安卓)
  831. * @param {string|number|Date} date - 日期对象 / 时间戳 / 日期字符串
  832. * @param {string} format - 格式模板,默认 'yyyy-MM-dd HH:mm:ss'
  833. * @returns {string} 格式化后的日期
  834. */
  835. formatDateTime(date, format = 'yyyy-MM-dd HH:mm') {
  836. if (!date) return '';
  837. // 如果是字符串,做 iOS 兼容(将 2025-08-12 替换为 2025/08/12)
  838. if (typeof date === 'string') {
  839. date = date.replace(/-/g, '/');
  840. }
  841. // 如果是数字(时间戳),判断是否是秒级
  842. if (typeof date === 'number') {
  843. if (date.toString().length === 10) {
  844. date *= 1000; // 秒转毫秒
  845. }
  846. }
  847. // 转换为 Date 对象
  848. date = new Date(date);
  849. if (isNaN(date.getTime())) return '';
  850. const map = {
  851. 'yyyy': date.getFullYear(),
  852. 'MM': String(date.getMonth() + 1).padStart(2, '0'),
  853. 'dd': String(date.getDate()).padStart(2, '0'),
  854. 'HH': String(date.getHours()).padStart(2, '0'),
  855. 'mm': String(date.getMinutes()).padStart(2, '0'),
  856. // 'ss': String(date.getSeconds()).padStart(2, '0')
  857. };
  858. return format.replace(/yyyy|MM|dd|HH|mm/g, match => map[match]);
  859. },
  860. // formatDateTime(timestamp) {
  861. // if (!timestamp) return '';
  862. // const date = parseDate(timestamp);
  863. // if (isNaN(date.getTime())) return '';
  864. // const year = date.getFullYear();
  865. // const month = String(date.getMonth() + 1).padStart(2, '0');
  866. // const day = String(date.getDate()).padStart(2, '0');
  867. // const hour = String(date.getHours()).padStart(2, '0');
  868. // const minute = String(date.getMinutes()).padStart(2, '0');
  869. // return `${year}-${month}-${day} ${hour}:${minute}`;
  870. // },
  871. // 选择图片
  872. chooseImage() {
  873. uni.chooseImage({
  874. count: 6 - this.formData.images.length,
  875. sizeType: ['original', 'compressed'],
  876. sourceType: ['album', 'camera'],
  877. success: (res) => {
  878. console.log('选择图片成功:', res);
  879. // 验证文件类型和大小
  880. const validFiles = [];
  881. const invalidFiles = [];
  882. const maxSize = 5 * 1024 * 1024; // 5MB 最大限制
  883. // 检查每个文件
  884. res.tempFiles.forEach((file, index) => {
  885. // 检查文件类型
  886. const isImage = /\.(jpg|jpeg|png|gif)$/i.test(file.name);
  887. // 检查文件大小
  888. const isValidSize = file.size <= maxSize;
  889. if (isImage && isValidSize) {
  890. validFiles.push(res.tempFilePaths[index]);
  891. } else {
  892. invalidFiles.push({
  893. path: file.path,
  894. size: file.size,
  895. reason: !isImage ? '文件格式不支持' : '文件大于5MB'
  896. });
  897. }
  898. });
  899. // 显示无效文件提示
  900. if (invalidFiles.length > 0) {
  901. uni.showToast({
  902. title: `${invalidFiles.length}个文件无效,请检查格式和大小`,
  903. icon: 'none',
  904. duration: 2000
  905. });
  906. }
  907. // 如果有有效文件,则上传
  908. if (validFiles.length > 0) {
  909. this.uploadImages(validFiles);
  910. }
  911. },
  912. fail: (err) => {
  913. console.error('选择图片失败:', err);
  914. uni.showToast({
  915. title: '选择图片失败',
  916. icon: 'none'
  917. });
  918. }
  919. });
  920. },
  921. // 上传图片到服务器
  922. uploadImages(tempFilePaths) {
  923. uni.showLoading({
  924. title: '上传中...',
  925. mask: true
  926. });
  927. // 上传成功的图片计数
  928. let successCount = 0;
  929. let failCount = 0;
  930. const totalFiles = tempFilePaths.length;
  931. const newImages = [];
  932. // 遍历处理每张图片
  933. tempFilePaths.forEach((path, index) => {
  934. // 调用上传API
  935. uni.uploadFile({
  936. // url: api.serve + '/base/tasks/uploadTaskImage',
  937. url: api.serve + '/file/upload',
  938. filePath: path,
  939. name: 'file', // 文件参数名称,需要与后端接口匹配
  940. formData: {
  941. type: 'task', // 标识文件类型,用于后端区分不同业务的文件
  942. // directory: '/opt/app/nongxiaoyu/uploadImage' // 指定保存目录
  943. },
  944. header: {
  945. 'Authorization': `Bearer ${storage.getAccessToken()}`
  946. },
  947. success: (res) => {
  948. try {
  949. const response = JSON.parse(res.data);
  950. uni.showToast({
  951. title: `返回: ${response.data}`,
  952. icon: 'none',
  953. });
  954. if (response.code === 200) {
  955. // 获取返回的URL
  956. const imageUrl = response.data.url;
  957. console.log('上传成功,返回的图片URL:', imageUrl);
  958. // 上传成功,将图片信息添加到数组
  959. newImages.push({
  960. url: imageUrl, // 保存原始URL,在显示时会通过getImageUrl方法处理
  961. path: path, // 保存本地路径用于预览
  962. status: 'success',
  963. fileName: response.data.fileName || '' // 保存文件名,如果后端返回的话
  964. });
  965. successCount++;
  966. } else {
  967. failCount++;
  968. console.error('上传失败:', response.msg);
  969. }
  970. } catch (e) {
  971. failCount++;
  972. uni.showToast({
  973. title: `解析响应失败: ${e}`,
  974. icon: 'none',
  975. });
  976. console.error('解析响应失败:', e);
  977. }
  978. },
  979. fail: (err) => {
  980. failCount++;
  981. console.error('上传请求失败:', err);
  982. uni.showToast({
  983. title: `上传请求失败: ${err}`,
  984. icon: 'none',
  985. });
  986. },
  987. complete: () => {
  988. // 当所有文件都已处理完成
  989. if (successCount + failCount === totalFiles) {
  990. if (newImages.length > 0) {
  991. // 将新上传的图片添加到已有图片列表
  992. this.formData.images = [...this.formData.images, ...newImages];
  993. console.log("this.formData.images",this.formData.images);
  994. // 更新taskImages字段,将图片URL用逗号连接
  995. this.formData.taskImages = this.formData.images.map(img => img.url).join(',');
  996. // 显示成功提示
  997. uni.hideLoading();
  998. uni.showToast({
  999. title: `成功上传${successCount}张图片`,
  1000. icon: 'success'
  1001. });
  1002. } else {
  1003. // 全部失败
  1004. uni.showToast({
  1005. title: `图片上传: ${newImages}`,
  1006. icon: 'none',
  1007. });
  1008. uni.hideLoading();
  1009. // uni.showToast({
  1010. // title: '图片上传失败',
  1011. // icon: 'none',
  1012. // });
  1013. }
  1014. }
  1015. }
  1016. });
  1017. });
  1018. },
  1019. // 删除图片
  1020. deletePic(index) {
  1021. uni.showModal({
  1022. title: '确认删除',
  1023. content: '确定要删除这张图片吗?',
  1024. success: (res) => {
  1025. if (res.confirm) {
  1026. this.formData.images.splice(index, 1);
  1027. // 更新taskImages字段
  1028. this.formData.taskImages = this.formData.images.map(img => img.url).join(',');
  1029. uni.showToast({
  1030. title: '已删除',
  1031. icon: 'none'
  1032. });
  1033. }
  1034. }
  1035. });
  1036. },
  1037. // 获取图片URL
  1038. getImageUrl(item) {
  1039. // 默认返回url或path
  1040. return api.upload + item.url;
  1041. },
  1042. // 预览图片
  1043. previewImage(item, index) {
  1044. // 获取所有图片的完整URL
  1045. // const urls = this.formData.images.map(file => this.getImageUrl(file));
  1046. const urls = this.formData.images.map(item => item.url);
  1047. uni.previewImage({
  1048. urls: urls,
  1049. current: index
  1050. });
  1051. },
  1052. // 表单验证
  1053. validateForm() {
  1054. // 重置错误信息
  1055. this.formErrors = {
  1056. completionDesc: ''
  1057. };
  1058. // 基本字段验证
  1059. if (!this.formData.taskName.trim()) {
  1060. uni.showToast({
  1061. title: '请输入任务名称',
  1062. icon: 'none'
  1063. });
  1064. return false;
  1065. }
  1066. if (!this.formData.typeName) {
  1067. uni.showToast({
  1068. title: '请选择任务类型',
  1069. icon: 'none'
  1070. });
  1071. return false;
  1072. }
  1073. // 验证负责人
  1074. if (!this.formData.assigneeId) {
  1075. uni.showToast({
  1076. title: '请选择负责人',
  1077. icon: 'none'
  1078. });
  1079. return false;
  1080. }
  1081. // 新建和编辑模式且已完成状态下的验证
  1082. if ((this.pageMode === 'create' || this.pageMode === 'edit') && this.formData.taskStatus === '1') {
  1083. // 验证完成说明
  1084. if (!this.formData.completionDesc.trim()) {
  1085. this.formErrors.completionDesc = '请填写完成说明';
  1086. uni.showToast({
  1087. title: '请填写完成说明',
  1088. icon: 'none'
  1089. });
  1090. return false;
  1091. }
  1092. // 验证是否上传了图片
  1093. if (!this.formData.images || this.formData.images.length === 0) {
  1094. uni.showToast({
  1095. title: '请上传至少一张现场图片',
  1096. icon: 'none'
  1097. });
  1098. return false;
  1099. }
  1100. }
  1101. return true;
  1102. },
  1103. // 提交表单
  1104. submitForm() {
  1105. if (!this.validateForm()) {
  1106. return;
  1107. }
  1108. this.isSubmitting = true;
  1109. // 构建提交数据
  1110. const submitData = {
  1111. // 如果是编辑模式,需要提供ID
  1112. ...(this.formData.id ? { id: this.formData.id } : {}),
  1113. farmId: this.formData.farmId,
  1114. plotId: this.formData.plotId,
  1115. fieldName: this.formData.fieldName,
  1116. growCrops: this.formData.growCrops,
  1117. taskName: this.formData.taskName,
  1118. taskImages: this.formData.taskImages,
  1119. typeName: this.formData.typeNameId,
  1120. taskStatus: this.formData.taskStatus,
  1121. executeTime: this.formatDateTime(this.formData.executeTime),
  1122. assigneeId: this.formData.assigneeId,
  1123. assigneeName:this.formData.assigneeName,
  1124. remark: this.formData.remark,
  1125. completionTime: this.formData.taskStatus === '1' ? this.formatDateTime(this.formData.completionTime) : null,
  1126. completionDesc: this.formData.taskStatus === '1' ? this.formData.completionDesc : null,
  1127. create_by: this.formData.create_by || null
  1128. };
  1129. console.log('提交数据:', submitData);
  1130. uni.showLoading({
  1131. title: '提交中...',
  1132. mask: true
  1133. });
  1134. // 根据模式决定是创建还是更新
  1135. const requestPromise = this.pageMode === 'create'
  1136. ? addAgriculturalTask(submitData)
  1137. : updateAgriculturalTask(submitData);
  1138. requestPromise.then(res => {
  1139. this.isSubmitting = false;
  1140. uni.hideLoading();
  1141. if (res.data.code === 200) {
  1142. const successMessage = this.pageMode === 'create' ? '任务创建成功' : '保存修改成功';
  1143. uni.showToast({
  1144. title: successMessage,
  1145. icon: 'success',
  1146. duration: 1500
  1147. });
  1148. setTimeout(() => {
  1149. uni.navigateBack();
  1150. }, 1500);
  1151. } else {
  1152. uni.showToast({
  1153. title: res.data.msg || '操作失败',
  1154. icon: 'none'
  1155. });
  1156. }
  1157. }).catch(err => {
  1158. console.error('提交表单失败:', err);
  1159. this.isSubmitting = false;
  1160. uni.hideLoading();
  1161. uni.showToast({
  1162. title: '操作失败,请稍后重试',
  1163. icon: 'none'
  1164. });
  1165. });
  1166. }
  1167. }
  1168. }
  1169. </script>
  1170. <style scoped>
  1171. .page-container {
  1172. height: 100vh;
  1173. background-color: #F5F5F5;
  1174. display: flex;
  1175. flex-direction: column;
  1176. }
  1177. .page-scroll {
  1178. flex: 1;
  1179. overflow: hidden;
  1180. }
  1181. /* 地块基础信息卡片 */
  1182. .info-card {
  1183. background: #F9F9F9;
  1184. margin: 24rpx;
  1185. padding: 24rpx;
  1186. border-radius: 8rpx;
  1187. border: 1rpx solid #E5E5E5;
  1188. }
  1189. .card-title {
  1190. font-size: 30rpx;
  1191. font-weight: 600;
  1192. color: #333333;
  1193. margin-bottom: 16rpx;
  1194. padding-bottom: 12rpx;
  1195. border-bottom: 1rpx solid #E5E5E5;
  1196. }
  1197. .info-item {
  1198. display: flex;
  1199. justify-content: space-between;
  1200. align-items: center;
  1201. margin-bottom: 12rpx;
  1202. padding: 8rpx 0;
  1203. }
  1204. .info-item:last-child {
  1205. margin-bottom: 0;
  1206. }
  1207. .info-label {
  1208. font-size: 28rpx;
  1209. color: #666666;
  1210. flex-shrink: 0;
  1211. }
  1212. .info-value {
  1213. font-size: 28rpx;
  1214. color: #333333;
  1215. font-weight: 500;
  1216. }
  1217. /* 任务填写表单 */
  1218. .form-card {
  1219. background: #FFFFFF;
  1220. margin: 0 24rpx 24rpx;
  1221. padding: 24rpx;
  1222. border-radius: 8rpx;
  1223. border: 1rpx solid #E5E5E5;
  1224. }
  1225. .section-divider {
  1226. height: 1rpx;
  1227. background: #F0F0F0;
  1228. margin: 32rpx 0 24rpx;
  1229. }
  1230. .section-title {
  1231. font-size: 30rpx;
  1232. font-weight: 600;
  1233. color: #333333;
  1234. margin-bottom: 16rpx;
  1235. padding-bottom: 12rpx;
  1236. border-bottom: 1rpx solid #F0F0F0;
  1237. }
  1238. .form-item {
  1239. margin-bottom: 24rpx;
  1240. }
  1241. .form-item:last-child {
  1242. margin-bottom: 0;
  1243. }
  1244. .form-label {
  1245. font-size: 28rpx;
  1246. color: #333333;
  1247. margin-bottom: 12rpx;
  1248. font-weight: 500;
  1249. }
  1250. .form-label.required::before {
  1251. content: '*';
  1252. color: #FF6B6B;
  1253. margin-right: 4rpx;
  1254. }
  1255. .form-input {
  1256. width: 100%;
  1257. line-height: 80rpx;
  1258. height: 80rpx;
  1259. background: #F8F8F8;
  1260. border: 1rpx solid #E5E5E5;
  1261. border-radius: 8rpx;
  1262. padding: 0 16rpx;
  1263. font-size: 28rpx;
  1264. color: #333333;
  1265. box-sizing: border-box;
  1266. }
  1267. .form-input:focus {
  1268. background: #FFFFFF;
  1269. border-color: #3BB44A;
  1270. }
  1271. .form-input::placeholder {
  1272. color: #CCCCCC;
  1273. }
  1274. .form-input:disabled {
  1275. background: #F8F8F8;
  1276. color: #999999;
  1277. }
  1278. .select-wrapper {
  1279. position: relative;
  1280. width: 100%;
  1281. }
  1282. .select-input {
  1283. cursor: pointer;
  1284. }
  1285. .select-arrow {
  1286. position: absolute;
  1287. right: 16rpx;
  1288. top: 50%;
  1289. transform: translateY(-50%);
  1290. color: #999999;
  1291. font-size: 20rpx;
  1292. pointer-events: none;
  1293. }
  1294. .form-textarea {
  1295. width: 100%;
  1296. min-height: 120rpx;
  1297. background: #F8F8F8;
  1298. border: 1rpx solid #E5E5E5;
  1299. border-radius: 8rpx;
  1300. padding: 16rpx;
  1301. font-size: 28rpx;
  1302. color: #333333;
  1303. box-sizing: border-box;
  1304. resize: none;
  1305. line-height: 1.5;
  1306. }
  1307. .form-textarea:focus {
  1308. background: #FFFFFF;
  1309. border-color: #3BB44A;
  1310. }
  1311. .form-textarea::placeholder {
  1312. color: #CCCCCC;
  1313. }
  1314. .form-textarea:disabled {
  1315. background: #F8F8F8;
  1316. color: #999999;
  1317. }
  1318. .char-count {
  1319. font-size: 24rpx;
  1320. color: #CCCCCC;
  1321. text-align: right;
  1322. margin-top: 8rpx;
  1323. }
  1324. .form-error {
  1325. font-size: 24rpx;
  1326. color: #FF6B6B;
  1327. margin-top: 8rpx;
  1328. }
  1329. /* 单选按钮样式 */
  1330. .radio-group {
  1331. display: flex;
  1332. gap: 32rpx;
  1333. }
  1334. .radio-item {
  1335. display: flex;
  1336. align-items: center;
  1337. cursor: pointer;
  1338. }
  1339. .radio-circle {
  1340. width: 32rpx;
  1341. height: 32rpx;
  1342. border: 2rpx solid #CCCCCC;
  1343. border-radius: 50%;
  1344. display: flex;
  1345. align-items: center;
  1346. justify-content: center;
  1347. margin-right: 8rpx;
  1348. }
  1349. .radio-circle.radio-checked {
  1350. border-color: #3BB44A;
  1351. background-color: #3BB44A;
  1352. }
  1353. .radio-dot {
  1354. width: 12rpx;
  1355. height: 12rpx;
  1356. background-color: #FFFFFF;
  1357. border-radius: 50%;
  1358. }
  1359. .radio-label {
  1360. font-size: 28rpx;
  1361. color: #333333;
  1362. }
  1363. /* 完成状态样式 */
  1364. .status-completed {
  1365. display: flex;
  1366. align-items: center;
  1367. color: #3BB44A;
  1368. font-size: 28rpx;
  1369. }
  1370. .status-icon {
  1371. width: 32rpx;
  1372. height: 32rpx;
  1373. background: #3BB44A;
  1374. color: #FFFFFF;
  1375. border-radius: 50%;
  1376. display: flex;
  1377. align-items: center;
  1378. justify-content: center;
  1379. margin-right: 8rpx;
  1380. font-size: 20rpx;
  1381. line-height: 1;
  1382. }
  1383. /* 图片上传样式 */
  1384. .image-upload, .image-view {
  1385. width: 100%;
  1386. }
  1387. .image-list {
  1388. display: flex;
  1389. flex-wrap: wrap;
  1390. gap: 16rpx;
  1391. }
  1392. .image-preview {
  1393. position: relative;
  1394. width: 160rpx;
  1395. height: 160rpx;
  1396. border-radius: 8rpx;
  1397. overflow: hidden;
  1398. background: #F5F5F5;
  1399. }
  1400. .image-preview image {
  1401. width: 100%;
  1402. height: 100%;
  1403. }
  1404. .delete-btn {
  1405. position: absolute;
  1406. top: -6rpx;
  1407. right: -6rpx;
  1408. width: 32rpx;
  1409. height: 32rpx;
  1410. background: rgba(0, 0, 0, 0.6);
  1411. color: #FFFFFF;
  1412. border-radius: 50%;
  1413. display: flex;
  1414. align-items: center;
  1415. justify-content: center;
  1416. font-size: 20rpx;
  1417. line-height: 1;
  1418. }
  1419. .upload-btn {
  1420. width: 160rpx;
  1421. height: 160rpx;
  1422. background: #F8F8F8;
  1423. border: 2rpx dashed #DDDDDD;
  1424. border-radius: 8rpx;
  1425. display: flex;
  1426. flex-direction: column;
  1427. align-items: center;
  1428. justify-content: center;
  1429. }
  1430. .upload-icon {
  1431. font-size: 32rpx;
  1432. margin-bottom: 8rpx;
  1433. color: #999999;
  1434. }
  1435. .upload-text {
  1436. font-size: 24rpx;
  1437. color: #999999;
  1438. }
  1439. .upload-tip {
  1440. font-size: 24rpx;
  1441. color: #999999;
  1442. margin-top: 12rpx;
  1443. }
  1444. .no-images {
  1445. padding: 40rpx 0;
  1446. text-align: center;
  1447. color: #CCCCCC;
  1448. font-size: 28rpx;
  1449. }
  1450. /* 底部安全区域 */
  1451. .bottom-safe {
  1452. height: 120rpx;
  1453. }
  1454. .footer-safe {
  1455. background: #FFFFFF;
  1456. border-top: 1rpx solid #E5E5E5;
  1457. padding-bottom: constant(safe-area-inset-bottom);
  1458. padding-bottom: env(safe-area-inset-bottom);
  1459. }
  1460. .footer-content {
  1461. padding: 24rpx;
  1462. }
  1463. .submit-button {
  1464. width: 100%;
  1465. height: 88rpx;
  1466. background: #3BB44A;
  1467. color: #FFFFFF;
  1468. border: none;
  1469. border-radius: 8rpx;
  1470. font-size: 32rpx;
  1471. font-weight: 600;
  1472. display: flex;
  1473. align-items: center;
  1474. justify-content: center;
  1475. }
  1476. .submit-button:active {
  1477. background: #2D8C3C;
  1478. }
  1479. .submit-button:disabled,
  1480. .submit-button.loading {
  1481. background: #CCCCCC;
  1482. }
  1483. /* 选择器弹窗样式 */
  1484. .picker-mask {
  1485. position: fixed;
  1486. top: 0;
  1487. left: 0;
  1488. width: 100%;
  1489. height: 100%;
  1490. background: rgba(0, 0, 0, 0.5);
  1491. z-index: 999;
  1492. }
  1493. .picker-popup {
  1494. position: fixed;
  1495. bottom: 0;
  1496. left: 0;
  1497. width: 100%;
  1498. background: #FFFFFF;
  1499. border-radius: 16rpx 16rpx 0 0;
  1500. z-index: 1000;
  1501. padding-bottom: constant(safe-area-inset-bottom);
  1502. padding-bottom: env(safe-area-inset-bottom);
  1503. }
  1504. .picker-header {
  1505. display: flex;
  1506. justify-content: space-between;
  1507. align-items: center;
  1508. padding: 24rpx;
  1509. border-bottom: 1rpx solid #E5E5E5;
  1510. }
  1511. .picker-cancel, .picker-confirm {
  1512. font-size: 30rpx;
  1513. color: #3BB44A;
  1514. }
  1515. .picker-title {
  1516. font-size: 32rpx;
  1517. font-weight: 600;
  1518. color: #333333;
  1519. }
  1520. .picker-content {
  1521. max-height: 400rpx;
  1522. overflow-y: auto;
  1523. }
  1524. .picker-item {
  1525. display: flex;
  1526. justify-content: space-between;
  1527. align-items: center;
  1528. padding: 24rpx;
  1529. border-bottom: 1rpx solid #F0F0F0;
  1530. font-size: 30rpx;
  1531. color: #333333;
  1532. }
  1533. .picker-item.selected {
  1534. color: #3BB44A;
  1535. }
  1536. .picker-item:last-child {
  1537. border-bottom: none;
  1538. }
  1539. .check-mark {
  1540. color: #3BB44A;
  1541. font-size: 28rpx;
  1542. }
  1543. /* 日期时间选择器样式 */
  1544. .datetime-picker {
  1545. height: 60vh;
  1546. }
  1547. .datetime-picker-content {
  1548. height: calc(100% - 100rpx);
  1549. padding: 0;
  1550. }
  1551. .datetime-picker-view {
  1552. width: 100%;
  1553. height: 100%;
  1554. }
  1555. .picker-view-item {
  1556. display: flex;
  1557. align-items: center;
  1558. justify-content: center;
  1559. height: 80rpx;
  1560. font-size: 32rpx;
  1561. color: #333333;
  1562. }
  1563. .picker-item-text {
  1564. font-size: 32rpx;
  1565. color: #333333;
  1566. line-height: 80rpx;
  1567. height: 80rpx;
  1568. text-align: center;
  1569. }
  1570. /* 添加加载中样式 */
  1571. .picker-loading {
  1572. display: flex;
  1573. justify-content: center;
  1574. align-items: center;
  1575. padding: 30rpx 0;
  1576. color: #999;
  1577. font-size: 28rpx;
  1578. }
  1579. .picker-empty {
  1580. padding: 30rpx 0;
  1581. text-align: center;
  1582. color: #CCCCCC;
  1583. font-size: 28rpx;
  1584. }
  1585. </style>