contract-form.vue 90 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743
  1. <!-- 合同流程专用表单组件 -->
  2. <template>
  3. <view class="contract-form-component">
  4. <!-- 基本信息 -->
  5. <uni-card>
  6. <uni-section titleFontSize="1.3rem" title="基本信息" type="line"></uni-section>
  7. <uni-forms ref="baseFormRef" :modelValue="baseForm" :rules="baseFormRules" label-position="left" :label-width="100" :border="true">
  8. <uni-forms-item name="contract_number" label="合同编号">
  9. <!-- 发起环节或字段可编辑时显示输入框 -->
  10. <uni-easyinput v-if="isInitiateOrFieldEditable('contract_number')"
  11. v-model="baseForm.contract_number"
  12. placeholder="请输入合同编号"></uni-easyinput>
  13. <!-- 否则显示只读文本 -->
  14. <uni-easyinput v-else v-model="baseForm.contract_number" disabled></uni-easyinput>
  15. </uni-forms-item>
  16. <uni-forms-item name="contract_name" label="合同名称" required>
  17. <uni-easyinput v-model="baseForm.contract_name"
  18. :disabled="!isInitiateOrFieldEditable('contract_name')"
  19. :placeholder="'请输入合同名称'"
  20. :clearable="false"></uni-easyinput>
  21. </uni-forms-item>
  22. <uni-forms-item name="contract_type" label="合同类型" required>
  23. <!-- 合同类型:发起环节或字段可编辑时显示选择器 -->
  24. <picker v-if="isInitiateOrFieldEditable('contract_type')"
  25. @change="onContractTypeChange"
  26. :range="contractTypeList"
  27. range-key="contract_type_name">
  28. <view class="picker">{{ baseForm.contract_type_name || '请选择合同类型' }}</view>
  29. </picker>
  30. <uni-easyinput v-else v-model="baseForm.contract_type_name" disabled></uni-easyinput>
  31. </uni-forms-item>
  32. <uni-forms-item name="applyDate" label="填报日期">
  33. <uni-easyinput v-model="baseForm.applyDate" disabled></uni-easyinput>
  34. </uni-forms-item>
  35. <uni-forms-item name="department" label="填报部门">
  36. <uni-easyinput v-model="baseForm.department" disabled></uni-easyinput>
  37. </uni-forms-item>
  38. <uni-forms-item name="salesman_name" label="经办人">
  39. <!-- 经办人:发起环节或字段可编辑时显示选择器 -->
  40. <!-- <uni-easyinput v-if="isInitiateOrFieldEditable('salesman_name')"
  41. v-model="baseForm.salesman_name"
  42. :disabled="!isInitiateOrFieldEditable('salesman_name')"
  43. placeholder="自动生成"></uni-easyinput>-->
  44. <uni-easyinput v-model="baseForm.salesman_name" disabled></uni-easyinput>
  45. </uni-forms-item>
  46. <uni-forms-item name="salesmanTel" label="联系电话">
  47. <uni-easyinput v-model="baseForm.salesmanTel"
  48. :disabled="!isInitiateOrFieldEditable('salesmanTel')"
  49. type="number"
  50. placeholder="请输入联系电话"></uni-easyinput>
  51. </uni-forms-item>
  52. <uni-forms-item name="firstparty_name" label="需方" required>
  53. <!-- 销售合同:显示选择客户按钮 -->
  54. <view v-if="isSalesContract && isInitiateOrFieldEditable('firstparty_name')"
  55. class="selector-wrapper"
  56. @click="openClientSelector">
  57. <text v-if="baseForm.firstparty_name">{{ baseForm.firstparty_name }}</text>
  58. <text v-else class="placeholder">请选择客户</text>
  59. </view>
  60. <!-- 其他合同类型或不可编辑时:显示公司名称(只读) -->
  61. <uni-easyinput v-else v-model="baseForm.firstparty_name" disabled></uni-easyinput>
  62. </uni-forms-item>
  63. <uni-forms-item name="secondparty_name" label="供方" required>
  64. <!-- 销售合同:显示公司名称(只读) -->
  65. <uni-easyinput v-if="isSalesContract" v-model="baseForm.secondparty_name" disabled></uni-easyinput>
  66. <!-- 其他合同类型:发起环节或字段可编辑时显示选择器 -->
  67. <view v-else-if="isInitiateOrFieldEditable('secondparty_name')"
  68. class="selector-wrapper"
  69. @click="openSupplierSelector">
  70. <text v-if="baseForm.secondparty_name">{{ baseForm.secondparty_name }}</text>
  71. <text v-else class="placeholder">请选择供应商</text>
  72. </view>
  73. <uni-easyinput v-else v-model="baseForm.secondparty_name" disabled></uni-easyinput>
  74. </uni-forms-item>
  75. <uni-forms-item name="contract_money" label="合同金额">
  76. <view>
  77. <uni-easyinput v-model="baseForm.contract_money"
  78. :disabled="!isInitiateOrFieldEditable('contract_money')"
  79. type="digit"
  80. placeholder="请输入合同金额"
  81. @blur="onContractMoneyBlur"></uni-easyinput>
  82. <view class="contract-money-uppercase">
  83. <text>{{ contractMoneyUppercase }}</text>
  84. </view>
  85. </view>
  86. </uni-forms-item>
  87. <uni-forms-item name="contractContent" label="合同内容">
  88. <uni-easyinput v-if="isInitiateOrFieldEditable('contractContent')"
  89. v-model="baseForm.contractContent"
  90. type="textarea"
  91. placeholder="请输入合同内容"></uni-easyinput>
  92. <text v-else class="field-value">{{ baseForm.contractContent || '-' }}</text>
  93. </uni-forms-item>
  94. <uni-forms-item name="projectItem" label="所属项目或产品">
  95. <uni-easyinput v-model="baseForm.projectItem"
  96. :disabled="!isInitiateOrFieldEditable('projectItem')"
  97. placeholder="请输入所属项目或产品"></uni-easyinput>
  98. </uni-forms-item>
  99. <uni-forms-item name="usePosition" label="使用位置">
  100. <uni-easyinput v-model="baseForm.usePosition"
  101. :disabled="!isInitiateOrFieldEditable('usePosition')"
  102. placeholder="请输入使用位置"></uni-easyinput>
  103. </uni-forms-item>
  104. <uni-forms-item name="other_contractor" label="对方联系人及电话">
  105. <uni-easyinput v-model="baseForm.other_contractor"
  106. :disabled="!isInitiateOrFieldEditable('other_contractor')"
  107. placeholder="请输入对方联系人及电话"></uni-easyinput>
  108. </uni-forms-item>
  109. <uni-forms-item name="otherFile" label="随合同提交的其它材料">
  110. <uni-easyinput v-if="isInitiateOrFieldEditable('otherFile')"
  111. v-model="baseForm.otherFile"
  112. type="textarea"
  113. placeholder="请输入其它材料说明"></uni-easyinput>
  114. <text v-else class="field-value">{{ baseForm.otherFile || '-' }}</text>
  115. </uni-forms-item>
  116. <uni-forms-item name="salesmanSign" label="经办人签字">
  117. <uni-easyinput v-model="baseForm.salesmanSign"
  118. :disabled="!isInitiateOrFieldEditable('salesmanSign')"
  119. placeholder="请输入经办人签字"></uni-easyinput>
  120. </uni-forms-item>
  121. <!-- 部门意见 -->
  122. <uni-forms-item label="部门意见" name="departmentalOpinion">
  123. <view class="element_value_container">
  124. <view v-if="isApprovalFieldEditable(departmentalOpinionElem)" class="element_value">
  125. <!-- 审批签字板 -->
  126. <view v-if="departmentalOpinionElem && (!departmentalOpinionElem.defaultValue || departmentalOpinionElem.defaultValue == '')">
  127. <uni-row>
  128. <uni-col :span="24">
  129. <button type="primary" @click="handleApprovalSignature('departmental_opinion')">手动签名</button>
  130. </uni-col>
  131. <uni-col :span="24">
  132. <button style="margin-top: 5px;" type="primary" @click="handleAutoSeal('departmental_opinion')">一键签名</button>
  133. </uni-col>
  134. </uni-row>
  135. </view>
  136. <view v-else-if="departmentalOpinionElem && departmentalOpinionElem.defaultValue" class="signature_img">
  137. <img style="width: 100%;" mode="widthFix" @click="handleApprovalSignature('departmental_opinion')"
  138. :src="config.baseUrlPre + departmentalOpinionElem.sealImgPath"
  139. :alt="departmentalOpinionElem.elementName + '签名'" />
  140. </view>
  141. </view>
  142. <view v-else-if="departmentalOpinionElem && typeof departmentalOpinionElem.sealImgPath === 'string' && departmentalOpinionElem.sealImgPath.startsWith('/shares')" class="signature_img">
  143. <img style="width: 100%;" mode="widthFix"
  144. :src="config.baseUrlPre + departmentalOpinionElem.sealImgPath" />
  145. </view>
  146. </view>
  147. </uni-forms-item>
  148. <!-- 财务意见 -->
  149. <uni-forms-item label="财务意见" name="financeOpinion">
  150. <view class="element_value_container">
  151. <view v-if="isApprovalFieldEditable(financeOpinionElem)" class="element_value">
  152. <!-- 审批签字板 -->
  153. <view v-if="financeOpinionElem && (!financeOpinionElem.defaultValue || financeOpinionElem.defaultValue == '')">
  154. <uni-row>
  155. <uni-col :span="24">
  156. <button type="primary" @click="handleApprovalSignature('finance_opinion')">手动签名</button>
  157. </uni-col>
  158. <uni-col :span="24">
  159. <button style="margin-top: 5px;" type="primary" @click="handleAutoSeal('finance_opinion')">一键签名</button>
  160. </uni-col>
  161. </uni-row>
  162. </view>
  163. <view v-else-if="financeOpinionElem && financeOpinionElem.defaultValue" class="signature_img">
  164. <img style="width: 100%;" mode="widthFix" @click="handleApprovalSignature('finance_opinion')"
  165. :src="config.baseUrlPre + financeOpinionElem.sealImgPath"
  166. :alt="financeOpinionElem.elementName + '签名'" />
  167. </view>
  168. </view>
  169. <view v-else-if="financeOpinionElem && typeof financeOpinionElem.sealImgPath === 'string' && financeOpinionElem.sealImgPath.startsWith('/shares')" class="signature_img">
  170. <img style="width: 100%;" mode="widthFix"
  171. :src="config.baseUrlPre + financeOpinionElem.sealImgPath" />
  172. </view>
  173. </view>
  174. </uni-forms-item>
  175. <!-- 分管副总意见 -->
  176. <uni-forms-item label="分管副总" name="dgmOpinion">
  177. <view class="element_value_container">
  178. <view v-if="isApprovalFieldEditable(deputyGeneralManagerOpinionElem)" class="element_value">
  179. <!-- 审批签字板 -->
  180. <view v-if="deputyGeneralManagerOpinionElem && (!deputyGeneralManagerOpinionElem.defaultValue || deputyGeneralManagerOpinionElem.defaultValue == '')">
  181. <uni-row>
  182. <uni-col :span="24">
  183. <button type="primary" @click="handleApprovalSignature('deputy_general_manager_opinion')">手动签名</button>
  184. </uni-col>
  185. <uni-col :span="24">
  186. <button style="margin-top: 5px;" type="primary" @click="handleAutoSeal('deputy_general_manager_opinion')">一键签名</button>
  187. </uni-col>
  188. </uni-row>
  189. </view>
  190. <view v-else-if="deputyGeneralManagerOpinionElem && deputyGeneralManagerOpinionElem.defaultValue" class="signature_img">
  191. <img style="width: 100%;" mode="widthFix" @click="handleApprovalSignature('deputy_general_manager_opinion')"
  192. :src="config.baseUrlPre + deputyGeneralManagerOpinionElem.sealImgPath"
  193. :alt="deputyGeneralManagerOpinionElem.elementName + '签名'" />
  194. </view>
  195. </view>
  196. <view v-else-if="deputyGeneralManagerOpinionElem && typeof deputyGeneralManagerOpinionElem.sealImgPath === 'string' && deputyGeneralManagerOpinionElem.sealImgPath.startsWith('/shares')" class="signature_img">
  197. <img style="width: 100%;" mode="widthFix"
  198. :src="config.baseUrlPre + deputyGeneralManagerOpinionElem.sealImgPath" />
  199. </view>
  200. </view>
  201. </uni-forms-item>
  202. <!-- 审核副总意见 -->
  203. <uni-forms-item label="分管副总" name="auditDgmOpinion">
  204. <view class="element_value_container">
  205. <view v-if="isApprovalFieldEditable(auditDeputyGeneralManagerOpinionElem)" class="element_value">
  206. <!-- 审批签字板 -->
  207. <view v-if="auditDeputyGeneralManagerOpinionElem && (!auditDeputyGeneralManagerOpinionElem.defaultValue || auditDeputyGeneralManagerOpinionElem.defaultValue == '')">
  208. <uni-row>
  209. <uni-col :span="24">
  210. <button type="primary" @click="handleApprovalSignature('audit_deputy_general_manager_opinion')">手动签名</button>
  211. </uni-col>
  212. <uni-col :span="24">
  213. <button style="margin-top: 5px;" type="primary" @click="handleAutoSeal('audit_deputy_general_manager_opinion')">一键签名</button>
  214. </uni-col>
  215. </uni-row>
  216. </view>
  217. <view v-else-if="auditDeputyGeneralManagerOpinionElem && auditDeputyGeneralManagerOpinionElem.defaultValue" class="signature_img">
  218. <img style="width: 100%;" mode="widthFix" @click="handleApprovalSignature('audit_deputy_general_manager_opinion')"
  219. :src="config.baseUrlPre + auditDeputyGeneralManagerOpinionElem.sealImgPath"
  220. :alt="auditDeputyGeneralManagerOpinionElem.elementName + '签名'" />
  221. </view>
  222. </view>
  223. <view v-else-if="auditDeputyGeneralManagerOpinionElem && typeof auditDeputyGeneralManagerOpinionElem.sealImgPath === 'string' && auditDeputyGeneralManagerOpinionElem.sealImgPath.startsWith('/shares')" class="signature_img">
  224. <img style="width: 100%;" mode="widthFix"
  225. :src="config.baseUrlPre + auditDeputyGeneralManagerOpinionElem.sealImgPath" />
  226. </view>
  227. </view>
  228. </uni-forms-item>
  229. <!-- 总经理意见 -->
  230. <uni-forms-item label="总经理" name="gmOpinion">
  231. <view class="element_value_container">
  232. <view v-if="isApprovalFieldEditable(generalManagerOpinionElem)" class="element_value">
  233. <!-- 审批签字板 -->
  234. <view v-if="generalManagerOpinionElem && (!generalManagerOpinionElem.defaultValue || generalManagerOpinionElem.defaultValue == '')">
  235. <uni-row>
  236. <uni-col :span="24">
  237. <button type="primary" @click="handleApprovalSignature('general_manager_opinion')">手动签名</button>
  238. </uni-col>
  239. <uni-col :span="24">
  240. <button style="margin-top: 5px;" type="primary" @click="handleAutoSeal('general_manager_opinion')">一键签名</button>
  241. </uni-col>
  242. </uni-row>
  243. </view>
  244. <view v-else-if="generalManagerOpinionElem && generalManagerOpinionElem.defaultValue" class="signature_img">
  245. <img style="width: 100%;" mode="widthFix" @click="handleApprovalSignature('general_manager_opinion')"
  246. :src="config.baseUrlPre + generalManagerOpinionElem.sealImgPath"
  247. :alt="generalManagerOpinionElem.elementName + '签名'" />
  248. </view>
  249. </view>
  250. <view v-else-if="generalManagerOpinionElem && typeof generalManagerOpinionElem.sealImgPath === 'string' && generalManagerOpinionElem.sealImgPath.startsWith('/shares')" class="signature_img">
  251. <img style="width: 100%;" mode="widthFix"
  252. :src="config.baseUrlPre + generalManagerOpinionElem.sealImgPath" />
  253. </view>
  254. </view>
  255. </uni-forms-item>
  256. <!-- 董事长意见 -->
  257. <uni-forms-item label="董事长意见" name="chairmanOpinion">
  258. <view class="element_value_container">
  259. <view v-if="isApprovalFieldEditable(chairmanOpinionElem)" class="element_value">
  260. <!-- 审批签字板 -->
  261. <view v-if="chairmanOpinionElem && (!chairmanOpinionElem.defaultValue || chairmanOpinionElem.defaultValue == '')">
  262. <uni-row>
  263. <uni-col :span="24">
  264. <button type="primary" @click="handleApprovalSignature('chairman_opinion')">手动签名</button>
  265. </uni-col>
  266. <uni-col :span="24">
  267. <button style="margin-top: 5px;" type="primary" @click="handleAutoSeal('chairman_opinion')">一键签名</button>
  268. </uni-col>
  269. </uni-row>
  270. </view>
  271. <view v-else-if="chairmanOpinionElem && chairmanOpinionElem.defaultValue" class="signature_img">
  272. <img style="width: 100%;" mode="widthFix" @click="handleApprovalSignature('chairman_opinion')"
  273. :src="config.baseUrlPre + chairmanOpinionElem.sealImgPath"
  274. :alt="chairmanOpinionElem.elementName + '签名'" />
  275. </view>
  276. </view>
  277. <view v-else-if="chairmanOpinionElem && typeof chairmanOpinionElem.sealImgPath === 'string' && chairmanOpinionElem.sealImgPath.startsWith('/shares')" class="signature_img">
  278. <img style="width: 100%;" mode="widthFix"
  279. :src="config.baseUrlPre + chairmanOpinionElem.sealImgPath" />
  280. </view>
  281. </view>
  282. </uni-forms-item>
  283. </uni-forms>
  284. </uni-card>
  285. <!-- 物料明细 -->
  286. <uni-card>
  287. <uni-section titleFontSize="1.3rem" title="物料明细" type="line"></uni-section>
  288. <!-- 添加物料按钮:发起环节或字段可编辑时显示 -->
  289. <view v-if="isSeModel" class="material-actions">
  290. <button type="primary" size="mini" @click="openMaterialSelector" plain>添加物料</button>
  291. </view>
  292. <!-- 物料列表 - 卡片式展示 -->
  293. <view v-if="materialList.length > 0" class="material-list">
  294. <view v-for="(item, index) in materialList" :key="'material-' + item.itemCode + '-' + index" class="material-card">
  295. <view class="material-header" @click="toggleMaterialExpand(index)">
  296. <view class="material-main-info">
  297. <text class="material-name">{{ item.itemName }}</text>
  298. <text class="material-code">{{ item.itemCode }}</text>
  299. </view>
  300. <view class="material-expand">
  301. <uni-icons :type="item.expanded ? 'up' : 'down'" size="16" color="#999"></uni-icons>
  302. </view>
  303. </view>
  304. <!-- 展开的详细信息 -->
  305. <view v-show="item.expanded" class="material-detail">
  306. <!-- 采购申请单编号(仅当从采购申请单添加且非销售合同时显示) -->
  307. <view v-if="!isSalesContract && item.purchaseNumber" class="detail-row purchase-number-row">
  308. <text class="detail-label">采购申请单号:</text>
  309. <text class="detail-value purchase-number">{{ item.purchaseNumber }}</text>
  310. </view>
  311. <view class="detail-row">
  312. <text class="detail-label">规格型号:</text>
  313. <text class="detail-value">{{ item.specification }}</text>
  314. </view>
  315. <view class="detail-row">
  316. <text class="detail-label">单位:</text>
  317. <text class="detail-value">{{ item.measureName }}</text>
  318. </view>
  319. <view class="detail-row">
  320. <text class="detail-label">数量:</text>
  321. <uni-easyinput
  322. v-model="item.qty"
  323. type="digit"
  324. v-if="isSeModel"
  325. placeholder="请输入数量"
  326. style="width: 100px; display: inline-block; margin-right: 15px;"
  327. @blur="onQuantityBlur(item)"
  328. />
  329. <text v-if="!isSeModel" class="detail-value">{{ item.qty }}</text>
  330. </view>
  331. <view class="detail-row">
  332. <text class="detail-label">税前单价:</text>
  333. <uni-easyinput
  334. v-model="item.price"
  335. type="digit"
  336. v-if="isSeModel"
  337. placeholder="请输入税前单价"
  338. style="width: 100px; display: inline-block; margin-right: 15px;"
  339. @blur="onPriceBlur(item)"
  340. />
  341. <text v-if="!isSeModel" class="detail-value">{{ item.price }}</text>
  342. </view>
  343. <view class="detail-row">
  344. <text class="detail-label">税率:</text>
  345. <uni-easyinput
  346. v-model="item.cess"
  347. type="digit"
  348. v-if="isSeModel"
  349. placeholder="请输入税率"
  350. style="width: 80px; display: inline-block; margin-right: 15px;"
  351. @blur="onCessBlur(item)"
  352. />
  353. <text v-if="!isSeModel" class="detail-value">{{ item.cess }}%</text>
  354. </view>
  355. <view class="detail-row">
  356. <text class="detail-label">税后单价:</text>
  357. <text class="detail-value price-value">{{ item.priceTax }}</text>
  358. </view>
  359. <view v-if="isSeModel" class="detail-row delete-row">
  360. <button type="warn" size="mini" @click="removeMaterial(index)">删除</button>
  361. </view>
  362. </view>
  363. </view>
  364. </view>
  365. <view v-else-if="materialList.length === 0" class="empty-materials">
  366. <text>暂无物料</text>
  367. </view>
  368. </uni-card>
  369. <!-- 付款明细 -->
  370. <uni-card>
  371. <uni-section titleFontSize="1.3rem" title="付款明细" type="line"></uni-section>
  372. <!-- 添加付款按钮:发起环节或字段可编辑时显示 -->
  373. <view v-if="isSeModel" class="payment-actions">
  374. <button type="primary" size="mini" @click="addPayment" :disabled="!canAddPayment" plain>添加付款</button>
  375. </view>
  376. <!-- 付款列表 - 卡片式展示 -->
  377. <view v-if="paymentList.length > 0" class="payment-list">
  378. <view v-for="(item, index) in paymentList" :key="'payment-' + item.payType + '-' + index" class="payment-card">
  379. <view class="payment-header" @click="togglePaymentExpand(index)">
  380. <view class="payment-main-info">
  381. <text class="payment-name">{{ item.payTypeName || item.payType }}</text>
  382. </view>
  383. <view class="payment-expand">
  384. <uni-icons :type="item.expanded ? 'up' : 'down'" size="16" color="#999"></uni-icons>
  385. </view>
  386. </view>
  387. <!-- 展开的详细信息 -->
  388. <view v-show="item.expanded" class="payment-detail">
  389. <view class="detail-row">
  390. <text class="detail-label">付款方式:</text>
  391. <picker
  392. v-if="isSeModel"
  393. @change="(e) => onPaymentTypeChange(index, e)"
  394. :range="paymentTypeList"
  395. range-key="payTypeName">
  396. <view class="picker">
  397. {{ item.payTypeName || '请选择付款方式' }}
  398. </view>
  399. </picker>
  400. <text v-if="!isSeModel" class="detail-value">{{ item.payTypeName || item.payType }}</text>
  401. </view>
  402. <view class="detail-row">
  403. <text class="detail-label">付款比例:</text>
  404. <uni-easyinput
  405. v-model="item.proportion"
  406. type="digit"
  407. v-if="isSeModel"
  408. placeholder="请输入比例"
  409. style="width: 80px; display: inline-block; margin-right: 10px;"
  410. @blur="onProportionBlur(item)"
  411. />
  412. <text v-if="!isSeModel" class="detail-value">{{ item.proportion }}</text>
  413. <text>%</text>
  414. </view>
  415. <view class="detail-row">
  416. <text class="detail-label">付款金额:</text>
  417. <text class="detail-value price-value">{{ item.amount }}</text>
  418. </view>
  419. <view class="detail-row">
  420. <text class="detail-label">已付金额:</text>
  421. <uni-easyinput
  422. v-model="item.amountPaid"
  423. type="digit"
  424. v-if="isSeModel"
  425. placeholder="请输入已付金额"
  426. style="width: 100px; display: inline-block; margin-right: 10px;"
  427. @blur="onAmountPaidBlur(item)"
  428. />
  429. <text v-if="!isSeModel" class="detail-value">{{ item.amountPaid || 0 }}</text>
  430. </view>
  431. <view class="detail-row">
  432. <text class="detail-label">付款时间:</text>
  433. <uni-datetime-picker
  434. v-if="isSeModel"
  435. v-model="item.payTime"
  436. type="date"
  437. placeholder="选择付款时间"
  438. style="display: inline-block;"
  439. @click.native.stop
  440. @change="(e) => onPayTimeChange(item, e)"
  441. />
  442. <text v-if="!isSeModel" class="detail-value">{{ item.payTime || '-' }}</text>
  443. </view>
  444. <view class="detail-row">
  445. <text class="detail-label">付款状态:</text>
  446. <text class="detail-value status-tag" :class="'status-' + (item.payStatus || '0')">{{ getPayStatusName(item) }}</text>
  447. </view>
  448. <view class="detail-row">
  449. <text class="detail-label">备注:</text>
  450. <uni-easyinput
  451. v-model="item.remark"
  452. v-if="isSeModel"
  453. placeholder="请输入备注"
  454. style="flex: 1; display: inline-block;"
  455. />
  456. <text v-if="!isSeModel" class="detail-value">{{ item.remark || '-' }}</text>
  457. <button v-if="isSeModel" type="warn" size="mini" @click="removePayment(index)" style="margin-left: 10px;">删除</button>
  458. </view>
  459. </view>
  460. </view>
  461. </view>
  462. <view v-else-if="paymentList.length === 0" class="empty-payments">
  463. <text v-if="!canAddPayment">请先输入合同金额</text>
  464. <text v-else>暂无付款明细</text>
  465. </view>
  466. </uni-card>
  467. <!-- 签名板弹出层 -->
  468. <uni-popup ref="signaturePopup" @maskClick="closeSignature">
  469. <view class="signature_container" :class="{ 'signature_container_landscape': isLandscape }">
  470. <view class="signature_content">
  471. <l-signature ref="signatureRef" v-if="signaturePopupShow" :landscape="isLandscape" :penSize="8"
  472. :minLineWidth="4" :maxLineWidth="12" :openSmooth="true" :preferToDataURL="true"
  473. backgroundColor="#ffffff" penColor="black"></l-signature>
  474. </view>
  475. <view class="signature_button_container">
  476. <uni-row :gutter="10">
  477. <uni-col :span="6">
  478. <button type="warn" @click="onclickSignatureButton('undo')">撤销</button>
  479. </uni-col>
  480. <uni-col :span="6">
  481. <button type="warn" @click="onclickSignatureButton('clear')">清空</button>
  482. </uni-col>
  483. <uni-col :span="6">
  484. <button type="primary" @click="onclickSignatureButton('save')">保存</button>
  485. </uni-col>
  486. <uni-col :span="6">
  487. <button @click="onclickSignatureButton('landscape')">全屏</button>
  488. </uni-col>
  489. </uni-row>
  490. </view>
  491. </view>
  492. </uni-popup>
  493. <!-- 选择器弹出层(物料、供应商) -->
  494. <uni-popup ref="selectorPopup" type="center">
  495. <view class="selector-popup">
  496. <view class="popup-header">
  497. <text class="popup-title">{{ popupTitle }}</text>
  498. <uni-icons type="closeempty" size="20" @click="closePopup"></uni-icons>
  499. </view>
  500. <!-- 物料来源选择(仅物料选择时显示,且非销售合同) -->
  501. <view v-if="selectorType === 'material' && !isSalesContract" class="material-source-selector">
  502. <view class="source-options">
  503. <view
  504. :class="['source-option', materialSource === 'mes' ? 'active' : '']"
  505. @click="materialSource = 'mes'; onMaterialSourceChange()">
  506. MES 系统
  507. </view>
  508. <view
  509. :class="['source-option', materialSource === 'purchase' ? 'active' : '']"
  510. @click="materialSource = 'purchase'; onMaterialSourceChange()">
  511. 采购申请
  512. </view>
  513. </view>
  514. </view>
  515. <!-- 搜索框 -->
  516. <view v-if="(selectorType === 'material' && materialSource === 'mes') || selectorType === 'supplier' || selectorType === 'client'" class="search-bar">
  517. <uni-easyinput
  518. v-model="searchKeyword"
  519. :placeholder="selectorType === 'material' ? '输入物料名称搜索' : (selectorType === 'supplier' ? '输入供应商名称搜索' : '输入客户名称搜索')"
  520. clearable
  521. @confirm="handleSearch"
  522. />
  523. <button type="primary" size="mini" @click="handleSearch">搜索</button>
  524. </view>
  525. <!-- 采购申请单选择器(采购申请模式,且非销售合同) -->
  526. <view v-if="selectorType === 'material' && materialSource === 'purchase' && !isSalesContract" class="purchase-selector">
  527. <picker @change="onPurchaseChange" :range="purchaseApplyList" range-key="contractPurchaseFormNumber">
  528. <view class="picker">
  529. {{ selectedPurchaseApply ? selectedPurchaseApply.contractPurchaseFormNumber : '全部采购申请单' }}
  530. </view>
  531. </picker>
  532. <button type="primary" size="mini" @click="loadPurchaseMaterials(true)">查询</button>
  533. <button type="warn" size="mini" @click="clearPurchaseSelection" v-if="selectedPurchaseApply">清空</button>
  534. </view>
  535. <scroll-view
  536. scroll-y
  537. class="popup-content"
  538. refresher-enabled
  539. :refresher-triggered="isRefreshing"
  540. @refresherrefresh="onRefresh"
  541. @scrolltolower="loadMore"
  542. >
  543. <view v-for="(item, index) in selectorList" :key="index"
  544. class="selector-item"
  545. @click="selectItem(item)">
  546. <view class="selector-item-content">
  547. <view class="item-info">
  548. <text class="item-name">{{ getSelectorItemName(item) }}</text>
  549. <!-- 采购申请模式显示采购单号(仅非销售合同) -->
  550. <text v-if="!isSalesContract && materialSource === 'purchase' && item.purchaseNumber" class="item-purchase">
  551. 采购单:{{ item.purchaseNumber }}
  552. </text>
  553. <text v-if="getItemCode(item)" class="item-code">{{ getItemCode(item) }}</text>
  554. <text v-if="getItemSpec(item)" class="item-spec">{{ getItemSpec(item) }}</text>
  555. <!-- 显示单位和数量 -->
  556. <text v-if="item.measureName || item.qty" class="item-extra">
  557. <text v-if="item.measureName">{{ item.measureName }}</text>
  558. <text v-if="item.qty"> 可用:{{ item.qty }}</text>
  559. </text>
  560. </view>
  561. <text v-if="isSelected(item)" class="selected-tag">已选择</text>
  562. </view>
  563. </view>
  564. <!-- 加载状态 -->
  565. <view v-if="isLoading && selectorList.length > 0" class="loading-text">
  566. <uni-load-more status="loading" />
  567. </view>
  568. <!-- 没有更多数据 -->
  569. <view v-else-if="!hasMore && selectorList.length > 0" class="no-more-text">
  570. <text>没有更多了</text>
  571. </view>
  572. <!-- 空数据提示 -->
  573. <view v-if="selectorList.length === 0 && !isLoading" class="empty-data">
  574. <text>{{ searchKeyword ? '暂无相关数据' : '暂无数据' }}</text>
  575. </view>
  576. </scroll-view>
  577. </view>
  578. </uni-popup>
  579. </view>
  580. </template>
  581. <script setup lang="ts">
  582. import { ref, watch, computed, nextTick, onMounted } from 'vue'
  583. import { useUserStore } from '@/store/user.js'
  584. import config from '@/config.js'
  585. import { uploadSignatureBoardImg, getSeal } from '@/api/process.js'
  586. import { getContractTypeList, getPaymentTypeList, getMaterialList, getSupplierList, getClientList, getPurchaseApplyList, getPurchaseDetailList, checkContractNumber } from '@/api/contract.js'
  587. import $modal from '@/plugins/modal.js'
  588. const userStore = useUserStore()
  589. // 组件挂载时加载合同类型和付款方式列表
  590. onMounted(() => {
  591. loadContractTypeList()
  592. loadPaymentTypeList()
  593. })
  594. // 加载合同类型列表
  595. function loadContractTypeList() {
  596. getContractTypeList(userStore.user.useId).then(({ returnParams }) => {
  597. if (returnParams && Array.isArray(returnParams.typeList)) {
  598. contractTypeList.value = returnParams.typeList
  599. }
  600. }).catch(err => {
  601. console.error('加载合同类型列表失败:', err)
  602. })
  603. }
  604. // 加载付款方式列表
  605. const paymentTypeList = ref<any[]>([])
  606. function loadPaymentTypeList() {
  607. getPaymentTypeList(userStore.user.useId).then(({ returnParams }) => {
  608. if (returnParams && Array.isArray(returnParams.paymentList)) {
  609. paymentTypeList.value = returnParams.paymentList
  610. }
  611. }).catch(err => {
  612. console.error('加载付款方式列表失败:', err)
  613. })
  614. }
  615. const props = defineProps({
  616. formData: {
  617. type: Object,
  618. default: () => ({})
  619. },
  620. formElements: {
  621. type: Array,
  622. default: () => []
  623. },
  624. repeatingForm: {
  625. type: Object,
  626. default: () => ({ elementItem: [], elements: [] })
  627. },
  628. // 是否为发起环节(seModel == '1')
  629. isInitiate: {
  630. type: Boolean,
  631. default: false
  632. },
  633. // 当前环节可编辑的字段列表
  634. editableFields: {
  635. type: Array,
  636. default: () => []
  637. },
  638. // 当前环节的审批意见配置
  639. currentTacheOpinion: {
  640. type: Object,
  641. default: null
  642. }
  643. })
  644. const emits = defineEmits(['update', 'signature-change'])
  645. // 表单数据
  646. const baseForm = ref<any>({
  647. contract_number: '',
  648. contract_name: '',
  649. contract_type: '',
  650. contract_type_name: '',
  651. applyDate: '',
  652. department: '',
  653. salesman: '',
  654. salesman_name: '',
  655. salesmanTel: '',
  656. supplierCode: '',
  657. supplierName: '',
  658. firstparty_name: '',
  659. contract_money: '',
  660. contractContent: '',
  661. projectItem: '',
  662. usePosition: '',
  663. other_contractor: '',
  664. otherFile: '',
  665. salesmanSign: '',
  666. // 录入人信息
  667. contract_entrying_operator: '',
  668. contract_entrying_operator_name: '',
  669. // 组织 ID
  670. unit_id: '',
  671. // 合同ID(用于审批流程中排除自己)
  672. universalid: ''
  673. })
  674. const baseFormRules = ref({
  675. contract_name: [
  676. {
  677. required: true,
  678. errorMessage: '请输入合同名称'
  679. }
  680. ]
  681. })
  682. const materialList = ref<any[]>([])
  683. const paymentList = ref<any[]>([])
  684. const signaturePopup = ref(null)
  685. const signatureRef = ref(null)
  686. const signatureImg = ref('')
  687. const currentApprovalText = ref('')
  688. const approvals = ref<any[]>([]) // 历史审批意见列表
  689. const signaturePopupShow = ref(false)
  690. const isLandscape = ref(false)
  691. // 合同类型列表
  692. const contractTypeList = ref<any[]>([])
  693. // 判断是否为销售合同
  694. const isSalesContract = computed(() => {
  695. return baseForm.value.contract_type === '1' || baseForm.value.contract_type === 1
  696. })
  697. // 选择器相关变量
  698. const selectorPopup = ref(null)
  699. const selectorList = ref<any[]>([])
  700. const selectorType = ref('') // 'material', 'supplier', 'client'
  701. const materialSource = ref('mes') // 'mes' - MES 系统,'purchase' - 采购申请
  702. const purchaseApplyList = ref<any[]>([]) // 采购申请单列表
  703. const selectedPurchaseApply = ref<any>(null) // 选中的采购申请单
  704. const currentPage = ref(1)
  705. const pageSize = 20
  706. const hasMore = ref(true)
  707. const isLoading = ref(false)
  708. const isRefreshing = ref(false)
  709. const searchKeyword = ref('')
  710. // 选择器弹出层标题
  711. const popupTitle = computed(() => {
  712. if (selectorType.value === 'material') return '选择物料'
  713. if (selectorType.value === 'supplier') return '选择供应商'
  714. if (selectorType.value === 'client') return '选择客户'
  715. return '选择'
  716. })
  717. // 获取指定的审批意见元素
  718. const getApprovalElement = (fieldName: string) => {
  719. if (!props.formElements || !Array.isArray(props.formElements)) {
  720. return null
  721. }
  722. // 从 formElements 中找到对应的字段
  723. const elem = props.formElements.find(e => e.tableField === fieldName || e.elementName.includes(fieldName.replace(/_/g, '')))
  724. return elem || null
  725. }
  726. // 获取各部门意见元素(计算属性,避免重复调用)
  727. const departmentalOpinionElem = computed(() => getApprovalElement('departmental_opinion'))
  728. const deputyGeneralManagerOpinionElem = computed(() => getApprovalElement('deputy_general_manager_opinion'))
  729. const auditDeputyGeneralManagerOpinionElem = computed(() => getApprovalElement('audit_deputy_general_manager_opinion'))
  730. const generalManagerOpinionElem = computed(() => getApprovalElement('general_manager_opinion'))
  731. const financeOpinionElem = computed(() => getApprovalElement('finance_opinion'))
  732. const chairmanOpinionElem = computed(() => getApprovalElement('chairman_opinion'))
  733. // 是否发起环节
  734. const isSeModel = computed(() => props.isInitiate)
  735. // 计算各部门意见是否可编辑
  736. const isApprovalFieldEditable = (elem: any) => {
  737. if (!elem) return false
  738. return props.editableFields && props.editableFields.includes(elem.tableField)
  739. }
  740. // 处理审批意见签名
  741. let lastFormInsId = ''
  742. // 计算字段是否可编辑 (只需要该字段在 table_fields 中即可)
  743. const getFieldEditable = (fieldName: string) => {
  744. // 如果没有配置 editableFields,则都不可编辑
  745. if (!props.editableFields || props.editableFields.length === 0) {
  746. return false
  747. }
  748. // 检查该字段是否在可编辑字段列表中 (table_fields 包含的字段才可以编辑)
  749. return props.editableFields.includes(fieldName)
  750. }
  751. // 计算是否为发起环节或字段可编辑 (满足任一条件即可)
  752. const isInitiateOrFieldEditable = (fieldName: string) => {
  753. return props.isInitiate || getFieldEditable(fieldName)
  754. }
  755. // 监听表单数据变化
  756. watch(() => props.formData, (newVal) => {
  757. if (!newVal || Object.keys(newVal).length === 0) {
  758. return
  759. }
  760. // 使用 formInsId 判断是否是新的数据(formInsId 肯定存在)
  761. const currentFormInsId = newVal.lFormInsId || newVal.universalid || ''
  762. // 只有当 formInsId 变化时,才认为是新的数据需要加载
  763. if (currentFormInsId && currentFormInsId !== lastFormInsId) {
  764. lastFormInsId = currentFormInsId
  765. // 填充基本信息 - 直接使用后端返回的字段
  766. baseForm.value = {
  767. contract_number: newVal.contract_number || '',
  768. contract_name: newVal.contract_name || '',
  769. contract_type: newVal.contract_type !== undefined && newVal.contract_type !== null ? newVal.contract_type : '',
  770. contract_type_name: newVal.contract_type_name || '', // 直接使用后端返回的名称
  771. applyDate: newVal.applyDate || '',
  772. department: newVal.department || '',
  773. salesman: newVal.salesman || '',
  774. salesman_name: newVal.salesman_name || '',
  775. salesmanTel: newVal.salesmanTel || '',
  776. supplierCode: newVal.supplierCode || '',
  777. supplierName: newVal.supplierName || '',
  778. firstparty_name: newVal.firstparty_name || '',
  779. secondparty_name: newVal.secondparty_name || '',
  780. // 合同金额:将数字转换为字符串,0 也要显示
  781. contract_money: (newVal.contract_money !== undefined && newVal.contract_money !== null) ? String(newVal.contract_money) : '',
  782. contractContent: newVal.contractContent || '',
  783. projectItem: newVal.projectItem || '',
  784. usePosition: newVal.usePosition || '',
  785. other_contractor: newVal.other_contractor || '',
  786. otherFile: newVal.otherFile || '',
  787. salesmanSign: newVal.salesmanSign || '',
  788. // 录入人信息
  789. contract_entrying_operator: newVal.contract_entrying_operator || '',
  790. contract_entrying_operator_name: newVal.contract_entrying_operator_name || '',
  791. // 组织 ID
  792. unit_id: newVal.unit_id || '',
  793. // 保存公司名称,用于销售合同时对调需方供方(从后端获取)
  794. companyName: newVal.companyName || '',
  795. // 合同ID(用于审批流程中排除自己)
  796. universalid: newVal.universalid || ''
  797. }
  798. // 初始化合同金额大写
  799. updateContractMoneyUppercase()
  800. // 初始化 lastContractType
  801. lastContractType = String(newVal.contract_type || '')
  802. // 从 contractMaterialList 中获取物料列表
  803. const rawMaterialList = newVal.details || newVal.contractMaterialList || []
  804. // 加载物料数据
  805. if (Array.isArray(rawMaterialList)) {
  806. materialList.value = rawMaterialList.map((item: any) => ({
  807. itemCode: item.itemCode || item.materialCode,
  808. itemName: item.itemName || item.materialName,
  809. specification: item.specification || item.materialModel,
  810. measureName: item.measureName || item.unit,
  811. qty: item.qty || 0,
  812. price: item.price || 0,
  813. cess: item.cess || 0,
  814. priceTax: item.priceTax || 0,
  815. purchaseId: item.purchaseId || '',
  816. purchaseNumber: item.purchaseNumber || '',
  817. unit_id: item.unit_id || newVal.unit_id || '', // 从明细或主表获取 unit_id
  818. expanded: true // 默认展开
  819. }))
  820. } else {
  821. materialList.value = []
  822. }
  823. // 如果是销售合同,立即清理来自采购申请单的物料(与 onContractTypeChange 逻辑一致)
  824. if ((newVal.contract_type === '1' || newVal.contract_type === 1) && materialList.value.length > 0) {
  825. const invalidMaterials: number[] = []
  826. materialList.value.forEach((item, idx) => {
  827. const purchaseId = item.purchaseId || ''
  828. const purchaseNumber = item.purchaseNumber || ''
  829. // 排除空值、null字符串、0等无效值
  830. if ((purchaseId && purchaseId !== 'null' && purchaseId !== '0' && purchaseId !== 'undefined') ||
  831. (purchaseNumber && purchaseNumber !== 'null' && purchaseNumber !== 'undefined')) {
  832. invalidMaterials.push(idx)
  833. }
  834. })
  835. if (invalidMaterials.length > 0) {
  836. // 直接删除不符合要求的物料(从后往前删,避免索引错乱)
  837. for (let i = invalidMaterials.length - 1; i >= 0; i--) {
  838. materialList.value.splice(invalidMaterials[i], 1)
  839. }
  840. // 重新计算总价
  841. calculateTotalPrice()
  842. console.log(`销售合同初始化:已清除 ${invalidMaterials.length} 条来自采购申请单的物料`)
  843. }
  844. }
  845. // 从 contractPaymentList 中获取付款列表
  846. const rawPaymentList = newVal.contractPaymentList || []
  847. // 加载付款数据
  848. if (Array.isArray(rawPaymentList)) {
  849. paymentList.value = rawPaymentList.map((item: any) => ({
  850. payType: item.payType || '',
  851. payTypeName: item.payTypeName || '',
  852. proportion: item.proportion || 0,
  853. amount: item.amount || 0,
  854. amountPaid: item.amountPaid || 0,
  855. payTime: item.payTime || '',
  856. payStatus: item.payStatus || '0',
  857. payStatusName: item.payStatusName || getPayStatusName(item),
  858. remark: item.remark || '',
  859. unit_id: item.unit_id || newVal.unit_id || '', // 从明细或主表获取 unit_id
  860. expanded: true // 默认展开
  861. }))
  862. // 如果付款方式名称为空,尝试从列表中查找
  863. if (paymentTypeList.value.length > 0) {
  864. paymentList.value.forEach(payment => {
  865. if (!payment.payTypeName && payment.payType) {
  866. const payType = paymentTypeList.value.find(p => p.payType == payment.payType)
  867. if (payType) {
  868. payment.payTypeName = payType.payTypeName
  869. }
  870. }
  871. })
  872. }
  873. } else {
  874. paymentList.value = []
  875. }
  876. }
  877. }, { immediate: true, deep: true })
  878. // 切换物料展开/收起状态
  879. function toggleMaterialExpand(index: number) {
  880. if (materialList.value[index]) {
  881. materialList.value[index].expanded = !materialList.value[index].expanded
  882. }
  883. }
  884. // 切换付款展开/收起状态
  885. function togglePaymentExpand(index: number) {
  886. if (paymentList.value[index]) {
  887. paymentList.value[index].expanded = !paymentList.value[index].expanded
  888. }
  889. }
  890. // 打开物料选择器
  891. async function openMaterialSelector() {
  892. selectorType.value = 'material'
  893. materialSource.value = 'mes' // 默认 MES 系统模式
  894. currentPage.value = 1
  895. hasMore.value = true
  896. selectorList.value = []
  897. await loadMaterials(1, false)
  898. openPopup()
  899. }
  900. // 打开供应商选择器
  901. async function openSupplierSelector() {
  902. selectorType.value = 'supplier'
  903. currentPage.value = 1
  904. hasMore.value = true
  905. selectorList.value = []
  906. await loadSuppliers(1, false)
  907. openPopup()
  908. }
  909. // 打开客户选择器(销售合同时使用)
  910. async function openClientSelector() {
  911. selectorType.value = 'client'
  912. currentPage.value = 1
  913. hasMore.value = true
  914. selectorList.value = []
  915. await loadClients(1, false)
  916. openPopup()
  917. }
  918. // 加载客户列表(分页)
  919. async function loadClients(page: number, append: boolean = false) {
  920. if (isLoading.value) return
  921. isLoading.value = true
  922. if (page === 1) {
  923. isRefreshing.value = true
  924. }
  925. try {
  926. const res = await getClientList(
  927. userStore.user.useId,
  928. page,
  929. pageSize,
  930. searchKeyword.value
  931. )
  932. if (res.returnCode === '1') {
  933. const result = res.returnParams
  934. const newList = result.list || []
  935. if (append) {
  936. // 追加模式(加载更多)
  937. selectorList.value = [...selectorList.value, ...newList]
  938. } else {
  939. // 覆盖模式(刷新/搜索)
  940. selectorList.value = newList
  941. }
  942. // 判断是否还有更多数据
  943. hasMore.value = selectorList.value.length < result.total
  944. } else {
  945. $modal.msgError('加载客户失败')
  946. }
  947. } catch (error) {
  948. console.error('加载客户失败', error)
  949. $modal.msgError('加载失败')
  950. } finally {
  951. isLoading.value = false
  952. isRefreshing.value = false
  953. }
  954. }
  955. // 打开弹出层
  956. function openPopup() {
  957. if (selectorPopup.value) {
  958. ;(selectorPopup.value as any).open()
  959. }
  960. }
  961. // 关闭弹出层
  962. function closePopup() {
  963. if (selectorPopup.value) {
  964. ;(selectorPopup.value as any).close()
  965. }
  966. }
  967. // 加载物料列表(分页)
  968. async function loadMaterials(page: number, append: boolean = false) {
  969. if (isLoading.value) return
  970. isLoading.value = true
  971. if (page === 1) {
  972. isRefreshing.value = true
  973. }
  974. try {
  975. const res = await getMaterialList(
  976. userStore.user.useId,
  977. page,
  978. pageSize,
  979. searchKeyword.value
  980. )
  981. if (res.returnCode === '1') {
  982. const result = res.returnParams
  983. const newList = result.list || []
  984. if (append) {
  985. // 追加模式(加载更多)
  986. selectorList.value = [...selectorList.value, ...newList]
  987. } else {
  988. // 覆盖模式(刷新/搜索)
  989. selectorList.value = newList
  990. }
  991. // 判断是否还有更多数据
  992. hasMore.value = selectorList.value.length < result.total
  993. } else {
  994. $modal.msgError('加载物料失败')
  995. }
  996. } catch (error) {
  997. console.error('加载物料失败', error)
  998. $modal.msgError('加载失败')
  999. } finally {
  1000. isLoading.value = false
  1001. isRefreshing.value = false
  1002. }
  1003. }
  1004. // 加载供应商列表(分页)
  1005. async function loadSuppliers(page: number, append: boolean = false) {
  1006. if (isLoading.value) return
  1007. isLoading.value = true
  1008. if (page === 1) {
  1009. isRefreshing.value = true
  1010. }
  1011. try {
  1012. const res = await getSupplierList(
  1013. userStore.user.useId,
  1014. page,
  1015. pageSize,
  1016. searchKeyword.value
  1017. )
  1018. if (res.returnCode === '1') {
  1019. const result = res.returnParams
  1020. const newList = result.list || []
  1021. if (append) {
  1022. selectorList.value = [...selectorList.value, ...newList]
  1023. } else {
  1024. selectorList.value = newList
  1025. }
  1026. hasMore.value = selectorList.value.length < result.total
  1027. } else {
  1028. $modal.msgError('加载供应商失败')
  1029. }
  1030. } catch (error) {
  1031. console.error('加载供应商失败', error)
  1032. $modal.msgError('加载失败')
  1033. } finally {
  1034. isLoading.value = false
  1035. isRefreshing.value = false
  1036. }
  1037. }
  1038. // 加载采购申请单列表
  1039. async function loadPurchaseApplyList() {
  1040. if (isLoading.value) return
  1041. isLoading.value = true
  1042. isRefreshing.value = true
  1043. try {
  1044. const res = await getPurchaseApplyList(userStore.user.useId)
  1045. if (res.returnCode === '1') {
  1046. const result = res.returnParams
  1047. purchaseApplyList.value = result.list || []
  1048. selectedPurchaseApply.value = null
  1049. selectorList.value = [] // 清空物料列表
  1050. } else {
  1051. $modal.msgError('加载采购申请单失败')
  1052. }
  1053. } catch (error) {
  1054. console.error('加载采购申请单失败', error)
  1055. $modal.msgError('加载失败')
  1056. } finally {
  1057. isLoading.value = false
  1058. isRefreshing.value = false
  1059. }
  1060. }
  1061. // 加载采购申请物料
  1062. async function loadPurchaseMaterials(resetPage: boolean = false) {
  1063. if (isLoading.value) return
  1064. // 如果是分页加载,不清空列表
  1065. if (resetPage) {
  1066. currentPage.value = 1
  1067. selectorList.value = []
  1068. }
  1069. isLoading.value = true
  1070. isRefreshing.value = resetPage
  1071. try {
  1072. // 如果不指定采购申请单,则查询所有物料
  1073. const purchaseFormId = selectedPurchaseApply.value
  1074. ? selectedPurchaseApply.value.contractPurchaseFormId.toString()
  1075. : ''
  1076. const res = await getPurchaseDetailList(
  1077. userStore.user.useId,
  1078. purchaseFormId,
  1079. currentPage.value,
  1080. 10 // 每页 10 条,与 PC 端一致
  1081. )
  1082. if (res.returnCode === '1') {
  1083. const result = res.returnParams
  1084. const newList = result.list || []
  1085. if (resetPage) {
  1086. selectorList.value = newList
  1087. } else {
  1088. // 追加数据
  1089. selectorList.value.push(...newList)
  1090. }
  1091. // 判断是否还有更多数据
  1092. hasMore.value = selectorList.value.length < result.total
  1093. if (!resetPage) {
  1094. currentPage.value++
  1095. }
  1096. } else {
  1097. $modal.msgError('加载物料失败')
  1098. }
  1099. } catch (error) {
  1100. console.error('加载物料失败', error)
  1101. $modal.msgError('加载失败')
  1102. } finally {
  1103. isLoading.value = false
  1104. isRefreshing.value = false
  1105. }
  1106. }
  1107. // 物料来源切换时
  1108. async function onMaterialSourceChange() {
  1109. currentPage.value = 1
  1110. hasMore.value = true
  1111. selectorList.value = []
  1112. searchKeyword.value = ''
  1113. if (materialSource.value === 'mes') {
  1114. await loadMaterials(1, false)
  1115. } else {
  1116. // 采购申请模式,先加载采购申请单列表,然后加载所有物料
  1117. await loadPurchaseApplyList()
  1118. // 加载完采购申请单后,立即查询物料(默认不指定具体采购单)
  1119. await loadPurchaseMaterials(true) // true 表示重置分页
  1120. }
  1121. }
  1122. // 采购申请单选择变化
  1123. function onPurchaseChange(e: any) {
  1124. const selectedIndex = e.detail.value
  1125. selectedPurchaseApply.value = purchaseApplyList.value[selectedIndex]
  1126. loadPurchaseMaterials(true)
  1127. }
  1128. // 清空采购申请单选择
  1129. function clearPurchaseSelection() {
  1130. selectedPurchaseApply.value = null
  1131. selectorList.value = []
  1132. loadPurchaseMaterials(true)
  1133. }
  1134. // 加载更多
  1135. function loadMore() {
  1136. if (!hasMore.value || isLoading.value) return
  1137. currentPage.value++
  1138. if (selectorType.value === 'material') {
  1139. if (materialSource.value === 'mes') {
  1140. loadMaterials(currentPage.value, true)
  1141. } else {
  1142. loadPurchaseMaterials(false)
  1143. }
  1144. } else if (selectorType.value === 'supplier') {
  1145. loadSuppliers(currentPage.value, true)
  1146. } else if (selectorType.value === 'client') {
  1147. loadClients(currentPage.value, true)
  1148. }
  1149. }
  1150. // 下拉刷新
  1151. function onRefresh() {
  1152. currentPage.value = 1
  1153. if (selectorType.value === 'material') {
  1154. if (materialSource.value === 'mes') {
  1155. loadMaterials(1, false)
  1156. } else {
  1157. loadPurchaseMaterials(true)
  1158. }
  1159. } else if (selectorType.value === 'supplier') {
  1160. loadSuppliers(1, false)
  1161. } else if (selectorType.value === 'client') {
  1162. loadClients(1, false)
  1163. }
  1164. }
  1165. // 搜索
  1166. function handleSearch() {
  1167. currentPage.value = 1
  1168. if (selectorType.value === 'material') {
  1169. loadMaterials(1, false)
  1170. } else if (selectorType.value === 'supplier') {
  1171. loadSuppliers(1, false)
  1172. } else if (selectorType.value === 'client') {
  1173. loadClients(1, false)
  1174. }
  1175. }
  1176. // 获取物料/供应商/客户名称
  1177. function getSelectorItemName(item: any): string {
  1178. if (selectorType.value === 'material') {
  1179. return item.itemName || ''
  1180. } else if (selectorType.value === 'supplier') {
  1181. return item.vendorName || ''
  1182. } else if (selectorType.value === 'client') {
  1183. return item.clientName || ''
  1184. }
  1185. return ''
  1186. }
  1187. // 获取编码
  1188. function getItemCode(item: any): string {
  1189. if (selectorType.value === 'material') {
  1190. return item.itemCode || ''
  1191. } else if (selectorType.value === 'supplier') {
  1192. return item.vendorCode || ''
  1193. } else if (selectorType.value === 'client') {
  1194. return item.clientCode || ''
  1195. }
  1196. return ''
  1197. }
  1198. // 获取规格型号(仅物料有)
  1199. function getItemSpec(item: any): string {
  1200. if (selectorType.value === 'material') {
  1201. return item.specification || ''
  1202. }
  1203. return ''
  1204. }
  1205. // 判断是否已选中
  1206. // 注意:只有当物料编码和采购申请单编号都相同时,才认为是重复选择
  1207. function isSelected(item: any): boolean {
  1208. if (selectorType.value === 'material') {
  1209. const itemCode = item.itemCode || ''
  1210. const purchaseNumber = item.purchaseNumber || ''
  1211. // 检查物料列表中是否存在相同的物料(物料编码 + 采购申请单编号都相同)
  1212. return materialList.value.some(m => {
  1213. const existItemCode = m.itemCode || ''
  1214. const existPurchaseNumber = m.purchaseNumber || ''
  1215. // 只有当物料编码和采购申请单编号都相同时,才认为是重复
  1216. return existItemCode === itemCode &&
  1217. ((existPurchaseNumber === purchaseNumber) ||
  1218. (!existPurchaseNumber && !purchaseNumber))
  1219. })
  1220. } else if (selectorType.value === 'supplier') {
  1221. const code = item.vendorCode || item.supplierCode
  1222. return baseForm.value.supplierCode === code
  1223. } else if (selectorType.value === 'client') {
  1224. const code = item.clientCode
  1225. return baseForm.value.supplierCode === code
  1226. }
  1227. return false
  1228. }
  1229. // 选择物料/供应商/客户
  1230. function selectItem(item: any) {
  1231. if (selectorType.value === 'material') {
  1232. // 检查是否已存在(根据物料编码 + 采购申请单编号判断)
  1233. const exists = isSelected(item)
  1234. if (exists) {
  1235. $modal.msg('该物料已添加')
  1236. return
  1237. }
  1238. // 销售合同不允许选择有采购申请单的物料
  1239. if (isSalesContract.value) {
  1240. const purchaseId = item.purchaseId || ''
  1241. const purchaseNumber = item.purchaseNumber || ''
  1242. if ((purchaseId && purchaseId !== 'null' && purchaseId !== '0' && purchaseId !== 'undefined') ||
  1243. (purchaseNumber && purchaseNumber !== 'null' && purchaseNumber !== 'undefined')) {
  1244. $modal.msgWarn('销售合同不允许选择来自采购申请单的物料')
  1245. return
  1246. }
  1247. }
  1248. // 确保数值字段正确转换(防止后端返回对象或字符串)
  1249. const qty = parseFloat(item.qty) || 0
  1250. const price = parseFloat(item.price) || 0
  1251. const cess = parseFloat(item.cess) || 0
  1252. const priceTax = parseFloat(item.priceTax) || 0
  1253. // 添加物料到列表
  1254. materialList.value.push({
  1255. itemCode: item.itemCode,
  1256. itemName: item.itemName,
  1257. specification: item.specification,
  1258. measureName: item.measureName,
  1259. qty: qty,
  1260. price: price,
  1261. cess: cess,
  1262. priceTax: priceTax,
  1263. purchaseId: isSalesContract.value ? '' : (item.purchaseId || selectedPurchaseApply.value?.contractPurchaseFormId || ''),
  1264. purchaseNumber: isSalesContract.value ? '' : (item.purchaseNumber || selectedPurchaseApply.value?.contractPurchaseFormNumber || ''),
  1265. expanded: true
  1266. })
  1267. // 重新计算总价
  1268. calculateTotalPrice()
  1269. } else if (selectorType.value === 'supplier') {
  1270. // 选择供应商:供方=供应商名称,supplierCode/supplierName 保存供应商信息
  1271. baseForm.value.secondparty_name = item.vendorName
  1272. baseForm.value.supplierCode = item.vendorCode
  1273. baseForm.value.supplierName = item.vendorName
  1274. } else if (selectorType.value === 'client') {
  1275. // 选择客户:需方=客户名称,supplierCode/supplierName 保存客户信息
  1276. baseForm.value.firstparty_name = item.clientName
  1277. baseForm.value.supplierCode = item.clientCode
  1278. baseForm.value.supplierName = item.clientName
  1279. }
  1280. closePopup()
  1281. }
  1282. // 合同类型变化
  1283. let lastContractType = '' // 保存上一次的合同类型
  1284. function onContractTypeChange(e: any) {
  1285. const index = e.detail.value
  1286. if (contractTypeList.value[index]) {
  1287. const oldContractType = lastContractType
  1288. const newContractType = contractTypeList.value[index].contract_type
  1289. // 如果合同类型发生变化,检查是否需要清理物料
  1290. if (oldContractType !== String(newContractType)) {
  1291. // 切换到销售合同时,检查是否有来自采购申请单的物料
  1292. if (newContractType === '1' || newContractType === 1) {
  1293. const invalidMaterials: number[] = []
  1294. materialList.value.forEach((item, idx) => {
  1295. const purchaseId = item.purchaseId || ''
  1296. const purchaseNumber = item.purchaseNumber || ''
  1297. // 排除空值、null字符串、0等无效值
  1298. if ((purchaseId && purchaseId !== 'null' && purchaseId !== '0' && purchaseId !== 'undefined') ||
  1299. (purchaseNumber && purchaseNumber !== 'null' && purchaseNumber !== 'undefined')) {
  1300. invalidMaterials.push(idx)
  1301. }
  1302. })
  1303. if (invalidMaterials.length > 0) {
  1304. $modal.confirm('', `检测到已选择的物料中有 ${invalidMaterials.length} 条来自采购申请单的数据,销售合同不允许使用采购申请单物料,是否清除这些不符合要求的物料?`)
  1305. .then(() => {
  1306. // 用户确认,删除不符合要求的物料(从后往前删,避免索引错乱)
  1307. for (let i = invalidMaterials.length - 1; i >= 0; i--) {
  1308. materialList.value.splice(invalidMaterials[i], 1)
  1309. }
  1310. // 重新计算总价
  1311. calculateTotalPrice()
  1312. // 更新合同类型为销售合同
  1313. baseForm.value.contract_type = newContractType
  1314. baseForm.value.contract_type_name = contractTypeList.value[index].contract_type_name
  1315. lastContractType = String(newContractType)
  1316. // 调整需方供方显示
  1317. adjustPartyDisplay()
  1318. $modal.msgSuccess(`已清除 ${invalidMaterials.length} 条不符合要求的物料`)
  1319. })
  1320. .catch(() => {
  1321. // 用户取消,恢复原来的合同类型
  1322. // 不需要做任何事,因为还没有更新 contract_type
  1323. })
  1324. return // 等待用户确认后再继续
  1325. }
  1326. }
  1327. }
  1328. // 更新合同类型
  1329. baseForm.value.contract_type = newContractType
  1330. baseForm.value.contract_type_name = contractTypeList.value[index].contract_type_name
  1331. lastContractType = String(newContractType)
  1332. // 根据合同类型调整需方和供方的显示
  1333. adjustPartyDisplay()
  1334. }
  1335. }
  1336. // 根据合同类型调整需方和供方的显示
  1337. function adjustPartyDisplay() {
  1338. // 从 baseForm 中获取保存的公司名称
  1339. const companyName = (baseForm.value as any).companyName || ''
  1340. if (isSalesContract.value) {
  1341. // 销售合同:需方=选择客户,供方=公司名称(只读)
  1342. baseForm.value.secondparty_name = companyName
  1343. baseForm.value.supplierCode = ''
  1344. baseForm.value.supplierName = ''
  1345. baseForm.value.firstparty_name = ''
  1346. } else {
  1347. // 其他合同类型:需方=公司名称(只读),供方=选择供应商
  1348. baseForm.value.firstparty_name = companyName
  1349. baseForm.value.secondparty_name = ''
  1350. baseForm.value.supplierCode = ''
  1351. baseForm.value.supplierName = ''
  1352. }
  1353. }
  1354. // 删除物料
  1355. function removeMaterial(index: number) {
  1356. $modal.confirm('', '确定删除该物料?')
  1357. .then(() => {
  1358. materialList.value.splice(index, 1)
  1359. // 重新计算总价
  1360. calculateTotalPrice()
  1361. }).catch(() => {})
  1362. }
  1363. // 物料价格计算 (与 start.vue 保持一致)
  1364. function calculateMaterialPrice(item: any) {
  1365. const price = parseFloat(item.price) || 0
  1366. const cess = parseFloat(item.cess) || 0
  1367. // 计算税后单价:税前单价 * (1 + 税率 / 100)
  1368. item.priceTax = (price * (1 + cess / 100)).toFixed(2)
  1369. // 重新计算总价
  1370. calculateTotalPrice()
  1371. // 更新大写金额
  1372. updateContractMoneyUppercase()
  1373. }
  1374. // 计算合同总价
  1375. let manualInput = false // 标记用户是否手动输入过合同金额
  1376. function calculateTotalPrice() {
  1377. let total = 0
  1378. const hasMaterial = materialList.value.length > 0
  1379. materialList.value.forEach(item => {
  1380. const qty = parseFloat(item.qty) || 0
  1381. const priceTax = parseFloat(item.priceTax) || 0
  1382. total += qty * priceTax
  1383. })
  1384. // 获取当前合同金额
  1385. const currentMoney = parseFloat(baseForm.value.contract_money)
  1386. if (hasMaterial) {
  1387. // 有物料时,检查是否需要更新合同金额
  1388. // 如果是手动输入且大于物料总额,保留用户输入
  1389. // 否则,使用物料总额
  1390. if (manualInput && currentMoney && currentMoney > total) {
  1391. // 保留用户手动输入的较大值
  1392. // 不更新合同金额
  1393. } else {
  1394. // 自动更新为物料总额(包括非手动输入,或手动输入但物料总额更大的情况)
  1395. baseForm.value.contract_money = total.toFixed(2)
  1396. // 重置手动输入标记
  1397. manualInput = false
  1398. }
  1399. } else {
  1400. // 没有物料时,只有非手动输入才清空
  1401. if (!manualInput) {
  1402. baseForm.value.contract_money = ''
  1403. // 合同金额被清空时,删除所有付款信息
  1404. clearAllPaymentInfo()
  1405. }
  1406. // 注意:如果是手动输入,即使没有物料也保留用户输入
  1407. }
  1408. // 更新付款信息中的金额
  1409. calculatePaymentAmount()
  1410. // 更新大写金额
  1411. updateContractMoneyUppercase()
  1412. }
  1413. // 计算物料总价值(仅返回数值,不更新合同金额)
  1414. function calculateTotalPriceValue(): number {
  1415. let total = 0
  1416. materialList.value.forEach(item => {
  1417. const qty = parseFloat(item.qty) || 0
  1418. const priceTax = parseFloat(item.priceTax) || 0
  1419. total += qty * priceTax
  1420. })
  1421. return total
  1422. }
  1423. // 合同金额大写
  1424. const contractMoneyUppercase = ref('')
  1425. // 数字转中文大写函数
  1426. function numberToChineseUppercase(n) {
  1427. const fraction = ['角', '分']
  1428. const digit = [
  1429. '零', '壹', '贰', '叁', '肆',
  1430. '伍', '陆', '柒', '捌', '玖'
  1431. ]
  1432. const unit = [
  1433. ['元', '万', '亿'],
  1434. ['', '拾', '佰', '仟']
  1435. ]
  1436. const head = n < 0 ? '欠' : ''
  1437. n = Math.abs(n)
  1438. let s = ''
  1439. for (let i = 0; i < fraction.length; i++) {
  1440. s += (digit[Math.floor(n * 10 * Math.pow(10, i)) % 10] + fraction[i]).replace(/零./, '')
  1441. }
  1442. s = s || '整'
  1443. n = Math.floor(n)
  1444. for (let i = 0; i < unit[0].length && n > 0; i++) {
  1445. let p = ''
  1446. for (let j = 0; j < unit[1].length && n > 0; j++) {
  1447. p = digit[n % 10] + unit[1][j] + p
  1448. n = Math.floor(n / 10)
  1449. }
  1450. s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s
  1451. }
  1452. return head + s.replace(/(零.)*零元/, '元')
  1453. .replace(/(零.)+/g, '零')
  1454. .replace(/^整$/, '零元整')
  1455. }
  1456. // 更新合同金额大写显示
  1457. function updateContractMoneyUppercase() {
  1458. const contractMoneyVal = baseForm.value.contract_money
  1459. // 如果金额为空(未输入),显示为空字符串
  1460. if (!contractMoneyVal || String(contractMoneyVal).trim() === '') {
  1461. contractMoneyUppercase.value = ''
  1462. } else {
  1463. const contractMoney = parseFloat(contractMoneyVal) || 0
  1464. contractMoneyUppercase.value = numberToChineseUppercase(contractMoney)
  1465. }
  1466. }
  1467. // 监听合同金额变化(检测用户手动输入)- 失焦时触发
  1468. function onContractMoneyBlur() {
  1469. // 当用户手动修改合同金额时,设置手动输入标记
  1470. const calculatedTotal = calculateTotalPriceValue()
  1471. const currentMoney = parseFloat(baseForm.value.contract_money)
  1472. if (currentMoney && currentMoney !== calculatedTotal) {
  1473. manualInput = true
  1474. }
  1475. // 更新大写金额
  1476. updateContractMoneyUppercase()
  1477. calculatePaymentAmount()
  1478. }
  1479. // 数量变化时
  1480. function onQuantityBlur(item: any) {
  1481. calculateTotalPrice()
  1482. }
  1483. // 合同金额失焦验证 (已移除,改为提交前统一验证)
  1484. // function onContractMoneyBlur() {}
  1485. // 税前单价变化时
  1486. function onPriceBlur(item: any) {
  1487. calculateMaterialPrice(item)
  1488. }
  1489. // 税率变化时
  1490. function onCessBlur(item: any) {
  1491. calculateMaterialPrice(item)
  1492. }
  1493. // 判断是否可以添加付款(有合同金额即可)
  1494. const canAddPayment = computed(() => {
  1495. const contractMoney = parseFloat(baseForm.value.contract_money) || 0
  1496. return contractMoney > 0
  1497. })
  1498. // 添加付款
  1499. function addPayment() {
  1500. if (!canAddPayment.value) {
  1501. $modal.msgWarn('请先输入合同金额')
  1502. return
  1503. }
  1504. paymentList.value.push({
  1505. payType: '',
  1506. payTypeName: '',
  1507. proportion: '0',
  1508. amount: '0',
  1509. amountPaid: '0',
  1510. payTime: '',
  1511. payStatus: '0',
  1512. payStatusName: '未支付',
  1513. remark: '',
  1514. expanded: true
  1515. })
  1516. }
  1517. // 比例变化时重新计算金额
  1518. function onProportionBlur(item: any) {
  1519. calculatePaymentAmount()
  1520. }
  1521. // 付款方式类型变化
  1522. function onPaymentTypeChange(index: number, e: any) {
  1523. const selectedIndex = e.detail.value
  1524. const selected = paymentTypeList.value[selectedIndex]
  1525. if (selected) {
  1526. paymentList.value[index].payType = selected.payType
  1527. paymentList.value[index].payTypeName = selected.payTypeName
  1528. }
  1529. }
  1530. // 已付金额变化时更新付款状态
  1531. function onAmountPaidBlur(item: any) {
  1532. updatePaymentStatus(item)
  1533. }
  1534. // 付款时间变化
  1535. function onPayTimeChange(item: any, e: any) {
  1536. // uni-datetime-picker 返回的可能是时间戳或 Date 对象,需要转换为字符串
  1537. const dateValue = e
  1538. if (dateValue) {
  1539. // 如果是时间戳或数字,转换为日期字符串
  1540. if (typeof dateValue === 'number' || typeof dateValue === 'string') {
  1541. const date = new Date(dateValue)
  1542. if (!isNaN(date.getTime())) {
  1543. const year = date.getFullYear()
  1544. const month = String(date.getMonth() + 1).padStart(2, '0')
  1545. const day = String(date.getDate()).padStart(2, '0')
  1546. item.payTime = `${year}-${month}-${day}`
  1547. }
  1548. } else if (typeof dateValue === 'object') {
  1549. // 如果已经是 Date 对象
  1550. const year = dateValue.getFullYear()
  1551. const month = String(dateValue.getMonth() + 1).padStart(2, '0')
  1552. const day = String(dateValue.getDate()).padStart(2, '0')
  1553. item.payTime = `${year}-${month}-${day}`
  1554. }
  1555. }
  1556. }
  1557. // 计算付款金额(根据比例)
  1558. function calculatePaymentAmount() {
  1559. const contractMoney = parseFloat(baseForm.value.contract_money) || 0
  1560. paymentList.value.forEach(item => {
  1561. const proportion = parseFloat(item.proportion) || 0
  1562. const amount = (contractMoney * proportion / 100).toFixed(2)
  1563. item.amount = amount
  1564. // 更新付款状态
  1565. updatePaymentStatus(item)
  1566. })
  1567. }
  1568. // 更新付款状态
  1569. function updatePaymentStatus(item: any) {
  1570. const amount = parseFloat(item.amount) || 0
  1571. const amountPaid = parseFloat(item.amountPaid) || 0
  1572. if (amountPaid === 0) {
  1573. item.payStatus = '0'
  1574. item.payStatusName = '未支付'
  1575. } else if (amountPaid < amount) {
  1576. item.payStatus = '1'
  1577. item.payStatusName = '部分支付'
  1578. } else if (amountPaid >= amount) {
  1579. item.payStatus = '2'
  1580. item.payStatusName = '已支付'
  1581. }
  1582. }
  1583. // 获取付款状态名称
  1584. function getPayStatusName(item: any): string {
  1585. const amount = parseFloat(item.amount) || 0
  1586. const amountPaid = parseFloat(item.amountPaid) || 0
  1587. if (amountPaid === 0) {
  1588. return '未支付'
  1589. } else if (amountPaid < amount) {
  1590. return '部分支付'
  1591. } else if (amountPaid >= amount) {
  1592. return '已支付'
  1593. }
  1594. return '未支付'
  1595. }
  1596. // 清空所有付款信息
  1597. function clearAllPaymentInfo() {
  1598. paymentList.value = []
  1599. }
  1600. // 删除付款
  1601. function removePayment(index: number) {
  1602. $modal.confirm('', '确定删除该付款明细?')
  1603. .then(() => {
  1604. paymentList.value.splice(index, 1)
  1605. }).catch(() => {})
  1606. }
  1607. // 处理审批意见签名
  1608. const currentApprovalFieldName = ref('')
  1609. // 存储每个签名字段的印章信息
  1610. const approvalSealInfo = ref<Record<string, any>>({})
  1611. function handleApprovalSignature(fieldName: string) {
  1612. currentApprovalFieldName.value = fieldName
  1613. signaturePopupShow.value = false
  1614. nextTick(() => {
  1615. signaturePopupShow.value = true
  1616. })
  1617. signaturePopup.value.open()
  1618. }
  1619. // 处理一键签名
  1620. function handleAutoSeal(fieldName: string) {
  1621. const elem = getApprovalElement(fieldName)
  1622. if (elem) {
  1623. getSeal(userStore.user.useId).then(({ returnParams }) => {
  1624. const elem = getApprovalElement(fieldName)
  1625. elem.defaultValue = returnParams.sealFileId.universalid
  1626. elem.sealImgPath = returnParams.sealFileId.path
  1627. // 保存印章信息(用于后端处理)
  1628. // 一键签名时,sealFileId.universalid 就是印章模板 ID
  1629. approvalSealInfo.value[fieldName] = {
  1630. sealInsId: returnParams.sealFileId.universalid, // 签名实例 ID(这里与印章模板 ID 相同)
  1631. sealFileId: returnParams.sealFileId.universalid, // 印章模板 ID(用于 imgval)
  1632. left: 0, // 默认左边距为 0
  1633. top: 0 // 默认上边距为 0
  1634. }
  1635. }).catch(err => {
  1636. $modal.msgError('获取签名失败:' + err)
  1637. })
  1638. }
  1639. }
  1640. // 初始化签字板
  1641. function initSignature() {
  1642. signaturePopupShow.value = false
  1643. setTimeout(() => {
  1644. signaturePopupShow.value = true
  1645. }, 100)
  1646. }
  1647. // 点击签字板按钮
  1648. function onclickSignatureButton(event: string) {
  1649. switch (event) {
  1650. case 'undo':
  1651. signatureRef.value.undo()
  1652. break
  1653. case 'clear':
  1654. signatureRef.value.clear()
  1655. break
  1656. case 'save':
  1657. signatureRef.value.canvasToTempFilePath({
  1658. success: (res: any) => {
  1659. if (res.isEmpty) {
  1660. $modal.msgError('签名不能为空!')
  1661. return
  1662. }
  1663. // 判断上传文件是否是 base64
  1664. if (res.tempFilePath.substring(0, 'data:image/png;base64,'.length) == 'data:image/png;base64,') {
  1665. const _fileData = res.tempFilePath
  1666. uploadSignatureBoardImg(userStore.user.useId, _fileData, getApprovalElement(currentApprovalFieldName.value).tableField)
  1667. .then(({ returnParams }) => {
  1668. const elem = getApprovalElement(currentApprovalFieldName.value)
  1669. elem.defaultValue = returnParams.sealInsID
  1670. elem.sealImgPath = returnParams.path
  1671. // 保存印章信息(用于后端处理)
  1672. // sealInsID 是手写签名的实例 ID
  1673. approvalSealInfo.value[currentApprovalFieldName.value] = {
  1674. sealInsId: returnParams.sealInsID, // 签名实例 ID
  1675. sealFileId: returnParams.sealInsID, // 手写签名图片 ID
  1676. left: 0, // 默认左边距为 0
  1677. top: 0 // 默认上边距为 0
  1678. }
  1679. currentApprovalFieldName.value = ''
  1680. signaturePopupShow.value = false
  1681. signaturePopup.value.close()
  1682. })
  1683. } else {
  1684. // 转 base64
  1685. uni.getFileSystemManager().readFile({
  1686. filePath: res.tempFilePath,
  1687. encoding: 'base64',
  1688. success: (fileData: any) => {
  1689. const _fileData = 'data:image/png;base64,' + fileData.data
  1690. uploadSignatureBoardImg(userStore.user.useId, _fileData, getApprovalElement(currentApprovalFieldName.value).tableField)
  1691. .then(({ returnParams }) => {
  1692. const elem = getApprovalElement(currentApprovalFieldName.value)
  1693. elem.defaultValue = returnParams.sealInsID
  1694. elem.sealImgPath = returnParams.path
  1695. // 保存印章信息(用于后端处理)
  1696. // sealInsID 是手写签名的实例 ID
  1697. approvalSealInfo.value[currentApprovalFieldName.value] = {
  1698. sealInsId: returnParams.sealInsID, // 签名实例 ID
  1699. sealFileId: returnParams.sealInsID, // 手写签名图片 ID
  1700. left: 0, // 默认左边距为 0
  1701. top: 0 // 默认上边距为 0
  1702. }
  1703. currentApprovalFieldName.value = ''
  1704. signaturePopupShow.value = false
  1705. signaturePopup.value.close()
  1706. })
  1707. }
  1708. })
  1709. }
  1710. }
  1711. })
  1712. break
  1713. case 'landscape':
  1714. isLandscape.value = !isLandscape.value
  1715. initSignature()
  1716. break
  1717. }
  1718. }
  1719. // 清空签名
  1720. function clearSignature() {
  1721. onclickSignatureButton('clear')
  1722. }
  1723. // 保存签名
  1724. function saveSignature() {
  1725. onclickSignatureButton('save')
  1726. }
  1727. // 关闭签名
  1728. function closeSignature() {
  1729. signaturePopupShow.value = false
  1730. signaturePopup.value.close()
  1731. }
  1732. // 暴露验证方法给父组件
  1733. defineExpose({
  1734. validate: async () => {
  1735. // 合同编号验证(发起环节或字段可编辑时需要验证)
  1736. if (isSeModel.value || getFieldEditable('contract_number')) {
  1737. if (!baseForm.value.contract_number || !baseForm.value.contract_number.trim()) {
  1738. return Promise.reject(new Error('合同编号不能为空!'))
  1739. }
  1740. // 验证合同编号唯一性(异步调用后端接口)
  1741. try {
  1742. const res = await checkContractNumber(userStore.user.useId, baseForm.value.contract_number, baseForm.value.universalid || '')
  1743. if (res.returnCode !== '1') {
  1744. return Promise.reject(new Error(res.returnMsg || '合同编号验证失败'))
  1745. }
  1746. } catch (error) {
  1747. console.error('合同编号验证失败', error)
  1748. return Promise.reject(new Error('合同编号验证失败,请重试'))
  1749. }
  1750. }
  1751. // 基本信息验证
  1752. if (!baseForm.value.contract_name || !baseForm.value.contract_name.trim()) {
  1753. return Promise.reject(new Error('合同名称不能为空!'))
  1754. }
  1755. // 合同名称长度验证(最多 100 字符)
  1756. if (baseForm.value.contract_name && baseForm.value.contract_name.length > 100) {
  1757. return Promise.reject(new Error('合同名称长度不能超过 100!'))
  1758. }
  1759. // 合同类型验证 (发起环节或字段可编辑时需要验证)
  1760. if (isSeModel.value || getFieldEditable('contract_type')) {
  1761. const contractType = baseForm.value.contract_type
  1762. if (!contractType || (typeof contractType === 'string' && !contractType.trim())) {
  1763. return Promise.reject(new Error('合同类型不能为空!'))
  1764. }
  1765. }
  1766. // 经办人验证(发起环节或字段可编辑时需要验证)
  1767. /*if (isSeModel.value || getFieldEditable('salesman_name')) {
  1768. if (!baseForm.value.salesman || !baseForm.value.salesman.trim()) {
  1769. return Promise.reject(new Error('经办人不能为空!'))
  1770. }
  1771. }*/
  1772. // 合同金额验证(发起环节或字段可编辑时需要验证)
  1773. if (isSeModel.value || getFieldEditable('contract_money')) {
  1774. if (baseForm.value.contract_money) {
  1775. // 验证是否为数字
  1776. const moneyRegex = /^\d+(\.\d{1,2})?$/
  1777. if (!moneyRegex.test(baseForm.value.contract_money)) {
  1778. return Promise.reject(new Error('合同金额只能是数字!'))
  1779. }
  1780. // 验证合同金额不能小于物料总金额
  1781. const totalPrice = calculateTotalPriceValue()
  1782. const contractMoney = parseFloat(baseForm.value.contract_money)
  1783. if (contractMoney < totalPrice) {
  1784. return Promise.reject(new Error('合同金额不能小于物料总金额:' + totalPrice.toFixed(2)))
  1785. }
  1786. }
  1787. }
  1788. // 根据合同类型验证不同字段:销售合同验证需方(客户),其他合同验证供方(供应商)
  1789. if (isSeModel.value || getFieldEditable(isSalesContract.value ? 'firstparty_name' : 'secondparty_name')) {
  1790. if (isSalesContract.value) {
  1791. // 销售合同:验证需方(客户)
  1792. if (!baseForm.value.firstparty_name || !baseForm.value.firstparty_name.trim()) {
  1793. return Promise.reject(new Error('需方不能为空!'))
  1794. }
  1795. } else {
  1796. // 其他合同:验证供方(供应商)
  1797. if (!baseForm.value.secondparty_name || !baseForm.value.secondparty_name.trim()) {
  1798. return Promise.reject(new Error('供方不能为空!'))
  1799. }
  1800. }
  1801. }
  1802. // 物料明细验证(发起环节时验证)
  1803. if (isSeModel.value && materialList.value.length > 0) {
  1804. for (let i = 0; i < materialList.value.length; i++) {
  1805. const item = materialList.value[i]
  1806. // 数量验证:不能为空且必须大于 0
  1807. if (!item.qty || Number(item.qty) <= 0) {
  1808. return Promise.reject(new Error(`请填写第${i + 1}个物料的数量,且必须大于 0`))
  1809. }
  1810. // 税前单价验证:不能为空且不能为负数
  1811. if (!item.price || Number(item.price) < 0) {
  1812. return Promise.reject(new Error(`请填写第${i + 1}个物料的税前单价,且不能为负数`))
  1813. }
  1814. // 税率验证:不能为空且不能为负数
  1815. if (!item.cess || Number(item.cess) < 0) {
  1816. return Promise.reject(new Error(`请填写第${i + 1}个物料的税率,且不能为负数`))
  1817. }
  1818. // 税后单价验证:不能为空且不能为负数
  1819. if (!item.priceTax || Number(item.priceTax) < 0) {
  1820. return Promise.reject(new Error(`请填写第${i + 1}个物料的税后单价,且不能为负数`))
  1821. }
  1822. }
  1823. }
  1824. // 付款明细验证(发起环节时验证)
  1825. if (isSeModel.value && paymentList.value.length > 0) {
  1826. for (let i = 0; i < paymentList.value.length; i++) {
  1827. const item = paymentList.value[i]
  1828. // 付款方式验证
  1829. if (!item.payType || !item.payTypeName) {
  1830. return Promise.reject(new Error(`第${i + 1}行付款方式不能为空!`))
  1831. }
  1832. // 比例验证:不能为空且必须大于 0
  1833. if (!item.proportion || Number(item.proportion) <= 0) {
  1834. return Promise.reject(new Error(`请填写第${i + 1}个付款比例,且必须大于 0`))
  1835. }
  1836. // 金额验证
  1837. /*if (!item.amount || item.amount.toString().trim() === '') {
  1838. return Promise.reject(new Error(`第${i + 1}行付款金额不能为空!`))
  1839. }*/
  1840. }
  1841. }
  1842. // 验证审批意见字段(如果在 table_fields 中)
  1843. const approvalFields = [
  1844. { fieldName: 'departmental_opinion', msg: '部门意见' },
  1845. { fieldName: 'deputy_general_manager_opinion', msg: '分管副总意见' },
  1846. { fieldName: 'audit_deputy_general_manager_opinion', msg: '分管副总意见' },
  1847. { fieldName: 'general_manager_opinion', msg: '总经理意见' },
  1848. { fieldName: 'finance_opinion', msg: '财务意见' },
  1849. { fieldName: 'chairman_opinion', msg: '董事长意见' }
  1850. ]
  1851. for (const field of approvalFields) {
  1852. const elem = getApprovalElement(field.fieldName)
  1853. if (elem) {
  1854. // 检查该字段是否在当前环节的可编辑字段列表中
  1855. // editableFields 包含了当前环节可编辑的所有字段(即 table_fields)
  1856. if (props.editableFields && props.editableFields.includes(field.fieldName)) {
  1857. if (!elem.defaultValue || elem.defaultValue === '') {
  1858. return Promise.reject(new Error(field.msg + '不能为空!'))
  1859. }
  1860. }
  1861. }
  1862. }
  1863. return Promise.resolve()
  1864. },
  1865. // 获取表单数据(返回 formElements 格式)
  1866. getFormElements: () => {
  1867. const formElements: any[] = []
  1868. // 添加基本信息字段
  1869. Object.keys(baseForm.value).forEach(key => {
  1870. const value = baseForm.value[key]
  1871. if (value !== undefined && value !== null) {
  1872. formElements.push({
  1873. name: key,
  1874. value: String(value),
  1875. type: '0' // 普通文本类型
  1876. })
  1877. }
  1878. })
  1879. // 添加审批意见字段
  1880. const approvalFields = ['departmental_opinion', 'deputy_general_manager_opinion', 'audit_deputy_general_manager_opinion', 'general_manager_opinion', 'finance_opinion', 'chairman_opinion']
  1881. approvalFields.forEach(fieldName => {
  1882. const elem = getApprovalElement(fieldName)
  1883. if (elem) {
  1884. formElements.push({
  1885. name: fieldName,
  1886. value: elem.defaultValue || '',
  1887. type: elem.type || '0'
  1888. })
  1889. // 添加签名图片的 imgval 值(用于后端处理)
  1890. // 格式:sealFileId_left_top(必须使用印章模板 ID,而不是签名实例 ID)
  1891. if (approvalSealInfo.value[fieldName]) {
  1892. const sealInfo = approvalSealInfo.value[fieldName]
  1893. formElements.push({
  1894. name: fieldName + '_imgval',
  1895. value: `${sealInfo.sealFileId}_${sealInfo.left}_${sealInfo.top}`,
  1896. type: '0'
  1897. })
  1898. } else if (elem.sealImgPath && elem.defaultValue) {
  1899. // 兼容旧数据:如果有 sealImgPath 和 defaultValue,提取 sealId 构建 imgval
  1900. // sealImgPath 格式:/shares/document/seal/593268258724500.png
  1901. const sealIdMatch = elem.sealImgPath.match(/\/seal\/(\d+)\.png$/)
  1902. if (sealIdMatch) {
  1903. const sealId = sealIdMatch[1]
  1904. formElements.push({
  1905. name: fieldName + '_imgval',
  1906. value: `${sealId}_0_0`,
  1907. type: '0'
  1908. })
  1909. }
  1910. }
  1911. }
  1912. })
  1913. // 添加物料明细(JSON 字符串格式)
  1914. if (materialList.value && materialList.value.length > 0) {
  1915. formElements.push({
  1916. name: 'contractMaterialList',
  1917. value: JSON.stringify(materialList.value),
  1918. type: '0'
  1919. })
  1920. }
  1921. // 添加付款明细(JSON 字符串格式)
  1922. if (paymentList.value && paymentList.value.length > 0) {
  1923. formElements.push({
  1924. name: 'contractPaymentList',
  1925. value: JSON.stringify(paymentList.value),
  1926. type: '0'
  1927. })
  1928. }
  1929. return formElements
  1930. },
  1931. // 获取表单数据(兼容旧版本,返回键值对格式)
  1932. getFormData: () => {
  1933. const formData: any = {
  1934. ...baseForm.value,
  1935. contractMaterialList: materialList.value || [],
  1936. contractPaymentList: paymentList.value || []
  1937. }
  1938. // 添加审批意见字段数据
  1939. const approvalFields = ['departmental_opinion', 'deputy_general_manager_opinion', 'audit_deputy_general_manager_opinion', 'general_manager_opinion', 'finance_opinion', 'chairman_opinion']
  1940. approvalFields.forEach(fieldName => {
  1941. const elem = getApprovalElement(fieldName)
  1942. if (elem) {
  1943. formData[fieldName] = elem.defaultValue || ''
  1944. // 添加签名图片的 imgval 值(用于后端处理)
  1945. // 格式:sealFileId_left_top(必须使用印章模板 ID,而不是签名实例 ID)
  1946. if (approvalSealInfo.value[fieldName]) {
  1947. const sealInfo = approvalSealInfo.value[fieldName]
  1948. formData[fieldName + '_imgval'] = `${sealInfo.sealFileId}_${sealInfo.left}_${sealInfo.top}`
  1949. } else if (elem.sealImgPath && elem.defaultValue) {
  1950. // 兼容旧数据:如果有 sealImgPath 和 defaultValue,提取 sealId 构建 imgval
  1951. // sealImgPath 格式:/shares/document/seal/593268258724500.png
  1952. const sealIdMatch = elem.sealImgPath.match(/\/seal\/(\d+)\.png$/)
  1953. if (sealIdMatch) {
  1954. const sealId = sealIdMatch[1]
  1955. formData[fieldName + '_imgval'] = `${sealId}_0_0`
  1956. }
  1957. }
  1958. }
  1959. })
  1960. return formData
  1961. }
  1962. })
  1963. </script>
  1964. <style lang="scss" scoped>
  1965. /* 基本信息中的禁用字段样式优化 */
  1966. ::v-deep .uni-forms {
  1967. .uni-forms-item__content {
  1968. .uni-easyinput__content-input {
  1969. font-size: calc(14px + 1.2*(1rem - 16px)) !important;
  1970. font-weight: 500;
  1971. color: #333;
  1972. }
  1973. .uni-date {
  1974. .uni-icons {
  1975. font-size: calc(22px + 1.2*(1rem - 16px)) !important;
  1976. font-weight: 500;
  1977. }
  1978. .uni-date__x-input {
  1979. height: auto;
  1980. font-size: calc(14px + 1.2*(1rem - 16px)) !important;
  1981. font-weight: 500;
  1982. color: #333;
  1983. }
  1984. }
  1985. }
  1986. }
  1987. // 审批意见字段样式(与 purchase-form 保持一致)
  1988. .element_value_container {
  1989. .signature_img {
  1990. width: 180px;
  1991. }
  1992. }
  1993. // 选择器样式
  1994. .selector-wrapper {
  1995. padding: 10px 0;
  1996. cursor: pointer;
  1997. .placeholder {
  1998. color: #c0c4cc;
  1999. }
  2000. }
  2001. // 合同金额大写样式
  2002. .contract-money-uppercase {
  2003. font-size: 12px;
  2004. color: #666;
  2005. margin-top: 5px;
  2006. padding-left: 2px;
  2007. }
  2008. .picker {
  2009. padding: 10px 0;
  2010. color: #333;
  2011. }
  2012. // 签名弹窗样式(与 purchase-form 保持一致)
  2013. .signature_container {
  2014. background-color: #f5f5f5;
  2015. height: 40vh;
  2016. width: 90vw;
  2017. .signature_content {
  2018. height: 80%;
  2019. width: 100%;
  2020. }
  2021. .signature_button_container {
  2022. height: 20%;
  2023. width: 100%;
  2024. button {
  2025. height: 100%;
  2026. }
  2027. }
  2028. }
  2029. .signature_container_landscape {
  2030. height: 100vh;
  2031. width: 100vw;
  2032. .signature_content {
  2033. height: 85%;
  2034. width: 100%;
  2035. }
  2036. .signature_button_container {
  2037. margin-top: 10%;
  2038. height: 15%;
  2039. width: 100%;
  2040. button {
  2041. height: 100%;
  2042. width: 100%;
  2043. transform: rotate(90deg);
  2044. }
  2045. }
  2046. }
  2047. // 物料列表 - 卡片式展示
  2048. .material-list {
  2049. display: flex;
  2050. flex-direction: column;
  2051. gap: 10px;
  2052. }
  2053. .material-actions {
  2054. display: flex;
  2055. gap: 10px;
  2056. margin-bottom: 15px;
  2057. button {
  2058. flex: 1;
  2059. }
  2060. }
  2061. .material-card {
  2062. border: 1px solid #e5e5e5;
  2063. border-radius: 6px;
  2064. overflow: hidden;
  2065. background-color: #fff;
  2066. margin-bottom: 8px;
  2067. .material-header {
  2068. display: flex;
  2069. justify-content: space-between;
  2070. align-items: center;
  2071. padding: 10px 12px;
  2072. background-color: #f8f9fa;
  2073. cursor: pointer;
  2074. .material-main-info {
  2075. display: flex;
  2076. align-items: center;
  2077. gap: 8px;
  2078. flex: 1;
  2079. .material-name {
  2080. font-size: 14px;
  2081. font-weight: bold;
  2082. color: #333;
  2083. }
  2084. .material-code {
  2085. font-size: 12px;
  2086. color: #999;
  2087. white-space: nowrap;
  2088. }
  2089. }
  2090. .material-expand {
  2091. display: flex;
  2092. align-items: center;
  2093. margin-left: 10px;
  2094. }
  2095. }
  2096. .material-detail {
  2097. padding: 10px 12px;
  2098. border-top: 1px solid #e5e5e5;
  2099. .detail-row {
  2100. display: flex;
  2101. align-items: center;
  2102. flex-wrap: wrap;
  2103. gap: 8px;
  2104. .detail-label {
  2105. font-size: 13px;
  2106. color: #666;
  2107. flex-shrink: 0;
  2108. }
  2109. .detail-value {
  2110. flex: 0 0 auto;
  2111. font-size: 13px;
  2112. color: #333;
  2113. }
  2114. }
  2115. // 删除按钮行的特殊样式
  2116. &.delete-row {
  2117. button {
  2118. margin-left: auto;
  2119. padding: 2px 10px;
  2120. height: auto;
  2121. line-height: 1.5;
  2122. }
  2123. }
  2124. // 价格样式
  2125. .price-value {
  2126. color: #f56c6c;
  2127. font-weight: bold;
  2128. }
  2129. // 采购申请单编号样式
  2130. .purchase-number {
  2131. color: #409eff;
  2132. font-weight: normal;
  2133. }
  2134. }
  2135. }
  2136. .empty-materials {
  2137. text-align: center;
  2138. padding: 30px;
  2139. color: #909399;
  2140. font-size: 14px;
  2141. }
  2142. // 付款状态标签样式
  2143. .status-tag {
  2144. padding: 2px 8px;
  2145. border-radius: 4px;
  2146. font-size: 12px;
  2147. &.status-0 {
  2148. background-color: #f0f0f0;
  2149. color: #333;
  2150. }
  2151. &.status-1 {
  2152. background-color: #fff7e6;
  2153. color: #fa8c16;
  2154. }
  2155. &.status-2 {
  2156. background-color: #f6ffed;
  2157. color: #52c41a;
  2158. }
  2159. }
  2160. // 付款列表 - 卡片式展示
  2161. .payment-list {
  2162. display: flex;
  2163. flex-direction: column;
  2164. gap: 10px;
  2165. }
  2166. .payment-actions {
  2167. display: flex;
  2168. gap: 10px;
  2169. margin-bottom: 15px;
  2170. button {
  2171. flex: 1;
  2172. }
  2173. }
  2174. // 付款列表 - 卡片式展示
  2175. .payment-list {
  2176. display: flex;
  2177. flex-direction: column;
  2178. gap: 10px;
  2179. }
  2180. .payment-actions {
  2181. display: flex;
  2182. gap: 10px;
  2183. margin-bottom: 15px;
  2184. button {
  2185. flex: 1;
  2186. }
  2187. }
  2188. .payment-card {
  2189. border: 1px solid #e5e5e5;
  2190. border-radius: 6px;
  2191. overflow: hidden;
  2192. background-color: #fff;
  2193. margin-bottom: 8px;
  2194. .payment-header {
  2195. display: flex;
  2196. justify-content: space-between;
  2197. align-items: center;
  2198. padding: 10px 12px;
  2199. background-color: #f8f9fa;
  2200. cursor: pointer;
  2201. .payment-main-info {
  2202. display: flex;
  2203. align-items: center;
  2204. gap: 8px;
  2205. flex: 1;
  2206. .payment-name {
  2207. font-size: 14px;
  2208. font-weight: bold;
  2209. color: #333;
  2210. }
  2211. }
  2212. .payment-expand {
  2213. display: flex;
  2214. align-items: center;
  2215. margin-left: 10px;
  2216. }
  2217. }
  2218. .payment-detail {
  2219. padding: 10px 12px;
  2220. border-top: 1px solid #e5e5e5;
  2221. .detail-row {
  2222. display: flex;
  2223. align-items: center;
  2224. flex-wrap: wrap;
  2225. gap: 8px;
  2226. .detail-label {
  2227. font-size: 13px;
  2228. color: #666;
  2229. flex-shrink: 0;
  2230. }
  2231. .detail-value {
  2232. flex: 0 0 auto;
  2233. font-size: 13px;
  2234. color: #333;
  2235. }
  2236. }
  2237. }
  2238. }
  2239. .empty-payments {
  2240. text-align: center;
  2241. padding: 30px;
  2242. color: #909399;
  2243. font-size: 14px;
  2244. }
  2245. // 付款状态标签样式
  2246. .status-tag {
  2247. padding: 2px 8px;
  2248. border-radius: 4px;
  2249. font-size: 12px;
  2250. &.status-0 {
  2251. background-color: #f0f0f0;
  2252. color: #999;
  2253. }
  2254. &.status-1 {
  2255. background-color: #ffecdb;
  2256. color: #ff8800;
  2257. }
  2258. &.status-2 {
  2259. background-color: #e8f5e9;
  2260. color: #4caf50;
  2261. }
  2262. }
  2263. // 采购申请单编号样式
  2264. .purchase-number {
  2265. color: #007aff;
  2266. font-weight: 500;
  2267. }
  2268. // 选择器弹窗样式
  2269. .selector-popup {
  2270. // ✅ 关键:使用固定宽度
  2271. width: 580px;
  2272. max-width: 95vw;
  2273. min-width: 300px;
  2274. max-height: 70vh;
  2275. background-color: #fff;
  2276. border-radius: 12px;
  2277. overflow: hidden;
  2278. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  2279. position: relative;
  2280. margin: 0 auto;
  2281. .popup-header {
  2282. display: flex;
  2283. justify-content: space-between;
  2284. align-items: center;
  2285. padding: 15px;
  2286. border-bottom: 1px solid #e5e5e5;
  2287. width: 100%;
  2288. box-sizing: border-box;
  2289. overflow: hidden;
  2290. .popup-title {
  2291. font-size: 16px;
  2292. font-weight: bold;
  2293. overflow: hidden;
  2294. text-overflow: ellipsis;
  2295. white-space: nowrap;
  2296. max-width: calc(100% - 30px);
  2297. }
  2298. }
  2299. // 物料来源选择
  2300. .material-source-selector {
  2301. padding: 10px 15px;
  2302. border-bottom: 1px solid #f0f0f0;
  2303. width: 100%;
  2304. box-sizing: border-box;
  2305. overflow: hidden;
  2306. .source-options {
  2307. display: flex;
  2308. gap: 10px;
  2309. width: 100%;
  2310. .source-option {
  2311. flex: 1;
  2312. min-width: 0;
  2313. padding: 8px 12px;
  2314. text-align: center;
  2315. background-color: #f5f5f5;
  2316. border-radius: 6px;
  2317. font-size: 14px;
  2318. cursor: pointer;
  2319. transition: all 0.2s;
  2320. overflow: hidden;
  2321. &.active {
  2322. background-color: #007aff;
  2323. color: #fff;
  2324. font-weight: 500;
  2325. }
  2326. }
  2327. }
  2328. }
  2329. // 采购申请单选择器
  2330. .purchase-selector {
  2331. display: flex;
  2332. gap: 8px;
  2333. padding: 10px 15px;
  2334. border-bottom: 1px solid #f0f0f0;
  2335. align-items: center;
  2336. width: 100%;
  2337. box-sizing: border-box;
  2338. overflow: hidden;
  2339. .picker {
  2340. flex: 1;
  2341. min-width: 0;
  2342. padding: 8px 12px;
  2343. background-color: #f5f5f5;
  2344. border-radius: 6px;
  2345. font-size: 14px;
  2346. overflow: hidden;
  2347. text-overflow: ellipsis;
  2348. }
  2349. button[type="warn"] {
  2350. flex-shrink: 0;
  2351. padding: 0 12px;
  2352. }
  2353. }
  2354. .search-bar {
  2355. display: flex;
  2356. gap: 10px;
  2357. padding: 10px 15px;
  2358. align-items: center;
  2359. width: 100%;
  2360. box-sizing: border-box;
  2361. overflow: hidden;
  2362. button {
  2363. flex-shrink: 0;
  2364. }
  2365. }
  2366. .popup-content {
  2367. max-height: 50vh;
  2368. width: 100%;
  2369. overflow-x: hidden;
  2370. position: relative;
  2371. .selector-item {
  2372. padding: 10px 12px;
  2373. border-bottom: 1px solid #f0f0f0;
  2374. width: 100%;
  2375. box-sizing: border-box;
  2376. overflow: hidden;
  2377. &:active {
  2378. background-color: #f5f5f5;
  2379. }
  2380. .selector-item-content {
  2381. display: flex;
  2382. justify-content: space-between;
  2383. align-items: center;
  2384. gap: 8px;
  2385. width: 100%;
  2386. max-width: 100%;
  2387. overflow: hidden;
  2388. min-width: 0;
  2389. .item-info {
  2390. flex: 1 1 auto !important;
  2391. min-width: 0 !important;
  2392. max-width: none !important;
  2393. display: flex;
  2394. flex-wrap: wrap;
  2395. align-items: center;
  2396. gap: 4px;
  2397. overflow: hidden;
  2398. position: relative;
  2399. .item-name {
  2400. font-size: 15px;
  2401. font-weight: bold;
  2402. color: #333;
  2403. flex-basis: 100%;
  2404. margin-bottom: 4px;
  2405. word-break: break-all;
  2406. overflow-wrap: break-word;
  2407. }
  2408. .item-purchase {
  2409. font-size: 12px;
  2410. color: #007aff;
  2411. flex-basis: 100%;
  2412. margin-bottom: 2px;
  2413. word-break: break-all;
  2414. overflow-wrap: break-word;
  2415. }
  2416. .item-code,
  2417. .item-spec,
  2418. .item-extra {
  2419. font-size: 13px;
  2420. color: #666;
  2421. white-space: nowrap;
  2422. font-weight: 500;
  2423. flex-shrink: 0;
  2424. max-width: 100%;
  2425. overflow: hidden;
  2426. text-overflow: ellipsis;
  2427. }
  2428. .item-code::after,
  2429. .item-spec::after {
  2430. content: ' | ';
  2431. margin: 0 4px;
  2432. color: #ddd;
  2433. }
  2434. .item-spec:last-child::after,
  2435. .item-extra:last-child::after {
  2436. content: '';
  2437. }
  2438. }
  2439. .selected-tag {
  2440. color: #409eff;
  2441. font-size: 12px;
  2442. padding: 4px 8px;
  2443. background-color: #ecf5ff;
  2444. border-radius: 4px;
  2445. }
  2446. }
  2447. }
  2448. .loading-text,
  2449. .no-more-text {
  2450. padding: 16px;
  2451. text-align: center;
  2452. color: #999;
  2453. font-size: 14px;
  2454. }
  2455. .empty-data {
  2456. padding: 40px 16px;
  2457. text-align: center;
  2458. color: #999;
  2459. font-size: 14px;
  2460. }
  2461. }
  2462. }
  2463. </style>