contract-form.vue 86 KB

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