activity-detail.vue 47 KB

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