index.vue 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956
  1. <template>
  2. <view class="bg-[#f5f7fa] min-h-screen pb-[120rpx]">
  3. <!-- 顶部操作栏 -->
  4. <view class="flex justify-around p-[20rpx] bg-white mb-[20rpx]">
  5. <view class="bg-[#e6f7ff] text-[#007aff] px-[30rpx] py-[10rpx] rounded-[40rpx] text-[28rpx]"
  6. @click="handleSave">暂存</view>
  7. <view class="bg-[#e6f7ff] text-[#007aff] px-[30rpx] py-[10rpx] rounded-[40rpx] text-[28rpx]"
  8. @click="handleSplitHousehold">分户</view>
  9. </view>
  10. <!-- 基本信息 -->
  11. <view class="bg-white rounded-[12rpx] mx-[20rpx] mb-[20rpx] p-[30rpx]">
  12. <view class="text-[32rpx] font-bold mb-[30rpx]">基本信息</view>
  13. <!-- 加载/错误状态 -->
  14. <view v-if="loading" class="py-[50rpx] text-center text-[28rpx] text-[#999]">加载中...</view>
  15. <view v-else-if="dictLoadError" class="py-[50rpx] text-center text-[28rpx] text-[#f56c6c]">
  16. <view>数据加载失败</view>
  17. <view class="text-[24rpx] mt-[10rpx]" @click="reloadDictData">点击重新加载</view>
  18. </view>
  19. <!-- 表单内容 -->
  20. <view v-else>
  21. <!-- 承包方姓名 -->
  22. <view class="flex items-center mb-[30rpx]">
  23. <view class="text-[30rpx] text-[#333] w-[200rpx]">承包方姓名<text class="text-red-500">*</text></view>
  24. <input
  25. class="flex-1 h-[80rpx] border border-[#e5e5e5] rounded-[12rpx] px-[20rpx] text-[30rpx] text-right"
  26. v-model="editForm.cbfmc" placeholder="请输入承包方姓名" />
  27. </view>
  28. <!-- 承包方编码 -->
  29. <view class="flex justify-between items-center mb-[30rpx]">
  30. <view class="text-[30rpx] text-[#666]">承包方编码</view>
  31. <view class="text-[30rpx] text-[#333]">{{ farmerInfo?.cbfbm || '未填写' }}</view>
  32. </view>
  33. <!-- 证件类型 -->
  34. <view class="flex items-center mb-[30rpx]">
  35. <view class="text-[30rpx] text-[#333] w-[200rpx]">证件类型<text class="text-red-500">*</text></view>
  36. <view class="flex-1">
  37. <view v-if="dictData.idType.length === 0"
  38. class="flex items-center justify-end text-[30rpx] text-[#999] h-[80rpx] border border-[#e5e5e5] rounded-[12rpx] px-[20rpx]">
  39. 暂无数据
  40. </view>
  41. <picker v-else @change="handleIdTypeChange" :range="dictData.idType" :range-key="'label'"
  42. :value="getIdTypeIndex()">
  43. <view
  44. class="flex items-center justify-end text-[30rpx] text-[#333] h-[80rpx] border border-[#e5e5e5] rounded-[12rpx] px-[20rpx]">
  45. {{ getDictLabel('idType', editForm.zjlx) || '请选择' }}
  46. </view>
  47. </picker>
  48. </view>
  49. </view>
  50. <!-- 证件号码 -->
  51. <view class="flex items-center mb-[30rpx]">
  52. <view class="text-[30rpx] text-[#333] w-[200rpx]">证件号码<text class="text-red-500">*</text></view>
  53. <input
  54. class="flex-1 h-[80rpx] border border-[#e5e5e5] rounded-[12rpx] px-[20rpx] text-[30rpx] text-right"
  55. v-model="editForm.cbfzjhm" placeholder="请输入证件号码" maxlength="18" />
  56. </view>
  57. <!-- 承包方类型 -->
  58. <view class="flex items-center mb-[30rpx]">
  59. <view class="text-[30rpx] text-[#333] w-[200rpx]">承包方类型<text class="text-red-500">*</text></view>
  60. <view class="flex-1">
  61. <view v-if="dictData.farmerType.length === 0"
  62. class="flex items-center justify-end text-[30rpx] text-[#999] h-[80rpx] border border-[#e5e5e5] rounded-[12rpx] px-[20rpx]">
  63. 暂无数据
  64. </view>
  65. <picker v-else @change="handleFarmerTypeChange" :range="dictData.farmerType"
  66. :range-key="'label'" :value="getFarmerTypeIndex()">
  67. <view
  68. class="flex items-center justify-end text-[30rpx] text-[#333] h-[80rpx] border border-[#e5e5e5] rounded-[12rpx] px-[20rpx]">
  69. {{ getDictLabel('farmerType', editForm.cbflx) || '请选择' }}
  70. </view>
  71. </picker>
  72. </view>
  73. </view>
  74. <!-- 联系电话 -->
  75. <view class="flex items-center mb-[30rpx]">
  76. <view class="text-[30rpx] text-[#333] w-[200rpx]">联系电话<text class="text-red-500">*</text></view>
  77. <input
  78. class="flex-1 h-[80rpx] border border-[#e5e5e5] rounded-[12rpx] px-[20rpx] text-[30rpx] text-right"
  79. v-model="editForm.lxdh" placeholder="请输入联系电话" type="number" maxlength="11" />
  80. </view>
  81. <!-- 邮政编码 -->
  82. <view class="flex items-center mb-[30rpx]">
  83. <view class="text-[30rpx] text-[#666] w-[200rpx]">邮政编码</view>
  84. <input
  85. class="flex-1 h-[80rpx] border border-[#e5e5e5] rounded-[12rpx] px-[20rpx] text-[30rpx] text-right"
  86. v-model="editForm.yzbm" placeholder="请输入邮政编码" type="number" maxlength="6" />
  87. </view>
  88. <!-- 承包方地址 -->
  89. <view class="flex items-start mb-[20rpx]">
  90. <view class="text-[30rpx] text-[#333] w-[200rpx] pt-[10rpx]">承包方地址<text
  91. class="text-red-500">*</text></view>
  92. <textarea
  93. class="flex-1 border border-[#e5e5e5] rounded-[12rpx] px-[20rpx] py-[15rpx] text-[30rpx] h-[120rpx]"
  94. v-model="editForm.cbfdz" placeholder="请输入承包方地址"></textarea>
  95. </view>
  96. </view>
  97. </view>
  98. <!-- 签名预览区域 -->
  99. <view class="bg-white rounded-[12rpx] mx-[20rpx] mb-[20rpx] p-[30rpx]" v-if="signaturePreview">
  100. <view class="text-[32rpx] font-bold mb-[20rpx]">签名预览</view>
  101. <image :src="signaturePreview" mode="widthFix"
  102. class="w-[300rpx] h-[120rpx] border border-[#eee] rounded-[10rpx]"></image>
  103. <view class="text-[28rpx] text-[#007aff] mt-[15rpx]" @click="openSignatureDialog">
  104. 重新签名
  105. </view>
  106. </view>
  107. <!-- 家庭成员/地块信息/附件信息 -->
  108. <view class="bg-white rounded-[12rpx] mx-[20rpx] mb-[20rpx] p-[30rpx]">
  109. <view class="flex justify-between items-center mb-[30rpx]">
  110. <view class="text-[32rpx] font-bold">家庭成员</view>
  111. <view class="text-[28rpx] text-[#007aff]" @click="handleChangeHouseholder">更改户主</view>
  112. </view>
  113. <view class="flex justify-between items-center mb-[20rpx]">
  114. <view class="text-[30rpx] text-[#333]">家庭成员信息({{ familyMembers.length }}) <text
  115. class="text-[#999] text-[24rpx]">✎</text></view>
  116. <view class="text-[28rpx] text-[#007aff]" @click="handleAddFamilyMember">+新增</view>
  117. </view>
  118. <view v-for="(item, index) in familyMembers" :key="index"
  119. class="bg-[#f8f9fa] rounded-[12rpx] p-[20rpx] mb-[20rpx] flex justify-between items-center"
  120. @click="handleEditFamilyMember(item, index)">
  121. <view>
  122. <view class="text-[30rpx] text-[#333] mb-[10rpx]">
  123. {{ item.cyxm }}&nbsp;&nbsp;&nbsp;&nbsp;
  124. {{ getDictLabel('cyxb', item.cyxb) }}&nbsp;&nbsp;&nbsp;&nbsp;
  125. {{ getDictLabel('yhzgx', item.yhzgx) }}
  126. </view>
  127. <view class="text-[32rpx] text-[#666] mt-[40rpx]">{{ item.cyzjhm }}</view>
  128. </view>
  129. <view class="text-[40rpx] text-[#ccc]">
  130. <uni-icons type="right" size="30"></uni-icons>
  131. </view>
  132. </view>
  133. </view>
  134. <view class="bg-white rounded-[12rpx] mx-[20rpx] mb-[20rpx] p-[30rpx]">
  135. <view class="flex items-center mb-[30rpx]">
  136. <view class="text-[32rpx] font-bold">地块信息</view>
  137. </view>
  138. <view class="text-[28rpx] text-[#666] mb-[20rpx] flex">
  139. <view class="flex-1 ">{{ landList.length }}块</view>
  140. <view class="text-[28rpx] text-[#007aff]" @click="handleAddLand">+新增 </view>
  141. <view class="text-[28rpx] text-[#007aff] ml-[20rpx]" @click="handleChooseRange('farmer','all')">+全部地块位置
  142. </view>
  143. </view>
  144. <view v-for="(item, index) in landList" :key="index"
  145. class="bg-[#f8f9fa] rounded-[12rpx] p-[20rpx] mb-[20rpx]" @click="handleEditLand(item, index)">
  146. <view class="flex justify-between items-center mb-[10rpx]">
  147. <view class="text-[30rpx] text-[#007aff] flex">
  148. <view class="">
  149. {{ item.dkDetail.dkbm ? item.dkDetail.dkbm.slice(-5) : '' }}
  150. </view>
  151. <view class="ml-[20rpx]">
  152. {{ item.dkDetail.dkmc }}
  153. </view>
  154. <view class="ml-[20rpx]">
  155. {{ item.htmjm }}亩
  156. </view>
  157. </view>
  158. <view class="text-[28rpx] text-[#007aff]" @click.stop="handleChooseRange('farmer','single',item)">
  159. 地块位置</view>
  160. </view>
  161. <view class="text-[32rpx] text-[#666] mb-[5rpx] flex ">
  162. <view class="">
  163. 地块东至:{{ item.dkDetail.dkdz }}
  164. </view>
  165. <view class="ml-[100rpx]">
  166. 地块西至:{{ item.dkDetail.dkxz }}
  167. </view>
  168. </view>
  169. <view class="text-[32rpx] text-[#666] flex mt-[20rpx]">
  170. <view class="">
  171. 地块南至:{{ item.dkDetail.dknz }}
  172. </view>
  173. <view class="ml-[100rpx]">
  174. 地块北至:{{ item.dkDetail.dkbz }}
  175. </view>
  176. </view>
  177. <view class="text-right text-[40rpx] text-[#ccc] mt-[10rpx]">
  178. <uni-icons type="right" size="30"></uni-icons>
  179. </view>
  180. </view>
  181. </view>
  182. <view class="bg-white rounded-[12rpx] mx-[20rpx] mb-[20rpx] p-[30rpx]">
  183. <view class="flex justify-between items-center">
  184. <view class="text-[32rpx] font-bold">附件信息</view>
  185. <view class="text-[28rpx] text-[#007aff]" @click="handleUploadAttachment">
  186. {{ attachmentList.length > 0 ? `√ 已上传${attachmentList.length}个` : '√ 暂无' }}
  187. </view>
  188. </view>
  189. </view>
  190. <!-- 底部 签名 + 提交 双按钮 -->
  191. <view class="fixed bottom-0 left-0 right-0 w-full bg-white pb-[20rpx] pt-[20rpx] shadow-lg px-[30rpx]">
  192. <view class="flex items-center mb-[20rpx]">
  193. <checkbox :checked="isAgree" @click="toggleAgree" @change="handleAgreeChange" />
  194. <text class="text-[24rpx] text-[#666] ml-[10rpx]">我已认真并确认以上信息无误</text>
  195. </view>
  196. <view class="flex gap-[20rpx]">
  197. <!-- 签名按钮 -->
  198. <button class="flex-1 h-[88rpx] bg-[#15b7b9] text-white rounded-[44rpx] text-[32rpx]"
  199. :disabled="!isAgree || dictLoadError" :class="{ 'bg-gray-400': !isAgree || dictLoadError }"
  200. @click="openSignatureDialog">
  201. {{ signaturePreview ? '重新签名' : '签名' }}
  202. </button>
  203. <!-- 提交按钮 -->
  204. <button class="flex-1 h-[88rpx] bg-[#007aff] text-white rounded-[44rpx] text-[32rpx]"
  205. :disabled="!isAgree || !signaturePreview || dictLoadError || dictData.farmerType.length === 0 || dictData.idType.length === 0"
  206. :class="{ 'bg-gray-400': !isAgree || !signaturePreview || dictLoadError || dictData.farmerType.length === 0 || dictData.idType.length === 0 }"
  207. @click="submitAllData">
  208. 提交
  209. </button>
  210. </view>
  211. </view>
  212. <!-- 🔴 签名弹窗 -->
  213. <view v-if="showSignatureDialog" class="signature-overlay" @click="closeSignatureDialog">
  214. <view class="signature-dialog" @click.stop>
  215. <view class="signature-header">
  216. <text class="signature-title">请手写签名</text>
  217. <text class="close-btn" @click="closeSignatureDialog">×</text>
  218. </view>
  219. <canvas ref="canvasRef" class="signature-canvas" canvas-id="signatureCanvas" @touchstart="onTouchStart"
  220. @touchmove="onTouchMove" @touchend="onTouchEnd" @touchcancel="onTouchEnd"></canvas>
  221. <view class="signature-btns">
  222. <button type="default" @click="clearSignature" class="signature-btn signature-btn-clear">清空</button>
  223. <button type="primary" @click="confirmSignatureOnly"
  224. class="signature-btn signature-btn-confirm">确认签名</button>
  225. </view>
  226. </view>
  227. </view>
  228. <!-- 🔴 更改户主弹窗 -->
  229. <view v-if="showChangeHouseholderDialog" class="dialog-overlay">
  230. <view class="dialog-container" @click.stop>
  231. <view class="dialog-header">
  232. <text class="dialog-title">更改户主</text>
  233. </view>
  234. <view class="dialog-content">
  235. <view class="dialog-tip">
  236. 通过两步更换户主,第一步选择新户主、第二步修改与户主关系,并上传户口本、身份证扫描件。
  237. </view>
  238. <view class="dialog-section">
  239. <view class="section-title">原户主信息</view>
  240. <view class="section-content">{{ originalHouseholderInfo }}</view>
  241. </view>
  242. <view class="dialog-section">
  243. <view class="section-title">选择新户主</view>
  244. <view class="radio-list">
  245. <view v-for="(member, index) in availableHouseholders" :key="member.cyzjhm || index"
  246. class="radio-item" @click.stop="selectNewHouseholder(member)">
  247. <view class="radio-icon"
  248. :class="{ 'radio-checked': selectedNewHouseholder?.cyzjhm === member.cyzjhm }">
  249. <text v-if="selectedNewHouseholder?.cyzjhm === member.cyzjhm"
  250. class="check-icon">✓</text>
  251. </view>
  252. <view class="radio-label">{{ member.cyxm }}({{ member.cyzjhm }})</view>
  253. </view>
  254. </view>
  255. </view>
  256. <view class="dialog-section">
  257. <view class="section-title">原户主处理方式</view>
  258. <view class="radio-list">
  259. <view class="radio-item" @click.stop="selectOriginalHandler('keep')">
  260. <view class="radio-icon" :class="{ 'radio-checked': originalHandler === 'keep' }">
  261. <text v-if="originalHandler === 'keep'" class="check-icon">✓</text>
  262. </view>
  263. <view class="radio-label">保留【改为家庭成员】</view>
  264. </view>
  265. <view class="radio-item" @click.stop="selectOriginalHandler('delete')">
  266. <view class="radio-icon" :class="{ 'radio-checked': originalHandler === 'delete' }">
  267. <text v-if="originalHandler === 'delete'" class="check-icon">✓</text>
  268. </view>
  269. <view class="radio-label">删除【去世、户口迁出等情况】</view>
  270. </view>
  271. </view>
  272. </view>
  273. </view>
  274. <view class="dialog-footer">
  275. <view class="dialog-btn dialog-btn-cancel" @click.stop="closeChangeHouseholderDialog">取消</view>
  276. <view class="dialog-btn dialog-btn-next" @click.stop="handleNextStep">下一步</view>
  277. </view>
  278. </view>
  279. </view>
  280. </view>
  281. </template>
  282. <script setup lang="ts">
  283. import { ref, computed, nextTick } from 'vue'
  284. import { onLoad } from '@dcloudio/uni-app';
  285. // @ts-ignore
  286. import { getfarmerLoginData, getDictData, getCbfjtcyData, getCbfdkxxData, getdkxxxxData, getAutographData, submitFamilyMembersData, submitParcelInformationData } from '@/api/farmerApi.js';
  287. // 核心状态
  288. const loading = ref<boolean>(false);
  289. const isAgree = ref<boolean>(false);
  290. const dictLoadError = ref<boolean>(false);
  291. // 签名相关
  292. const showSignatureDialog = ref(false);
  293. const canvasRef = ref(null);
  294. const signaturePreview = ref(''); // 签名预览图
  295. const signatureBase64 = ref(''); // 签名base64
  296. const signatureFileName = ref(''); // 签名文件名
  297. let ctx = null;
  298. let isWriting = false;
  299. let points = [];
  300. const CANVAS_WIDTH = 350;
  301. const CANVAS_HEIGHT = 200;
  302. // 更改户主弹窗
  303. const showChangeHouseholderDialog = ref<boolean>(false);
  304. const selectedNewHouseholder = ref<any>(null);
  305. const originalHandler = ref<string>('keep');
  306. const farmerInfo = ref<any>(null);
  307. const dictData = ref<any>({
  308. idType: [],
  309. farmerType: [],
  310. cyxb: [],
  311. yhzgx: []
  312. });
  313. const getCbfbmjump = ref<string[]>([]);
  314. const editForm = ref({
  315. cbfmc: '',
  316. zjlx: '',
  317. cbfzjhm: '',
  318. cbflx: '',
  319. lxdh: '',
  320. yzbm: '',
  321. cbfdz: ''
  322. });
  323. const familyMembers = ref<any[]>([]);
  324. const landList = ref([]);
  325. const attachmentList = ref<any[]>([]);
  326. // 户主相关计算
  327. const originalHouseholderInfo = computed(() => {
  328. const householder = familyMembers.value.find(item => item.yhzgx === 0);
  329. if (householder) {
  330. return `${householder.cyxm}(${householder.cyzjhm})`;
  331. }
  332. return farmerInfo.value ? `${farmerInfo.value.cbfmc}(${farmerInfo.value.cbfzjhm})` : '无';
  333. });
  334. const availableHouseholders = computed(() => {
  335. const householder = familyMembers.value.find(item => item.yhzgx === 0);
  336. return familyMembers.value.filter(item => item.cyzjhm !== householder?.cyzjhm);
  337. });
  338. // 工具方法
  339. const transformDictFormat = (originalDict : Record<string | number, string>) => {
  340. if (!originalDict || typeof originalDict !== 'object') return [];
  341. return Object.entries(originalDict).map(([key, value]) => ({
  342. value: String(key),
  343. label: value as string
  344. }));
  345. };
  346. const getDictLabel = (dictType : string, value : string | number | undefined) => {
  347. if (!value || !dictData.value[dictType]?.length) return '未知';
  348. const item = dictData.value[dictType].find((item : any) => String(item.value) === String(value));
  349. return item?.label || '未知';
  350. };
  351. const getIdTypeIndex = () => {
  352. const targetValue = editForm.value.zjlx || farmerInfo.value?.cbfzjlx;
  353. if (!targetValue || !dictData.value.idType.length) return 0;
  354. return dictData.value.idType.findIndex((item : any) => String(item.value) === String(targetValue));
  355. };
  356. const getFarmerTypeIndex = () => {
  357. const targetValue = editForm.value.cbflx || farmerInfo.value?.cbflx;
  358. if (!targetValue || !dictData.value.farmerType.length) return 0;
  359. return dictData.value.farmerType.findIndex((item : any) => String(item.value) === String(targetValue));
  360. };
  361. const handleIdTypeChange = (e : any) => {
  362. if (!dictData.value.idType.length) return;
  363. editForm.value.zjlx = dictData.value.idType[e.detail.value]?.value || '';
  364. };
  365. const handleFarmerTypeChange = (e : any) => {
  366. if (!dictData.value.farmerType.length) return;
  367. editForm.value.cbflx = dictData.value.farmerType[e.detail.value]?.value || '';
  368. };
  369. const handleAgreeChange = (e : any) => {
  370. isAgree.value = e.detail.value;
  371. };
  372. const toggleAgree = () => {
  373. isAgree.value = !isAgree.value;
  374. };
  375. // ==================== 签名功能 ====================
  376. function initCanvas() {
  377. ctx = uni.createCanvasContext('signatureCanvas');
  378. ctx.fillStyle = '#ffffff';
  379. ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  380. ctx.setStrokeStyle('#000000');
  381. ctx.setLineWidth(2);
  382. ctx.setLineCap('round');
  383. ctx.setLineJoin('round');
  384. ctx.setMiterLimit(10);
  385. ctx.draw(true);
  386. points = [];
  387. isWriting = false;
  388. }
  389. function drawSmoothLine(points) {
  390. if (points.length < 2) return;
  391. ctx.beginPath();
  392. ctx.moveTo(points[0].x, points[0].y);
  393. for (let i = 1; i < points.length - 1; i++) {
  394. const p0 = points[i - 1];
  395. const p1 = points[i];
  396. const cpX = (p0.x + p1.x) / 2;
  397. const cpY = (p0.y + p1.y) / 2;
  398. ctx.quadraticCurveTo(cpX, cpY, p1.x, p1.y);
  399. }
  400. const last = points[points.length - 1];
  401. const prev = points[points.length - 2];
  402. ctx.quadraticCurveTo(prev.x, prev.y, last.x, last.y);
  403. ctx.stroke();
  404. ctx.draw(true);
  405. }
  406. function onTouchStart(e) {
  407. isWriting = true;
  408. const { x, y } = e.touches[0];
  409. points = [{ x, y }];
  410. }
  411. function onTouchMove(e) {
  412. if (!isWriting) return;
  413. const { x, y } = e.touches[0];
  414. points.push({ x, y });
  415. drawSmoothLine(points);
  416. }
  417. function onTouchEnd() {
  418. isWriting = false;
  419. points = [];
  420. }
  421. function clearSignature() {
  422. if (!ctx) return;
  423. ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  424. ctx.fillStyle = '#ffffff';
  425. ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  426. ctx.draw(true);
  427. points = [];
  428. isWriting = false;
  429. }
  430. function openSignatureDialog() {
  431. if (!isAgree.value || !validateForm()) return;
  432. showSignatureDialog.value = true;
  433. nextTick(() => {
  434. initCanvas();
  435. });
  436. }
  437. function closeSignatureDialog() {
  438. showSignatureDialog.value = false;
  439. ctx = null;
  440. points = [];
  441. isWriting = false;
  442. }
  443. // 仅确认签名 → 本地暂存 + 预览
  444. function confirmSignatureOnly() {
  445. if (!ctx) {
  446. uni.showToast({ title: '画布异常', icon: 'none' });
  447. return;
  448. }
  449. uni.showLoading({ title: '生成签名...' });
  450. uni.canvasToTempFilePath({
  451. canvasId: 'signatureCanvas',
  452. width: 350,
  453. height: 200,
  454. destWidth: 700,
  455. destHeight: 400,
  456. success: (res) => {
  457. const tempPath = res.tempFilePath;
  458. const lastSlash = tempPath.lastIndexOf('/');
  459. const pngPos = tempPath.lastIndexOf('.png');
  460. const fileName = tempPath.slice(lastSlash + 1, pngPos + 4);
  461. uni.getFileSystemManager().readFile({
  462. filePath: tempPath,
  463. encoding: 'base64',
  464. success: (data) => {
  465. uni.hideLoading();
  466. signatureBase64.value = 'data:image/png;base64,' + data.data;
  467. signatureFileName.value = fileName;
  468. signaturePreview.value = tempPath; // 预览图
  469. uni.showToast({ title: '签名成功', icon: 'success' });
  470. closeSignatureDialog();
  471. },
  472. fail: () => {
  473. uni.hideLoading();
  474. uni.showToast({ title: '签名失败', icon: 'none' });
  475. }
  476. });
  477. },
  478. fail: () => {
  479. uni.hideLoading();
  480. uni.showToast({ title: '签名失败', icon: 'none' });
  481. }
  482. });
  483. }
  484. // 最终提交 → 上传签名
  485. async function submitAllData() {
  486. //家庭成员判断, bizStatus: 不为null的数据为新增数据
  487. let farmerResult = familyMembers.value.filter(item => item.bizStatus !== null);
  488. let farmerIDS = farmerResult.filter(i => i?.id).map(i => i.id);
  489. //判断地块中存在bizStatus则为新增,无此字段为老数据
  490. const lanDresult = landList.value.filter(item => 'bizStatus' in item.dkDetail);
  491. let landListIDS = lanDresult.filter(i => i?.dkDetail.bsm).map(i => i?.dkDetail.bsm);
  492. console.log(123, familyMembers.value, landList.value,landListIDS)
  493. if (!signatureBase64.value) {
  494. uni.showToast({ title: '请先完成签名', icon: 'none' });
  495. return;
  496. }
  497. uni.showLoading({ title: '提交中...' });
  498. try {
  499. const autographData = {
  500. base64: signatureBase64.value,
  501. fileName: signatureFileName.value,
  502. businessType: 'APPLICANT_SIGNATURE_JTCY'
  503. };
  504. await getAutographData(autographData);
  505. await submitFamilyMembersData(farmerIDS);
  506. await submitParcelInformationData(landListIDS);
  507. uni.hideLoading();
  508. uni.showToast({ title: '提交成功', icon: 'success' });
  509. setTimeout(() => {
  510. uni.navigateTo({ url: `/pages/farmer/index` })
  511. }, 1500);
  512. } catch (err) {
  513. uni.hideLoading();
  514. uni.showToast({ title: '提交失败', icon: 'none' });
  515. }
  516. }
  517. // 更改户主
  518. const handleChangeHouseholder = () => {
  519. selectedNewHouseholder.value = null;
  520. originalHandler.value = 'keep';
  521. showChangeHouseholderDialog.value = true;
  522. };
  523. const closeChangeHouseholderDialog = () => {
  524. showChangeHouseholderDialog.value = false;
  525. };
  526. const selectNewHouseholder = (member : any) => {
  527. selectedNewHouseholder.value = member;
  528. };
  529. const selectOriginalHandler = (type : string) => {
  530. originalHandler.value = type;
  531. };
  532. const handleNextStep = () => {
  533. if (!selectedNewHouseholder.value) {
  534. uni.showToast({ title: '请选择新户主', icon: 'none' });
  535. return;
  536. }
  537. closeChangeHouseholderDialog();
  538. uni.showToast({ title: '已选择新户主', icon: 'success' });
  539. };
  540. // 加载数据
  541. const loadDictData = async () => {
  542. try {
  543. dictLoadError.value = false;
  544. const [resFarmer, resIdType, resCyxb, resYhzgx] = await Promise.all([
  545. getDictData('dic_cbflx'),
  546. getDictData('dic_zjlx'),
  547. getDictData('dic_xb'),
  548. getDictData('dic_yhzgx')
  549. ]);
  550. dictData.value.farmerType = resFarmer?.code === 200 ? transformDictFormat(resFarmer.data) : [];
  551. dictData.value.idType = resIdType?.code === 200 ? transformDictFormat(resIdType.data) : [];
  552. dictData.value.cyxb = resCyxb?.code === 200 ? transformDictFormat(resCyxb.data) : [];
  553. dictData.value.yhzgx = resYhzgx?.code === 200 ? transformDictFormat(resYhzgx.data) : [];
  554. } catch (err) {
  555. dictLoadError.value = true;
  556. }
  557. };
  558. const reloadDictData = async () => {
  559. loading.value = true;
  560. await loadDictData();
  561. loading.value = false;
  562. };
  563. const getTotal = async (cbfbm : string) => {
  564. try {
  565. const res = await getfarmerLoginData({ cbfbm });
  566. if (res?.data?.length) {
  567. const info = res.data[0];
  568. farmerInfo.value = { ...info };
  569. editForm.value = {
  570. cbfmc: info.cbfmc || '',
  571. zjlx: info.cbfzjlx || '',
  572. cbfzjhm: info.cbfzjhm || '',
  573. cbflx: info.cbflx || '',
  574. lxdh: info.lxdh || '',
  575. yzbm: info.yzbm || '',
  576. cbfdz: info.cbfdz || ''
  577. };
  578. }
  579. } catch (err) { }
  580. };
  581. const getcbfjtcyTotal = async (cbfbm : string) => {
  582. try {
  583. const res = await getCbfjtcyData({ cbfbm });
  584. if (res?.data?.length){
  585. familyMembers.value = res.data;
  586. }
  587. } catch (err) { }
  588. };
  589. const getcbfDkxxTotal = async (cbfbm : string) => {
  590. try {
  591. const res = await getCbfdkxxData({ cbfbm });
  592. if (!res?.data?.length) return;
  593. const list = [...res.data];
  594. for (let i = 0; i < list.length; i++) {
  595. if (list[i]?.dkbm) {
  596. const dk = await getdkxxxxData({ dkbm: list[i].dkbm });
  597. list[i].dkDetail = dk?.data[0].dk || dk?.data[0].dkTemp || {};
  598. list[i].attachments = dk?.data[0].attachments || {};
  599. }
  600. }
  601. landList.value = list;
  602. console.log(123, landList.value)
  603. } catch (err) { }
  604. };
  605. const validateForm = () => {
  606. const { cbfmc, zjlx, cbfzjhm, cbflx, lxdh, cbfdz } = editForm.value;
  607. if (!cbfmc) return uni.showToast({ title: '请输入姓名', icon: 'none' });
  608. if (!zjlx) return uni.showToast({ title: '请选择证件类型', icon: 'none' });
  609. if (!cbfzjhm) return uni.showToast({ title: '请输入证件号', icon: 'none' });
  610. if (!cbflx) return uni.showToast({ title: '请选择承包方类型', icon: 'none' });
  611. if (!lxdh || lxdh.length !== 11) return uni.showToast({ title: '请输入11位手机号', icon: 'none' });
  612. if (!cbfdz) return uni.showToast({ title: '请输入地址', icon: 'none' });
  613. return true;
  614. };
  615. onLoad(async (options : any) => {
  616. loading.value = true;
  617. await loadDictData();
  618. if (options.groupInfo) {
  619. getCbfbmjump.value = JSON.parse(decodeURIComponent(options.groupInfo)) || [];
  620. console.log(12333, getCbfbmjump.value.cbfbm)
  621. const cbfbm = getCbfbmjump.value.cbfbm || '';
  622. const cbfmc = getCbfbmjump.value.cbfmc || '';
  623. await getTotal(cbfbm);
  624. await getcbfjtcyTotal(cbfbm);
  625. await getcbfDkxxTotal(cbfbm);
  626. }
  627. loading.value = false;
  628. });
  629. const handleSave = () => {
  630. if (!validateForm()) return;
  631. uni.showToast({ title: '暂存成功', icon: 'success' });
  632. };
  633. const handleSplitHousehold = () => {
  634. uni.showModal({ title: '提示', content: '确定分户?' });
  635. };
  636. const handleAddFamilyMember = () => uni.navigateTo({ url: `/pages/farmer/addFamilyMember?memberInfo=${encodeURIComponent(JSON.stringify(getCbfbmjump.value))}` });
  637. const handleEditFamilyMember = (item : any, index : number) => uni.navigateTo({
  638. url: `/pages/farmer/editFamilyMember?memberInfo=${encodeURIComponent(JSON.stringify(item))}&index=${index}`
  639. });
  640. const handleAddLand = () => uni.navigateTo({ url: `/pages/land/addLand?cbfbm=${getCbfbmjump.value.cbfbm}` });
  641. const handleChooseRange = (index, dsType, item) => {
  642. const tem_code = ref('');
  643. if (item) {
  644. tem_code.value = item.dkbm;
  645. } else {
  646. tem_code.value = getCbfbmjump.value.cbfbm;
  647. }
  648. uni.navigateTo({ url: `/pages/map/map?memberInfo=${encodeURIComponent(JSON.stringify(tem_code.value))}&type=${index}&displayType=${dsType}` })
  649. };
  650. const handleEditLand = (item) => uni.navigateTo({ url: `/pages/land/editLand?info=${JSON.stringify(item)}&cbfbm=${getCbfbmjump.value.cbfbm}` });
  651. const handleViewLandPosition = (item) => uni.navigateTo({ url: `/pages/land/landPosition?code=${item.dkbm}` });
  652. const handleUploadAttachment = () => {
  653. uni.chooseImage({
  654. count: 5,
  655. success: (res) => {
  656. attachmentList.value.push(...res.tempFilePaths);
  657. }
  658. });
  659. };
  660. </script>
  661. <style lang="scss" scoped>
  662. page {
  663. background-color: #f5f7fa;
  664. }
  665. button[disabled] {
  666. background-color: #999 !important;
  667. }
  668. .bg-gray-400 {
  669. background-color: #999 !important;
  670. }
  671. input,
  672. textarea {
  673. box-sizing: border-box;
  674. }
  675. /* 签名弹窗 */
  676. .signature-overlay {
  677. position: fixed;
  678. top: 0;
  679. left: 0;
  680. right: 0;
  681. bottom: 0;
  682. background: rgba(0, 0, 0, 0.6);
  683. display: flex;
  684. align-items: center;
  685. justify-content: center;
  686. z-index: 99999;
  687. }
  688. .signature-dialog {
  689. width: 90%;
  690. max-width: 700rpx;
  691. background: #ffffff;
  692. border-radius: 16rpx;
  693. padding: 30rpx;
  694. box-sizing: border-box;
  695. }
  696. .signature-header {
  697. display: flex;
  698. justify-content: space-between;
  699. align-items: center;
  700. margin-bottom: 20rpx;
  701. }
  702. .signature-title {
  703. font-size: 32rpx;
  704. font-weight: bold;
  705. color: #333;
  706. }
  707. .close-btn {
  708. font-size: 48rpx;
  709. color: #999;
  710. line-height: 1;
  711. padding: 0 10rpx;
  712. }
  713. .signature-canvas {
  714. width: 100%;
  715. height: 400rpx;
  716. border: 2rpx solid #e5e5e5;
  717. background: #ffffff;
  718. border-radius: 8rpx;
  719. touch-action: none;
  720. box-sizing: border-box;
  721. }
  722. .signature-btns {
  723. display: flex;
  724. justify-content: center;
  725. gap: 30rpx;
  726. margin-top: 30rpx;
  727. }
  728. .signature-btn {
  729. flex: 1;
  730. height: 80rpx;
  731. border-radius: 40rpx;
  732. font-size: 30rpx;
  733. line-height: 80rpx;
  734. padding: 0;
  735. }
  736. .signature-btn-clear {
  737. background: #f5f5f5;
  738. color: #666;
  739. border: 1rpx solid #e5e5e5;
  740. }
  741. .signature-btn-confirm {
  742. background: #00b42a;
  743. color: #fff;
  744. border: none;
  745. }
  746. /* 户主弹窗 */
  747. .dialog-overlay {
  748. position: fixed;
  749. top: 0;
  750. left: 0;
  751. right: 0;
  752. bottom: 0;
  753. background: rgba(0, 0, 0, 0.5);
  754. display: flex;
  755. align-items: center;
  756. justify-content: center;
  757. z-index: 9999;
  758. }
  759. .dialog-container {
  760. background: #fff;
  761. border-radius: 16rpx;
  762. width: 90%;
  763. max-width: 700rpx;
  764. overflow: hidden;
  765. }
  766. .dialog-header {
  767. background: #409eff;
  768. padding: 20rpx;
  769. text-align: center;
  770. }
  771. .dialog-title {
  772. color: #fff;
  773. font-size: 34rpx;
  774. font-weight: bold;
  775. }
  776. .dialog-content {
  777. padding: 30rpx;
  778. max-height: 70vh;
  779. overflow-y: auto;
  780. }
  781. .dialog-tip {
  782. color: #f5222d;
  783. font-size: 28rpx;
  784. margin-bottom: 20rpx;
  785. }
  786. .dialog-section {
  787. margin-bottom: 30rpx;
  788. }
  789. .section-title {
  790. font-size: 32rpx;
  791. margin-bottom: 15rpx;
  792. font-weight: 500;
  793. }
  794. .radio-list {
  795. display: flex;
  796. flex-direction: column;
  797. gap: 20rpx;
  798. }
  799. .radio-item {
  800. display: flex;
  801. align-items: center;
  802. gap: 16rpx;
  803. }
  804. .radio-icon {
  805. width: 36rpx;
  806. height: 36rpx;
  807. border-radius: 50%;
  808. border: 2rpx solid #ccc;
  809. display: flex;
  810. align-items: center;
  811. justify-content: center;
  812. }
  813. .radio-checked {
  814. background: #409eff;
  815. border-color: #409eff;
  816. }
  817. .check-icon {
  818. color: #fff;
  819. font-size: 24rpx;
  820. }
  821. .dialog-footer {
  822. display: flex;
  823. padding: 20rpx 30rpx 30rpx;
  824. gap: 20rpx;
  825. }
  826. .dialog-btn {
  827. flex: 1;
  828. height: 80rpx;
  829. border-radius: 40rpx;
  830. display: flex;
  831. align-items: center;
  832. justify-content: center;
  833. font-size: 32rpx;
  834. }
  835. .dialog-btn-cancel {
  836. background: #e5e5e5;
  837. color: #666;
  838. }
  839. .dialog-btn-next {
  840. background: #409eff;
  841. color: #fff;
  842. }
  843. </style>