| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156 |
- <template>
- <div class="app-container">
- <!-- 列表页搜索区 -->
- <el-card shadow="never" class="search-card">
- <el-form :model="queryParams" ref="queryRef" label-width="85px">
- <el-row :gutter="24">
- <el-col :span="6">
- <el-form-item label="所属公司" prop="companyNo">
- <el-select v-model="queryParams.companyNo" placeholder="请选择" style="width: 100%" clearable>
- <el-option v-for="item in companyOptions" :key="item.companyCode || item.id" :label="item.companyName" :value="item.companyCode || item.id" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="项目名称" prop="projectName">
- <el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="客户名称" prop="customName">
- <el-input v-model="queryParams.customName" placeholder="请输入客户名称" clearable />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="负责人" prop="leader">
- <el-select v-model="queryParams.leader" placeholder="请选择" style="width: 100%" clearable>
- <el-option v-for="item in userOptions" :key="item.staffId || item.userId" :label="item.staffName || item.nickName" :value="String(item.staffId || item.userId)" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="24" style="margin-top: 5px;">
- <el-col :span="6">
- <el-form-item label="部门" prop="deptNo">
- <el-select v-model="queryParams.deptNo" placeholder="所属部门" style="width: 100%" clearable>
- <el-option v-for="item in deptOptions" :key="item.value" :label="item.label" :value="item.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="产品支持" prop="productSupport">
- <el-select v-model="queryParams.productSupport" placeholder="请选择" style="width: 100%" clearable>
- <el-option v-for="item in userOptions" :key="item.staffId || item.userId" :label="item.staffName || item.nickName" :value="String(item.staffId || item.userId)" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="项目级别" prop="projectLevel">
- <el-select v-model="queryParams.projectLevel" placeholder="请选择" style="width: 100%" clearable>
- <el-option v-for="item in projectLevelOptions" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="项目类型" prop="businessType">
- <el-select v-model="queryParams.businessType" placeholder="请选择" style="width: 100%" clearable>
- <el-option v-for="item in projectTypeOptions" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="24" style="margin-top: 5px;">
- <el-col :span="6">
- <el-form-item label="入围类型" prop="shortlistedType">
- <el-select v-model="queryParams.shortlistedType" placeholder="请选择" style="width: 100%" clearable>
- <el-option v-for="item in shortlistedTypeOptions" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="项目状态" prop="projectStatus">
- <el-select v-model="queryParams.projectStatus" style="width: 100%" clearable>
- <el-option v-for="item in statusOptions" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="成交结果" prop="result">
- <el-select v-model="queryParams.result" placeholder="请选择" style="width: 100%" clearable>
- <el-option v-for="item in resultOptions" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="查询类型" prop="timeType" label-width="85px">
- <el-select v-model="queryParams.timeType" style="width: 100%" clearable>
- <el-option v-for="item in timeTypeOptions" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="24" style="margin-top: 5px;">
- <el-col :span="8">
- <el-form-item label="时间范围" prop="dateRange">
- <el-date-picker v-model="dateRange" type="daterange" range-separator="To" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD" style="width: 100%" />
- </el-form-item>
- </el-col>
- <el-col :span="16">
- <el-form-item label-width="0">
- <div class="search-btns">
- <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
- <el-button icon="Refresh" @click="resetQuery">重置</el-button>
- <el-button type="primary" icon="Plus" @click="handleAdd" class="btn-new">新增</el-button>
- </div>
- </el-form-item>
- </el-col>
- </el-row>
- </el-form>
- </el-card>
- <!-- Tab栏 + 统计 + 操作按钮 -->
- <div class="tabs-control">
- <div class="tab-items">
- <div class="tab-item" :class="{ current: activeTab === 'all' }" @click="handleTabChange('all')">年度入围(平台)信息列表</div>
- <div class="tab-item" :class="{ current: activeTab === 'mine' }" @click="handleTabChange('mine')">我负责的年度入围</div>
- <div class="tab-item" :class="{ current: activeTab === 'join' }" @click="handleTabChange('join')">我参与的年度入围</div>
- <div class="tab-item badge-item" :class="{ current: activeTab === 'total' }" @click="handleTabChange('total')">
- 全部年度入围
- <span v-if="totalCountBadge > 99" class="badge">99+</span>
- <span v-else-if="totalCountBadge > 0" class="badge">{{ totalCountBadge }}</span>
- </div>
- </div>
- <div class="stripe-right-wrap">
- <div class="summary-line">
- <span class="stripe-total">金额总计<span class="red-text">{{ Number(totalAmount).toFixed(2) }}</span> (万)</span>
- </div>
- <div class="stripe-actions">
- <el-button type="info" size="small" icon="Delete" :disabled="multipleSelection.length === 0" @click="handleBatchDelete">批量删除</el-button>
- <el-button type="primary" size="small" icon="Switch" :disabled="multipleSelection.length === 0" @click="handleTransfer">转移给他人</el-button>
- </div>
- </div>
- </div>
- <!-- 数据表格 -->
- <el-card shadow="never" class="table-card">
- <el-table ref="selectionTable" v-loading="loading" :data="dataList" border class="standard-table" @selection-change="handleSelectionChange">
- <el-table-column type="selection" width="50" align="center" fixed />
- <el-table-column label="项目名称" align="center" prop="projectName" min-width="240" show-overflow-tooltip fixed />
- <el-table-column label="所属公司" align="center" min-width="180" show-overflow-tooltip>
- <template #default="scope">
- {{ companyOptions.find(c => String(c.companyCode || c.id) === String(scope.row.companyNo))?.companyName || scope.row.companyNo || '--' }}
- </template>
- </el-table-column>
- <el-table-column label="客户名称" align="center" prop="customName" min-width="200" show-overflow-tooltip />
- <el-table-column label="项目级别" align="center" prop="projectLevel" width="100">
- <template #default="scope">
- {{ projectLevelOptions.find(o => String(o.dictValue) === String(scope.row.projectLevel))?.dictLabel || scope.row.projectLevel || '--' }}
- </template>
- </el-table-column>
- <el-table-column label="项目类型" align="center" prop="businessType" width="100">
- <template #default="scope">
- {{ projectTypeOptions.find(o => String(o.dictValue) === String(scope.row.businessType))?.dictLabel || scope.row.businessType || '--' }}
- </template>
- </el-table-column>
- <el-table-column label="金额(万)" align="center" prop="amount" width="100" sortable>
- <template #default="scope">
- {{ scope.row.amount != null ? Number(scope.row.amount).toFixed(2) : '--' }}
- </template>
- </el-table-column>
- <el-table-column label="服务期(年)" align="center" prop="standardPeriod" width="100">
- <template #default="scope">
- {{ scope.row.standardPeriod != null ? scope.row.standardPeriod : '--' }}
- </template>
- </el-table-column>
- <el-table-column label="服务时间段" align="center" prop="serviceTime" width="150" show-overflow-tooltip>
- <template #default="scope">
- {{ scope.row.serviceTime || '--' }}
- </template>
- </el-table-column>
- <el-table-column label="投标截止时间" align="center" prop="tenderDeadline" width="110">
- <template #default="scope">
- <span>{{ scope.row.tenderDeadline ? parseTime(scope.row.tenderDeadline, '{y}-{m}-{d}') : '--' }}</span>
- </template>
- </el-table-column>
- <el-table-column label="部门编号" align="center" prop="deptNo" width="100">
- <template #default="scope">
- {{ scope.row.deptNo || '--' }}
- </template>
- </el-table-column>
- <el-table-column label="操作" align="center" width="220" fixed="right">
- <template #default="scope">
- <el-button link type="primary" @click="handleUpdate(scope.row)">详情</el-button>
- <el-button link type="primary" @click="handleProgress(scope.row)">进度</el-button>
- <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
- </el-card>
- <!-- ========== 详情/编辑抽屉 ========== -->
- <el-drawer
- v-model="drawerVisible"
- direction="rtl"
- size="80%"
- :before-close="handleDrawerClose"
- class="platform-detail-drawer"
- :with-header="false"
- >
- <!-- 抽屉顶部标题栏 -->
- <div class="detail-header">
- <span class="project-title">{{ drawerModeTitle }}</span>
- <el-icon class="close-btn" @click="handleDrawerClose"><Close /></el-icon>
- </div>
- <!-- 模式 1: 详情展示模式 -->
- <template v-if="!drawerIsEdit">
- <!-- 项目进度步骤条 -->
- <div class="progress-section">
- <div class="section-label">项目进度</div>
- <div class="step-bar">
- <div class="step" v-for="(step, idx) in stepList" :key="idx"
- :class="{ active: (drawerForm.projectStatus || 0) === (idx + 1), finished: (drawerForm.projectStatus || 0) > (idx + 1) }"
- @click="handleStepClick(idx + 1)">
- <span class="step-text">{{ step }}</span>
- </div>
- </div>
- <div class="progress-link-row">
- <span class="progress-label">最新进度</span>
- <span class="progress-link" @click="showProgressDrawer = true">全部进度</span>
- </div>
- </div>
- <div class="detail-container">
- <!-- 左侧详情内容 -->
- <div class="detail-left">
- <el-tabs v-model="drawerActiveTab" class="custom-tabs">
- <el-tab-pane label="项目信息" name="info">
- <el-button type="primary" class="floating-edit-btn" @click="drawerIsEdit = true">
- <el-icon style="margin-right: 4px;"><Edit /></el-icon> 编辑
- </el-button>
- <!-- 基本信息 -->
- <div class="info-block">
- <div class="block-header"><div class="block-title">基本信息</div></div>
- <div class="info-grid info-grid-2col">
- <div class="info-item full-row"><span class="label">项目名称</span><span class="value">{{ drawerForm.projectName || '--' }}</span></div>
- <div class="info-item"><span class="label">归属公司</span><span class="value">{{ drawerCompanyName }}</span></div>
- <div class="info-item"><span class="label">客户名称</span><span class="value">{{ drawerForm.customName || '--' }}</span></div>
- <div class="info-item"><span class="label">平台名称</span><span class="value">{{ drawerForm.platformName || '--' }}</span></div>
- <div class="info-item"><span class="label">行业</span><span class="value">{{ drawerProfessionName }}</span></div>
- <div class="info-item"><span class="label">部门</span><span class="value">{{ drawerForm.deptNo || '--' }}</span></div>
- <div class="info-item"><span class="label">项目状态</span><span class="value status-text">{{ drawerForm.projectStatus === 1 ? '跟进中' : '已归档' }}</span></div>
- <div class="info-item"><span class="label">项目级别</span><span class="value grade-text">{{ drawerProjectLevelName }}</span></div>
- <div class="info-item left-only"><span class="label">项目类型</span><span class="value">{{ drawerProjectTypeName }}</span></div>
- </div>
- </div>
- <!-- 项目情况 -->
- <div class="info-block mt-24">
- <div class="block-header"><div class="block-title">项目情况</div></div>
- <div class="info-grid info-grid-3col">
- <div class="info-item"><span class="label">登记日期</span><span class="value">{{ parseTime(drawerForm.createTime, '{y}-{m}-{d}') }}</span></div>
- <div class="info-item"><span class="label">金额(万)</span><span class="value">{{ drawerForm.amount || '0.00' }}</span></div>
- <div class="info-item"><span class="label">报名费</span><span class="value">{{ drawerForm.entryFee || '0.00' }}</span></div>
- <div class="info-item"><span class="label">投标保证金</span><span class="value">{{ drawerForm.bidBond || '0.00' }}</span></div>
- <div class="info-item"><span class="label">赢单率(%)</span><span class="value">{{ drawerForm.winningRate || '0' }}%</span></div>
- <div class="info-item"><span class="label">报名截止时间</span><span class="value">{{ parseTime(drawerForm.signUpDeadline, '{y}-{m}-{d}') }}</span></div>
- <div class="info-item"><span class="label">投标截止时间</span><span class="value">{{ parseTime(drawerForm.tenderDeadline, '{y}-{m}-{d}') }}</span></div>
- <div class="info-item"><span class="label">服务期(年)</span><span class="value">{{ drawerForm.standardPeriod || '--' }}</span></div>
- <div class="info-item"><span class="label">服务时间段</span><span class="value">{{ drawerForm.serviceTime || '--' }}</span></div>
- <div class="info-item"><span class="label">入围类型</span><span class="value">{{ drawerShortlistedTypeName }}</span></div>
- <div class="info-item"><span class="label">物资类目</span><span class="value">{{ drawerProfessionName }}</span></div>
- <div class="info-item"><span class="label">招标代理机构</span><span class="value">{{ drawerForm.biddingAgency || '--' }}</span></div>
- <div class="info-item"><span class="label">标期类型</span><span class="value">{{ drawerForm.bidPeriodType === 1 ? '单项目入围' : '周期性框架' }}</span></div>
- <div class="info-item empty-cell"></div>
- <div class="info-item full-row-3col"><span class="label">平台链接</span><span class="value link-text" @click="openLink(drawerForm.platformLink)">{{ drawerForm.platformLink || '--' }}</span></div>
- <div class="info-item full-row-3col"><span class="label">入围要求</span><span class="value wrap-text">{{ drawerForm.condition || '--' }}</span></div>
- <div class="info-item full-row-3col"><span class="label">项目描述</span><span class="value wrap-text">{{ drawerForm.projectDesc || '--' }}</span></div>
- </div>
- </div>
- </el-tab-pane>
- <!-- 项目联系人 -->
- <el-tab-pane label="项目联系人" name="contact">
- <div class="tab-contact">
- <el-button type="primary" class="add-contact-btn" size="small" style="background-color: #409eff; border-color: #409eff;" @click="handleAddContact"><el-icon style="margin-right: 4px;"><Plus /></el-icon> 新建联系人</el-button>
- <el-table :data="contactList" border class="contact-table" v-loading="contactLoading">
- <el-table-column label="姓名" align="center" prop="name" min-width="100" show-overflow-tooltip />
- <el-table-column label="性别" align="center" prop="sex" width="70">
- <template #default="scope">{{ scope.row.sex || '--' }}</template>
- </el-table-column>
- <el-table-column label="部门" align="center" prop="department" min-width="100" show-overflow-tooltip />
- <el-table-column label="职位" align="center" prop="position" min-width="100" show-overflow-tooltip />
- <el-table-column label="项目角色" align="center" prop="projectRole" width="100" show-overflow-tooltip />
- <el-table-column label="是否关键人" align="center" prop="izCruxPerson" width="100">
- <template #default="scope">{{ scope.row.izCruxPerson === '是' ? '是' : '否' }}</template>
- </el-table-column>
- <el-table-column label="手机号码" align="center" prop="phoneNumber" min-width="120" show-overflow-tooltip />
- <el-table-column label="操作" align="center" width="120">
- <template #default="scope"><el-button link type="primary" size="small">编辑</el-button><el-button link type="danger" size="small">删除</el-button></template>
- </el-table-column>
- </el-table>
- <div v-if="!contactLoading && (!contactList || contactList.length === 0)" class="tab-empty">暂无数据</div>
- </div>
- </el-tab-pane>
- <!-- 结果分析 -->
- <el-tab-pane label="结果分析" name="analysis">
- <div class="tab-analysis">
- <div class="analysis-form">
- <div class="form-field">
- <div class="field-label">成交结果</div>
- <el-radio-group v-model="drawerForm.resultType" size="small" style="margin-top: 4px;">
- <el-radio :value="1">赢单</el-radio><el-radio :value="2">丢单</el-radio>
- </el-radio-group>
- </div>
- <div class="form-field">
- <div class="field-label">{{ drawerForm.resultType === 1 ? '赢单总结' : '丢单原因' }}</div>
- <el-input v-model="drawerForm.winSummary" type="textarea" :placeholder="drawerForm.resultType === 1 ? '请输入赢单总结' : '请输入丢单原因'" :rows="5" resize="none" maxlength="500" show-word-limit />
- </div>
- <div style="display: flex; justify-content: flex-end; margin-top: 16px;">
- <el-button type="primary" @click="submitAnalysis" style="background-color: #409eff; border-color: #409eff;">保存</el-button>
- </div>
- </div>
- </div>
- </el-tab-pane>
- <!-- 附件 -->
- <el-tab-pane label="附件" name="files">
- <div class="tab-files">
- <div class="file-upload-row"><el-button icon="Upload" size="small" plain>上传附件</el-button></div>
- <el-table :data="(drawerForm.fileList || [])" border class="file-table">
- <el-table-column label="文件名称" align="left" prop="fileName" min-width="200" show-overflow-tooltip />
- <el-table-column label="文件类型" align="center" prop="fileType" width="100" />
- <el-table-column label="文件大小" align="center" prop="fileSize" width="100" />
- <el-table-column label="上传时间" align="center" prop="uploadTime" width="160" />
- <el-table-column label="操作" align="center" width="140">
- <template #default="scope"><el-button link type="primary" size="small">下载</el-button><el-button link type="danger" size="small">删除</el-button></template>
- </el-table-column>
- </el-table>
- <div v-if="!(drawerForm.fileList && drawerForm.fileList.length)" class="tab-empty">暂无附件</div>
- </div>
- </el-tab-pane>
- <!-- 报价单 -->
- <el-tab-pane label="报价单" name="quotes">
- <div class="tab-quotes">
- <div class="quote-upload-row"><el-button type="primary" size="small"><el-icon style="margin-right: 4px;"><Plus /></el-icon> 新增报价单</el-button></div>
- <el-table :data="(drawerForm.quoteList || [])" border class="quote-table">
- <el-table-column label="报价单号" align="center" prop="quoteNo" min-width="160" show-overflow-tooltip />
- <el-table-column label="报价金额(万)" align="center" prop="quoteAmount" width="120" />
- <el-table-column label="报价日期" align="center" prop="quoteDate" width="110" />
- <el-table-column label="状态" align="center" prop="status" width="90">
- <template #default="scope"><el-tag :type="scope.row.status === 1 ? 'warning' : scope.row.status === 2 ? 'success' : 'info'" size="small" plain>{{ scope.row.status === 1 ? '待审核' : scope.row.status === 2 ? '已通过' : '已驳回' }}</el-tag></template>
- </el-table-column>
- <el-table-column label="操作" align="center" width="180">
- <template #default="scope"><el-button link type="primary" size="small">查看</el-button><el-button link type="primary" size="small">编辑</el-button><el-button link type="danger" size="small">删除</el-button></template>
- </el-table-column>
- </el-table>
- <div v-if="!(drawerForm.quoteList && drawerForm.quoteList.length)" class="tab-empty">暂无报价单</div>
- </div>
- </el-tab-pane>
- </el-tabs>
- </div>
- <!-- 右侧侧边栏 -->
- <div class="detail-right">
- <el-tabs v-model="drawerActiveSideTab" class="custom-tabs">
- <el-tab-pane label="团队成员" name="team">
- <div class="side-content">
- <div class="team-header">
- <span>团队成员 ({{ (drawerForm.memberList || []).length }})</span>
- <el-icon class="add-icon"><Plus /></el-icon>
- </div>
- <div class="search-box">
- <el-input placeholder="请输入成员" prefix-icon="Search" />
- </div>
- <div class="team-list">
- <div v-for="(m, i) in (drawerForm.memberList || [])" :key="i" class="team-item">
- <el-avatar :size="36" class="user-avatar">{{ (m.memberName || m.staffName || m.realName || '').charAt(0) }}</el-avatar>
- <div class="member-info">
- <div class="info-top">
- <span class="member-name">{{ m.memberName || m.staffName || m.realName }}{{ m.deptName ? '/' + m.deptName : '' }}</span>
- <el-tag v-if="m.isLeader || m.izManager === 1" size="small" type="success" effect="plain" class="leader-tag">负责人</el-tag>
- <span class="role-desc">{{ m.roleName }}</span>
- <span class="permission-desc">{{ m.permission }}</span>
- </div>
- </div>
- <div class="member-actions">
- <el-button link class="btn-edit" size="small">编辑</el-button>
- <el-button link class="btn-delete" size="small">删除</el-button>
- </div>
- </div>
- <div v-if="!(drawerForm.memberList && drawerForm.memberList.length)" class="empty-data">暂无团队成员</div>
- </div>
- </div>
- </el-tab-pane>
- <el-tab-pane label="跟进记录" name="records">
- <div class="side-content" style="padding: 15px;">
- <div class="record-op-row">
- <el-button type="primary" link @click="handleOpenAddFollowUp"><el-icon style="margin-right:4px;"><Plus /></el-icon>新建跟进记录</el-button>
- <div class="record-filter">
- <span class="filter-label">拜访方式</span>
- <el-select v-model="followUpQueryType" placeholder="请选择" size="small" style="width: 120px;" clearable @change="fetchFollowUps(drawerForm.id)">
- <el-option v-for="item in callTypeOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
- </el-select>
- </div>
- </div>
- <div class="record-list" v-loading="followUpLoading">
- <div v-for="(record, index) in followUpList" :key="index" class="record-card-item">
- <div class="card-header">
- <span class="visit-type">{{ callTypeOptions.find(d => String(d.dictValue) === String(record.callTypeCode))?.dictLabel || '上门拜访' }}</span>
- <span class="visit-time">{{ record.callDate }}</span>
- </div>
- <div class="card-content">
- <div class="content-row"><span class="label">拜访人:</span><span class="value">{{ record.visitor }}</span></div>
- <div class="content-row"><span class="label">拜访目的:</span><span class="value">{{ record.callAim }}</span></div>
- <div class="content-row"><span class="label">跟进情况:</span><span class="value">{{ record.followUpCondition }}</span></div>
- </div>
- </div>
- <el-empty v-if="followUpList.length === 0 && !followUpLoading" description="暂无跟进记录" :image-size="60" />
- </div>
- </div>
- </el-tab-pane>
- <el-tab-pane label="操作日志" name="logs">
- <div class="side-content" style="padding: 15px;">
- <div class="log-list" v-loading="logLoading">
- <div v-for="(log, index) in logList" :key="index" class="log-item">
- <span class="log-time">{{ log.createTime }}</span>
- <span class="log-user">{{ log.operator }}</span>
- <span class="log-action">{{ formatLogAction(log) }}</span>
- <span class="log-target" style="margin-left: 4px;">{{ translateDetails(log.actionDetails) }}<template v-if="formatLogTarget(log)">:</template>{{ formatLogTarget(log) }}</span>
- </div>
- <div v-if="logList.length === 0 && !logLoading" class="empty-data">
- <el-empty description="暂无操作日志" :image-size="60" />
- </div>
- </div>
- </div>
- </el-tab-pane>
- <el-tab-pane label="管理信息" name="manage"></el-tab-pane>
- </el-tabs>
- </div>
- </div>
- </template>
- <!-- 模式 2: 表单编辑模式 -->
- <template v-else>
- <div class="edit-form-container">
- <el-form :model="drawerForm" :rules="drawerRules" ref="drawerFormRef" label-width="0">
- <!-- 基本信息 -->
- <div class="form-section">
- <div class="form-section-header"><span class="section-title">基本信息</span></div>
- <div class="form-grid form-grid-3col">
- <el-form-item prop="companyNo">
- <div class="form-item">
- <span class="field-label"><span class="required-star">*</span>归属公司</span>
- <el-select v-model="drawerForm.companyNo" placeholder="请选择" size="small">
- <el-option v-for="item in drawerOptions.company" :key="item.companyCode || item.id" :label="item.companyName" :value="item.companyCode || item.id" />
- </el-select>
- </div>
- </el-form-item>
- <el-form-item prop="customName">
- <div class="form-item">
- <span class="field-label"><span class="required-star">*</span>客户名称</span>
- <el-select v-model="drawerForm.customName" placeholder="请选择" size="small" filterable>
- <el-option v-for="item in drawerOptions.customer" :key="item.id" :label="item.customerName" :value="item.customerName" />
- </el-select>
- </div>
- </el-form-item>
- <el-form-item prop="businessType">
- <div class="form-item">
- <span class="field-label"><span class="required-star">*</span>项目类型</span>
- <el-select v-model="drawerForm.businessType" placeholder="请选择" size="small">
- <el-option v-for="item in drawerOptions.type" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" />
- </el-select>
- </div>
- </el-form-item>
- <el-form-item prop="projectLevel">
- <div class="form-item">
- <span class="field-label"><span class="required-star">*</span>项目级别</span>
- <el-select v-model="drawerForm.projectLevel" placeholder="请选择" size="small">
- <el-option v-for="item in drawerOptions.level" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" />
- </el-select>
- </div>
- </el-form-item>
- <el-form-item prop="leader">
- <div class="form-item">
- <span class="field-label">项目负责人</span>
- <el-select v-model="drawerForm.leader" placeholder="请选择" size="small">
- <el-option v-for="item in drawerOptions.user" :key="item.staffId || item.userId" :label="item.staffName || item.nickName" :value="String(item.staffId || item.userId)" />
- </el-select>
- </div>
- </el-form-item>
- <div></div>
- <el-form-item prop="projectName" class="form-full-row">
- <div class="form-item form-full-item"><span class="field-label"><span class="required-star">*</span>项目名称</span><el-input v-model="drawerForm.projectName" placeholder="请输入" size="small" /></div>
- </el-form-item>
- <el-form-item prop="platformName" class="form-full-row">
- <div class="form-item form-full-item"><span class="field-label">平台名称</span><el-input v-model="drawerForm.platformName" placeholder="建议输入平台中文全称" size="small" /></div>
- </el-form-item>
- <el-form-item prop="platformLink" class="form-full-row">
- <div class="form-item form-full-item"><span class="field-label">平台链接</span><el-input v-model="drawerForm.platformLink" placeholder="请输入平台访问链接" size="small" /></div>
- </el-form-item>
- </div>
- </div>
- <!-- 项目情况 -->
- <div class="form-section">
- <div class="form-section-header"><span class="section-title">项目情况</span></div>
- <div class="form-grid form-grid-3col">
- <el-form-item prop="amount">
- <div class="form-item"><span class="field-label"><span class="required-star">*</span>金额</span><div class="has-unit"><el-input v-model="drawerForm.amount" placeholder="请输入" size="small" /><span class="unit-append">万</span></div></div>
- </el-form-item>
- <el-form-item prop="entryFee">
- <div class="form-item"><span class="field-label"><span class="required-star">*</span>报名费</span><el-input v-model="drawerForm.entryFee" placeholder="请输入" size="small" /></div>
- </el-form-item>
- <el-form-item prop="bidBond">
- <div class="form-item"><span class="field-label">投标保证金</span><el-input v-model="drawerForm.bidBond" placeholder="请输入" size="small" /></div>
- </el-form-item>
- <el-form-item prop="winningRate">
- <div class="form-item"><span class="field-label"><span class="required-star">*</span>赢单率</span><div class="has-unit"><el-input v-model="drawerForm.winningRate" placeholder="请输入" size="small" /><span class="unit-append">%</span></div></div>
- </el-form-item>
- <el-form-item prop="signUpDeadline">
- <div class="form-item"><span class="field-label"><span class="required-star">*</span>报名截止时间</span><el-date-picker v-model="drawerForm.signUpDeadline" type="date" placeholder="请选择" size="small" value-format="YYYY-MM-DD" /></div>
- </el-form-item>
- <el-form-item prop="tenderDeadline">
- <div class="form-item"><span class="field-label"><span class="required-star">*</span>投标截止时间</span><el-date-picker v-model="drawerForm.tenderDeadline" type="date" placeholder="请选择" size="small" value-format="YYYY-MM-DD" /></div>
- </el-form-item>
- <el-form-item prop="standardPeriod">
- <div class="form-item"><span class="field-label"><span class="required-star">*</span>服务期</span><div class="has-unit"><el-input v-model="drawerForm.standardPeriod" placeholder="请输入" size="small" /><span class="unit-append">年</span></div></div>
- </el-form-item>
- <el-form-item prop="serviceTime">
- <div class="form-item"><span class="field-label">服务时间段</span><el-input v-model="drawerForm.serviceTime" placeholder="请输入" size="small" /></div>
- </el-form-item>
- <el-form-item prop="shortlistedType">
- <div class="form-item"><span class="field-label"><span class="required-star">*</span>入围类型</span><el-select v-model="drawerForm.shortlistedType" placeholder="请选择" size="small"><el-option v-for="item in drawerOptions.shortlisted" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" /></el-select></div>
- </el-form-item>
- <el-form-item prop="profession">
- <div class="form-item">
- <span class="field-label"><span class="required-star">*</span>物资类目</span>
- <el-select v-model="drawerForm.profession" placeholder="请选择" size="small">
- <el-option v-for="item in drawerOptions.profession" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
- </el-select>
- </div>
- </el-form-item>
- <el-form-item prop="biddingAgency">
- <div class="form-item"><span class="field-label">招标代理机构</span><el-input v-model="drawerForm.biddingAgency" placeholder="请输入" size="small" /></div>
- </el-form-item>
- <el-form-item prop="bidPeriodType" class="form-full-row">
- <div class="form-item form-full-item"><span class="field-label"><span class="required-star">*</span>标期类型</span><div style="display:flex;gap:24px;"><el-radio-group v-model="drawerForm.bidPeriodType" size="small"><el-radio :value="1">单项目入围</el-radio><el-radio :value="2">周期性框架</el-radio></el-radio-group></div></div>
- </el-form-item>
- <el-form-item prop="biddingLink" class="form-full-row" v-if="drawerForm.bidPeriodType === 2">
- <div class="form-item form-full-item"><span class="field-label">招标/平台链接</span><el-input v-model="drawerForm.biddingLink" placeholder="请输入/粘贴链接" size="small" /></div>
- </el-form-item>
- <el-form-item prop="condition" class="form-full-row">
- <div class="form-item form-full-item form-item-vertical"><span class="field-label"><span class="required-star">*</span>入围要求</span><el-input v-model="drawerForm.condition" type="textarea" placeholder="请输入" :rows="4" maxlength="500" show-word-limit resize="none" /></div>
- </el-form-item>
- <el-form-item prop="projectDesc" class="form-full-row">
- <div class="form-item form-full-item form-item-vertical"><span class="field-label">项目描述</span><el-input v-model="drawerForm.projectDesc" type="textarea" placeholder="请输入" :rows="4" maxlength="500" show-word-limit resize="none" /></div>
- </el-form-item>
- </div>
- </div>
- <!-- 附件 -->
- <div class="form-section">
- <div class="form-section-header"><span class="section-title">附件</span><span class="upload-link"><el-icon><Upload /></el-icon> 上传附件</span></div>
- <el-table :data="(drawerForm.fileList || [])" class="attachment-table" empty-text="暂无附件" border>
- <el-table-column label="文件名称" align="left" prop="fileName" show-overflow-tooltip />
- <el-table-column label="文件类型" align="center" prop="fileType" width="100" />
- <el-table-column label="上传时间" align="center" prop="uploadTime" width="160" />
- <el-table-column label="操作" align="center" width="80"><template #default><el-button link type="danger" size="small">删除</el-button></template></el-table-column>
- </el-table>
- </div>
- </el-form>
- <div class="edit-drawer-footer">
- <el-button size="small" @click="handleCancelEdit">取消</el-button>
- <el-button type="primary" size="small" @click="handleSubmitDrawer">保存</el-button>
- </div>
- </div>
- </template>
- </el-drawer>
- <!-- ========== 新建联系人抽屉 ========== -->
- <el-drawer v-model="contactDrawerOpen" size="70%" direction="rtl" destroy-on-close :with-header="false" class="project-contact-drawer">
- <div class="drawer-header-standard">
- <span class="header-title-text">新建项目联系人</span>
- <el-icon class="close-btn" @click="contactDrawerOpen = false"><Close /></el-icon>
- </div>
- <div class="drawer-body-standard">
- <el-form :model="contactForm" :rules="contactRules" ref="contactFormRef" label-width="100px" label-position="right">
- <div class="section-title">基本信息</div>
- <el-row :gutter="32">
- <el-col :span="8">
- <el-form-item label="姓名" prop="name" required>
- <el-input v-model="contactForm.name" placeholder="请输入" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="联系人类型" prop="contactType" required>
- <el-radio-group v-model="contactForm.contactType">
- <el-radio value="公司职员">公司职员</el-radio>
- <el-radio value="关系资源人">关系资源人</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="性别" prop="sex" required>
- <el-radio-group v-model="contactForm.sex">
- <el-radio value="男">男</el-radio>
- <el-radio value="女">女</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="32">
- <el-col :span="8">
- <el-form-item label="年龄" prop="age">
- <el-input v-model="contactForm.age" placeholder="请输入" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="籍贯" prop="nativePlace">
- <el-input v-model="contactForm.nativePlace" placeholder="请输入" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="生日" prop="birthday">
- <el-date-picker v-model="contactForm.birthday" type="date" placeholder="选择日期" style="width: 100%" value-format="YYYY-MM-DD" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row>
- <el-col :span="24">
- <el-form-item label="描述" prop="description">
- <el-input v-model="contactForm.description" type="textarea" :rows="3" placeholder="请输入" />
- </el-form-item>
- </el-col>
- </el-row>
- <div class="section-title">办公信息</div>
- <el-row :gutter="32">
- <el-col :span="8">
- <el-form-item label="在职状态" prop="status" required>
- <el-radio-group v-model="contactForm.status">
- <el-radio value="在职">在职</el-radio>
- <el-radio value="离职">离职</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="手机号码" prop="phonenumber" required>
- <el-input v-model="contactForm.phonenumber" placeholder="请输入" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="部门" prop="deptName" required>
- <el-input v-model="contactForm.deptName" placeholder="请输入部门" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="32">
- <el-col :span="8">
- <el-form-item label="职位" prop="position" required>
- <el-input v-model="contactForm.position" placeholder="请输入" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="办公座机" prop="officePhone">
- <el-input v-model="contactForm.officePhone" placeholder="请输入" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="32">
- <el-col :span="16">
- <el-form-item label="办公地址" prop="provinceCity">
- <div style="display: flex; gap: 12px; width: 100%;">
- <el-cascader
- v-model="contactForm.provinceCity"
- :options="provinceCityOptions"
- :props="{ value: 'areaName', label: 'areaName', children: 'children', checkStrictly: true }"
- placeholder="请选择"
- style="flex: 0 0 200px;"
- clearable
- />
- <el-input v-model="contactForm.addressDetail" placeholder="请输入" style="flex: 1;" />
- </div>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row>
- <el-col :span="24">
- <el-form-item label="工作内容" prop="jobContent">
- <el-input v-model="contactForm.jobContent" type="textarea" :rows="3" placeholder="请输入" />
- </el-form-item>
- </el-col>
- </el-row>
- <div class="section-title">项目决策</div>
- <el-row :gutter="32">
- <el-col :span="12">
- <el-form-item label="项目角色" prop="projectRole">
- <el-select v-model="contactForm.projectRole" placeholder="请选择" style="width: 100%">
- <el-option
- v-for="item in projectRoleOptions"
- :key="item.dictValue"
- :label="item.dictLabel"
- :value="item.dictValue"
- />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="是否关键人" prop="isKeyPerson">
- <el-select v-model="contactForm.isKeyPerson" placeholder="请选择" style="width: 100%">
- <el-option label="是" :value="1" />
- <el-option label="否" :value="0" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row>
- <el-col :span="24">
- <el-form-item label="公关情况" prop="prCondition">
- <el-input v-model="contactForm.prCondition" type="textarea" :rows="3" placeholder="请输入" />
- </el-form-item>
- </el-col>
- </el-row>
- </el-form>
- </div>
- <div class="drawer-footer-standard">
- <el-button type="primary" @click="submitContact" class="btn-save">保 存</el-button>
- <el-button @click="contactDrawerOpen = false" class="btn-cancel">取 消</el-button>
- </div>
- </el-drawer>
- <!-- ========== 查看进度子抽屉 ========== -->
- <el-drawer v-model="showProgressDrawer" direction="rtl" size="480px" :with-header="false" class="progress-drawer" :close-on-click-modal="true">
- <div class="progress-drawer-header">
- <span class="progress-drawer-title">查看进度</span>
- <el-icon class="progress-close-btn" @click="showProgressDrawer = false"><Close /></el-icon>
- </div>
- <div class="progress-drawer-body">
- <div class="progress-input-wrapper">
- <el-input v-model="progressContent" type="textarea" placeholder="请输入进度描述" :maxlength="500" show-word-limit resize="none" :autosize="{ minRows: 6, maxRows: 6 }" />
- </div>
- <div class="publish-btn-row">
- <el-button type="primary" class="publish-btn" style="background-color: #409eff; border-color: #409eff;" @click="handlePublishProgress" :loading="progressLoading"><el-icon style="margin-right: 4px;"><Plus /></el-icon> 发布</el-button>
- </div>
- <div class="record-list" v-loading="recordLoading">
- <div
- v-for="item in progressRecordList"
- :key="item.id"
- class="record-item"
- >
- <div class="record-header">
- <span class="record-user">{{ item.createByName || item.createBy || '' }}</span>
- <span class="record-time">{{ item.createTime || '' }}</span>
- </div>
- <div class="record-content">{{ item.followUpCondition || item.content || '' }}</div>
- </div>
- <el-empty v-if="!recordLoading && (!progressRecordList || progressRecordList.length === 0)" description="暂无进度记录" :image-size="60" />
- <div class="pagination-wrapper" v-if="progressTotal > 0">
- <el-pagination
- small
- layout="prev, pager, next"
- :total="progressTotal"
- v-model:current-page="progressQueryParams.pageNum"
- v-model:page-size="progressQueryParams.pageSize"
- @current-change="loadProgressRecordList"
- />
- </div>
- </div>
- </div>
- </el-drawer>
- <!-- 弹窗:转移他人 -->
- <el-dialog title="是否将选中的年度入围转移给其他负责人?" v-model="transferVisible" width="550px">
- <el-form label-width="100px" style="padding: 20px 0;">
- <el-form-item label="新负责人:" required>
- <el-select v-model="transferOwner" placeholder="请选择" style="width: 100%" filterable>
- <el-option v-for="item in userOptions" :key="item.staffId || item.userId" :label="item.staffName || item.nickName" :value="String(item.staffId || item.userId)" />
- </el-select>
- </el-form-item>
- <el-form-item class="label-nowrap">
- <el-checkbox v-model="keepAsMember">保留【负责人】为销售团队成员</el-checkbox>
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button type="primary" @click="confirmTransfer">确认</el-button>
- <el-button @click="transferVisible = false">取消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 弹窗:新建跟进记录 -->
- <el-dialog title="新建跟进记录" v-model="showAddFollowUp" width="600px" append-to-body class="follow-up-dialog">
- <el-form ref="followUpRef" :model="followUpForm" :rules="followUpRules" label-width="100px">
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="拜访人" prop="visitor">
- <el-input v-model="followUpForm.visitor" disabled />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="随访人">
- <el-select v-model="followUpForm.followPeople" placeholder="请选择" filterable style="width: 100%">
- <el-option v-for="u in userOptions" :key="u.staffId" :label="u.staffName" :value="u.staffName" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="拜访方式" prop="callTypeCode">
- <el-select v-model="followUpForm.callTypeCode" placeholder="请选择" style="width: 100%">
- <el-option v-for="d in followUpQueryTypeOptions" :key="d.dictValue" :label="d.dictLabel" :value="d.dictValue" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="拜访时间" prop="callDate">
- <el-date-picker v-model="followUpForm.callDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-form-item label="拜访目的" prop="callAim">
- <el-input v-model="followUpForm.callAim" type="textarea" :rows="2" placeholder="请输入拜访目的" />
- </el-form-item>
- <el-form-item label="跟进情况" prop="followUpCondition">
- <el-input v-model="followUpForm.followUpCondition" type="textarea" :rows="3" placeholder="请输入跟进情况" />
- </el-form-item>
- <el-form-item label="上传图片">
- <image-upload v-model="followUpForm.recordPicture" :limit="3" :isShowTip="false" />
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer" style="display: flex; justify-content: center; gap: 20px;">
- <el-button type="primary" @click="submitFollowUp" style="padding: 8px 24px;">确 认</el-button>
- <el-button @click="showAddFollowUp = false" style="padding: 8px 24px;">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 团队成员管理弹窗 (平台甄选补全) -->
- <el-dialog
- :title="memberEditVisible ? '编辑团队成员' : '添加团队成员'"
- v-model="memberDialogVisible"
- width="440px"
- append-to-body
- destroy-on-close
- class="team-member-dialog"
- >
- <el-form label-width="100px" label-position="left">
- <el-form-item label="人员姓名:" v-if="memberAddVisible">
- <el-select v-model="memberAddForm.staffId" placeholder="请选择人员" filterable style="width: 100%">
- <el-option v-for="user in userOptions" :key="user.staffId" :label="user.staffName" :value="user.staffId" />
- </el-select>
- </el-form-item>
- <el-form-item label="人员姓名:" v-if="memberEditVisible">
- <span style="color: #333; font-weight: 500;">{{ memberEditForm.staffName }}{{ memberEditForm.deptName ? '/' + memberEditForm.deptName : '' }}</span>
- </el-form-item>
- <el-form-item label="成员角色:">
- <el-select v-if="memberEditVisible" v-model="memberEditForm.role" placeholder="请选择角色" style="width: 100%">
- <el-option v-for="role in memberRoleOptions" :key="role.dictValue" :label="role.dictLabel" :value="role.dictValue" />
- </el-select>
- <el-select v-else v-model="memberAddForm.role" placeholder="请选择角色" style="width: 100%">
- <el-option v-for="role in memberRoleOptions" :key="role.dictValue" :label="role.dictLabel" :value="role.dictValue" />
- </el-select>
- </el-form-item>
- <el-form-item label="权限:">
- <el-radio-group v-if="memberEditVisible" v-model="memberEditForm.permission">
- <el-radio v-for="p in permissionOptions" :key="p.dictValue" :value="p.dictValue">{{ p.dictLabel }}</el-radio>
- </el-radio-group>
- <el-radio-group v-else v-model="memberAddForm.permission">
- <el-radio v-for="p in permissionOptions" :key="p.dictValue" :value="p.dictValue">{{ p.dictLabel }}</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-form>
- <template #footer>
- <div style="text-align: right; padding-right: 10px;">
- <el-button type="primary" @click="memberEditVisible ? submitMemberEdit() : submitMemberAdd()" style="padding: 8px 24px;">确 认</el-button>
- <el-button @click="memberEditVisible = false; memberAddVisible = false" style="padding: 8px 24px;">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <el-dialog title="跟进记录" v-model="showAddFollowUp" width="700px" custom-class="follow-up-dialog" append-to-body>
- <el-form :model="followUpForm" :rules="followUpRules" ref="followUpRef" label-width="100px">
- <div style="margin-bottom: 20px; padding-left: 20px; font-size: 14px; color: #1d2129;">
- 客户:{{ drawerForm.customName }}
- </div>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="拜访人:" prop="visitor">
- <el-select v-model="followUpForm.visitor" placeholder="请选择" style="width: 100%" filterable>
- <el-option v-for="item in userOptions" :key="item.staffId || item.userId" :label="item.staffName || item.nickName" :value="item.staffName || item.nickName" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="随访人:">
- <el-select v-model="followUpForm.followPeople" placeholder="请选择" style="width: 100%" filterable multiple collapse-tags>
- <el-option v-for="item in userOptions" :key="item.staffId || item.userId" :label="item.staffName || item.nickName" :value="item.staffName || item.nickName" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="拜访方式:" prop="callTypeCode">
- <el-select v-model="followUpForm.callTypeCode" placeholder="请选择" style="width: 100%">
- <el-option v-for="item in callTypeOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="拜访时间:" prop="callDate">
- <el-date-picker v-model="followUpForm.callDate" type="date" placeholder="请选择" style="width: 100%" value-format="YYYY-MM-DD" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="下次拜访时间:">
- <el-date-picker v-model="followUpForm.nextCallDate" type="date" placeholder="请选择" style="width: 100%" value-format="YYYY-MM-DD" />
- </el-form-item>
- </el-col>
- <el-col :span="24">
- <el-form-item label="拜访目的:" prop="callAim">
- <el-input v-model="followUpForm.callAim" type="textarea" :rows="3" placeholder="请输入内容" maxlength="500" show-word-limit />
- </el-form-item>
- </el-col>
- <el-col :span="24">
- <el-form-item label="跟进情况:" prop="followUpCondition">
- <el-input v-model="followUpForm.followUpCondition" type="textarea" :rows="3" placeholder="请输入内容" maxlength="1000" show-word-limit />
- </el-form-item>
- </el-col>
- <el-col :span="24">
- <el-form-item label="记录图片:">
- <div class="upload-placeholder">
- <el-icon><Plus /></el-icon>
- </div>
- </el-form-item>
- </el-col>
- </el-row>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button type="primary" @click="submitFollowUp" style="background-color: #165dff; border-color: #165dff;">确认</el-button>
- <el-button @click="showAddFollowUp = false">取消</el-button>
- </div>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup name="PlatformSelection">
- import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue';
- import { listPlatformSelection, getPlatformSelection, delPlatformSelection, addPlatformSelection, updatePlatformSelection, transferPlatformSelection, publishPlatformProgress } from '@/api/saleManage/platformSelection/index';
- import { listCommonDict } from "@/api/customer/customerDict";
- import { selectStaffOptionList } from "@/api/system/comStaff/index";
- import { listCompanyOption, listCustomerInfo } from "@/api/customer/customerInfo/index";
- import { listIndustryCategory } from "@/api/customer/industryCategory";
- import { deptTreeSelect } from "@/api/system/user";
- import { Upload, Close, Edit, Plus } from '@element-plus/icons-vue';
- import { listOperationLog } from '@/api/customer/operationLog';
- import { listRecord, addRecord, delRecord } from "@/api/visit/record";
- import { listContact, addContact } from '@/api/customer/crmContact';
- import { listProvinceWithCities } from '@/api/customer/addressArea';
- import { getSalesResultAnalyzeByObjectNo, addSalesResultAnalyze, updateSalesResultAnalyze } from '@/api/saleManage/leads/salesResultAnalyze';
- const { proxy } = getCurrentInstance();
- // ==================== 列表页状态 ====================
- const loading = ref(false);
- const total = ref(0);
- const dataList = ref([]);
- const dateRange = ref([]);
- const multipleSelection = ref([]);
- const totalAmount = ref(0);
- const activeTab = ref('all');
- const totalCountBadge = ref(0);
- const transferVisible = ref(false);
- const transferOwner = ref('');
- const keepAsMember = ref(false);
- // ==================== 抽屉状态 ====================
- const drawerVisible = ref(false);
- const drawerIsEdit = ref(false);
- const drawerActiveTab = ref('info');
- const drawerActiveSideTab = ref('team');
- const drawerForm = ref({});
- const drawerFormRef = ref(null);
- const showProgressDrawer = ref(false);
- const progressContent = ref('');
- const progressLoading = ref(false);
- const recordLoading = ref(false);
- const progressRecordList = ref([]);
- const progressTotal = ref(0);
- const progressQueryParams = reactive({
- pageNum: 1,
- pageSize: 10,
- objectNo: undefined
- });
- // 联系人相关
- const contactDrawerOpen = ref(false);
- const contactFormRef = ref(null);
- const contactLoading = ref(false);
- const contactList = ref([]);
- const provinceCityOptions = ref([]);
- const projectRoleOptions = ref([]);
- const contactForm = reactive({
- name: '',
- contactType: '公司职员',
- sex: '男',
- age: undefined,
- nativePlace: '',
- birthday: undefined,
- description: '',
- status: '在职',
- phonenumber: '',
- deptName: '',
- position: '',
- officePhone: '',
- provinceCity: [],
- addressDetail: '',
- jobContent: '',
- projectRole: '',
- isKeyPerson: undefined,
- prCondition: ''
- });
- const contactRules = reactive({
- name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
- contactType: [{ required: true, message: '请选择联系人类型', trigger: 'change' }],
- sex: [{ required: true, message: '请选择性别', trigger: 'change' }],
- status: [{ required: true, message: '请选择在职状态', trigger: 'change' }],
- phonenumber: [{ required: true, message: '手机号码不能为空', trigger: 'blur' }],
- deptName: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
- position: [{ required: true, message: '职位不能为空', trigger: 'blur' }]
- });
- // 结果分析相关
- const analysisData = ref(null);
- const analysisLoading = ref(false);
- const loadAnalysisData = async () => {
- if (!drawerForm.value.projectNo) return;
- analysisLoading.value = true;
- try {
- const res = await getSalesResultAnalyzeByObjectNo(drawerForm.value.projectNo);
- if (res.data) {
- analysisData.value = res.data;
- drawerForm.value.resultType = res.data.resultType || 1;
- drawerForm.value.winSummary = res.data.summary || '';
- }
- } catch (err) {
- console.error('加载结果分析失败:', err);
- } finally {
- analysisLoading.value = false;
- }
- };
- const submitAnalysis = async () => {
- try {
- const data = {
- objectNo: drawerForm.value.projectNo,
- resultType: drawerForm.value.resultType,
- summary: drawerForm.value.winSummary
- };
- if (analysisData.value && analysisData.value.id) {
- await updateSalesResultAnalyze({ ...data, id: analysisData.value.id });
- } else {
- await addSalesResultAnalyze(data);
- }
- proxy.$modal.msgSuccess("保存成功");
- loadAnalysisData();
- } catch (err) {
- console.error('保存结果分析失败:', err);
- }
- };
- const stepList = ['获取信息', '正式立项', '竞价/投标', '项目跟进', '结案'];
- /** 处理进度步骤点击 */
- const handleStepClick = (newStatus) => {
- if (!drawerForm.value.id) return;
- if (drawerForm.value.projectStatus === newStatus) return;
- const statusName = stepList[newStatus - 1];
- proxy.$modal.confirm(`确定将项目进度变更至“${statusName}”吗?`).then(async () => {
- try {
- const updateData = {
- id: drawerForm.value.id,
- projectStatus: newStatus
- };
- await updatePlatformSelection(updateData);
- proxy.$modal.msgSuccess("进度变更成功");
- // 更新本地状态
- drawerForm.value.projectStatus = newStatus;
- // 刷新列表和日志
- getList();
- fetchOperationLogs(drawerForm.value.id);
- } catch (err) {
- console.error("变更进度失败:", err);
- }
- }).catch(() => {});
- };
- // ==================== 操作日志 ====================
- const logList = ref([]);
- const logLoading = ref(false);
- const getLogActionText = (actionType) => {
- const map = {
- 1: '添加了',
- 2: '编辑了',
- 3: '删除了',
- 4: '认领了',
- 5: '转移了',
- 6: '分析了'
- };
- return map[actionType] || '操作了';
- };
- const formatLogAction = (log) => {
- return getLogActionText(log.actionType);
- };
- const translateDetails = (details) => {
- if (!details) return '';
- if (details.includes('projectStatus') || details.includes('年度入围')) {
- return '年度入围(平台)';
- }
- return details;
- };
- const formatLogTarget = (log) => {
- if (!log.targetObject) return '';
- const details = log.actionDetails || '';
- if (details.includes('projectStatus') || details.includes('年度入围')) {
- const val = log.targetObject;
- if (/^\d+$/.test(val) && statusOptions.value) {
- const dict = statusOptions.value.find(o => String(o.dictValue) === String(val));
- return `进度状态变更,${dict ? dict.dictLabel : val}`;
- }
- return val.includes('进度状态变更') ? val : `进度状态变更,${val}`;
- }
- return log.targetObject;
- };
- const fetchOperationLogs = async (id) => {
- if (!id) return;
- logLoading.value = true;
- try {
- const res = await listOperationLog({ dataType: 2, objectNo: id });
- logList.value = res.data || [];
- } catch (error) {
- } finally {
- logLoading.value = false;
- }
- };
- // ==================== 下拉选项(列表搜索用) ====================
- const queryParams = reactive({
- pageNum: 1, pageSize: 10,
- companyNo: undefined, projectName: undefined, customName: undefined, leader: undefined,
- deptNo: undefined, productSupport: undefined, projectLevel: undefined, businessType: undefined, projectStatus: 1,
- shortlistedType: undefined, result: undefined, timeType: undefined
- });
- const projectLevelOptions = ref([]);
- const projectTypeOptions = ref([]);
- const shortlistedTypeOptions = ref([]);
- const professionOptions = ref([]);
- const userOptions = ref([]);
- const companyOptions = ref([]);
- const statusOptions = ref([]);
- const resultOptions = ref([]);
- const timeTypeOptions = ref([]);
- const deptOptions = ref([]);
- // ==================== 抽屉内下拉选项 ====================
- const drawerOptions = ref({
- company: [], customer: [], user: [], level: [], type: [], shortlisted: [], profession: [], industryList: []
- });
- const drawerRules = {
- companyNo: [{ required: true, message: "请选择归属公司", trigger: "change" }],
- customName: [{ required: true, message: "请选择客户名称", trigger: "change" }],
- projectLevel: [{ required: true, message: "请选择项目级别", trigger: "change" }],
- businessType: [{ required: true, message: "请选择项目类型", trigger: "change" }],
- projectName: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
- amount: [{ required: true, message: "请输入金额", trigger: "blur" }],
- entryFee: [{ required: true, message: "请输入报名费", trigger: "blur" }],
- winningRate: [{ required: true, message: "请输入赢单率", trigger: "blur" }],
- signUpDeadline: [{ required: true, message: "请选择报名截止时间", trigger: "change" }],
- tenderDeadline: [{ required: true, message: "请选择投标截止时间", trigger: "change" }],
- standardPeriod: [{ required: true, message: "请输入服务期", trigger: "blur" }],
- shortlistedType: [{ required: true, message: "请选择入围类型", trigger: "change" }],
- profession: [{ required: true, message: "请选择物资类目", trigger: "change" }],
- bidPeriodType: [{ required: true, message: "请选择标期类型", trigger: "change" }],
- condition: [{ required: true, message: "请输入入围要求", trigger: "blur" }],
- };
- // ==================== 跟进记录相关 ====================
- const followUpList = ref([]);
- const followUpLoading = ref(false);
- const followUpTotal = ref(0);
- const showAddFollowUp = ref(false);
- const followUpQueryType = ref(undefined);
- const followUpForm = ref({
- visitor: undefined,
- followPeople: undefined,
- callTypeCode: undefined,
- callDate: undefined,
- nextCallDate: undefined,
- callAim: undefined,
- followUpCondition: undefined,
- recordPicture: undefined
- });
- const followUpRules = {
- visitor: [{ required: true, message: "请选择拜访人", trigger: "change" }],
- callTypeCode: [{ required: true, message: "请选择拜访方式", trigger: "change" }],
- callDate: [{ required: true, message: "请选择拜访时间", trigger: "change" }],
- callAim: [{ required: true, message: "请输入拜访目的", trigger: "blur" }],
- followUpCondition: [{ required: true, message: "请输入跟进情况", trigger: "blur" }],
- };
- const fetchFollowUps = async (id) => {
- if (!id) return;
- followUpLoading.value = true;
- try {
- const res = await listRecord({
- dataType: 1, // 项目级别记录
- goalObject: drawerForm.value.projectName,
- callTypeCode: followUpQueryType.value,
- pageSize: 50
- });
- followUpList.value = res.rows || [];
- followUpTotal.value = res.total || 0;
- } catch (error) {
- } finally {
- followUpLoading.value = false;
- }
- };
- const handleOpenAddFollowUp = () => {
- followUpForm.value = {
- visitor: proxy.$store.state.user.staffName || proxy.$store.state.user.nickName,
- callDate: new Date().toISOString().split('T')[0],
- dataType: 1,
- goalObject: drawerForm.value.projectName,
- customerName: drawerForm.value.customName
- };
- showAddFollowUp.value = true;
- };
- const submitFollowUp = async () => {
- proxy.$refs.followUpRef.validate(async (valid) => {
- if (valid) {
- try {
- await addRecord(followUpForm.value);
- proxy.$modal.msgSuccess("新增成功");
- showAddFollowUp.value = false;
- fetchFollowUps(drawerForm.value.id);
- } catch (err) {}
- }
- });
- };
- const followUpQueryTypeOptions = ref([]);
- const memberRoleOptions = ref([]);
- const permissionOptions = ref([]);
- // 团队成员相关状态 (补全)
- const memberEditVisible = ref(false);
- const memberAddVisible = ref(false);
- const memberDialogVisible = computed({
- get: () => memberEditVisible.value || memberAddVisible.value,
- set: (val) => {
- if (!val) {
- memberEditVisible.value = false;
- memberAddVisible.value = false;
- }
- }
- });
- const memberEditForm = reactive({ staffId: '', staffName: '', deptName: '', role: '', permission: '0' });
- const memberAddForm = reactive({ staffId: '', role: '', permission: '0' });
- let editingMemberIndex = -1;
- /** 联系人相关 */
- const handleAddContact = () => {
- Object.assign(contactForm, {
- name: '',
- contactType: '公司职员',
- sex: '男',
- age: undefined,
- nativePlace: '',
- birthday: undefined,
- description: '',
- status: '在职',
- phonenumber: '',
- deptName: '',
- position: '',
- officePhone: '',
- provinceCity: [],
- addressDetail: '',
- jobContent: '',
- projectRole: '',
- isKeyPerson: undefined,
- prCondition: ''
- });
- contactDrawerOpen.value = true;
- };
- const submitContact = () => {
- contactFormRef.value.validate(async (valid) => {
- if (valid) {
- const province = contactForm.provinceCity?.[0] || '';
- const city = contactForm.provinceCity?.[1] || '';
- const fullAddress = [province, city, contactForm.addressDetail].filter(Boolean).join('');
- const data = {
- name: contactForm.name,
- contactType: contactForm.contactType,
- sex: contactForm.sex,
- age: contactForm.age,
- nativePlace: contactForm.nativePlace,
- birthday: contactForm.birthday,
- characterDesc: contactForm.description,
- employmentStatusCode: contactForm.status,
- phoneNumber: contactForm.phonenumber,
- department: contactForm.deptName,
- position: contactForm.position,
- landline: contactForm.officePhone,
- province: province,
- city: city,
- workAddress: fullAddress,
- workDesc: contactForm.jobContent,
- projectRole: contactForm.projectRole,
- izCruxPerson: contactForm.isKeyPerson ? '是' : '否',
- authorityDesc: contactForm.prCondition,
- companyNo: drawerForm.value.companyNo,
- customerNo: drawerForm.value.customerNo,
- customName: drawerForm.value.customName
- };
- try {
- await addContact(data);
- proxy.$modal.msgSuccess("保存成功");
- contactDrawerOpen.value = false;
- loadContactList();
- } catch (err) {
- console.error('新增联系人失败:', err);
- }
- }
- });
- };
- const loadContactList = async () => {
- if (!drawerForm.value.id) return;
- contactLoading.value = true;
- try {
- const res = await listContact({
- customerNo: drawerForm.value.customerNo,
- companyNo: drawerForm.value.companyNo
- });
- contactList.value = res.rows || [];
- } catch (err) {
- console.error('加载联系人列表失败:', err);
- } finally {
- contactLoading.value = false;
- }
- };
- /** 加载团队成员列表 (补全) */
- const fetchTeamMembers = async (id) => {
- if (!id) return;
- try {
- const res = await listTeamMember(id);
- drawerForm.value.memberList = res.rows || res.data || [];
- } catch (err) {
- console.error('加载团队成员失败:', err);
- }
- };
- /** 打开添加团队成员弹窗 (补全) */
- const handleAddMember = () => {
- memberAddForm.staffId = undefined;
- memberAddForm.role = '';
- memberAddForm.permission = '0';
- memberAddVisible.value = true;
- };
- /** 提交添加团队成员 (补全) */
- const submitMemberAdd = async () => {
- if (!memberAddForm.staffId) {
- proxy.$modal.msgWarning("请选择人员");
- return;
- }
- const selectedUser = userOptions.value.find(u => u.staffId == memberAddForm.staffId);
- try {
- await addTeamMember({
- dataType: 2, // 2为年度入围
- objectNo: String(drawerForm.value.id),
- userNo: memberAddForm.staffId,
- realName: selectedUser?.staffName || '',
- roleCode: memberAddForm.role,
- updateAccredit: Number(memberAddForm.permission)
- });
- proxy.$modal.msgSuccess("添加成功");
- memberAddVisible.value = false;
- fetchTeamMembers(drawerForm.value.id);
- } catch (err) {}
- };
- /** 打开编辑团队成员弹窗 (补全) */
- const handleEditMember = (member) => {
- editingMemberIndex = drawerForm.value.memberList.indexOf(member);
- Object.assign(memberEditForm, {
- staffId: member.userNo || member.staffId,
- staffName: member.realName || member.staffName,
- deptName: member.deptName,
- role: member.roleCode || '',
- permission: String(member.updateAccredit ?? '0')
- });
- memberEditVisible.value = true;
- };
- /** 提交编辑团队成员 (补全) */
- const submitMemberEdit = async () => {
- const member = drawerForm.value.memberList[editingMemberIndex];
- try {
- await updateTeamMember({
- id: member.id,
- roleCode: memberEditForm.role,
- updateAccredit: Number(memberEditForm.permission)
- });
- proxy.$modal.msgSuccess("修改成功");
- memberEditVisible.value = false;
- fetchTeamMembers(drawerForm.value.id);
- } catch (err) {}
- };
- /** 删除团队成员 (补全) */
- const handleRemoveMember = (member) => {
- proxy.$modal.confirm(`确认移除成员"${member.realName || member.staffName}"吗?`).then(async () => {
- try {
- await delTeamMember(member.id);
- proxy.$modal.msgSuccess("移除成功");
- fetchTeamMembers(drawerForm.value.id);
- } catch (err) {}
- }).catch(() => {});
- };
- // ==================== 计算属性 - 抽屉数据匹配 ====================
- const drawerModeTitle = computed(() => {
- if (!drawerForm.value.id) return '新增年度入围平台';
- return drawerIsEdit.value ? '修改年度入围平台' : drawerForm.value.projectName;
- });
- const drawerCompanyName = computed(() => {
- const target = drawerOptions.value.company.find(c => String(c.companyCode || c.id) === String(drawerForm.value.companyNo));
- return target?.companyName || target?.name || drawerForm.value.companyNo || '--';
- });
- const drawerProjectLevelName = computed(() => drawerOptions.value.level.find(o => String(o.dictValue) === String(drawerForm.value.projectLevel))?.dictLabel || drawerForm.value.projectLevel || '--');
- const drawerProjectTypeName = computed(() => drawerOptions.value.type.find(o => String(o.dictValue) === String(drawerForm.value.businessType))?.dictLabel || drawerForm.value.businessType || '--');
- const drawerShortlistedTypeName = computed(() => drawerOptions.value.shortlisted.find(o => String(o.dictValue) === String(drawerForm.value.shortlistedType))?.dictLabel || drawerForm.value.shortlistedType || '--');
- const drawerProfessionName = computed(() => {
- const val = String(drawerForm.value.profession || '');
- if (!val) return '--';
- const p = drawerOptions.value.profession.find(item =>
- String(item.id) === val || String(item.dictValue) === val
- );
- return p?.industryCategoryName || p?.name || p?.dictLabel || val;
- });
- // ==================== 列表方法 ====================
- const getList = async () => {
- loading.value = true;
- try {
- const res = await listPlatformSelection(proxy.addDateRange(queryParams, dateRange.value));
- dataList.value = res.rows || [];
- total.value = res.total || 0;
- totalAmount.value = dataList.value.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
- try {
- const allRes = await listPlatformSelection({ pageNum: 1, pageSize: 1 });
- totalCountBadge.value = allRes.total || 0;
- } catch (e) { /* ignore */ }
- } catch (err) { console.error(err); }
- finally { loading.value = false; }
- };
- const handleQuery = () => { queryParams.pageNum = 1; getList(); };
- const handleTabChange = (tab) => {
- if (activeTab.value === tab) return;
- activeTab.value = tab;
- switch (tab) {
- case 'all': delete queryParams.queryType; break;
- case 'mine': queryParams.queryType = 1; break;
- case 'join': queryParams.queryType = 2; break;
- case 'total': delete queryParams.queryType; break;
- }
- handleQuery();
- };
- const resetQuery = () => { dateRange.value = []; proxy.resetForm("queryRef"); handleQuery(); };
- const handleSelectionChange = (selection) => { multipleSelection.value = selection; };
- const handleAdd = () => {
- drawerForm.value = {
- projectStatus: 1,
- bidPeriodType: 1,
- winningRate: 0,
- amount: 0,
- entryFee: 0,
- bidBond: 0,
- standardPeriod: 0
- };
- drawerIsEdit.value = true;
- drawerVisible.value = true;
- drawerActiveTab.value = 'info';
- };
- const handleUpdate = async (row) => {
- const id = row.id || multipleSelection.value[0].id;
- loading.value = true;
- try {
- const res = await getPlatformSelection(id);
- drawerForm.value = res.data;
- drawerIsEdit.value = false;
- drawerVisible.value = true;
- drawerActiveTab.value = 'info';
- fetchOperationLogs(id);
- fetchFollowUps(id);
- fetchTeamMembers(id);
- loadContactList();
- } catch (err) {
- console.error(err);
- } finally {
- loading.value = false;
- }
- };
- const handleProgress = (row) => {
- drawerForm.value = { ...row };
- showProgressDrawer.value = true;
- if (drawerForm.value.id) {
- progressQueryParams.objectNo = String(drawerForm.value.id);
- progressQueryParams.pageNum = 1;
- loadProgressRecordList();
- }
- };
- const loadProgressRecordList = async () => {
- if (!progressQueryParams.objectNo) return;
- recordLoading.value = true;
- try {
- const res = await listRecord(progressQueryParams);
- progressRecordList.value = res.rows || [];
- progressTotal.value = res.total || 0;
- } catch (err) {
- console.error('加载进度记录失败:', err);
- } finally {
- recordLoading.value = false;
- }
- };
- const handleDelete = (row) => {
- const ids = row.id || multipleSelection.value.map(item => item.id);
- proxy.$modal.confirm(`是否确认删除记录?`).then(() => delPlatformSelection(ids)).then(() => { getList(); proxy.$modal.msgSuccess("删除成功"); }).catch(() => {});
- };
- const handleBatchDelete = () => { handleDelete({}); };
- const handleTransfer = () => { transferVisible.value = true; };
- const confirmTransfer = async () => {
- if (!transferOwner.value) { proxy.$modal.msgWarning('请选择新负责人'); return; }
- try {
- await transferPlatformSelection({ ids: multipleSelection.value.map(i => i.id), newLeaderId: transferOwner.value, keepAsMember: keepAsMember.value });
- proxy.$modal.msgSuccess('转移成功'); transferVisible.value = false; getList();
- } catch (err) { console.error('权属转移失败:', err); }
- };
- // ==================== 抽屉方法 ====================
- const handleDrawerClose = () => { drawerVisible.value = false; };
- const handleCancelEdit = () => {
- if (!drawerForm.value.id) { drawerVisible.value = false; }
- else { drawerIsEdit.value = false; }
- };
- const handleSubmitDrawer = () => {
- if (!drawerFormRef.value) return;
- drawerFormRef.value.validate((valid) => {
- if (valid) {
- submitForm(drawerForm.value);
- }
- });
- };
- const submitForm = async (data) => {
- try {
- if (data.id) {
- await updatePlatformSelection(data);
- proxy.$modal.msgSuccess("修改成功");
- } else {
- await addPlatformSelection(data);
- proxy.$modal.msgSuccess("新增成功");
- }
- drawerVisible.value = false;
- getList();
- } catch (err) {
- console.error(err);
- }
- };
- const openLink = (url) => { if (url && (url.startsWith('http') || url.startsWith('https'))) window.open(url, '_blank'); };
- const handlePublishProgress = async () => {
- if (!progressContent.value.trim()) return;
- progressLoading.value = true;
- try {
- await addRecord({
- objectNo: String(drawerForm.value.id),
- followUpCondition: progressContent.value.trim(),
- dataType: '2'
- });
- proxy.$modal.msgSuccess('发布成功');
- progressContent.value = '';
- progressQueryParams.pageNum = 1;
- loadProgressRecordList();
- } catch (err) { console.error('进度发布失败:', err); }
- finally { progressLoading.value = false; }
- };
- // ==================== 加载下拉选项 ====================
- function getProjectLevelList() { listCommonDict('XMJB0001').then(r => projectLevelOptions.value = r.data); }
- function getProjectTypeList() { listCommonDict('L0001').then(r => projectTypeOptions.value = r.data); }
- function getShortlistedTypeList() { listCommonDict('R0001').then(r => shortlistedTypeOptions.value = r.data); }
- function getProfessionList() { listIndustryCategory().then(r => professionOptions.value = r.data); }
- function getUserList() { selectStaffOptionList().then(r => userOptions.value = r.data); }
- function getCompanyList() { listCompanyOption().then(r => companyOptions.value = r.data); }
- function getStatusList() { listCommonDict('J0001').then(r => statusOptions.value = r.data || []); }
- function getResultList() { listCommonDict('deal_result').then(r => resultOptions.value = r.data || []); }
- function getTimeTypeList() { listCommonDict('time_query_type').then(r => timeTypeOptions.value = r.data || []); }
- function getDeptList() {
- deptTreeSelect().then(res => {
- const flatList = (list) => { let arr = []; for (const item of list || []) { arr.push({ label: item.label, value: Number(item.id) || item.id }); if (item.children?.length) arr.push(...flatList(item.children)); } return arr; };
- deptOptions.value = flatList(res.data);
- });
- }
- // 抽屉内的选项加载
- function loadDrawerOptions() {
- listCommonDict('XMJB0001').then(r => drawerOptions.value.level = r.data || []);
- listCommonDict('L0001').then(r => drawerOptions.value.type = r.data || []);
- listCommonDict('R0001').then(r => drawerOptions.value.shortlisted = r.data || []);
- listCommonDict('ZBPL0001').then(r => drawerOptions.value.profession = r.data || []);
- listIndustryCategory().then(r => { if (!r.data) return; drawerOptions.value.industryList = r.data; });
- listCompanyOption().then(r => drawerOptions.value.company = r.data || r.rows || []);
- listCustomerInfo().then(r => drawerOptions.value.customer = r.rows || r.data || []);
- selectStaffOptionList().then(r => drawerOptions.value.user = r.data || r.rows || []);
- }
- onMounted(() => {
- getList(); loadDrawerOptions();
- getProjectLevelList(); getProjectTypeList(); getShortlistedTypeList(); getProfessionList();
- getUserList(); getCompanyList(); getStatusList(); getResultList(); getTimeTypeList(); getDeptList();
- listCommonDict("visit_type").then(res => {
- followUpQueryTypeOptions.value = res.data || [];
- });
- listCommonDict('T0001').then(res => {
- memberRoleOptions.value = res.data || [];
- });
- listCommonDict('team_permission').then(res => {
- permissionOptions.value = res.data || [];
- });
- listProvinceWithCities().then(res => {
- provinceCityOptions.value = res.data || [];
- });
- listCommonDict('LXRJE0001').then(res => {
- projectRoleOptions.value = res.data || [];
- });
- });
- </script>
- <style scoped lang="scss">
- .app-container { padding: 20px; background-color: #f7f9fb; min-height: calc(100vh - 84px); }
- .search-card { margin-bottom: 10px; border: none; background: #fff; border-radius: 8px; :deep(.el-form-item) { margin-bottom: 2px; } }
- .search-btns { display: flex; gap: 12px; }
- .tabs-control {
- margin: 12px 0 14px; display: flex; align-items: center; justify-content: space-between; padding-bottom: 1px;
- .tab-items { display: flex; align-items: flex-end;
- .tab-item { padding: 8px 20px 6px; cursor: pointer; font-size: 13px; color: #86909c; position: relative; white-space: nowrap;
- &.current { color: #f53f3f; font-weight: 500; &::after { content: ''; position: absolute; left: 50%; bottom: 0; transform: translateX(-50%); width: 32px; height: 3px; background-color: #f53f3f; border-radius: 2px; } }
- &:hover:not(.current) { color: #4e5969; }
- &.badge-item { position: relative; margin-left: 4px;
- .badge { position: absolute; top: -7px; right: -6px; background: #f53f3f; color: #fff; font-size: 10px; padding: 1px 5px; border-radius: 8px; line-height: 16px; min-width: 20px; text-align: center; transform: scale(0.92); font-weight: 500; letter-spacing: -0.5px; }
- }
- }
- }
- .stripe-right-wrap { display: flex; align-items: center; gap: 24px; }
- .summary-line { display: flex; align-items: center; .stripe-total { font-size: 13px; color: #1d2129; white-space: nowrap; .red-text { color: #f53f3f; font-weight: 600; margin: 0 4px; } } }
- .stripe-actions { display: flex; gap: 10px; flex-shrink: 0; }
- }
- .table-card { border: none; }
- .standard-table {
- :deep(th.el-table__cell) { background-color: #f7f8fa !important; color: #4e5969; font-weight: 600; font-size: 13px; }
- :deep(td.el-table__cell) { font-size: 13px; color: #1d2129; }
- :deep(.el-button--primary.is-link) { color: #165dff; &:hover { color: #4080ff; } }
- :deep(.el-button--danger.is-link) { color: #f53f3f; &:hover { color: #ff6a78; } }
- }
- .label-nowrap :deep(.el-form-item__label) { white-space: nowrap !important; }
- .dialog-footer { display: flex; justify-content: center; gap: 20px; }
- /* ====== 抽屉样式 ====== */
- ::deep(.el-drawer__body) { padding: 0 !important; display: flex; flex-direction: column; overflow: hidden; }
- .detail-header { height: 48px; flex-shrink: 0; display: flex; justify-content: space-between; align-items: center; padding: 0 20px; border-bottom: 1px solid #e5e6eb; background-color: #fff; .project-title { font-size: 25px; color: #1d2129; line-height: 48px; } .close-btn { font-size: 16px; color: #86909c; cursor: pointer; &:hover { color: #f53f3f; } } }
- .progress-section {
- padding: 16px 24px 4px;
- flex-shrink: 0;
- border-bottom: 1px solid #f2f3f5;
- .section-label {
- font-size: 13px;
- font-weight: 600;
- color: #1d2129;
- margin-bottom: 8px;
- display: flex;
- align-items: center;
- &::before {
- content: '';
- width: 3px;
- height: 12px;
- background: #2bc48d;
- border-radius: 2px;
- margin-right: 6px;
- }
- }
- .step-bar {
- display: flex;
- align-items: center;
- margin: 8px 0 16px;
- height: 30px;
- width: 100%;
- .step {
- flex: 1;
- height: 100%;
- position: relative;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #fff;
- margin-right: 0;
- padding-left: 15px; /* 为箭型留出空间 */
- border: 1px solid #f2f3f5;
- border-left: none;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-
- &:first-child {
- border-left: 1px solid #f2f3f5;
- border-radius: 20px 0 0 20px;
- padding-left: 0;
- }
-
- &:last-child {
- border-radius: 0 20px 20px 0;
- &::after { display: none; }
- }
- &:nth-child(1) { z-index: 5; }
- &:nth-child(2) { z-index: 4; }
- &:nth-child(3) { z-index: 3; }
- &:nth-child(4) { z-index: 2; }
- &:nth-child(5) { z-index: 1; }
- .step-text {
- font-size: 12px;
- color: #86909c;
- z-index: 10;
- white-space: nowrap;
- font-weight: 500;
- transition: all 0.3s;
- }
- &::after {
- content: "";
- position: absolute;
- top: -1px;
- right: -15px;
- width: 30px;
- height: 30px;
- background: inherit;
- transform: scale(0.707) rotate(45deg);
- z-index: 5;
- border-right: 1px solid #f2f3f5;
- border-top: 1px solid #f2f3f5;
- border-radius: 0 4px 0 0;
- transition: all 0.3s;
- }
- &.active {
- background: #2bc48d !important;
- border-color: #2bc48d;
- .step-text { color: #ffffff !important; font-weight: 600; }
- &::after {
- background: #2bc48d !important;
- border-color: #fff;
- border-width: 2px;
- }
- & + .step { border-left: none; }
- }
- &.finished {
- background-color: #2bc48d;
- border-color: #2bc48d;
- opacity: 0.8;
- .step-text { color: #ffffff !important; }
- &::after {
- background-color: #2bc48d !important;
- border-color: #fff;
- border-width: 2px;
- }
- }
- }
- }
- .progress-link-row { margin-top: 2px; font-size: 12px; display: inline-flex; align-items: center; gap: 8px; .progress-label { color: #86909c; } .progress-link { color: #165dff; cursor: pointer; } }
- }
- .log-item {
- padding: 12px 0;
- font-size: 13px;
- line-height: 1.6;
- .log-time { color: #c0c4cc; margin-right: 12px; }
- .log-user { color: #409eff; margin-right: 8px; }
- .log-action { color: #86909c; }
- .log-target { color: #409eff; margin-left: 4px; }
- }
- .detail-container { flex: 1; display: flex; background-color: #ffffff !important; overflow: hidden; }
- .detail-left { flex: 7; background-color: #ffffff !important; padding: 0 24px; display: flex; flex-direction: column; }
- .detail-right { flex: 3; background-color: #ffffff !important; padding: 0 24px; border-left: 1px solid #f0f0f0; display: flex; flex-direction: column; }
- .custom-tabs {
- height: 100%;
- :deep(.el-tabs__header) { margin-bottom: 0; border-bottom: 1px solid #f2f3f5; }
- :deep(.el-tabs__nav-wrap)::after { display: none; }
- :deep(.el-tabs__active-bar) {
- background-color: #f53f3f;
- height: 3px;
- border-radius: 2px;
- }
- :deep(.el-tabs__item) {
- font-size: 14px;
- color: #4e5969 !important;
- padding: 0 20px !important;
- height: 48px;
- line-height: 48px;
- font-weight: 400;
- background: transparent !important;
- &.is-active { color: #f53f3f !important; font-weight: 500; }
- &:hover { color: #f53f3f !important; }
- }
- :deep(.el-tabs__content) { padding-top: 20px; }
- }
- .info-block { padding-bottom: 16px; position: relative; .block-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; .block-title { font-size: 15px; font-weight: 600; color: #1d2129; } } }
- .floating-edit-btn { position: absolute; top: -44px; right: 0; background-color: #165dff; border: none; border-radius: 4px; height: 30px; padding: 0 14px; font-size: 13px; z-index: 10; }
- .info-grid { display: grid; gap: 8px 40px; margin-bottom: 8px;
- &.info-grid-2col { grid-template-columns: auto 1fr auto 1fr; }
- &.info-grid-3col { grid-template-columns: auto 1fr auto 1fr auto 1fr; }
- .info-item { display: contents;
- .label { color: #86909c; min-width: 80px; text-align: left; padding-right: 12px; line-height: 24px; white-space: nowrap; font-size: 12px; }
- .value { color: #1d2129; line-height: 24px; min-width: 120px; word-break: break-all; font-size: 12px; }
- }
- .status-text { color: #1d2129; }
- .grade-text { font-weight: 500; color: #1d2129; }
- .link-text { color: #165dff; cursor: pointer; }
- .wrap-text { white-space: pre-wrap; line-height: 1.6; }
- > .info-item.full-row { display: flex; grid-column: 1/-1; .label { min-width: 80px; } .value { flex: 1; font-size: 16px; color: #1d2129; } }
- > .info-item.left-only { display: contents; .label { grid-column: 1; } .value { grid-column: 2; } }
- > .info-item.full-row-3col { display: flex; grid-column: 1/-1; .label { min-width: 80px; } .value { flex: 1; } }
- > .info-item.empty-cell { display: contents; &::before, &::after { content: ''; } }
- }
- .side-content {
- padding-bottom: 20px;
- }
- .team-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 15px;
- font-size: 14px;
- font-weight: bold;
- color: #333;
-
- .add-icon {
- cursor: pointer;
- color: #86909C;
- font-size: 20px;
- &:hover {
- color: #00B881;
- }
- }
- }
- .search-box {
- margin-bottom: 20px;
- :deep(.el-input__wrapper) {
- background-color: #fff;
- box-shadow: 0 0 0 1px #E5E6EB inset;
- border-radius: 2px;
- height: 32px;
-
- &.is-focus {
- box-shadow: 0 0 0 1px #00B881 inset;
- }
- .el-input__inner {
- font-size: 13px;
- &::placeholder {
- color: #C9CDD4;
- }
- }
- }
- }
- .team-list {
- display: flex;
- flex-direction: column;
-
- .team-item {
- display: flex;
- align-items: center;
- background: transparent;
- padding: 12px 0;
- transition: all 0.2s;
- border-bottom: 1px solid #f2f3f5;
-
- &:last-child {
- border-bottom: none;
- }
-
- .member-info {
- flex: 1;
- margin-left: 12px;
- display: flex;
- flex-direction: column;
-
- .info-top {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 8px;
-
- .member-name {
- font-size: 14px;
- font-weight: 500;
- color: #1d2129;
- }
-
- .leader-tag {
- font-size: 12px;
- height: 20px;
- line-height: 18px;
- padding: 0 6px;
- color: #00B42A;
- background-color: #fff;
- border: 1px solid #00B42A;
- border-radius: 2px;
- margin-right: 4px;
- }
-
- .role-desc, .permission-desc {
- font-size: 12px;
- color: #86909C;
- }
- }
- }
-
- .member-actions {
- display: flex;
- gap: 12px;
- .el-button {
- padding: 0;
- font-size: 12px;
- &.btn-edit {
- color: #FF7D00;
- }
- &.btn-delete {
- color: #86909C;
- }
-
- &:hover { opacity: 0.8; }
- }
- }
- }
- }
- .empty-data {
- text-align: center;
- color: #86909C;
- padding: 30px 0;
- font-size: 13px;
- }
- .edit-form-container {
- flex: 1;
- display: flex;
- flex-direction: column;
- background-color: #fff;
- overflow: hidden;
- .el-form {
- flex: 1;
- overflow-y: auto;
- padding: 20px 24px;
- &::-webkit-scrollbar { width: 4px; }
- &::-webkit-scrollbar-thumb { background-color: #e5e6eb; border-radius: 2px; }
- }
- }
- .form-section{margin-bottom:20px;}
- .form-section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;.section-title{font-size:14px;font-weight:500;color:#165dff;}.upload-link{font-size:13px;color:#165dff;cursor:pointer;display:flex;align-items:center;gap:4px;&:hover{opacity:0.8;}}}
- .form-grid{&.form-grid-3col{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 28px;>.el-form-item{margin-bottom:14px;}}}
- .form-item{display:flex;align-items:center;gap:8px;width:100%;
- .field-label{white-space:nowrap;font-size:13px;color:#1d2129;min-width:fit-content;line-height:32px;.required-star{color:#f53f3f;margin-right:2px;}}
- :deep(.el-input),:deep(.el-select){flex:1;}
- :deep(.el-input__wrapper){box-shadow:0 0 0 1px#c9cdd4 inset;border-radius:4px;&:hover{box-shadow:0 0 0 1px#86909c inset;}&.is-focus{box-shadow:0 0 0 1px#165dff inset;}}
- :deep(.el-textarea__inner){box-shadow:0 0 0 1px#c9cdd4 inset;border-radius:4px;&:hover{box-shadow:0 0 0 1px#86909c inset;}&:focus{box-shadow:0 0 0 1px#165dff inset;}}
- :deep(.el-select){--el-border-color:#c9cdd4;--el-hover-border-color:#86909c;}
- :deep(.el-date-editor.el-input){width:100%;}
- .has-unit{display:flex;align-items:center;flex:1;gap:4px;:deep(.el-input){flex:1;}.unit-append{font-size:13px;color:#86909c;white-space:nowrap;line-height:32px;padding-left:2px;}}
- }
- .form-full-row{grid-column:span 3;}
- .form-full-item{display:flex;align-items:center;gap:8px;}
- .form-item-vertical{align-items:flex-start!important;.field-label{line-height:34px;}:deep(.el-textarea){flex:1;}:deep(.el-input__count){bottom:2px;right:10px;}}
- .edit-drawer-footer{flex-shrink:0;display:flex;justify-content:flex-end;align-items:center;gap:12px;padding:12px 24px;background-color:#fff;border-top:1px solid#f0f0f0;.el-button{height:30px;padding:0 18px;font-size:13px;border-radius:4px;}}
- .attachment-table{width:100%;:deep(th.el-table__cell){background-color:#f8fafc!important;font-size:13px;font-weight:500;color:#475569;}:deep(td.el-table__cell){font-size:13px;}}
- .mt-24{margin-top:24px;}
- .tab-contact,.tab-analysis,.tab-files,.tab-quotes{height:100%;}
- .tab-empty{text-align:center;padding:60px 0;color:#c9cdd4;font-size:13px;}
- .tab-contact{.add-contact-btn{margin-bottom:12px;background-color:#165dff;border-color:#165dff;border-radius:4px;}.contact-table{:deep(th.el-table__cell){background-color:#f8fafc!important;color:#475569;font-weight:600;font-size:13px;}:deep(td.el-table__cell){font-size:13px;}:deep(.el-table__empty-text){display:none;}}}
- .tab-analysis{.analysis-form{padding:4px 0;.form-field{margin-bottom:24px;&:last-child{margin-bottom:0;}.field-label{font-size:13px;color:#86909c;margin-bottom:8px;line-height:1.5;}:deep(.el-radio-group){.el-radio{color:#4e5969;font-size:13px;margin-right:28px;}}:deep(.el-textarea__inner){border-radius:4px;box-shadow:0 0 0 1px#c9cdd4 inset;padding:8px 12px;font-size:13px;line-height:22px;color:#4e5969;&::placeholder{color:#c9cdd4;}&:hover{box-shadow:0 0 0 1px#86909c inset;}&:focus{box-shadow:0 0 0 1px#165dff inset;}}:deep(.el-input__count){bottom:6px;right:10px;font-size:12px;color:#86909c;background:transparent;}}}}
- .tab-files {
- .file-upload-row { margin-bottom: 12px; }
- .file-table {
- :deep(th.el-table__cell) { background-color: #f8fafc !important; color: #475569; font-weight: 600; font-size: 13px; }
- :deep(td.el-table__cell) { font-size: 13px; }
- :deep(.el-table__empty-text) { display: none; }
- }
- }
- .tab-quotes {
- .quote-upload-row { margin-bottom: 12px; }
- .quote-table {
- :deep(th.el-table__cell) { background-color: #f8fafc !important; color: #475569; font-weight: 600; font-size: 13px; }
- :deep(td.el-table__cell) { font-size: 13px; }
- :deep(.el-table__empty-text) { display: none; }
- }
- }
- .progress-drawer{:deep(.el-drawer__body){padding:0!important;display:flex;flex-direction:column;}}
- .progress-drawer-header{height:48px;flex-shrink:0;display:flex;justify-content:space-between;align-items:center;padding:0 20px;border-bottom:1px solid#e5e6eb;background-color:#fff;.progress-drawer-title{font-size:15px;font-weight:500;color:#1d2129;}.progress-close-btn{font-size:16px;color:#86909c;cursor:pointer;transition:color 0.2s;&:hover{color:#f53f3f;}}}
- .progress-drawer-body{padding:24px 20px;flex:1;
- .progress-input-wrapper{:deep(.el-textarea__inner){border-radius:4px;box-shadow:0 0 0 1px#c9cdd4 inset;padding:8px 12px;font-size:13px;line-height:22px;color:#4e5969;&::placeholder{color:#c9cdd4;}&:hover{box-shadow:0 0 0 1px#86909c inset;}&:focus{box-shadow:0 0 0 1px#165dff inset;}}:deep(.el-input__count){bottom:6px;right:10px;font-size:12px;color:#86909c;background:transparent;}}
- .publish-btn-row{display:flex;justify-content:flex-end;margin-top:16px;}
- .publish-btn{background-color:#409eff;border-color:#409eff;border-radius:4px;height:32px;padding:0 20px;font-size:13px;&:hover,&:focus{background-color:#66b1ff;border-color:#66b1ff;}}
- .record-list{margin-top:24px;.record-item{background:#fff;border-radius:4px;padding:14px 18px;margin-bottom:12px;.record-header{display:flex;align-items:center;gap:16px;margin-bottom:8px;.record-user{font-size:14px;color:#333;}.record-time{font-size:13px;color:#999;}}.record-content{font-size:14px;color:#606266;line-height:1.6;}}.pagination-wrapper{display:flex;justify-content:center;margin-top:16px;}
- }
- }
- /* 跟进记录样式 */
- .record-op-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;
- .record-filter { display: flex; align-items: center; gap: 8px; .filter-label { font-size: 13px; color: #4e5969; } }
- }
- .record-list { max-height: 500px; overflow-y: auto; &::-webkit-scrollbar { width: 0; } }
- .record-card-item { padding: 16px; border: 1px solid #f2f3f5; border-radius: 4px; margin-bottom: 12px; transition: all 0.2s;
- &:hover { box-shadow: 0 4px 10px rgba(0,0,0,0.05); }
- .card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; .visit-type { font-size: 14px; font-weight: 500; color: #1d2129; } .visit-time { font-size: 12px; color: #86909c; } }
- .card-content { .content-row { display: flex; margin-bottom: 6px; font-size: 13px; line-height: 1.5; .label { color: #86909c; width: 70px; flex-shrink: 0; } .value { color: #4e5969; word-break: break-all; } &:last-child { margin-bottom: 0; } } }
- }
- .follow-up-dialog { :deep(.el-dialog__header) { border-bottom: 1px solid #f2f3f5; margin-right: 0; padding-bottom: 16px; } :deep(.el-dialog__title) { font-size: 16px; font-weight: 500; } }
- .upload-placeholder { width: 80px; height: 80px; border: 1px dashed #c9cdd4; border-radius: 4px; display: flex; justify-content: center; align-items: center; color: #86909c; font-size: 20px; cursor: pointer; &:hover { border-color: #165dff; color: #165dff; } }
- /* 联系人抽屉样式 */
- .drawer-header-standard {
- height: 60px;
- padding: 0 24px;
- background: #fff;
- border-bottom: 1px solid #f1f5f9;
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-shrink: 0;
- .header-title-text {
- font-size: 18px;
- font-weight: 600;
- color: #1e293b;
- }
-
- .close-btn {
- font-size: 20px;
- color: #94a3b8;
- cursor: pointer;
- &:hover { color: #ef4444; }
- }
- }
- .drawer-body-standard {
- flex: 1;
- padding: 24px;
- background-color: #f8fafc;
- overflow-y: auto;
- .section-title {
- color: #409eff;
- font-size: 15px;
- padding: 12px 16px;
- margin: 0 -24px 20px -24px;
- background: #f5f7fa;
- }
- }
- .drawer-footer-standard {
- padding: 20px 24px;
- background: #fff;
- border-top: 1px solid #f1f5f9;
- display: flex;
- justify-content: flex-end;
- gap: 16px;
- .btn-save {
- background-color: #409eff;
- border-color: #409eff;
- padding: 8px 24px;
- }
- .btn-cancel {
- padding: 8px 24px;
- color: #64748b;
- }
- }
- .project-contact-drawer {
- :deep(.el-drawer__body) {
- padding: 0;
- display: flex;
- flex-direction: column;
- }
-
- :deep(.el-form-item) {
- margin-bottom: 18px;
- }
-
- :deep(.el-form-item__label) {
- color: #475569;
- }
- }
- </style>
|