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.plotName || '未知' }}</text>
  13. </view>
  14. <view class="info-item">
  15. <text class="info-label">作物名称</text>
  16. <text class="info-value">{{ formData.crop || '未知' }}</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. plotName: '',
  363. crop: '',
  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.plotName = decodeURIComponent(options.plotName || '未知');
  469. this.formData.crop = decodeURIComponent(options.crop || '未知');
  470. this.formData.manager = decodeURIComponent(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. completionStatus: taskDetail.taskStatus, // 将后端的taskStatus映射为前端的completionStatus
  532. };
  533. // 转换任务类型值
  534. // 找到对应的字典项
  535. if (this.taskStatusList && this.taskStatusList.length > 0) {
  536. const typeDict = this.taskStatusList.find(item => item.dictValue === taskDetail.typeName.toString());
  537. if (typeDict) {
  538. this.formData.typeName = typeDict.dictLabel;
  539. this.formData.typeNameId = typeDict.dictValue;
  540. }
  541. }
  542. // 处理图片数据
  543. if (this.formData.taskImages) {
  544. try {
  545. // 尝试解析图片数据字符串
  546. const imageUrls = this.formData.taskImages.split(',');
  547. this.formData.images = imageUrls.filter(url => url && url.trim()).map(url => ({
  548. url: url.trim(), // 保存原始URL,显示时会通过getImageUrl方法处理
  549. status: 'success'
  550. }));
  551. console.log('解析后的图片数据:', this.formData.images);
  552. } catch (e) {
  553. console.error('解析图片数据失败:', e);
  554. this.formData.images = [];
  555. }
  556. } else {
  557. this.formData.images = [];
  558. }
  559. console.log("this.formData.assigneeId",this.formData.assigneeId);
  560. console.log("!this.formData.assigneeName",this.formData.assigneeName);
  561. // 如果有负责人ID但没有负责人名称,获取负责人信息
  562. if (this.formData.assigneeId && !this.formData.assigneeName) {
  563. this.loadAssigneeInfo(this.formData.assigneeId);
  564. }
  565. uni.hideLoading();
  566. } else {
  567. uni.hideLoading();
  568. uni.showToast({
  569. title: res.data.msg || '获取任务详情失败',
  570. icon: 'none'
  571. });
  572. // 失败后返回上一页
  573. setTimeout(() => {
  574. uni.navigateBack();
  575. }, 1500);
  576. }
  577. }).catch(err => {
  578. console.error('获取任务详情失败:', err);
  579. uni.hideLoading();
  580. uni.showToast({
  581. title: '获取任务详情失败',
  582. icon: 'none'
  583. });
  584. // 失败后返回上一页
  585. setTimeout(() => {
  586. uni.navigateBack();
  587. }, 1500);
  588. });
  589. },
  590. // 加载负责人信息
  591. loadAssigneeInfo(userId) {
  592. getUserInfo(userId).then(res => {
  593. console.log("你是谁:",res);
  594. if (res.data.code === 200) {
  595. const userData = res.data.data;
  596. this.formData.assigneeName = userData.userName || '未知用户';
  597. this.formData.assigneeId = userData.userId;
  598. }
  599. }).catch(err => {
  600. console.error('获取负责人信息失败:', err);
  601. this.formData.assigneeName = '未知用户';
  602. });
  603. },
  604. // 加载用户列表
  605. loadUserList(farmId) {
  606. this.usersLoading = true;
  607. const params = {
  608. pageNum: 1,
  609. pageSize: 10,
  610. deptId: farmId
  611. }
  612. getUsersByPlotId(params).then(res => {
  613. console.log("加载用户:",res);
  614. if (res.data.code === 200) {
  615. this.userList = res.data.rows || [];
  616. // 设置默认选中第一个用户
  617. if (this.userList.length > 0 && !this.formData.assigneeId) {
  618. this.formData.assigneeId = this.userList[0].userId;
  619. this.formData.assigneeName = this.userList[0].userName;
  620. }
  621. } else {
  622. console.error('获取用户列表失败:', res.data.msg);
  623. uni.showToast({
  624. title: '获取用户列表失败',
  625. icon: 'none'
  626. });
  627. }
  628. }).catch(err => {
  629. console.error('获取用户列表失败:', err);
  630. uni.showToast({
  631. title: '获取用户列表失败',
  632. icon: 'none'
  633. });
  634. }).finally(() => {
  635. this.usersLoading = false;
  636. });
  637. },
  638. // 初始化时间选择器范围数据
  639. initDateTimeRange() {
  640. // 年份:从2020年开始显示10年
  641. const startYear = 2020;
  642. const years = [];
  643. for (let i = 0; i < 10; i++) {
  644. years.push((startYear + i) + '年');
  645. }
  646. // 月份:1-12月
  647. const months = [];
  648. for (let i = 1; i <= 12; i++) {
  649. months.push(i + '月');
  650. }
  651. // 日期:1-31日
  652. const days = [];
  653. for (let i = 1; i <= 31; i++) {
  654. days.push(i + '日');
  655. }
  656. // 小时:0-23时
  657. const hours = [];
  658. for (let i = 0; i <= 23; i++) {
  659. hours.push(i.toString().padStart(2, '0') + '时');
  660. }
  661. // 分钟:0-59分
  662. const minutes = [];
  663. for (let i = 0; i <= 59; i++) {
  664. minutes.push(i.toString().padStart(2, '0') + '分');
  665. }
  666. this.dateTimePickerRange = [years, months, days, hours, minutes];
  667. console.log('日期选择器范围:', {
  668. years: years.length,
  669. months: months.length,
  670. days: days.length,
  671. hours: hours.length,
  672. minutes: minutes.length
  673. });
  674. },
  675. // 初始化日期时间选择器
  676. initDateTimePicker(timestamp) {
  677. const date = this.normalizeToDate(timestamp);
  678. const currentYear = date.getFullYear();
  679. const currentMonth = date.getMonth() + 1; // 1-12
  680. const currentDate = date.getDate(); // 1-31
  681. const currentHour = date.getHours();
  682. const currentMinute = date.getMinutes();
  683. // 基准年份,用于计算索引
  684. const startYear = 2020;
  685. // 如果当前年份小于起始年份或大于结束年份,调整为合法范围内
  686. let yearIndex = currentYear - startYear;
  687. if (yearIndex < 0) yearIndex = 0;
  688. if (yearIndex >= 10) yearIndex = 9;
  689. console.log('初始化日期选择器:', {
  690. timestamp,
  691. year: currentYear,
  692. month: currentMonth,
  693. date: currentDate,
  694. hour: currentHour,
  695. minute: currentMinute,
  696. yearIndex
  697. });
  698. // 设置选择器的初始值
  699. this.dateTimePickerValue = [
  700. yearIndex, // 年份索引
  701. currentMonth - 1, // 月份索引(0-11)
  702. currentDate - 1, // 日期索引(0-30)
  703. currentHour, // 小时索引
  704. currentMinute // 分钟索引
  705. ];
  706. console.log('选择器初始值:', this.dateTimePickerValue);
  707. // 更新日期范围
  708. this.updateDaysRange();
  709. },
  710. // 更新日期范围(根据选择的年月)
  711. updateDaysRange() {
  712. const yearIndex = this.dateTimePickerValue[0];
  713. const monthIndex = this.dateTimePickerValue[1];
  714. // 计算实际年月
  715. const year = 2020 + yearIndex;
  716. const month = monthIndex + 1; // 索引为0-11,实际月份为1-12
  717. // 获取该月的天数
  718. const daysInMonth = new Date(year, month, 0).getDate();
  719. // 更新天数范围
  720. const days = [];
  721. for (let i = 1; i <= daysInMonth; i++) {
  722. days.push(i + '日');
  723. }
  724. // 更新日期列
  725. this.dateTimePickerRange[2] = days;
  726. // 如果当前选择的日期超过了该月的天数,调整为该月最后一天
  727. if (this.dateTimePickerValue[2] >= daysInMonth) {
  728. this.dateTimePickerValue[2] = daysInMonth - 1;
  729. }
  730. console.log('更新日期范围:', {
  731. year,
  732. month,
  733. daysInMonth,
  734. daysLength: days.length
  735. });
  736. },
  737. // 显示任务类型选择器
  738. showTaskTypeSelector() {
  739. this.tempTaskTypeIndex = this.taskTypeIndex;
  740. this.showTaskTypePicker = true;
  741. },
  742. // 确认任务类型选择
  743. confirmTaskType() {
  744. const selectedType = this.taskTypeOptions[this.tempTaskTypeIndex];
  745. console.log("选中:",selectedType);
  746. this.formData.typeName = selectedType.dictLabel;
  747. this.formData.typeNameId = selectedType.dictValue; // 设置typeNameId为dictValue值
  748. this.showTaskTypePicker = false;
  749. },
  750. // 选择执行时间
  751. selectExecuteTime() {
  752. this.currentTimeType = 'executeTime';
  753. this.initDateTimePicker(this.formData.executeTime);
  754. this.showDateTimeSelector = true;
  755. },
  756. // 选择完成时间
  757. selectCompletionTime() {
  758. this.currentTimeType = 'completionTime';
  759. this.initDateTimePicker(this.formData.completionTime);
  760. this.showDateTimeSelector = true;
  761. },
  762. // 日期时间选择器变更
  763. onDateTimePickerChange(e) {
  764. this.dateTimePickerValue = e.detail.value;
  765. // 当年月变更时,更新日期范围
  766. this.updateDaysRange();
  767. },
  768. // 确认时间选择
  769. confirmDateTime() {
  770. const values = this.dateTimePickerValue;
  771. // 计算实际日期时间
  772. const year = 2020 + values[0];
  773. const month = values[1] + 1; // 索引为0-11,实际月份为1-12
  774. const date = values[2] + 1; // 索引为0-30,实际日期为1-31
  775. const hour = values[3];
  776. const minute = values[4];
  777. console.log('确认日期时间:', {
  778. selectedValues: values,
  779. convertedDate: `${year}-${month}-${date} ${hour}:${minute}`
  780. });
  781. // 创建日期对象
  782. const selectedTime = new Date(year, month - 1, date, hour, minute).getTime();
  783. // 更新相应的表单字段
  784. if (this.currentTimeType === 'executeTime') {
  785. this.formData.executeTime = selectedTime;
  786. } else if (this.currentTimeType === 'completionTime') {
  787. this.formData.completionTime = selectedTime;
  788. }
  789. // 关闭选择器
  790. this.showDateTimeSelector = false;
  791. uni.showToast({
  792. title: '时间已设置',
  793. icon: 'success',
  794. duration: 1500
  795. });
  796. },
  797. // 取消时间选择
  798. cancelDateTime() {
  799. this.showDateTimeSelector = false;
  800. },
  801. // 显示用户选择器
  802. showUserSelector() {
  803. this.tempUserIndex = this.userList.findIndex(user => user.userId === this.formData.assigneeId);
  804. this.showUserPicker = true;
  805. },
  806. // 确认用户选择
  807. confirmUser() {
  808. const selectedUser = this.userList[this.tempUserIndex];
  809. this.formData.assigneeId = selectedUser.userId;
  810. this.formData.assigneeName = selectedUser.userName;
  811. this.showUserPicker = false;
  812. },
  813. // 关闭所有选择器
  814. closePickers() {
  815. this.showTaskTypePicker = false;
  816. this.showDateTimeSelector = false;
  817. this.showUserPicker = false;
  818. },
  819. // 选择完成状态
  820. selectCompletionStatus(value) {
  821. this.formData.completionStatus = value;
  822. this.formData.taskStatus = value; // 同时设置taskStatus字段
  823. if (value === '1') {
  824. this.formData.completionTime = new Date();
  825. }
  826. },
  827. // 格式化日期时间
  828. /**
  829. * 日期格式化工具(兼容 iOS 和安卓)
  830. * @param {string|number|Date} date - 日期对象 / 时间戳 / 日期字符串
  831. * @param {string} format - 格式模板,默认 'yyyy-MM-dd HH:mm:ss'
  832. * @returns {string} 格式化后的日期
  833. */
  834. formatDateTime(date, format = 'yyyy-MM-dd HH:mm') {
  835. if (!date) return '';
  836. // 如果是字符串,做 iOS 兼容(将 2025-08-12 替换为 2025/08/12)
  837. if (typeof date === 'string') {
  838. date = date.replace(/-/g, '/');
  839. }
  840. // 如果是数字(时间戳),判断是否是秒级
  841. if (typeof date === 'number') {
  842. if (date.toString().length === 10) {
  843. date *= 1000; // 秒转毫秒
  844. }
  845. }
  846. // 转换为 Date 对象
  847. date = new Date(date);
  848. if (isNaN(date.getTime())) return '';
  849. const map = {
  850. 'yyyy': date.getFullYear(),
  851. 'MM': String(date.getMonth() + 1).padStart(2, '0'),
  852. 'dd': String(date.getDate()).padStart(2, '0'),
  853. 'HH': String(date.getHours()).padStart(2, '0'),
  854. 'mm': String(date.getMinutes()).padStart(2, '0'),
  855. // 'ss': String(date.getSeconds()).padStart(2, '0')
  856. };
  857. return format.replace(/yyyy|MM|dd|HH|mm/g, match => map[match]);
  858. },
  859. // formatDateTime(timestamp) {
  860. // if (!timestamp) return '';
  861. // const date = parseDate(timestamp);
  862. // if (isNaN(date.getTime())) return '';
  863. // const year = date.getFullYear();
  864. // const month = String(date.getMonth() + 1).padStart(2, '0');
  865. // const day = String(date.getDate()).padStart(2, '0');
  866. // const hour = String(date.getHours()).padStart(2, '0');
  867. // const minute = String(date.getMinutes()).padStart(2, '0');
  868. // return `${year}-${month}-${day} ${hour}:${minute}`;
  869. // },
  870. // 选择图片
  871. chooseImage() {
  872. uni.chooseImage({
  873. count: 6 - this.formData.images.length,
  874. sizeType: ['original', 'compressed'],
  875. sourceType: ['album', 'camera'],
  876. success: (res) => {
  877. console.log('选择图片成功:', res);
  878. // 验证文件类型和大小
  879. const validFiles = [];
  880. const invalidFiles = [];
  881. const maxSize = 5 * 1024 * 1024; // 5MB 最大限制
  882. // 检查每个文件
  883. res.tempFiles.forEach((file, index) => {
  884. // 检查文件类型
  885. const isImage = /\.(jpg|jpeg|png|gif)$/i.test(file.name);
  886. // 检查文件大小
  887. const isValidSize = file.size <= maxSize;
  888. if (isImage && isValidSize) {
  889. validFiles.push(res.tempFilePaths[index]);
  890. } else {
  891. invalidFiles.push({
  892. path: file.path,
  893. size: file.size,
  894. reason: !isImage ? '文件格式不支持' : '文件大于5MB'
  895. });
  896. }
  897. });
  898. // 显示无效文件提示
  899. if (invalidFiles.length > 0) {
  900. uni.showToast({
  901. title: `${invalidFiles.length}个文件无效,请检查格式和大小`,
  902. icon: 'none',
  903. duration: 2000
  904. });
  905. }
  906. // 如果有有效文件,则上传
  907. if (validFiles.length > 0) {
  908. this.uploadImages(validFiles);
  909. }
  910. },
  911. fail: (err) => {
  912. console.error('选择图片失败:', err);
  913. uni.showToast({
  914. title: '选择图片失败',
  915. icon: 'none'
  916. });
  917. }
  918. });
  919. },
  920. // 上传图片到服务器
  921. uploadImages(tempFilePaths) {
  922. uni.showLoading({
  923. title: '上传中...',
  924. mask: true
  925. });
  926. // 上传成功的图片计数
  927. let successCount = 0;
  928. let failCount = 0;
  929. const totalFiles = tempFilePaths.length;
  930. const newImages = [];
  931. // 遍历处理每张图片
  932. tempFilePaths.forEach((path, index) => {
  933. // 调用上传API
  934. uni.uploadFile({
  935. // url: api.serve + '/base/tasks/uploadTaskImage',
  936. url: api.serve + '/file/upload',
  937. filePath: path,
  938. name: 'file', // 文件参数名称,需要与后端接口匹配
  939. formData: {
  940. type: 'task', // 标识文件类型,用于后端区分不同业务的文件
  941. // directory: '/opt/app/nongxiaoyu/uploadImage' // 指定保存目录
  942. },
  943. header: {
  944. 'Authorization': `Bearer ${storage.getAccessToken()}`
  945. },
  946. success: (res) => {
  947. try {
  948. const response = JSON.parse(res.data);
  949. uni.showToast({
  950. title: `返回: ${response.data}`,
  951. icon: 'none',
  952. });
  953. if (response.code === 200) {
  954. // 获取返回的URL
  955. const imageUrl = response.data.url;
  956. console.log('上传成功,返回的图片URL:', imageUrl);
  957. // 上传成功,将图片信息添加到数组
  958. newImages.push({
  959. url: imageUrl, // 保存原始URL,在显示时会通过getImageUrl方法处理
  960. path: path, // 保存本地路径用于预览
  961. status: 'success',
  962. fileName: response.data.fileName || '' // 保存文件名,如果后端返回的话
  963. });
  964. successCount++;
  965. } else {
  966. failCount++;
  967. console.error('上传失败:', response.msg);
  968. }
  969. } catch (e) {
  970. failCount++;
  971. uni.showToast({
  972. title: `解析响应失败: ${e}`,
  973. icon: 'none',
  974. });
  975. console.error('解析响应失败:', e);
  976. }
  977. },
  978. fail: (err) => {
  979. failCount++;
  980. console.error('上传请求失败:', err);
  981. uni.showToast({
  982. title: `上传请求失败: ${err}`,
  983. icon: 'none',
  984. });
  985. },
  986. complete: () => {
  987. // 当所有文件都已处理完成
  988. if (successCount + failCount === totalFiles) {
  989. if (newImages.length > 0) {
  990. // 将新上传的图片添加到已有图片列表
  991. this.formData.images = [...this.formData.images, ...newImages];
  992. console.log("this.formData.images",this.formData.images);
  993. // 更新taskImages字段,将图片URL用逗号连接
  994. this.formData.taskImages = this.formData.images.map(img => img.url).join(',');
  995. // 显示成功提示
  996. uni.hideLoading();
  997. uni.showToast({
  998. title: `成功上传${successCount}张图片`,
  999. icon: 'success'
  1000. });
  1001. } else {
  1002. // 全部失败
  1003. uni.showToast({
  1004. title: `图片上传: ${newImages}`,
  1005. icon: 'none',
  1006. });
  1007. uni.hideLoading();
  1008. // uni.showToast({
  1009. // title: '图片上传失败',
  1010. // icon: 'none',
  1011. // });
  1012. }
  1013. }
  1014. }
  1015. });
  1016. });
  1017. },
  1018. // 删除图片
  1019. deletePic(index) {
  1020. uni.showModal({
  1021. title: '确认删除',
  1022. content: '确定要删除这张图片吗?',
  1023. success: (res) => {
  1024. if (res.confirm) {
  1025. this.formData.images.splice(index, 1);
  1026. // 更新taskImages字段
  1027. this.formData.taskImages = this.formData.images.map(img => img.url).join(',');
  1028. uni.showToast({
  1029. title: '已删除',
  1030. icon: 'none'
  1031. });
  1032. }
  1033. }
  1034. });
  1035. },
  1036. // 获取图片URL
  1037. getImageUrl(item) {
  1038. // 默认返回url或path
  1039. return api.upload + item.url;
  1040. },
  1041. // 预览图片
  1042. previewImage(item, index) {
  1043. // 获取所有图片的完整URL
  1044. // const urls = this.formData.images.map(file => this.getImageUrl(file));
  1045. const urls = this.formData.images.map(item => item.url);
  1046. uni.previewImage({
  1047. urls: urls,
  1048. current: index
  1049. });
  1050. },
  1051. // 表单验证
  1052. validateForm() {
  1053. // 重置错误信息
  1054. this.formErrors = {
  1055. completionDesc: ''
  1056. };
  1057. // 基本字段验证
  1058. if (!this.formData.taskName.trim()) {
  1059. uni.showToast({
  1060. title: '请输入任务名称',
  1061. icon: 'none'
  1062. });
  1063. return false;
  1064. }
  1065. if (!this.formData.typeName) {
  1066. uni.showToast({
  1067. title: '请选择任务类型',
  1068. icon: 'none'
  1069. });
  1070. return false;
  1071. }
  1072. // 验证负责人
  1073. if (!this.formData.assigneeId) {
  1074. uni.showToast({
  1075. title: '请选择负责人',
  1076. icon: 'none'
  1077. });
  1078. return false;
  1079. }
  1080. // 新建和编辑模式且已完成状态下的验证
  1081. if ((this.pageMode === 'create' || this.pageMode === 'edit') && this.formData.taskStatus === '1') {
  1082. // 验证完成说明
  1083. if (!this.formData.completionDesc.trim()) {
  1084. this.formErrors.completionDesc = '请填写完成说明';
  1085. uni.showToast({
  1086. title: '请填写完成说明',
  1087. icon: 'none'
  1088. });
  1089. return false;
  1090. }
  1091. // 验证是否上传了图片
  1092. if (!this.formData.images || this.formData.images.length === 0) {
  1093. uni.showToast({
  1094. title: '请上传至少一张现场图片',
  1095. icon: 'none'
  1096. });
  1097. return false;
  1098. }
  1099. }
  1100. return true;
  1101. },
  1102. // 提交表单
  1103. submitForm() {
  1104. if (!this.validateForm()) {
  1105. return;
  1106. }
  1107. this.isSubmitting = true;
  1108. // 构建提交数据
  1109. const submitData = {
  1110. // 如果是编辑模式,需要提供ID
  1111. ...(this.formData.id ? { id: this.formData.id } : {}),
  1112. farmId: this.formData.farmId,
  1113. plotId: this.formData.plotId,
  1114. fieldName: this.formData.plotName,
  1115. growCrops: this.formData.crop,
  1116. taskName: this.formData.taskName,
  1117. taskImages: this.formData.taskImages,
  1118. typeName: this.formData.typeNameId,
  1119. taskStatus: this.formData.taskStatus,
  1120. executeTime: this.formatDateTime(this.formData.executeTime),
  1121. assigneeId: this.formData.assigneeId,
  1122. assigneeName:this.formData.assigneeName,
  1123. remark: this.formData.remark,
  1124. completionTime: this.formData.taskStatus === '1' ? this.formatDateTime(this.formData.completionTime) : null,
  1125. completionDesc: this.formData.taskStatus === '1' ? this.formData.completionDesc : null,
  1126. create_by: this.formData.create_by || null
  1127. };
  1128. console.log('提交数据:', submitData);
  1129. uni.showLoading({
  1130. title: '提交中...',
  1131. mask: true
  1132. });
  1133. // 根据模式决定是创建还是更新
  1134. const requestPromise = this.pageMode === 'create'
  1135. ? addAgriculturalTask(submitData)
  1136. : updateAgriculturalTask(submitData);
  1137. requestPromise.then(res => {
  1138. this.isSubmitting = false;
  1139. uni.hideLoading();
  1140. if (res.data.code === 200) {
  1141. const successMessage = this.pageMode === 'create' ? '任务创建成功' : '保存修改成功';
  1142. uni.showToast({
  1143. title: successMessage,
  1144. icon: 'success',
  1145. duration: 1500
  1146. });
  1147. setTimeout(() => {
  1148. uni.navigateBack();
  1149. }, 1500);
  1150. } else {
  1151. uni.showToast({
  1152. title: res.data.msg || '操作失败',
  1153. icon: 'none'
  1154. });
  1155. }
  1156. }).catch(err => {
  1157. console.error('提交表单失败:', err);
  1158. this.isSubmitting = false;
  1159. uni.hideLoading();
  1160. uni.showToast({
  1161. title: '操作失败,请稍后重试',
  1162. icon: 'none'
  1163. });
  1164. });
  1165. }
  1166. }
  1167. }
  1168. </script>
  1169. <style scoped>
  1170. .page-container {
  1171. height: 100vh;
  1172. background-color: #F5F5F5;
  1173. display: flex;
  1174. flex-direction: column;
  1175. }
  1176. .page-scroll {
  1177. flex: 1;
  1178. overflow: hidden;
  1179. }
  1180. /* 地块基础信息卡片 */
  1181. .info-card {
  1182. background: #F9F9F9;
  1183. margin: 24rpx;
  1184. padding: 24rpx;
  1185. border-radius: 8rpx;
  1186. border: 1rpx solid #E5E5E5;
  1187. }
  1188. .card-title {
  1189. font-size: 30rpx;
  1190. font-weight: 600;
  1191. color: #333333;
  1192. margin-bottom: 16rpx;
  1193. padding-bottom: 12rpx;
  1194. border-bottom: 1rpx solid #E5E5E5;
  1195. }
  1196. .info-item {
  1197. display: flex;
  1198. justify-content: space-between;
  1199. align-items: center;
  1200. margin-bottom: 12rpx;
  1201. padding: 8rpx 0;
  1202. }
  1203. .info-item:last-child {
  1204. margin-bottom: 0;
  1205. }
  1206. .info-label {
  1207. font-size: 28rpx;
  1208. color: #666666;
  1209. flex-shrink: 0;
  1210. }
  1211. .info-value {
  1212. font-size: 28rpx;
  1213. color: #333333;
  1214. font-weight: 500;
  1215. }
  1216. /* 任务填写表单 */
  1217. .form-card {
  1218. background: #FFFFFF;
  1219. margin: 0 24rpx 24rpx;
  1220. padding: 24rpx;
  1221. border-radius: 8rpx;
  1222. border: 1rpx solid #E5E5E5;
  1223. }
  1224. .section-divider {
  1225. height: 1rpx;
  1226. background: #F0F0F0;
  1227. margin: 32rpx 0 24rpx;
  1228. }
  1229. .section-title {
  1230. font-size: 30rpx;
  1231. font-weight: 600;
  1232. color: #333333;
  1233. margin-bottom: 16rpx;
  1234. padding-bottom: 12rpx;
  1235. border-bottom: 1rpx solid #F0F0F0;
  1236. }
  1237. .form-item {
  1238. margin-bottom: 24rpx;
  1239. }
  1240. .form-item:last-child {
  1241. margin-bottom: 0;
  1242. }
  1243. .form-label {
  1244. font-size: 28rpx;
  1245. color: #333333;
  1246. margin-bottom: 12rpx;
  1247. font-weight: 500;
  1248. }
  1249. .form-label.required::before {
  1250. content: '*';
  1251. color: #FF6B6B;
  1252. margin-right: 4rpx;
  1253. }
  1254. .form-input {
  1255. width: 100%;
  1256. line-height: 80rpx;
  1257. height: 80rpx;
  1258. background: #F8F8F8;
  1259. border: 1rpx solid #E5E5E5;
  1260. border-radius: 8rpx;
  1261. padding: 0 16rpx;
  1262. font-size: 28rpx;
  1263. color: #333333;
  1264. box-sizing: border-box;
  1265. }
  1266. .form-input:focus {
  1267. background: #FFFFFF;
  1268. border-color: #3BB44A;
  1269. }
  1270. .form-input::placeholder {
  1271. color: #CCCCCC;
  1272. }
  1273. .form-input:disabled {
  1274. background: #F8F8F8;
  1275. color: #999999;
  1276. }
  1277. .select-wrapper {
  1278. position: relative;
  1279. width: 100%;
  1280. }
  1281. .select-input {
  1282. cursor: pointer;
  1283. }
  1284. .select-arrow {
  1285. position: absolute;
  1286. right: 16rpx;
  1287. top: 50%;
  1288. transform: translateY(-50%);
  1289. color: #999999;
  1290. font-size: 20rpx;
  1291. pointer-events: none;
  1292. }
  1293. .form-textarea {
  1294. width: 100%;
  1295. min-height: 120rpx;
  1296. background: #F8F8F8;
  1297. border: 1rpx solid #E5E5E5;
  1298. border-radius: 8rpx;
  1299. padding: 16rpx;
  1300. font-size: 28rpx;
  1301. color: #333333;
  1302. box-sizing: border-box;
  1303. resize: none;
  1304. line-height: 1.5;
  1305. }
  1306. .form-textarea:focus {
  1307. background: #FFFFFF;
  1308. border-color: #3BB44A;
  1309. }
  1310. .form-textarea::placeholder {
  1311. color: #CCCCCC;
  1312. }
  1313. .form-textarea:disabled {
  1314. background: #F8F8F8;
  1315. color: #999999;
  1316. }
  1317. .char-count {
  1318. font-size: 24rpx;
  1319. color: #CCCCCC;
  1320. text-align: right;
  1321. margin-top: 8rpx;
  1322. }
  1323. .form-error {
  1324. font-size: 24rpx;
  1325. color: #FF6B6B;
  1326. margin-top: 8rpx;
  1327. }
  1328. /* 单选按钮样式 */
  1329. .radio-group {
  1330. display: flex;
  1331. gap: 32rpx;
  1332. }
  1333. .radio-item {
  1334. display: flex;
  1335. align-items: center;
  1336. cursor: pointer;
  1337. }
  1338. .radio-circle {
  1339. width: 32rpx;
  1340. height: 32rpx;
  1341. border: 2rpx solid #CCCCCC;
  1342. border-radius: 50%;
  1343. display: flex;
  1344. align-items: center;
  1345. justify-content: center;
  1346. margin-right: 8rpx;
  1347. }
  1348. .radio-circle.radio-checked {
  1349. border-color: #3BB44A;
  1350. background-color: #3BB44A;
  1351. }
  1352. .radio-dot {
  1353. width: 12rpx;
  1354. height: 12rpx;
  1355. background-color: #FFFFFF;
  1356. border-radius: 50%;
  1357. }
  1358. .radio-label {
  1359. font-size: 28rpx;
  1360. color: #333333;
  1361. }
  1362. /* 完成状态样式 */
  1363. .status-completed {
  1364. display: flex;
  1365. align-items: center;
  1366. color: #3BB44A;
  1367. font-size: 28rpx;
  1368. }
  1369. .status-icon {
  1370. width: 32rpx;
  1371. height: 32rpx;
  1372. background: #3BB44A;
  1373. color: #FFFFFF;
  1374. border-radius: 50%;
  1375. display: flex;
  1376. align-items: center;
  1377. justify-content: center;
  1378. margin-right: 8rpx;
  1379. font-size: 20rpx;
  1380. line-height: 1;
  1381. }
  1382. /* 图片上传样式 */
  1383. .image-upload, .image-view {
  1384. width: 100%;
  1385. }
  1386. .image-list {
  1387. display: flex;
  1388. flex-wrap: wrap;
  1389. gap: 16rpx;
  1390. }
  1391. .image-preview {
  1392. position: relative;
  1393. width: 160rpx;
  1394. height: 160rpx;
  1395. border-radius: 8rpx;
  1396. overflow: hidden;
  1397. background: #F5F5F5;
  1398. }
  1399. .image-preview image {
  1400. width: 100%;
  1401. height: 100%;
  1402. }
  1403. .delete-btn {
  1404. position: absolute;
  1405. top: -6rpx;
  1406. right: -6rpx;
  1407. width: 32rpx;
  1408. height: 32rpx;
  1409. background: rgba(0, 0, 0, 0.6);
  1410. color: #FFFFFF;
  1411. border-radius: 50%;
  1412. display: flex;
  1413. align-items: center;
  1414. justify-content: center;
  1415. font-size: 20rpx;
  1416. line-height: 1;
  1417. }
  1418. .upload-btn {
  1419. width: 160rpx;
  1420. height: 160rpx;
  1421. background: #F8F8F8;
  1422. border: 2rpx dashed #DDDDDD;
  1423. border-radius: 8rpx;
  1424. display: flex;
  1425. flex-direction: column;
  1426. align-items: center;
  1427. justify-content: center;
  1428. }
  1429. .upload-icon {
  1430. font-size: 32rpx;
  1431. margin-bottom: 8rpx;
  1432. color: #999999;
  1433. }
  1434. .upload-text {
  1435. font-size: 24rpx;
  1436. color: #999999;
  1437. }
  1438. .upload-tip {
  1439. font-size: 24rpx;
  1440. color: #999999;
  1441. margin-top: 12rpx;
  1442. }
  1443. .no-images {
  1444. padding: 40rpx 0;
  1445. text-align: center;
  1446. color: #CCCCCC;
  1447. font-size: 28rpx;
  1448. }
  1449. /* 底部安全区域 */
  1450. .bottom-safe {
  1451. height: 120rpx;
  1452. }
  1453. .footer-safe {
  1454. background: #FFFFFF;
  1455. border-top: 1rpx solid #E5E5E5;
  1456. padding-bottom: constant(safe-area-inset-bottom);
  1457. padding-bottom: env(safe-area-inset-bottom);
  1458. }
  1459. .footer-content {
  1460. padding: 24rpx;
  1461. }
  1462. .submit-button {
  1463. width: 100%;
  1464. height: 88rpx;
  1465. background: #3BB44A;
  1466. color: #FFFFFF;
  1467. border: none;
  1468. border-radius: 8rpx;
  1469. font-size: 32rpx;
  1470. font-weight: 600;
  1471. display: flex;
  1472. align-items: center;
  1473. justify-content: center;
  1474. }
  1475. .submit-button:active {
  1476. background: #2D8C3C;
  1477. }
  1478. .submit-button:disabled,
  1479. .submit-button.loading {
  1480. background: #CCCCCC;
  1481. }
  1482. /* 选择器弹窗样式 */
  1483. .picker-mask {
  1484. position: fixed;
  1485. top: 0;
  1486. left: 0;
  1487. width: 100%;
  1488. height: 100%;
  1489. background: rgba(0, 0, 0, 0.5);
  1490. z-index: 999;
  1491. }
  1492. .picker-popup {
  1493. position: fixed;
  1494. bottom: 0;
  1495. left: 0;
  1496. width: 100%;
  1497. background: #FFFFFF;
  1498. border-radius: 16rpx 16rpx 0 0;
  1499. z-index: 1000;
  1500. padding-bottom: constant(safe-area-inset-bottom);
  1501. padding-bottom: env(safe-area-inset-bottom);
  1502. }
  1503. .picker-header {
  1504. display: flex;
  1505. justify-content: space-between;
  1506. align-items: center;
  1507. padding: 24rpx;
  1508. border-bottom: 1rpx solid #E5E5E5;
  1509. }
  1510. .picker-cancel, .picker-confirm {
  1511. font-size: 30rpx;
  1512. color: #3BB44A;
  1513. }
  1514. .picker-title {
  1515. font-size: 32rpx;
  1516. font-weight: 600;
  1517. color: #333333;
  1518. }
  1519. .picker-content {
  1520. max-height: 400rpx;
  1521. overflow-y: auto;
  1522. }
  1523. .picker-item {
  1524. display: flex;
  1525. justify-content: space-between;
  1526. align-items: center;
  1527. padding: 24rpx;
  1528. border-bottom: 1rpx solid #F0F0F0;
  1529. font-size: 30rpx;
  1530. color: #333333;
  1531. }
  1532. .picker-item.selected {
  1533. color: #3BB44A;
  1534. }
  1535. .picker-item:last-child {
  1536. border-bottom: none;
  1537. }
  1538. .check-mark {
  1539. color: #3BB44A;
  1540. font-size: 28rpx;
  1541. }
  1542. /* 日期时间选择器样式 */
  1543. .datetime-picker {
  1544. height: 60vh;
  1545. }
  1546. .datetime-picker-content {
  1547. height: calc(100% - 100rpx);
  1548. padding: 0;
  1549. }
  1550. .datetime-picker-view {
  1551. width: 100%;
  1552. height: 100%;
  1553. }
  1554. .picker-view-item {
  1555. display: flex;
  1556. align-items: center;
  1557. justify-content: center;
  1558. height: 80rpx;
  1559. font-size: 32rpx;
  1560. color: #333333;
  1561. }
  1562. .picker-item-text {
  1563. font-size: 32rpx;
  1564. color: #333333;
  1565. line-height: 80rpx;
  1566. height: 80rpx;
  1567. text-align: center;
  1568. }
  1569. /* 添加加载中样式 */
  1570. .picker-loading {
  1571. display: flex;
  1572. justify-content: center;
  1573. align-items: center;
  1574. padding: 30rpx 0;
  1575. color: #999;
  1576. font-size: 28rpx;
  1577. }
  1578. .picker-empty {
  1579. padding: 30rpx 0;
  1580. text-align: center;
  1581. color: #CCCCCC;
  1582. font-size: 28rpx;
  1583. }
  1584. </style>