| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675 |
- <template>
- <el-dialog
- v-model="dialogVisible"
- :title="formTitle"
- width="100%"
- top="20px"
- class="dialog-form-layout contract-form-dialog"
- destroy-on-close
- append-to-body
- modal-class="order-module-dialog-mask"
- :close-on-click-modal="false"
- @opened="handleOpened"
- >
- <div
- class="contract-form-body"
- v-loading="formLoading"
- element-loading-text="正在加载合同数据..."
- >
- <el-form
- ref="formRef"
- :model="form"
- :rules="rules"
- label-position="right"
- label-width="120px"
- class="crm-dialog-form"
- :disabled="formLoading"
- >
- <!-- 合同基础信息(一行 4 列) -->
- <div class="contract-basic-info">
- <el-form-item label="合同编号" prop="contractNo">
- <el-input v-model="form.contractNo" placeholder="系统自动生成,格式:SX/X-JT20250926002" maxlength="50" />
- </el-form-item>
- <el-form-item label="合同名称" prop="contractName">
- <el-input
- v-model="form.contractName"
- placeholder="可不填,自动生成:供方简称-需方简称(合同编号)"
- maxlength="100"
- show-word-limit
- />
- </el-form-item>
- <el-form-item label="合同类型" prop="contractType" required>
- <el-select v-model="form.contractType" placeholder="请选择合同类型" style="width: 100%" @change="handleContractTypeChange">
- <el-option v-for="dict in dictStore.getOptions('crm_contract_type')" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="合同模版" prop="contractTemplate" required>
- <el-select v-model="form.contractTemplate" placeholder="请选择合同模版" style="width: 100%" @change="handleContractTemplateChange">
- <el-option v-for="dict in dictStore.getOptions('crm_contract_template')" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="客户名称" prop="customerId" required>
- <el-input v-model="form.customerName" placeholder="请选择客户" readonly>
- <template #append>
- <el-button text @click="handleSelectCustomer">
- <el-icon><Search /></el-icon>
- </el-button>
- </template>
- </el-input>
- </el-form-item>
- <el-form-item label="关联报价单" prop="quoteId">
- <el-input v-model="form.quoteNo" placeholder="请选择报价单(可选)" readonly clearable>
- <template #append>
- <el-button text @click="handleSelectQuote">
- <el-icon><Search /></el-icon>
- </el-button>
- </template>
- </el-input>
- </el-form-item>
- <el-form-item label="联系人" prop="contactId" required>
- <el-input v-model="form.contactsName" placeholder="请选择联系人" maxlength="50">
- <template #append>
- <el-button text @click="handleSelectContact" :disabled="!form.customerId">
- <el-icon><Search /></el-icon>
- </el-button>
- </template>
- </el-input>
- </el-form-item>
- <el-form-item label="联系电话" prop="contactsPhone">
- <el-input v-model="form.contactsPhone" placeholder="从客户/联系人带出" maxlength="20" />
- </el-form-item>
- <el-form-item label="签订日期" prop="signDate">
- <el-date-picker
- v-model="form.signDate"
- type="date"
- placeholder="选择签订日期"
- value-format="YYYY-MM-DD"
- style="width: 100%"
- @change="handleSignDateChange"
- />
- </el-form-item>
- <el-form-item label="负责人" prop="ownerUserId" required>
- <el-select
- v-model="form.ownerUserId"
- placeholder="请选择负责人"
- filterable
- clearable
- style="width: 100%"
- @change="handleOwnerChange"
- >
- <el-option v-for="user in userList" :key="user.userId" :label="user.nickName" :value="user.userId" />
- </el-select>
- </el-form-item>
- <el-form-item label="合同金额(元)" prop="amount">
- <el-input-number
- v-model="form.amount"
- :precision="2"
- :min="0"
- placeholder="可选填,留空则从产品明细自动计算"
- style="width: 100%"
- class="money-field-input"
- />
- </el-form-item>
- <el-form-item label="合同期限" prop="startDate" required>
- <el-date-picker
- v-model="contractDateRange"
- type="daterange"
- range-separator="-"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- value-format="YYYY-MM-DD"
- style="width: 100%"
- @change="handleDateRangeChange"
- />
- </el-form-item>
- <el-form-item label="交易主体" prop="supplierId" required>
- <el-select v-model="form.supplierId" placeholder="请选择交易主体" clearable style="width: 100%" @change="handleSupplierChange">
- <el-option v-for="supplier in supplierList" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="结算方式" prop="settlementMethod">
- <el-select v-model="form.settlementMethod" placeholder="请选择结算方式" clearable style="width: 100%">
- <el-option v-for="dict in dictStore.getOptions('crm_settlement_method')" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- </div>
- <!-- 供方 / 需方信息(左右布局,默认缩略、各自独立展开) -->
- <div class="parties-section">
- <div class="parties-section-toolbar">
- <el-button text type="primary" size="small" @click="partiesExpanded = !partiesExpanded">
- <el-icon>
- <ArrowUp v-if="partiesExpanded" />
- <ArrowDown v-else />
- </el-icon>
- {{ partiesExpanded ? '收起供需方' : '展开供需方' }}
- </el-button>
- <el-button type="primary" size="small" @click="handleSwapParties">
- <el-icon><Sort /></el-icon>
- 供需方调换
- </el-button>
- </div>
- <div class="parties-row">
- <div class="party-panel" :class="{ 'is-collapsed': !partiesExpanded }">
- <div class="party-panel__header">
- <div class="party-panel__title">供方信息</div>
- </div>
- <div v-if="!partiesExpanded" class="party-panel__summary">
- <span class="party-panel__summary-label">单位名称</span>
- <span class="party-panel__summary-value" :class="{ 'is-empty': !form.partyA }">
- {{ form.partyA || '未填写' }}
- </span>
- </div>
- <div v-show="partiesExpanded" class="form-grid-2">
- <el-form-item label="单位名称(章)" prop="partyA">
- <el-input v-model="form.partyA" placeholder="请输入单位名称" maxlength="100" />
- </el-form-item>
- <el-form-item label="单位地址" prop="partyAAddress">
- <el-input v-model="form.partyAAddress" placeholder="请输入单位地址" maxlength="200" />
- </el-form-item>
- <el-form-item label="法定代表人" prop="partyALegalPerson">
- <el-input v-model="form.partyALegalPerson" placeholder="请输入法定代表人" maxlength="100" />
- </el-form-item>
- <el-form-item label="代理人" prop="partyAAgent">
- <el-input v-model="form.partyAAgent" placeholder="请输入代理人(可选)" maxlength="100" />
- </el-form-item>
- <el-form-item label="电话/传真" prop="partyAPhoneFax">
- <el-input v-model="form.partyAPhoneFax" placeholder="请输入电话/传真" maxlength="100" />
- </el-form-item>
- <el-form-item label="开户银行" prop="partyABank">
- <el-input v-model="form.partyABank" placeholder="请输入开户银行" maxlength="200" />
- </el-form-item>
- <el-form-item label="账号" prop="partyAAccount">
- <el-input v-model="form.partyAAccount" placeholder="请输入账号" maxlength="100" />
- </el-form-item>
- </div>
- </div>
- <div class="party-flow-bridge" aria-hidden="true">
- <span class="party-flow-bridge__label">供货</span>
- <span class="party-flow-bridge__arrow">
- <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
- <path
- d="M5 12h11.5"
- stroke="currentColor"
- stroke-width="2.75"
- stroke-linecap="round"
- />
- <path
- d="M13.5 8.5 18.5 12l-5 3.5"
- stroke="currentColor"
- stroke-width="2.75"
- stroke-linecap="round"
- stroke-linejoin="round"
- />
- </svg>
- </span>
- </div>
- <div class="party-panel" :class="{ 'is-collapsed': !partiesExpanded }">
- <div class="party-panel__header">
- <div class="party-panel__title">需方信息</div>
- </div>
- <div v-if="!partiesExpanded" class="party-panel__summary">
- <span class="party-panel__summary-label">单位名称</span>
- <span class="party-panel__summary-value" :class="{ 'is-empty': !form.partyB }">
- {{ form.partyB || '未填写' }}
- </span>
- </div>
- <div v-show="partiesExpanded" class="form-grid-2">
- <el-form-item label="单位名称(章)" prop="partyB">
- <el-input v-model="form.partyB" placeholder="从客户开票信息带出" maxlength="200" />
- </el-form-item>
- <el-form-item label="单位地址" prop="partyBAddress">
- <el-input v-model="form.partyBAddress" placeholder="从客户开票信息带出" maxlength="500" />
- </el-form-item>
- <el-form-item label="法定代表人" prop="partyBLegalPerson">
- <el-input v-model="form.partyBLegalPerson" placeholder="从客户开票信息带出" maxlength="100" />
- </el-form-item>
- <el-form-item label="代理人" prop="partyBAgent">
- <el-input v-model="form.partyBAgent" placeholder="从客户开票信息带出(可选)" maxlength="100" />
- </el-form-item>
- <el-form-item label="电话/传真" prop="partyBPhoneFax">
- <el-input v-model="form.partyBPhoneFax" placeholder="从客户开票信息带出" maxlength="100" />
- </el-form-item>
- <el-form-item label="开户银行" prop="partyBBank">
- <el-input v-model="form.partyBBank" placeholder="从客户开票信息带出" maxlength="200" />
- </el-form-item>
- <el-form-item label="账号" prop="partyBAccount">
- <el-input v-model="form.partyBAccount" placeholder="从客户开票信息带出" maxlength="100" />
- </el-form-item>
- </div>
- </div>
- </div>
- </div>
- <!-- 产品明细部分 -->
- <el-divider content-position="left">产品明细</el-divider>
- <div class="panel dialog-list-panel mb-4">
- <div class="list-toolbar">
- <div class="list-toolbar-row">
- <div class="list-toolbar-selected">
- <el-button class="btn-main" type="primary" @click="handleAddProduct">
- <el-icon><Plus /></el-icon>添加产品
- </el-button>
- <el-button class="btn-ghost" @click="handleRemoveSelectedProducts" :disabled="selectedProducts.length === 0">
- <el-icon><Delete /></el-icon>删除选中
- </el-button>
- </div>
- </div>
- </div>
- <div class="list-body list-wrap">
- <ListTable table-key="crm-contract-components-contract-form-dialog" :data="form.products" class="list-table" stripe @selection-change="handleProductSelectionChange">
- <el-table-column type="selection" min-width="55" align="center" header-align="center" />
- <el-table-column label="序号" type="index" min-width="60" align="center" header-align="center" />
- <el-table-column label="产品名称" min-width="160" :show-overflow-tooltip="true">
- <template #default="scope">
- <el-input
- :model-value="scope.row.standardProductName || scope.row.productName"
- placeholder="产品名称"
- clearable
- size="small"
- readonly
- />
- </template>
- </el-table-column>
- <el-table-column label="别名" min-width="140">
- <template #default="scope">
- <div class="line-meta-cell">
- <el-select
- v-if="(scope.row.aliasOptions || []).length"
- v-model="scope.row.aliasName"
- placeholder="不选则用产品名称"
- clearable
- filterable
- size="small"
- class="line-meta-cell__control"
- >
- <el-option
- v-for="name in scope.row.aliasOptions"
- :key="name"
- :label="name"
- :value="name"
- />
- </el-select>
- <span v-else class="text-muted line-meta-cell__empty">—</span>
- <el-button
- v-if="scope.row.productId"
- link
- type="primary"
- class="line-meta-cell__btn"
- @click="openAliasBrandManage(scope.row, 'alias')"
- >
- <el-icon><Setting /></el-icon>
- </el-button>
- </div>
- </template>
- </el-table-column>
- <el-table-column label="品牌" min-width="140">
- <template #default="scope">
- <div class="line-meta-cell">
- <el-select
- v-if="(scope.row.brandOptions || []).length"
- v-model="scope.row.brandId"
- placeholder="不限品牌"
- clearable
- filterable
- size="small"
- class="line-meta-cell__control"
- @change="(val) => handleBrandIdChange(scope.row, val)"
- >
- <el-option
- v-for="b in scope.row.brandOptions"
- :key="b.id"
- :label="b.brandName"
- :value="b.id"
- />
- </el-select>
- <span v-else class="text-muted line-meta-cell__empty">—</span>
- <el-button
- v-if="scope.row.productId"
- link
- type="primary"
- class="line-meta-cell__btn"
- @click="openAliasBrandManage(scope.row, 'brand')"
- >
- <el-icon><Setting /></el-icon>
- </el-button>
- </div>
- </template>
- </el-table-column>
- <el-table-column label="型号/规格" prop="productModel" min-width="100" :show-overflow-tooltip="true">
- <template #default="scope">
- <el-input v-model="scope.row.productModel" placeholder="请输入型号/规格" clearable size="small" readonly />
- </template>
- </el-table-column>
- <el-table-column label="税率(%)" prop="taxRate" min-width="80">
- <template #default="scope">
- <el-input v-model="scope.row.taxRate" placeholder="税率" @input="handleTaxRateChange(scope.row)" clearable size="small" />
- </template>
- </el-table-column>
- <el-table-column label="单位" prop="unit" width="100" align="center">
- <template #default="scope">
- <el-select
- v-model="scope.row.unit"
- placeholder="请选择单位"
- clearable
- size="small"
- style="width: 100%"
- @change="handleUnitChange(scope.row)"
- >
- <el-option v-for="dict in dictStore.getOptions('crm_product_unit')" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column prop="quantity" width="110">
- <template #header> <span style="color: #f56c6c">*</span> 数量</template>
- <template #default="scope">
- <el-input v-model="scope.row.quantity" placeholder="数量" @input="handleQuantityChange(scope.row)" clearable size="small" />
- </template>
- </el-table-column>
- <el-table-column prop="price" width="130">
- <template #header>
- <span v-if="isFrameworkContract">含税单价(元)</span>
- <span v-else><span style="color: #f56c6c">*</span> 含税单价(元)</span>
- </template>
- <template #default="scope">
- <span v-if="isFrameworkContract" class="text-muted">随行就市</span>
- <el-input
- v-else
- v-model="scope.row.price"
- placeholder="含税单价"
- @input="handlePriceChange(scope.row)"
- clearable
- size="small"
- class="money-field-input"
- />
- </template>
- </el-table-column>
- <el-table-column prop="subtotal" width="130" align="right">
- <template #header>
- <span v-if="isFrameworkContract">含税总价(元)</span>
- <span v-else><span style="color: #f56c6c">*</span> 含税总价(元)</span>
- </template>
- <template #default="scope">
- <span v-if="isFrameworkContract" class="text-muted">随行就市</span>
- <span v-else class="money-display">¥{{ parseFloat(scope.row.subtotal || 0).toFixed(2) }}</span>
- </template>
- </el-table-column>
- <el-table-column label="税费(元)" prop="taxAmount" min-width="90" align="right">
- <template #default="scope">
- <span v-if="isFrameworkContract" class="text-muted">随行就市</span>
- <span v-else class="money-display">¥{{ parseFloat(scope.row.taxAmount || 0).toFixed(2) }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="packageSpec" min-width="150">
- <template #header> <span style="color: #f56c6c">*</span> 包装规格</template>
- <template #default="scope">
- <el-select
- v-model="scope.row.packageSpec"
- placeholder="请选择包装规格"
- filterable
- allow-create
- default-first-option
- clearable
- size="small"
- style="width: 100%"
- @change="handlePackageSpecChange(scope.row, $event)"
- >
- <el-option v-for="dict in dictStore.getOptions('crm_product_gg')" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column prop="deliveryMethod" min-width="150">
- <template #header> <span style="color: #f56c6c">*</span> 送货方式</template>
- <template #default="scope">
- <el-select v-model="scope.row.deliveryMethod" placeholder="请选择送货方式" clearable size="small" style="width: 100%">
- <el-option v-for="dict in dictStore.getOptions('crm_shfs')" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column prop="remark" label="备注" min-width="160">
- <template #default="scope">
- <el-input
- v-model="scope.row.remark"
- type="textarea"
- :rows="2"
- placeholder="请输入备注"
- maxlength="500"
- show-word-limit
- clearable
- size="small"
- />
- </template>
- </el-table-column>
- <el-table-column label="操作" min-width="80" align="center" header-align="center" fixed="right" class-name="col-actions">
- <template #default="scope">
- <el-button link type="danger" @click="handleRemoveProduct(scope.$index)">
- <el-icon><Delete /></el-icon>
- </el-button>
- </template>
- </el-table-column>
- </ListTable>
- </div>
- </div>
- <!-- 框架合同调价区间 -->
- <template v-if="isFrameworkContractType(form.contractType)">
- <el-divider content-position="left">框架合同调价区间</el-divider>
- <p class="text-muted mb-3" style="font-size: 13px">
- 按产品维护生效日期区间与含税单价(可选);若填写区间则须完整(日期、单价、调价函)。合同总金额不与此联动,仍为 0。
- </p>
- <div v-for="(prod, pIdx) in form.products" :key="'fw-tier-' + pIdx" class="framework-tier-block mb-4">
- <div class="fw-tier-title mb-2">{{ prod.productName || '产品' + (pIdx + 1) }}</div>
- <el-button type="primary" link class="mb-2 fw-tier-letter-add-btn" @click="addPriceTierRow(pIdx)">
- <el-icon><Plus /></el-icon>调价函
- </el-button>
- <div class="panel dialog-list-panel mb-3">
- <div class="list-body list-wrap dialog-list-body-only">
- <ListTable table-key="crm-contract-components-contract-form-dialog-2" :data="prod.priceTiers || []" class="list-table" stripe empty-text="暂无调价区间">
- <el-table-column type="index" label="#" min-width="50" align="center" header-align="center" />
- <el-table-column label="开始日期" min-width="150">
- <template #default="scope">
- <el-date-picker v-model="scope.row.startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" style="width: 100%" />
- </template>
- </el-table-column>
- <el-table-column label="结束日期" min-width="150">
- <template #default="scope">
- <el-date-picker v-model="scope.row.endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" style="width: 100%" />
- </template>
- </el-table-column>
- <el-table-column label="含税单价(元)" width="140">
- <template #default="scope">
- <el-input v-model="scope.row.price" placeholder="含税单价" clearable size="small" class="money-field-input" />
- </template>
- </el-table-column>
- <el-table-column label="调价函" min-width="300">
- <template #default="scope">
- <el-upload
- :http-request="(opt) => handleTierLetterUploadRequest(opt, pIdx, scope.$index)"
- list-type="picture-card"
- :limit="20"
- multiple
- accept="image/*"
- :file-list="scope.row._letterFiles || []"
- :on-remove="(file) => handleTierLetterRemove(file, pIdx, scope.$index)"
- class="tier-letter-upload"
- >
- <el-icon><Plus /></el-icon>
- </el-upload>
- <div class="text-muted" style="font-size: 11px; margin-top: 4px">支持多张图片</div>
- </template>
- </el-table-column>
- <el-table-column label="操作" min-width="70" align="center" header-align="center" fixed="right" class-name="col-actions">
- <template #default="scope">
- <el-button link type="danger" @click="removePriceTierRow(pIdx, scope.$index)">
- <el-icon><Delete /></el-icon>
- </el-button>
- </template>
- </el-table-column>
- </ListTable>
- </div>
- </div>
- </div>
- </template>
- <el-divider />
- <div class="total-section">
- <el-row :gutter="20">
- <el-col :span="16"></el-col>
- <el-col :span="8">
- <div class="total-item">
- <span class="total-label">合计金额(元):</span>
- <span class="total-value money-display">¥{{ parseFloat(form.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</span>
- </div>
- </el-col>
- </el-row>
- </div>
- <!-- 附件上传部分 - OpenSpec 标准卡片化布局 -->
- <el-divider content-position="left">附件上传</el-divider>
- <el-form-item label="附件" prop="attachmentFiles" :required="form.contractTemplate === 'customer_template'">
- <ContractAttachmentPanel
- v-model="attachmentList"
- :biz-type="CRM_UPLOAD_BIZ_TYPE.CONTRACT_ATTACHMENT"
- />
- </el-form-item>
- </el-form>
- </div>
- <template #footer>
- <div class="dialog-footer">
- <el-button class="btn-ghost" @click="handleClose">
- <el-icon><Close /></el-icon>取消
- </el-button>
- <el-button
- class="btn-ghost"
- :loading="saveDraftLoading"
- :disabled="formLoading"
- @click="handleSaveDraft"
- >
- <el-icon><FolderChecked /></el-icon>保存草稿
- </el-button>
- <el-button
- class="btn-main"
- type="primary"
- :loading="submitLoading"
- :disabled="formLoading"
- @click="handleSubmit"
- >
- <el-icon><Promotion /></el-icon>提交审批
- </el-button>
- </div>
- </template>
- <!-- 产品选择器 -->
- <ProductSelector
- :visible="productSelectorVisible"
- :supplier-id="form.supplierId"
- @update:visible="productSelectorVisible = $event"
- @confirm="handleProductSelected"
- />
- <ProductAliasBrandDialog
- v-model="aliasBrandManageVisible"
- :product-id="aliasBrandManageProductId"
- :product-name="aliasBrandManageProductName"
- :mode="aliasBrandManageMode"
- @saved="handleAliasBrandManaged"
- />
- <!-- 客户选择器 -->
- <CustomerSelector :visible="customerSelectorVisible" @update:visible="customerSelectorVisible = $event" @confirm="handleCustomerConfirm" />
- <!-- 联系人选择器 -->
- <ContactSelector
- :visible="contactSelectorVisible"
- @update:visible="contactSelectorVisible = $event"
- :customerId="form.customerId"
- @confirm="handleContactConfirm"
- />
- <!-- 报价单选择器 -->
- <QuoteSelector
- :visible="quoteSelectorVisible"
- @update:visible="quoteSelectorVisible = $event"
- :customerId="form.customerId"
- @confirm="handleQuoteConfirm"
- />
- <!-- 用户选择器 -->
- <!-- 审批弹窗 -->
- <ApprovalDialog ref="approvalDialogRef" @success="handleApprovalSuccess" />
- </el-dialog>
- </template>
- <script setup lang="ts">
- import { ref, reactive, computed, nextTick, watch } from 'vue'
- import type { FormInstance, UploadRequestOptions } from 'element-plus'
- import { ElMessage } from 'element-plus'
- import {
- getContract,
- addContract,
- updateContract,
- type ContractForm,
- type ContractProductForm,
- type ContractProductPriceTierForm
- } from '@/api/crm/contract/index'
- import { isFrameworkContractType } from '../contractType'
- import { useDictStore } from '@/store/modules/dict'
- import { useUserStore } from '@/store/modules/user'
- import { $modal } from '@/utils/modal'
- import {
- Document,
- Delete,
- Sort,
- Plus,
- Search,
- Close,
- FolderChecked,
- Promotion,
- Loading,
- ArrowDown,
- ArrowUp,
- Setting
- } from '@element-plus/icons-vue'
- import {
- buildUploadFileListFromField,
- fillAttachmentInfoCache,
- serializeContractAttachments,
- type ContractAttachmentItem
- } from '@/utils/crm-attachment'
- import { buildAttachmentUploadFormData, CRM_UPLOAD_BIZ_TYPE } from '@/utils/crm-upload'
- import ContractAttachmentPanel from './ContractAttachmentPanel.vue'
- import { getOpportunity } from '@/api/crm/opportunity'
- import { getQuote } from '@/api/crm/quote'
- import ProductSelector from './ProductSelector.vue'
- import ProductAliasBrandDialog from '@/components/business/product-alias-brand-dialog/index.vue'
- import CustomerSelector from '@/components/CustomerSelector.vue'
- import ContactSelector from '@/components/ContactSelector.vue'
- import QuoteSelector from '@/components/QuoteSelector.vue'
- import { removeProductsNotAllowedByHazard } from '@/utils/crm-supplier-hazard'
- import {
- applyBrandSelectChange,
- applyManagedProductMetaToLines,
- initLineBrandMetaFromProduct,
- prepareLineAliasAndBrandForSubmit,
- resolveAliasFromSavedName
- } from '@/utils/crm-product-brand'
- import { fetchProductDetail } from '@/api/crm/product'
- import ApprovalDialog from './ApprovalDialog.vue'
- import { omitEmptyExceptPartyFields } from '../partyFields'
- import { getContactByCustomerId, fetchCustomerDetail } from '@/api/crm/customer'
- import type { CustomerVO, ContactVO } from '@/api/crm/customer/types'
- import { getDefaultInvoiceInfoByCustomerId } from '@/api/crm/invoiceInfo'
- import { listAllSupplierInfo, type SupplierInfoVO } from '@/api/crm/supplierInfo'
- import { fetchUploadAttachment, fetchUserOptions } from '@/api/admin'
- interface UserVO {
- userId: string | number
- nickName: string
- userName?: string
- }
- // 扩展表单类型,添加显示用字段
- type ExtendedContractForm = ContractForm & {
- customerName?: string
- opportunityName?: string
- quoteNo?: string
- contactsName?: string
- contactsPhone?: string
- ownerUserName?: string
- supplierId?: number
- settlementMethod?: string
- templateContent?: string
- }
- // 扩展产品类型,添加显示字段
- type PriceTierRow = ContractProductPriceTierForm & { _letterFiles?: any[] }
- type ExtendedContractProductForm = ContractProductForm & {
- productName?: string
- productModel?: string
- unit?: string
- unitPrice?: number
- subtotal?: number
- priceTiers?: PriceTierRow[]
- }
- const props = defineProps<{
- visible: boolean
- editId: string | null
- }>()
- const emit = defineEmits<{
- 'update:visible': [value: boolean]
- success: []
- }>()
- const dialogVisible = computed({
- get: () => props.visible,
- set: (val) => emit('update:visible', val)
- })
- const dictStore = useDictStore()
- const userStore = useUserStore()
- // 表单标题
- const formTitle = computed(() => {
- return form.value.id ? '编辑合同' : '新增合同'
- })
- // 合同期限日期范围
- const contractDateRange = ref<[string, string]>([])
- // 表单数据
- const form = ref<ExtendedContractForm>({
- id: undefined,
- contractNo: undefined,
- contractName: '',
- contractType: 'sale',
- contractTemplate: 'shanxia',
- customerId: 0,
- customerName: '',
- contactId: undefined,
- opportunityId: undefined,
- opportunityName: '',
- quoteId: undefined,
- quoteNo: '',
- contactsName: '',
- contactsPhone: '',
- signDate: '',
- startDate: '',
- endDate: '',
- ownerUserId: undefined,
- ownerUserName: '',
- amount: 0,
- supplierId: undefined,
- settlementMethod: undefined,
- partyA: '',
- partyAAddress: '',
- partyAContact: '',
- partyAMobile: '',
- partyALegalPerson: '',
- partyAAgent: '',
- partyAPhoneFax: '',
- partyABank: '',
- partyAAccount: '',
- partyB: '',
- partyBAddress: '',
- partyBContact: '',
- partyBMobile: '',
- partyBLegalPerson: '',
- partyBAgent: '',
- partyBPhoneFax: '',
- partyBBank: '',
- partyBAccount: '',
- paymentTerms: '',
- deliveryTerms: '',
- contractTerms: '',
- products: [] as ExtendedContractProductForm[],
- remark: ''
- })
- // 供需方是否已调换的标志
- const isPartiesSwapped = ref(false)
- /** 供方 / 需方信息展开状态(默认缩略,两侧共用) */
- const partiesExpanded = ref(false)
- const resetPartyExpand = () => {
- partiesExpanded.value = false
- }
- /** 校验失败时,自动展开供需方面板 */
- const expandPartiesForInvalidFields = (invalidFields?: Record<string, unknown>) => {
- if (!invalidFields) return
- const keys = Object.keys(invalidFields)
- if (keys.some((k) => k.startsWith('partyA') || k.startsWith('partyB'))) {
- partiesExpanded.value = true
- }
- }
- // 记录原始的模板内容(用于检测是否被意外删除)
- const originalTemplateContent = ref<string>('')
- // 供方信息列表
- const supplierList = ref<SupplierInfoVO[]>([])
- // 客户简称 / 开票公司名(用于按供需方面板匹配简称)
- const customerShortName = ref('')
- const customerInvoiceName = ref('')
- // 表单校验规则
- const rules = ref({
- contractType: [{ required: true, message: '合同类型不能为空', trigger: 'change' }],
- customerId: [{ required: true, message: '客户不能为空', trigger: 'change' }],
- contactId: [{ required: true, message: '联系人不能为空', trigger: 'change' }],
- signDate: [{ required: true, message: '签订日期不能为空', trigger: 'change' }],
- ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'change' }],
- supplierId: [{ required: true, message: '交易主体不能为空', trigger: 'change' }],
- startDate: [
- {
- validator: (_rule: any, _value: any, callback: any) => {
- if (!contractDateRange.value || contractDateRange.value.length !== 2 || !contractDateRange.value[0] || !contractDateRange.value[1]) {
- callback(new Error('合同期限不能为空'))
- } else {
- callback()
- }
- },
- trigger: ['change', 'blur']
- }
- ],
- attachmentFiles: []
- })
- // 附件列表
- const attachmentList = ref<ContractAttachmentItem[]>([])
- const isFrameworkContract = computed(() => isFrameworkContractType(form.value.contractType))
- // 加载状态
- const formLoading = ref(false)
- const saveDraftLoading = ref(false)
- const submitLoading = ref(false)
- /** 编辑打开时立即进入加载态,避免空白表单可被误改 */
- watch(
- () => props.visible,
- (visible) => {
- if (visible && props.editId) {
- formLoading.value = true
- } else if (!visible) {
- formLoading.value = false
- }
- }
- )
- // 组件引用
- const formRef = ref<FormInstance>()
- const approvalDialogRef = ref()
- // 选择器状态
- const customerSelectorVisible = ref(false)
- const contactSelectorVisible = ref(false)
- const opportunitySelectorVisible = ref(false)
- const quoteSelectorVisible = ref(false)
- const userSelectorVisible = ref(false)
- const productSelectorVisible = ref(false)
- // 用户列表(负责人下拉)
- const userList = ref<any[]>([])
- // 选中的产品列表
- const selectedProducts = ref<ExtendedContractProductForm[]>([])
- // 临时接口定义
- interface OpportunityVO {
- id: number
- opportunityName: string
- customerId?: number
- customerName?: string
- }
- interface QuoteVO {
- id: number
- quoteNo: string
- quoteTitle: string
- customerId?: number
- customerName?: string
- contactId?: number
- contactName?: string
- productList?: any[]
- quoteDate?: string
- }
- /** 对话框打开后的初始化 */
- const handleOpened = async () => {
- resetPartyExpand()
- const isEdit = !!props.editId
- if (isEdit) {
- formLoading.value = true
- }
- try {
- await Promise.all([loadSupplierList(), loadUserList()])
- if (props.editId) {
- await loadContractDetail(props.editId)
- } else {
- resetForm()
- form.value.signDate = new Date().toISOString().split('T')[0]
- handleSignDateChange(form.value.signDate)
- // 设置默认供方主体为"SX"
- const defaultSupplier = supplierList.value.find((supplier) => supplier.supplierCode === 'SX')
- if (defaultSupplier) {
- form.value.supplierId = defaultSupplier.id
- await handleSupplierChange(defaultSupplier.id)
- }
- }
- } catch (error: any) {
- console.error('合同表单初始化失败:', error)
- $modal.msgError(error?.message || '加载合同数据失败')
- if (isEdit) {
- dialogVisible.value = false
- }
- } finally {
- formLoading.value = false
- }
- }
- /** 重置表单 */
- const resetForm = () => {
- form.value = {
- id: undefined,
- contractNo: undefined,
- contractName: '',
- contractType: 'sale',
- contractTemplate: 'shanxia',
- customerId: 0,
- customerName: '',
- contactId: undefined,
- opportunityId: undefined,
- opportunityName: '',
- quoteId: undefined,
- quoteNo: '',
- contactsName: '',
- contactsPhone: '',
- signDate: '',
- startDate: '',
- endDate: '',
- ownerUserId: undefined,
- ownerUserName: '',
- amount: 0,
- supplierId: undefined,
- settlementMethod: undefined,
- partyA: '',
- partyAAddress: '',
- partyAContact: '',
- partyAMobile: '',
- partyALegalPerson: '',
- partyAAgent: '',
- partyAPhoneFax: '',
- partyABank: '',
- partyAAccount: '',
- partyB: '',
- partyBAddress: '',
- partyBContact: '',
- partyBMobile: '',
- partyBLegalPerson: '',
- partyBAgent: '',
- partyBPhoneFax: '',
- partyBBank: '',
- partyBAccount: '',
- paymentTerms: '',
- deliveryTerms: '',
- contractTerms: '',
- products: [] as ExtendedContractProductForm[],
- remark: ''
- }
- isPartiesSwapped.value = false
- resetPartyExpand()
- originalTemplateContent.value = ''
- customerShortName.value = ''
- customerInvoiceName.value = ''
- contractDateRange.value = [] as any
- attachmentList.value = []
- selectedProducts.value = []
- }
- /** 加载合同详情 */
- const loadContractDetail = async (id: number | string) => {
- const res = await getContract(id)
- const contractData = res.data
- Object.assign(form.value, contractData)
- originalTemplateContent.value = contractData.templateContent || ''
- if (contractData.contactName) {
- form.value.contactsName = contractData.contactName
- }
- if (contractData.contactMobile) {
- form.value.contactsPhone = contractData.contactMobile
- }
- if (contractData.ownerUserName) {
- form.value.ownerUserName = contractData.ownerUserName
- }
- if (contractData.ownerUserId) {
- form.value.ownerUserId = contractData.ownerUserId
- }
- if (contractData.opportunityId) {
- try {
- const oppRes = await getOpportunity(contractData.opportunityId)
- if (oppRes.data) {
- form.value.opportunityName = oppRes.data.opportunityName
- }
- } catch (error) {
- console.warn('加载商机信息失败:', error)
- }
- }
- if (contractData.quoteId) {
- try {
- const quoteRes = await getQuote(contractData.quoteId)
- if (quoteRes.data) {
- form.value.quoteNo = quoteRes.data.quoteNo
- }
- } catch (error) {
- console.warn('加载报价单信息失败:', error)
- }
- }
- if (contractData.products && Array.isArray(contractData.products)) {
- form.value.products = contractData.products.map((product: any) => ({
- ...product,
- productModel: product.model || '',
- isToxic: Number(product.isToxic || 0),
- isExplosive: Number(product.isExplosive || 0),
- brandMode: product.brandMode || undefined,
- brandId: product.brandId || undefined,
- brandName: product.brandName || undefined,
- brandOptions: [],
- priceTiers: (product.priceTiers || []).map((t: any) => {
- let ids: string[] = []
- if (Array.isArray(t.letterOssIds) && t.letterOssIds.length) {
- ids = t.letterOssIds.map((id: any) => String(id))
- } else if (t.letterOssId != null && t.letterOssId !== '') {
- ids = [String(t.letterOssId)]
- }
- return {
- ...t,
- letterOssIds: ids,
- _letterFiles: [] as any[]
- }
- }),
- taxRate: product.taxRate || 6,
- taxAmount: 0,
- packageSpec: product.packageSpec || '',
- deliveryMethod: product.deliveryMethod || ''
- }))
- await enrichContractProductBrandMeta(form.value.products)
- if (isFrameworkContractType(contractData.contractType)) {
- form.value.products.forEach((product: any) => {
- product.price = 0
- product.subtotal = 0
- product.taxAmount = 0
- })
- form.value.amount = 0
- await enrichFrameworkTierLetterFiles()
- } else {
- form.value.products.forEach((product: any) => {
- calculateProductAmount(product)
- })
- }
- }
- if (form.value.startDate && form.value.endDate) {
- contractDateRange.value = [form.value.startDate, form.value.endDate]
- }
- if (form.value.attachmentFiles) {
- await handleAttachmentData()
- }
- customerShortName.value = ''
- customerInvoiceName.value = ''
- if (form.value.customerId) {
- try {
- const [invoiceRes, customerRes] = await Promise.all([
- getDefaultInvoiceInfoByCustomerId(form.value.customerId),
- fetchCustomerDetail({ id: String(form.value.customerId) })
- ])
- customerInvoiceName.value = invoiceRes?.data?.companyName || ''
- customerShortName.value = customerRes?.data?.shortName || ''
- if (!form.value.customerName && customerRes?.data?.customerName) {
- form.value.customerName = customerRes.data.customerName
- }
- } catch {
- customerInvoiceName.value = ''
- }
- }
- syncPartiesSwappedFlagFromPanels()
- rebuildContractName()
- }
- /** 选择客户 */
- const handleSelectCustomer = () => {
- customerSelectorVisible.value = true
- }
- /** 客户选择确认 */
- const handleCustomerConfirm = async (customer: CustomerVO) => {
- if (customer) {
- form.value.customerId = customer.id
- form.value.customerName = customer.customerName
- const addressParts: string[] = []
- if (customer.province) addressParts.push(customer.province)
- if (customer.city) addressParts.push(customer.city)
- if (customer.district) addressParts.push(customer.district)
- if (customer.address) addressParts.push(customer.address)
- const fullAddress = addressParts.join('')
- form.value.contactId = undefined
- form.value.opportunityId = undefined
- form.value.opportunityName = ''
- form.value.quoteId = undefined
- form.value.quoteNo = ''
- try {
- const [invoiceRes, customerRes] = await Promise.all([
- getDefaultInvoiceInfoByCustomerId(customer.id),
- fetchCustomerDetail({ id: String(customer.id) })
- ])
- const invoiceData = invoiceRes?.data
- // 简称以客户主数据为准(列表/选择器可能缺字段)
- customerShortName.value =
- (customerRes?.data?.shortName || customer.shortName || '').trim()
- if (customerRes?.data?.customerName) {
- form.value.customerName = customerRes.data.customerName
- }
- customerInvoiceName.value = invoiceData?.companyName || ''
- if (invoiceData && invoiceData.companyName) {
- if (isPartiesSwapped.value) {
- form.value.partyA = invoiceData.companyName || ''
- form.value.partyAAddress = invoiceData.companyAddress || fullAddress || ''
- form.value.partyALegalPerson = invoiceData.legalRepresentative || ''
- form.value.partyAAgent = invoiceData.agent || ''
- form.value.partyAPhoneFax = invoiceData.phoneFax || ''
- form.value.partyABank = invoiceData.bankName || ''
- form.value.partyAAccount = invoiceData.bankAccount || ''
- } else {
- form.value.partyB = invoiceData.companyName || ''
- form.value.partyBAddress = invoiceData.companyAddress || fullAddress || ''
- form.value.partyBLegalPerson = invoiceData.legalRepresentative || ''
- form.value.partyBAgent = invoiceData.agent || ''
- form.value.partyBPhoneFax = invoiceData.phoneFax || ''
- form.value.partyBBank = invoiceData.bankName || ''
- form.value.partyBAccount = invoiceData.bankAccount || ''
- }
- } else {
- if (isPartiesSwapped.value) {
- form.value.partyA = customer.customerName
- form.value.partyAAddress = fullAddress
- form.value.partyALegalPerson = ''
- form.value.partyAAgent = ''
- form.value.partyAPhoneFax = customer.telephone || ''
- form.value.partyABank = ''
- form.value.partyAAccount = ''
- } else {
- form.value.partyB = customer.customerName
- form.value.partyBAddress = fullAddress
- form.value.partyBLegalPerson = ''
- form.value.partyBAgent = ''
- form.value.partyBPhoneFax = customer.telephone || ''
- form.value.partyBBank = ''
- form.value.partyBAccount = ''
- }
- }
- } catch (error) {
- console.error('获取客户开票信息失败:', error)
- customerInvoiceName.value = ''
- if (isPartiesSwapped.value) {
- form.value.partyA = customer.customerName
- form.value.partyAAddress = fullAddress
- form.value.partyALegalPerson = ''
- form.value.partyAAgent = ''
- form.value.partyAPhoneFax = customer.telephone || ''
- form.value.partyABank = ''
- form.value.partyAAccount = ''
- } else {
- form.value.partyB = customer.customerName
- form.value.partyBAddress = fullAddress
- form.value.partyBLegalPerson = ''
- form.value.partyBAgent = ''
- form.value.partyBPhoneFax = customer.telephone || ''
- form.value.partyBBank = ''
- form.value.partyBAccount = ''
- }
- }
- if (isPartiesSwapped.value) {
- form.value.partyAContact = ''
- form.value.partyAMobile = customer.mobile || customer.telephone || ''
- } else {
- form.value.partyBContact = ''
- form.value.partyBMobile = customer.mobile || customer.telephone || ''
- }
- form.value.contactsName = ''
- form.value.contactsPhone = customer.mobile || customer.telephone || ''
- try {
- const res = await getContactByCustomerId(customer.id)
- if (res.data && res.data.length > 0) {
- const primaryContact = res.data.find((contact: any) => contact.isPrimary === 1)
- const contact = primaryContact || res.data[0]
- form.value.contactId = contact.id
- form.value.contactsName = contact.contactName
- form.value.contactsPhone = contact.mobile || contact.telephone || ''
- if (isPartiesSwapped.value) {
- form.value.partyAContact = contact.contactName
- form.value.partyAMobile = contact.mobile || contact.telephone || ''
- } else {
- form.value.partyBContact = contact.contactName
- form.value.partyBMobile = contact.mobile || contact.telephone || ''
- }
- }
- } catch (error) {
- console.error('获取客户联系人失败:', error)
- }
- if (!customerShortName.value) {
- customerShortName.value = (customer.shortName || '').trim()
- }
- rebuildContractName()
- }
- }
- /** 选择联系人 */
- const handleSelectContact = () => {
- if (!form.value.customerId) {
- $modal.msgWarning('请先选择客户')
- return
- }
- contactSelectorVisible.value = true
- }
- /** 联系人选择确认 */
- const handleContactConfirm = (contact: ContactVO) => {
- if (contact) {
- form.value.contactId = contact.id
- form.value.contactsName = contact.contactName
- form.value.contactsPhone = contact.mobile || contact.telephone || ''
- if (isPartiesSwapped.value) {
- form.value.partyAContact = contact.contactName
- form.value.partyAMobile = contact.mobile || contact.telephone || ''
- } else {
- form.value.partyBContact = contact.contactName
- form.value.partyBMobile = contact.mobile || contact.telephone || ''
- }
- formRef.value?.validateField('contactId')
- }
- }
- /** 选择商机 */
- const handleSelectOpportunity = () => {
- opportunitySelectorVisible.value = true
- }
- /** 商机选择确认 */
- const handleOpportunityConfirm = (opportunity: OpportunityVO) => {
- if (opportunity) {
- form.value.opportunityId = opportunity.id
- form.value.opportunityName = opportunity.opportunityName
- if (opportunity.customerId && !form.value.customerId) {
- form.value.customerId = opportunity.customerId
- form.value.customerName = opportunity.customerName || ''
- }
- }
- }
- /** 选择报价单 */
- const handleSelectQuote = () => {
- quoteSelectorVisible.value = true
- }
- /** 报价单选择确认 */
- const handleQuoteConfirm = (quote: QuoteVO) => {
- if (quote) {
- form.value.quoteId = quote.id
- form.value.quoteNo = quote.quoteNo
- if (quote.customerId && !form.value.customerId) {
- form.value.customerId = quote.customerId
- form.value.customerName = quote.customerName || ''
- }
- if (quote.contactId) {
- form.value.contactId = quote.contactId
- form.value.contactsName = quote.contactName || ''
- if (isPartiesSwapped.value) {
- form.value.partyAContact = quote.contactName || ''
- } else {
- form.value.partyBContact = quote.contactName || ''
- }
- }
- if (quote.productList && quote.productList.length > 0) {
- $modal
- .confirm('是否要从报价单中带入产品明细?')
- .then(async () => {
- const isFw = isFrameworkContractType(form.value.contractType)
- form.value.products = quote.productList!.map((item: any) => ({
- productId: item.productId,
- productName: item.productName,
- productModel: item.model,
- categoryId: item.categoryId,
- categoryName: item.categoryName || '',
- unit: item.unit || '个',
- quantity: item.quantity || 1,
- price: isFw ? 0 : item.price || 0,
- unitPrice: isFw ? 0 : item.price || 0,
- taxRate: item.taxRate || 13,
- taxAmount: isFw ? 0 : item.taxAmount || 0,
- discount: item.discount || 0,
- subtotal: isFw ? 0 : item.subtotal || (item.quantity || 1) * (item.price || 0) * (1 - (item.discount || 0) / 100),
- packageSpec: item.packageSpec || '',
- deliveryMethod: item.deliveryMethod || '',
- deliveryDate: '',
- description: item.description || '',
- remark: item.remark || '',
- sort: item.sort,
- brandMode: item.brandMode || undefined,
- brandId: item.brandId || undefined,
- brandName: item.brandName || undefined,
- brandOptions: [],
- priceTiers: isFw ? [] : undefined
- }))
- await enrichContractProductBrandMeta(form.value.products)
- calculateTotalAmount()
- })
- .catch(() => {})
- }
- }
- }
- /** 加载用户列表(负责人下拉) */
- const loadUserList = async () => {
- try {
- const res = await fetchUserOptions()
- const options = (res as any)?.data ?? res ?? []
- userList.value = (Array.isArray(options) ? options : []).map((item: any) => ({
- userId: item.value ?? item.userId ?? item.id,
- nickName: item.label ?? item.nickName ?? item.userName ?? ''
- }))
- } catch (error) {
- console.error('加载用户列表失败:', error)
- userList.value = []
- }
- }
- /** 负责人变化 */
- const handleOwnerChange = (userId: string | number) => {
- const user = userList.value.find((item: any) => String(item.userId) === String(userId))
- form.value.ownerUserName = user?.nickName || ''
- }
- /** 选择负责人 */
- const handleSelectUser = () => {
- userSelectorVisible.value = true
- }
- /** 用户选择确认 */
- const handleUserConfirm = (user: UserVO) => {
- if (user) {
- form.value.ownerUserId = user.userId
- form.value.ownerUserName = user.nickName
- }
- }
- /** 供方主体变化处理 */
- const handleSupplierChange = async (supplierId: number | undefined) => {
- if (!supplierId) {
- if (isPartiesSwapped.value) {
- form.value.partyB = ''
- form.value.partyBAddress = ''
- form.value.partyBLegalPerson = ''
- form.value.partyBAgent = ''
- form.value.partyBPhoneFax = ''
- form.value.partyBBank = ''
- form.value.partyBAccount = ''
- } else {
- form.value.partyA = ''
- form.value.partyAAddress = ''
- form.value.partyALegalPerson = ''
- form.value.partyAAgent = ''
- form.value.partyAPhoneFax = ''
- form.value.partyABank = ''
- form.value.partyAAccount = ''
- }
- return
- }
- const supplier = supplierList.value.find((s) => s.id === supplierId)
- if (supplier) {
- if (isPartiesSwapped.value) {
- form.value.partyB = supplier.supplierName || ''
- form.value.partyBAddress = supplier.supplierAddress || ''
- form.value.partyBLegalPerson = supplier.legalPerson || ''
- form.value.partyBAgent = supplier.agent || ''
- form.value.partyBPhoneFax = supplier.phoneFax || ''
- form.value.partyBBank = supplier.bankName || ''
- form.value.partyBAccount = supplier.bankAccount || ''
- } else {
- form.value.partyA = supplier.supplierName || ''
- form.value.partyAAddress = supplier.supplierAddress || ''
- form.value.partyALegalPerson = supplier.legalPerson || ''
- form.value.partyAAgent = supplier.agent || ''
- form.value.partyAPhoneFax = supplier.phoneFax || ''
- form.value.partyABank = supplier.bankName || ''
- form.value.partyAAccount = supplier.bankAccount || ''
- }
- updateContractNoBySupplier(supplier.supplierCode)
- rebuildContractName()
- const allowToxic = Number((supplier as any).allowToxic || 0)
- const allowExplosive = Number((supplier as any).allowExplosive || 0)
- const { kept, removedNames } = removeProductsNotAllowedByHazard(
- form.value.products || [],
- allowToxic,
- allowExplosive
- )
- form.value.products = kept
- if (removedNames.length) {
- $modal.msgWarning(`当前主体不支持以下产品,已自动移除:${removedNames.join('、')}`)
- form.value.products.forEach((p) => calculateProductAmount(p))
- calculateTotalAmount()
- }
- }
- }
- /** 更新合同编号中的供方编码 */
- const updateContractNoBySupplier = (newSupplierCode: string | undefined) => {
- if (!form.value.contractNo || !newSupplierCode) return
- const contractNoPattern = /^([A-Z]+)\/X-JT(\d{8})(\d{3})$/
- const match = form.value.contractNo.match(contractNoPattern)
- if (match) {
- const date = match[2]
- const serialNum = match[3]
- form.value.contractNo = `${newSupplierCode}/X-JT${date}${serialNum}`
- } else {
- const index = form.value.contractNo.indexOf('/X-JT')
- if (index > 0) {
- const oldSupplierCode = form.value.contractNo.substring(0, index)
- form.value.contractNo = form.value.contractNo.replace(oldSupplierCode, newSupplierCode)
- }
- }
- }
- const CONTRACT_NO_STD_RE = /^([A-Z]+)\/X-JT(\d{8})(\d{3})$/
- /** 签订日变更时:标准编号只换日期段,保留序号;并同步名称括号内编号 */
- const syncContractNoDateFromSignDate = (signDateVal: string | null) => {
- if (!signDateVal || !form.value.contractNo) return
- const m = String(form.value.contractNo).match(CONTRACT_NO_STD_RE)
- if (!m) return
- const dateStr = signDateVal.replace(/-/g, '')
- if (dateStr.length !== 8) return
- if (m[2] === dateStr) return
- const nextNo = `${m[1]}/X-JT${dateStr}${m[3]}`
- form.value.contractNo = nextNo
- rebuildContractName()
- }
- /** 面板单位全称 → 匹配交易主体/客户主数据取简称;对不上用全称 */
- const resolvePartyShortName = (partyName?: string): string => {
- const party = (partyName || '').trim()
- if (!party) return ''
- const supplier = form.value.supplierId ? supplierList.value.find((s) => s.id === form.value.supplierId) : undefined
- const supplierName = (supplier?.supplierName || '').trim()
- const supplierShort = (supplier?.shortName || supplierName || '').trim()
- if (supplierName && party === supplierName) return supplierShort || party
- if (supplierShort && party === supplierShort) return supplierShort
- const customerName = (form.value.customerName || '').trim()
- const customerShort = (customerShortName.value || '').trim()
- const invoiceName = (customerInvoiceName.value || '').trim()
- if (customerName && party === customerName) return customerShort || customerName
- if (customerShort && party === customerShort) return customerShort
- if (invoiceName && party === invoiceName) return customerShort || invoiceName
- return party
- }
- /** 重建合同名称:始终 {供方简称}-{需方简称}({合同编号}),随面板调换 */
- const rebuildContractName = () => {
- const partyAShort = resolvePartyShortName(form.value.partyA)
- const partyBShort = resolvePartyShortName(form.value.partyB)
- const nameBody = [partyAShort, partyBShort].filter(Boolean).join('-')
- if (!nameBody) return
- const contractNo = (form.value.contractNo || '').trim()
- form.value.contractName = contractNo ? `${nameBody}(${contractNo})` : nameBody
- }
- /** 根据面板与交易主体/客户推断是否已调换(编辑回显用) */
- const syncPartiesSwappedFlagFromPanels = () => {
- const partyA = (form.value.partyA || '').trim()
- const partyB = (form.value.partyB || '').trim()
- if (!partyA && !partyB) {
- isPartiesSwapped.value = false
- return
- }
- const supplier = form.value.supplierId
- ? supplierList.value.find((s) => s.id === form.value.supplierId)
- : undefined
- const supplierName = (supplier?.supplierName || '').trim()
- if (supplierName) {
- if (partyB === supplierName && partyA !== supplierName) {
- isPartiesSwapped.value = true
- return
- }
- if (partyA === supplierName) {
- isPartiesSwapped.value = false
- return
- }
- }
- const customerName = (form.value.customerName || '').trim()
- const invoiceName = (customerInvoiceName.value || '').trim()
- if ((customerName && partyA === customerName) || (invoiceName && partyA === invoiceName)) {
- isPartiesSwapped.value = true
- return
- }
- if ((customerName && partyB === customerName) || (invoiceName && partyB === invoiceName)) {
- isPartiesSwapped.value = false
- }
- }
- /** 供需方调换:仅交换面板字段,勿再调 handleSupplierChange(会按交易主体覆盖一侧,导致两边都变成交易主体) */
- const handleSwapParties = async () => {
- const temp = {
- partyA: form.value.partyA,
- partyAAddress: form.value.partyAAddress,
- partyAContact: form.value.partyAContact,
- partyAMobile: form.value.partyAMobile,
- partyALegalPerson: form.value.partyALegalPerson,
- partyAAgent: form.value.partyAAgent,
- partyAPhoneFax: form.value.partyAPhoneFax,
- partyABank: form.value.partyABank,
- partyAAccount: form.value.partyAAccount
- }
- form.value.partyA = form.value.partyB
- form.value.partyAAddress = form.value.partyBAddress
- form.value.partyAContact = form.value.partyBContact
- form.value.partyAMobile = form.value.partyBMobile
- form.value.partyALegalPerson = form.value.partyBLegalPerson
- form.value.partyAAgent = form.value.partyBAgent
- form.value.partyAPhoneFax = form.value.partyBPhoneFax
- form.value.partyABank = form.value.partyBBank
- form.value.partyAAccount = form.value.partyBAccount
- form.value.partyB = temp.partyA
- form.value.partyBAddress = temp.partyAAddress
- form.value.partyBContact = temp.partyAContact
- form.value.partyBMobile = temp.partyAMobile
- form.value.partyBLegalPerson = temp.partyALegalPerson
- form.value.partyBAgent = temp.partyAAgent
- form.value.partyBPhoneFax = temp.partyAPhoneFax
- form.value.partyBBank = temp.partyABank
- form.value.partyBAccount = temp.partyAAccount
- isPartiesSwapped.value = !isPartiesSwapped.value
- rebuildContractName()
- $modal.msgSuccess('供需方信息已调换')
- }
- /** 合同类型变化处理 */
- const handleContractTypeChange = (value: string) => {
- if (isFrameworkContractType(value)) {
- form.value.amount = 0
- form.value.products?.forEach((p) => {
- p.price = 0
- p.subtotal = 0
- p.taxAmount = 0
- if (!p.priceTiers) {
- p.priceTiers = []
- }
- })
- }
- if (value === 'purchase' && !isPartiesSwapped.value) {
- $modal
- .confirm('检测到您选择了采购合同,系统将自动调换供需方信息,是否继续?')
- .then(async () => {
- await handleSwapParties()
- })
- .catch(() => {
- rebuildContractName()
- })
- } else {
- rebuildContractName()
- }
- }
- /** 合同模版变化处理 */
- const handleContractTemplateChange = (value: string) => {
- if (value === 'customer_template') {
- ElMessage({
- message: '使用客户模版时,必须上传合同附件!',
- type: 'warning',
- duration: 3000
- })
- }
- }
- /** 合同期限日期范围变化 */
- const handleDateRangeChange = (val: [string, string] | null) => {
- if (val && val.length === 2) {
- form.value.startDate = val[0]
- form.value.endDate = val[1]
- } else {
- form.value.startDate = ''
- form.value.endDate = ''
- contractDateRange.value = [] as any
- }
- nextTick(() => {
- formRef.value?.validateField('startDate')
- })
- }
- /** 签订日期变化处理 */
- const handleSignDateChange = (val: string | null) => {
- if (val) {
- const signDate = new Date(val)
- const endDate = new Date(signDate)
- endDate.setDate(endDate.getDate() + 30)
- const formatDate = (date: Date): string => {
- const year = date.getFullYear()
- const month = String(date.getMonth() + 1).padStart(2, '0')
- const day = String(date.getDate()).padStart(2, '0')
- return `${year}-${month}-${day}`
- }
- const startDateStr = formatDate(signDate)
- const endDateStr = formatDate(endDate)
- contractDateRange.value = [startDateStr, endDateStr]
- form.value.startDate = startDateStr
- form.value.endDate = endDateStr
- } else {
- contractDateRange.value = [] as any
- form.value.startDate = ''
- form.value.endDate = ''
- }
- syncContractNoDateFromSignDate(val)
- nextTick(() => {
- formRef.value?.validateField('startDate')
- })
- }
- /** 添加产品 */
- const handleAddProduct = () => {
- if (!form.value.supplierId) {
- $modal.msgWarning('请先选择交易主体')
- return
- }
- productSelectorVisible.value = true
- }
- /** 产品选择确认 */
- const handleProductSelected = (products: any[]) => {
- const isFw = isFrameworkContractType(form.value.contractType)
- products.forEach((product) => {
- const brandMeta = initLineBrandMetaFromProduct(product)
- form.value.products.push({
- productId: product.id,
- productName: product.productName,
- productModel: product.model || '',
- categoryId: product.categoryId,
- categoryName: product.categoryName || '',
- unit: product.unit || '个',
- quantity: 1,
- price: isFw ? 0 : product.price || 0,
- taxRate: normalizeTaxRateDisplay(product.taxRate, 13),
- taxAmount: 0,
- subtotal: isFw ? 0 : product.price || 0,
- packageSpec: '',
- deliveryMethod: '',
- description: product.description || '',
- remark: '',
- isToxic: Number(product.isToxic || 0),
- isExplosive: Number(product.isExplosive || 0),
- ...brandMeta,
- priceTiers: isFw ? [] : undefined
- })
- })
- form.value.products.forEach((p) => {
- calculateProductAmount(p)
- })
- calculateTotalAmount()
- }
- const handleBrandIdChange = (row: ExtendedContractProductForm, val: string | undefined | null) => {
- applyBrandSelectChange(row, val, row.brandOptions || [])
- }
- const aliasBrandManageVisible = ref(false)
- const aliasBrandManageProductId = ref('')
- const aliasBrandManageProductName = ref('')
- const aliasBrandManageMode = ref<'alias' | 'brand'>('alias')
- function openAliasBrandManage(row: ExtendedContractProductForm, mode: 'alias' | 'brand') {
- if (!row.productId) return
- aliasBrandManageProductId.value = String(row.productId)
- aliasBrandManageProductName.value = String(
- row.standardProductName || row.productName || ''
- )
- aliasBrandManageMode.value = mode
- aliasBrandManageVisible.value = true
- }
- function handleAliasBrandManaged(product: Record<string, any>) {
- applyManagedProductMetaToLines(
- form.value.products || [],
- aliasBrandManageProductId.value,
- product
- )
- }
- async function enrichContractProductBrandMeta(lines: ExtendedContractProductForm[]) {
- await Promise.all(
- (lines || []).map(async (line) => {
- if (!line.productId) return
- try {
- const res: any = await fetchProductDetail({ id: String(line.productId) })
- const data = res?.data ?? res
- if (!data) return
- const meta = initLineBrandMetaFromProduct(data)
- line.standardProductName = meta.standardProductName
- line.aliasOptions = meta.aliasOptions
- line.aliasName = resolveAliasFromSavedName(
- line.productName,
- meta.standardProductName,
- meta.aliasOptions
- )
- line.brandOptions = meta.brandOptions
- if (meta.brandOptions.length) {
- if (line.brandMode === 'required' && line.brandId) {
- applyBrandSelectChange(line, line.brandId, meta.brandOptions)
- } else {
- line.brandMode = 'any'
- line.brandId = undefined
- line.brandName = undefined
- }
- } else {
- line.brandMode = undefined
- line.brandId = undefined
- line.brandName = undefined
- }
- } catch (e) {
- console.warn('enrich contract brand meta failed', line.productId, e)
- }
- })
- )
- }
- /** 规范化税率展示 */
- const normalizeTaxRateDisplay = (value: any, defaultValue = 0): number => {
- if (value === null || value === undefined || value === '') {
- return defaultValue
- }
- const num = Number(value)
- if (Number.isNaN(num)) {
- return defaultValue
- }
- return Number.isInteger(num) ? num : parseFloat(num.toString())
- }
- /** 产品选择变化 */
- const handleProductSelectionChange = (selection: ExtendedContractProductForm[]) => {
- selectedProducts.value = selection
- }
- /** 删除选中的产品 */
- const handleRemoveSelectedProducts = () => {
- if (selectedProducts.value.length === 0) return
- $modal
- .confirm(`确认删除选中的 ${selectedProducts.value.length} 个产品吗?`)
- .then(() => {
- selectedProducts.value.forEach((selectedProduct) => {
- const index = form.value.products.findIndex((p) => p === selectedProduct)
- if (index > -1) {
- form.value.products.splice(index, 1)
- }
- })
- selectedProducts.value = []
- calculateTotalAmount()
- $modal.msgSuccess('删除成功')
- })
- .catch(() => {})
- }
- /** 删除产品 */
- const handleRemoveProduct = (index: number) => {
- form.value.products.splice(index, 1)
- calculateTotalAmount()
- }
- /** 单价变化 */
- const handlePriceChange = (row: ExtendedContractProductForm) => {
- calculateProductAmount(row)
- calculateTotalAmount()
- }
- /** 数量变化 */
- const handleQuantityChange = (row: ExtendedContractProductForm) => {
- calculateProductAmount(row)
- calculateTotalAmount()
- }
- /** 税率变化 */
- const handleTaxRateChange = (row: ExtendedContractProductForm) => {
- calculateProductAmount(row)
- calculateTotalAmount()
- }
- /** 单位变化 */
- const handleUnitChange = (_row: ExtendedContractProductForm) => {
- // 单位变化通常不需要重新计算金额
- }
- /** 包装规格变更处理 */
- const handlePackageSpecChange = (product: ExtendedContractProductForm, value: string) => {
- product.packageSpec = value
- }
- /** 计算产品金额 */
- const calculateProductAmount = (product: ExtendedContractProductForm) => {
- if (isFrameworkContractType(form.value.contractType)) {
- product.price = 0
- product.subtotal = 0
- product.taxAmount = 0
- return
- }
- const price = parseFloat(product.price?.toString() || '0')
- const quantity = parseFloat(product.quantity?.toString() || '0')
- const taxRate = parseFloat(product.taxRate?.toString() || '0')
- const subtotal = Math.round(price * quantity * 100) / 100
- product.subtotal = subtotal
- const taxRateDecimal = taxRate / 100
- let taxAmount = 0
- if (taxRate > 0 && subtotal > 0) {
- const taxBase = subtotal / (1 + taxRateDecimal)
- taxAmount = Math.round(taxBase * taxRateDecimal * 100) / 100
- }
- product.taxAmount = taxAmount
- }
- /** 计算合计金额 */
- const calculateTotalAmount = () => {
- if (isFrameworkContractType(form.value.contractType)) {
- form.value.amount = 0
- return
- }
- let total = 0
- form.value.products.forEach((item) => {
- const quantity = item.quantity || 0
- const price = item.price || 0
- const discount = item.discount || 0
- const subtotal = quantity * price * (1 - discount / 100)
- ;(item as ExtendedContractProductForm).subtotal = subtotal
- total += subtotal
- })
- form.value.amount = total
- }
- /** 框架合同:批量解析调价函 URL */
- const enrichFrameworkTierLetterFiles = async () => {
- const products = form.value.products || []
- const idSet = new Set<string>()
- products.forEach((p: any) => {
- ;(p.priceTiers || []).forEach((t: any) => {
- ;(t.letterOssIds || []).forEach((id: string) => idSet.add(String(id)))
- })
- })
- if (idSet.size === 0) return
- const cache = new Map<string, { name: string; url: string }>()
- fillAttachmentInfoCache([...idSet], cache)
- products.forEach((p: any) => {
- ;(p.priceTiers || []).forEach((t: any) => {
- const ids = t.letterOssIds || []
- t._letterFiles = ids.map((ossId: string, i: number) => {
- const info = cache.get(String(ossId))
- return {
- name: info?.name || `调价函${i + 1}`,
- ossId: String(ossId),
- url: info?.url || ossId,
- uid: `tier-prev-${t.id ?? 'n'}-${i}-${ossId}`,
- status: 'success'
- }
- })
- })
- })
- }
- /** 组装提交/保存的产品明细 */
- const buildProductsPayload = () => {
- const list = form.value.products || []
- list.forEach((product) => prepareLineAliasAndBrandForSubmit(product))
- return list.map((product: any, idx: number) => {
- const base: any = {
- contractId: product.contractId || form.value.id,
- productId: product.productId,
- productName: product.productName,
- productNo: product.productNo,
- categoryId: product.categoryId,
- categoryName: product.categoryName,
- model: product.productModel || product.model,
- unit: product.unit,
- quantity: product.quantity,
- price: product.price,
- discount: product.discount,
- taxRate: product.taxRate,
- taxAmount: product.taxAmount,
- subtotal: product.subtotal,
- deliveryDate: product.deliveryDate,
- description: product.description,
- packageSpec: product.packageSpec,
- deliveryMethod: product.deliveryMethod,
- sort: product.sort ?? idx + 1,
- remark: product.remark,
- brandMode: product.brandMode || null,
- brandId: product.brandMode === 'required' ? product.brandId || null : null,
- brandName: product.brandMode === 'required' ? product.brandName || null : null
- }
- if (product.id) {
- base.id = product.id
- }
- if (isFrameworkContractType(form.value.contractType)) {
- base.price = 0
- base.subtotal = 0
- base.taxAmount = 0
- base.priceTiers = (product.priceTiers || [])
- .filter(
- (t: any) => t.startDate && t.endDate && t.price != null && t.price !== '' && Array.isArray(t.letterOssIds) && t.letterOssIds.length > 0
- )
- .map((t: any, i: number) => ({
- id: t.id,
- startDate: t.startDate,
- endDate: t.endDate,
- price: Number(t.price),
- letterOssIds: (t.letterOssIds || []).map((id: string) => String(id).trim()).filter(Boolean),
- sort: t.sort ?? i + 1
- }))
- }
- return base
- })
- }
- const addPriceTierRow = (pIdx: number) => {
- const p = form.value.products[pIdx]
- if (!p) return
- if (!p.priceTiers) {
- p.priceTiers = []
- }
- p.priceTiers.push({ startDate: '', endDate: '', price: undefined, letterOssIds: [], _letterFiles: [] })
- }
- const removePriceTierRow = (pIdx: number, tIdx: number) => {
- const p = form.value.products[pIdx]
- if (!p?.priceTiers) return
- p.priceTiers.splice(tIdx, 1)
- }
- const handleTierLetterUploadRequest = async (
- options: UploadRequestOptions,
- pIdx: number,
- tIdx: number
- ) => {
- const p = form.value.products[pIdx]
- const tiers = p?.priceTiers
- if (!tiers || !tiers[tIdx]) {
- options.onError?.(new Error('调价区间不存在'))
- return
- }
- const row = tiers[tIdx]
- const formData = buildAttachmentUploadFormData(options.file, {
- bizType: CRM_UPLOAD_BIZ_TYPE.CONTRACT_PRICE_LETTER
- })
- try {
- const res = await fetchUploadAttachment(formData)
- const fileUrl = res?.fileUrl
- if (!fileUrl) {
- throw new Error('上传失败')
- }
- const oid = String(fileUrl)
- row.letterOssIds = row.letterOssIds || []
- row.letterOssIds.push(oid)
- row._letterFiles = row._letterFiles || []
- row._letterFiles.push({
- name: options.file.name || `调价函${row._letterFiles.length + 1}`,
- ossId: oid,
- url: oid,
- uid: options.file.uid,
- status: 'success'
- })
- options.onSuccess?.(res)
- } catch (error: any) {
- ElMessage.error(error?.message || '上传失败')
- options.onError?.(error)
- }
- }
- const handleTierLetterRemove = (file: any, pIdx: number, tIdx: number) => {
- const p = form.value.products[pIdx]
- const tiers = p?.priceTiers
- if (!tiers || !tiers[tIdx]) return
- const row = tiers[tIdx]
- const uid = file.uid
- const idx = (row._letterFiles || []).findIndex((f: any) => f.uid === uid)
- if (idx >= 0) {
- const removed = row._letterFiles[idx]
- row._letterFiles.splice(idx, 1)
- if (removed?.ossId) {
- row.letterOssIds = (row.letterOssIds || []).filter((id: string) => String(id) !== String(removed.ossId))
- }
- }
- }
- /** 处理附件数据 */
- const handleAttachmentData = async () => {
- attachmentList.value = buildUploadFileListFromField(
- form.value.attachmentFiles
- ) as ContractAttachmentItem[]
- }
- /** 保存草稿 */
- const handleSaveDraft = async () => {
- if (!form.value.customerId || form.value.customerId === 0) {
- $modal.msgWarning('客户名称不能为空')
- return
- }
- if (!form.value.supplierId) {
- $modal.msgWarning('交易主体不能为空')
- return
- }
- rebuildContractName()
- saveDraftLoading.value = true
- try {
- const attachmentJson = serializeContractAttachments(attachmentList.value)
- const data: any = { ...form.value }
- data.status = 'draft'
- data.approvalStatus = null
- if (attachmentJson) {
- data.attachmentFiles = attachmentJson
- }
- omitEmptyExceptPartyFields(data)
- if (data.contactsName) {
- data.contactName = data.contactsName
- }
- if (data.contactsPhone) {
- data.contactMobile = data.contactsPhone
- }
- delete data.customerName
- delete data.opportunityName
- delete data.quoteNo
- delete data.contactsName
- delete data.contactsPhone
- delete data.ownerUserName
- delete data.templateContent
- if (data.products && Array.isArray(data.products) && data.products.length > 0) {
- ;(form.value.products || []).forEach((p) => prepareLineAliasAndBrandForSubmit(p))
- for (let i = 0; i < form.value.products.length; i++) {
- const product = form.value.products[i]
- const rowNum = i + 1
- if (!product.productName || product.productName.trim() === '') {
- $modal.msgWarning(`第${rowNum}行产品的产品名称不能为空`)
- return
- }
- }
- data.products = buildProductsPayload()
- } else {
- delete data.products
- }
- if (form.value.id) {
- await updateContract(data)
- $modal.msgSuccess('保存成功')
- } else {
- await addContract(data)
- $modal.msgSuccess('保存成功')
- }
- handleClose()
- emit('success')
- } finally {
- saveDraftLoading.value = false
- }
- }
- /** 提交审批 */
- const handleSubmit = async () => {
- try {
- await formRef.value?.validate()
- } catch (error) {
- expandPartiesForInvalidFields(error as Record<string, unknown>)
- ElMessage.warning({
- message: '请填写完整的必填信息',
- duration: 3000
- })
- return
- }
- if (form.value.contractTemplate === 'customer') {
- const hasAttachments = attachmentList.value.some((file) => file.status === 'success' && file.ossId)
- if (!hasAttachments) {
- ElMessage.warning({
- message: '使用客户模版时,必须上传合同附件',
- duration: 3000
- })
- return
- }
- }
- const requiresProducts = form.value.contractType === 'sale' || form.value.contractType === 'purchase' || isFrameworkContractType(form.value.contractType)
- const validateProductRowCommon = (product: any, rowNum: number): boolean => {
- if (!product.productName || String(product.productName).trim() === '') {
- $modal.msgWarning(`第${rowNum}行产品的产品名称不能为空`)
- return false
- }
- if (!product.quantity || Number(product.quantity) <= 0) {
- $modal.msgWarning(`第${rowNum}行产品的数量不能为空且必须大于0`)
- return false
- }
- if (!product.packageSpec || String(product.packageSpec).trim() === '') {
- $modal.msgWarning(`第${rowNum}行产品的包装规格不能为空`)
- return false
- }
- if (!product.deliveryMethod || String(product.deliveryMethod).trim() === '') {
- $modal.msgWarning(`第${rowNum}行产品的送货方式不能为空`)
- return false
- }
- return true
- }
- if (requiresProducts) {
- if (!form.value.products || form.value.products.length === 0) {
- $modal.msgWarning('销售合同、采购合同或框架合同必须至少添加一个产品')
- return
- }
- ;(form.value.products || []).forEach((p) => prepareLineAliasAndBrandForSubmit(p))
- for (let i = 0; i < form.value.products.length; i++) {
- const product = form.value.products[i]
- const rowNum = i + 1
- if (!validateProductRowCommon(product, rowNum)) return
- const isFw = isFrameworkContractType(form.value.contractType)
- if (!isFw) {
- if (!product.price || Number(product.price) <= 0) {
- $modal.msgWarning(`第${rowNum}行产品的含税单价不能为空且必须大于0`)
- return
- }
- if (!product.subtotal || Number(product.subtotal) <= 0) {
- $modal.msgWarning(`第${rowNum}行产品的含税总价不能为空且必须大于0`)
- return
- }
- } else {
- const tiers = product.priceTiers || []
- for (let j = 0; j < tiers.length; j++) {
- const t = tiers[j]
- const tRow = j + 1
- if (!t.startDate || !t.endDate) {
- $modal.msgWarning(`产品「${product.productName}」调价区间第${tRow}行:请选择开始与结束日期`)
- return
- }
- if (String(t.startDate) > String(t.endDate)) {
- $modal.msgWarning(`产品「${product.productName}」调价区间第${tRow}行:开始日期不能晚于结束日期`)
- return
- }
- const pVal = t.price === null || t.price === '' ? NaN : Number(t.price)
- if (Number.isNaN(pVal) || pVal <= 0) {
- $modal.msgWarning(`产品「${product.productName}」调价区间第${tRow}行:含税单价必须大于0`)
- return
- }
- if (!(t.letterOssIds?.length > 0)) {
- $modal.msgWarning(`产品「${product.productName}」调价区间第${tRow}行:请上传调价函`)
- return
- }
- }
- }
- }
- } else if (form.value.products && form.value.products.length > 0) {
- ;(form.value.products || []).forEach((p) => prepareLineAliasAndBrandForSubmit(p))
- for (let i = 0; i < form.value.products.length; i++) {
- const product = form.value.products[i]
- const rowNum = i + 1
- if (!validateProductRowCommon(product, rowNum)) return
- if (!product.price || Number(product.price) <= 0) {
- $modal.msgWarning(`第${rowNum}行产品的含税单价不能为空且必须大于0`)
- return
- }
- if (!product.subtotal || Number(product.subtotal) <= 0) {
- $modal.msgWarning(`第${rowNum}行产品的含税总价不能为空且必须大于0`)
- return
- }
- }
- }
- submitLoading.value = true
- try {
- rebuildContractName()
- const attachmentJson = serializeContractAttachments(attachmentList.value)
- const data: any = { ...form.value }
- if (attachmentJson) {
- data.attachmentFiles = attachmentJson
- }
- omitEmptyExceptPartyFields(data)
- if (data.contactsName) {
- data.contactName = data.contactsName
- }
- if (data.contactsPhone) {
- data.contactMobile = data.contactsPhone
- }
- delete data.customerName
- delete data.opportunityName
- delete data.quoteNo
- delete data.contactsName
- delete data.contactsPhone
- delete data.ownerUserName
- delete data.templateContent
- if (data.products && Array.isArray(data.products) && data.products.length > 0) {
- data.products = buildProductsPayload()
- } else {
- delete data.products
- }
- let contractId: string | number
- if (form.value.id) {
- await updateContract(data)
- contractId = form.value.id
- $modal.msgSuccess('保存成功')
- } else {
- const res = await addContract(data)
- contractId = res.data
- if (contractId == null || contractId === '') {
- $modal.msgError('保存成功但未返回合同 ID,请从列表重新提交审批')
- emit('success')
- return
- }
- form.value.id = contractId as any
- $modal.msgSuccess('保存成功')
- }
- approvalDialogRef.value?.open(contractId, 'submit')
- } finally {
- submitLoading.value = false
- }
- }
- /** 审批成功回调 */
- const handleApprovalSuccess = () => {
- handleClose()
- emit('success')
- }
- /** 关闭对话框 */
- const handleClose = () => {
- dialogVisible.value = false
- }
- /** 加载供方信息列表 */
- const loadSupplierList = async () => {
- try {
- const res = await listAllSupplierInfo({ status: '0' })
- supplierList.value = res.data || []
- return res.data || []
- } catch (error) {
- console.error('加载供方信息列表失败:', error)
- return []
- }
- }
- </script>
- <style scoped lang="scss">
- .contract-form-dialog {
- :deep(.el-dialog__wrapper) {
- padding: 0 !important;
- top: 20px !important;
- height: calc(100vh - 40px) !important;
- }
- :deep(.el-dialog) {
- backdrop-filter: none;
- height: 100% !important;
- max-height: calc(100vh - 40px);
- display: flex;
- flex-direction: column;
- }
- :deep(.dialog-footer) {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 16px;
- width: 100%;
- }
- :deep(.el-dialog__header),
- :deep(.el-dialog__footer) {
- flex: 0 0 auto;
- }
- :deep(.el-dialog__body) {
- padding-top: 8px;
- flex: 1 1 auto;
- min-height: 0;
- max-height: none;
- overflow-y: auto;
- overflow-x: hidden;
- }
- }
- .contract-form-body {
- position: relative;
- min-height: 280px;
- padding: 0 4px;
- }
- .contract-basic-info {
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 12px 14px;
- margin-bottom: 20px;
- :deep(.el-form-item) {
- margin-bottom: 0;
- }
- :deep(.el-input-number) {
- width: 100%;
- }
- }
- @media (max-width: 1100px) {
- .contract-basic-info {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- }
- @media (max-width: 560px) {
- .contract-basic-info {
- grid-template-columns: 1fr;
- }
- }
- .parties-section {
- margin: 8px 0 20px;
- }
- .parties-section-toolbar {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- gap: 8px;
- margin-bottom: 12px;
- }
- .parties-row {
- display: flex;
- align-items: stretch;
- gap: 0;
- min-height: 0;
- }
- .party-panel {
- flex: 1 1 0;
- min-width: 0;
- padding: 16px;
- border: 1px solid var(--border, #d9e8e2);
- border-radius: 10px;
- background: #fff;
- &.is-collapsed {
- padding: 12px 16px;
- }
- :deep(.el-form-item) {
- margin-bottom: 0;
- }
- }
- .party-panel__header {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
- .party-panel.is-collapsed & {
- margin-bottom: 8px;
- }
- }
- .party-panel__title {
- margin-bottom: 0;
- padding-left: 10px;
- font-size: 14px;
- font-weight: 600;
- color: var(--section-bar-title-color, var(--primary-dark, #059669));
- line-height: 1.5;
- border-left: 3px solid var(--section-bar-border-color, var(--primary, #10b981));
- }
- .party-panel__summary {
- display: flex;
- align-items: baseline;
- gap: 8px;
- min-width: 0;
- padding: 2px 0 2px 10px;
- }
- .party-panel__summary-label {
- flex-shrink: 0;
- font-size: 12px;
- color: var(--mint-text-muted, #64748b);
- line-height: 1.5;
- }
- .party-panel__summary-value {
- min-width: 0;
- font-size: 14px;
- font-weight: 500;
- color: var(--mint-text, #0f172a);
- line-height: 1.5;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- &.is-empty {
- font-weight: 400;
- color: var(--mint-text-muted, #94a3b8);
- }
- }
- .party-flow-bridge {
- flex: 0 0 88px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 10px;
- padding: 0 8px;
- }
- .party-flow-bridge__label {
- font-size: 12px;
- font-weight: 600;
- color: var(--primary-dark, #168f45);
- letter-spacing: 0.06em;
- padding: 4px 10px;
- border-radius: 999px;
- background: #e6f8ed;
- border: 1px solid #a8e0b8;
- line-height: 1.2;
- }
- .party-flow-bridge__arrow {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 44px;
- height: 44px;
- border-radius: 50%;
- color: var(--primary-dark, #168f45);
- background: linear-gradient(145deg, #e6f8ed 0%, #d4f0de 100%);
- border: 1px solid #a8e0b8;
- box-shadow: 0 2px 8px rgba(28, 168, 81, 0.14);
- svg {
- width: 22px;
- height: 22px;
- display: block;
- }
- }
- @media (max-width: 1100px) {
- .parties-row {
- flex-direction: column;
- min-height: 0;
- }
- .party-flow-bridge {
- flex: 0 0 auto;
- flex-direction: row;
- gap: 12px;
- padding: 12px 0;
- }
- .party-flow-bridge__arrow {
- transform: rotate(90deg);
- }
- }
- .total-section {
- margin-top: 20px;
- padding: 15px;
- background-color: #f5f7fa;
- border-radius: 4px;
- }
- .total-item {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- margin-bottom: 10px;
- font-size: 16px;
- &:last-child {
- margin-bottom: 0;
- }
- }
- .total-label {
- font-weight: bold;
- margin-right: 10px;
- }
- .total-value {
- font-size: 18px;
- font-weight: bold;
- }
- .text-muted {
- color: #1e293b;
- }
- :deep(.money-field-input.el-input-number .el-input__inner),
- :deep(.money-field-input.el-input-number .el-input__wrapper input) {
- color: #dc2626 !important;
- -webkit-text-fill-color: #dc2626 !important;
- font-weight: 600;
- }
- /* 统一表格内输入框和下拉框高度 */
- :deep(.el-table .el-input__wrapper) {
- height: 32px !important;
- min-height: 32px !important;
- }
- :deep(.el-table .el-select__wrapper) {
- height: 32px !important;
- min-height: 32px !important;
- }
- :deep(.el-table .el-input--small .el-input__wrapper) {
- height: 32px !important;
- min-height: 32px !important;
- }
- :deep(.el-table .el-select--small .el-select__wrapper) {
- height: 32px !important;
- min-height: 32px !important;
- }
- /* 价格输入框红色文字 */
- .price-input :deep(.el-input__inner) {
- color: #e34d59;
- }
- /* 去除输入框后置按钮的边框和背景 */
- :deep(.el-input-group__append) {
- background: transparent !important;
- border: none !important;
- box-shadow: none !important;
- }
- :deep(.el-input-group__append .el-button) {
- border: none !important;
- background: transparent !important;
- box-shadow: none !important;
- padding: 0 15px !important;
- }
- :deep(.el-input-group__append .el-button:hover) {
- background: transparent !important;
- border: none !important;
- }
- :deep(.el-input-group__append .el-button:focus) {
- background: transparent !important;
- border: none !important;
- }
- /* 框架合同调价函:表格内多图卡片上传 */
- .tier-letter-upload :deep(.el-upload-list--picture-card) {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
- .tier-letter-upload :deep(.el-upload--picture-card) {
- width: 72px;
- height: 72px;
- line-height: 72px;
- }
- .tier-letter-upload :deep(.el-upload-list--picture-card .el-upload-list__item) {
- width: 72px;
- height: 72px;
- margin: 0;
- }
- /* 框架合同:增加「调价函」行内按钮可点区域宽度 */
- .fw-tier-letter-add-btn.el-button--primary.is-link {
- min-width: 108px;
- padding-left: 12px;
- padding-right: 12px;
- justify-content: center;
- }
- .mb-3 {
- margin-bottom: 12px;
- }
- .mb-4 {
- margin-bottom: 16px;
- }
- .ml-2 {
- margin-left: 8px;
- }
- .line-meta-cell {
- display: flex;
- align-items: center;
- gap: 4px;
- min-width: 0;
- }
- .line-meta-cell__control {
- flex: 1;
- min-width: 0;
- }
- .line-meta-cell__empty {
- flex: 1;
- min-width: 0;
- }
- .line-meta-cell__btn {
- flex-shrink: 0;
- padding: 4px;
- }
- </style>
|