| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367 |
- <template>
- <div class="editor-page">
- <!-- 主体 -->
- <div class="editor-body">
- <!-- 左侧面板 -->
- <div class="left-panel" :style="{ width: leftPanelWidth + 'px' }">
- <!-- Tab 切换 -->
- <div class="panel-tabs">
- <div
- class="panel-tab"
- :class="{ active: leftPanelTab === 'reports' }"
- @click="leftPanelTab = 'reports'"
- >
- 📄 报告
- <span class="tab-count">{{ myReports?.length || 0 }}</span>
- </div>
- <transition name="tab-slide">
- <div
- v-if="hasActiveDocument"
- class="panel-tab"
- :class="{ active: leftPanelTab === 'files' }"
- @click="leftPanelTab = 'files'"
- >
- 📁 资源
- <span class="tab-count">{{ sourceFiles?.length || 0 }}</span>
- </div>
- </transition>
- <transition name="tab-slide">
- <div
- v-if="hasActiveDocument"
- class="panel-tab"
- :class="{ active: leftPanelTab === 'toc' }"
- @click="leftPanelTab = 'toc'"
- >
- 📑 目录
- <span class="tab-count">{{ tocItems?.length || 0 }}</span>
- </div>
- </transition>
- </div>
- <!-- 我的报告面板 -->
- <div class="panel-body reports-panel" v-show="leftPanelTab === 'reports'">
- <!-- 新建报告按钮(带下拉菜单) -->
- <el-popover
- placement="bottom-start"
- :width="240"
- trigger="click"
- popper-class="new-report-popover"
- >
- <template #reference>
- <el-button
- class="new-report-btn"
- type="primary"
- :icon="Plus"
- >
- 新建报告
- </el-button>
- </template>
- <div class="new-report-menu">
- <div class="menu-item" @click="handleCreateReport">
- <div class="menu-icon">📄</div>
- <div class="menu-content">
- <div class="menu-title">创建空白报告</div>
- <div class="menu-desc">从零开始创建新报告</div>
- </div>
- </div>
- <div class="menu-item" @click="handleUploadFile">
- <div class="menu-icon">📁</div>
- <div class="menu-content">
- <div class="menu-title">上传文件创建</div>
- <div class="menu-desc">上传 PDF/Word 自动解析</div>
- </div>
- </div>
- </div>
- </el-popover>
- <!-- 报告搜索 -->
- <el-input
- v-model="reportSearchKeyword"
- placeholder="搜索报告..."
- :prefix-icon="Search"
- clearable
- class="report-search"
- />
- <!-- 报告列表 -->
- <div class="report-list" v-if="filteredReports.length > 0">
- <div
- v-for="report in filteredReports"
- :key="report.id"
- class="report-item"
- :class="{ active: currentReportId === report.id }"
- @click="switchReport(report)"
- >
- <div class="report-icon">📄</div>
- <div class="report-info">
- <div class="report-name">{{ report.name }}</div>
- <div class="report-meta">
- <span class="report-time">{{ formatReportTime(report.updatedAt || report.createdAt) }}</span>
- <span class="report-status" :class="report.status">
- {{ getReportStatusText(report.status) }}
- </span>
- </div>
- </div>
- </div>
- </div>
-
- <!-- 空状态 -->
- <div class="report-empty" v-else-if="!loadingReports">
- <div class="empty-icon">📄</div>
- <div class="empty-text">{{ reportSearchKeyword ? '未找到匹配的报告' : '暂无报告' }}</div>
- <div class="empty-hint">点击上方按钮创建新报告</div>
- </div>
- <!-- 加载状态 -->
- <div class="report-loading" v-if="loadingReports">
- <el-icon class="is-loading"><Loading /></el-icon>
- <span>加载中...</span>
- </div>
- </div>
- <!-- 来源文件面板 -->
- <div class="panel-body" v-show="leftPanelTab === 'files'">
- <!-- 上传区 -->
- <el-upload
- class="upload-zone"
- drag
- action="/api/v1/parse/upload"
- :on-success="handleFileUpload"
- :show-file-list="false"
- >
- <div class="upload-content">
- <div class="upload-icon">📄</div>
- <div class="upload-text">拖拽或点击上传</div>
- <div class="upload-hint">支持 PDF / Word / Excel</div>
- </div>
- </el-upload>
- <!-- 来源文件列表 -->
- <div class="file-list">
- <div
- v-for="file in sourceFiles"
- :key="file.id"
- class="file-item"
- :class="{ active: selectedFile?.id === file.id }"
- @click="selectFile(file)"
- >
- <span class="file-icon">{{ getFileIcon(file) }}</span>
- <div class="file-info">
- <div class="file-name">{{ file.alias }}</div>
- <div class="file-meta">
- <span v-if="file.required" class="required">必需</span>
- <span v-else>可选</span>
- </div>
- </div>
- <el-button
- size="small"
- :icon="Delete"
- circle
- @click.stop="removeSourceFile(file)"
- />
- </div>
- </div>
- <!-- 添加来源文件定义 -->
- <el-button
- class="add-source-btn"
- :icon="Plus"
- @click="showAddSourceDialog = true"
- >
- 添加来源文件定义
- </el-button>
- </div>
- <!-- 目录面板 -->
- <div class="panel-body toc-panel" v-show="leftPanelTab === 'toc'">
- <div class="toc-list" v-if="tocItems && tocItems.length > 0">
- <div
- v-for="(item, index) in tocItems"
- :key="item.index || index"
- class="toc-item"
- :class="['toc-level-' + item.level]"
- @click="scrollToHeading(item)"
- >
- <span class="toc-bullet">{{ getTocBullet(item.level) }}</span>
- <span class="toc-text">{{ item.title || item.text }}</span>
- </div>
- </div>
- <div class="toc-empty" v-else>
- <div class="empty-icon">📑</div>
- <div class="empty-text">暂无目录</div>
- <div class="empty-hint">文档解析后将自动生成目录</div>
- </div>
- </div>
- </div>
- <!-- 左侧拖拽分隔条 -->
- <div
- class="resize-handle left-resize"
- @mousedown="startResizeLeft"
- ></div>
- <!-- 中间编辑区 -->
- <div class="center-panel">
- <!-- 欢迎页:没有选中文档时显示 -->
- <div class="welcome-page" v-if="!hasActiveDocument">
- <div class="welcome-content">
- <div class="welcome-logo">灵</div>
- <div class="welcome">
- <h1>{{ greetingText }},{{ userName }}!<span>智能报告,洞察未来。</span></h1>
- <p>今天是个创作的好日子,开始您的智能报告之旅吧</p>
- </div>
- </div>
- </div>
- <!-- 编辑器:有选中文档时显示 -->
- <template v-else>
- <div class="editor-title-bar">
- <!-- 左侧:标题和保存状态 -->
- <div class="title-section">
- <div class="title-input-wrapper" :style="{ width: titleInputWidth + 'px' }">
- <el-input
- v-model="reportTitle"
- class="title-input"
- placeholder="请输入报告标题"
- />
- <span class="title-measure" ref="titleMeasure">{{ reportTitle || '请输入报告标题' }}</span>
- </div>
- <span class="save-status" v-if="saved">✓ 已保存</span>
- </div>
-
- <!-- 中间:视图切换 -->
- <div class="view-toggle">
- <el-radio-group v-model="viewMode" size="small">
- <el-radio-button value="edit">📝 编辑</el-radio-button>
- <el-radio-button value="preview">👁 预览</el-radio-button>
- </el-radio-group>
- </div>
-
- <!-- 右侧:操作按钮 -->
- <div class="toolbar-actions">
- <el-button
- size="small"
- :icon="Refresh"
- :loading="regenerating"
- @click="handleRegenerateBlocks"
- title="重新生成文档结构"
- >
- 重新生成
- </el-button>
- <el-button size="small" :icon="Clock" title="版本历史">版本</el-button>
- <el-button size="small" :icon="Share" circle @click="showGraphModal = true" title="知识图谱" />
- <el-divider direction="vertical" />
- <el-button size="small" type="primary" :icon="Check" @click="handleSave">保存</el-button>
- </div>
- </div>
- <div class="editor-scroll" ref="editorRef">
- <div
- class="editor-content"
- contenteditable="true"
- @contextmenu.prevent="handleContextMenu"
- ref="editorContentRef"
- />
- </div>
- </template>
- </div>
- <!-- 右侧拖拽分隔条:仅在选中文档时显示 -->
- <div
- v-if="hasActiveDocument"
- class="resize-handle right-resize"
- @mousedown="startResizeRight"
- ></div>
- <!-- 右侧要素面板:仅在选中文档时显示 -->
- <div v-if="hasActiveDocument" class="right-panel" :style="{ width: rightPanelWidth + 'px' }">
- <!-- 要素管理(展示文档中识别的实体) -->
- <div class="element-section">
- <div class="element-header">
- <span class="element-title">
- 🏷️ 要素管理
- <span class="element-count">({{ allFilteredEntities?.length || 0 }}/{{ entities?.length || 0 }})</span>
- </span>
- <el-button size="small" :icon="Plus" @click="showAddVariableDialog = true">
- 添加
- </el-button>
- </div>
- <!-- 搜索和筛选 -->
- <div class="element-filter" v-if="entities && entities.length > 0">
- <el-input
- v-model="entitySearchKeyword"
- placeholder="搜索要素..."
- size="small"
- :prefix-icon="Search"
- clearable
- class="entity-search"
- />
- <div class="entity-type-filter">
- <el-tag
- v-for="(count, type) in entityTypeCounts"
- :key="type"
- :class="['filter-tag', { active: entityTypeFilter === type }]"
- size="small"
- @click="toggleEntityTypeFilter(type)"
- >
- {{ getEntityTypeIcon(type) }} {{ getEntityTypeName(type) }} ({{ count }})
- </el-tag>
- <el-tag
- v-if="entityTypeFilter"
- class="filter-tag clear"
- size="small"
- @click="entityTypeFilter = ''"
- >
- 清除筛选
- </el-tag>
- </div>
- </div>
- <div class="element-body">
- <div class="element-tags-wrap" v-if="filteredEntities && filteredEntities.length > 0">
- <div
- v-for="entity in filteredEntities"
- :key="entity.id"
- class="var-tag"
- :class="[getEntityTypeClass(entity.type), { confirmed: entity.confirmed }]"
- :title="`${getEntityTypeName(entity.type)}: ${entity.text}`"
- @click="scrollToEntity(entity.id)"
- @dblclick="openEntityEditModal(entity)"
- >
- <span class="tag-icon">{{ getEntityTypeIcon(entity.type) }}</span>
- <span class="tag-name">{{ entity.text }}</span>
- <span class="tag-status" v-if="entity.confirmed">✓</span>
- </div>
- </div>
- <!-- 加载更多按钮 -->
- <div class="load-more-wrap" v-if="hasMoreEntities">
- <el-button
- size="small"
- text
- @click="loadMoreEntities"
- >
- 加载更多 (还有 {{ remainingEntitiesCount }} 个)
- </el-button>
- </div>
- <div class="element-hint" v-if="!entities || entities.length === 0">
- 选中文本后右键标记为实体
- </div>
- <div class="element-hint" v-else-if="(!filteredEntities || filteredEntities.length === 0) && !hasMoreEntities">
- 没有匹配的要素
- </div>
- </div>
- </div>
- <!-- 按类别分组显示 -->
- <div class="category-section" v-for="(vars, category) in groupedVariables" :key="category">
- <div class="category-header">
- <span
- class="category-dot"
- :style="{ background: getCategoryColor(category) }"
- />
- <span>{{ getCategoryLabel(category) }}</span>
- <span class="category-count">{{ vars.length }}</span>
- </div>
- <div class="category-items">
- <div
- v-for="v in vars"
- :key="v.id"
- class="category-item"
- @click="editVariable(v)"
- >
- <span>{{ v.displayName }}</span>
- <span class="item-value">{{ v.exampleValue || '-' }}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 右键菜单 -->
- <div
- v-show="contextMenuVisible"
- class="context-menu"
- :style="{ left: contextMenuPos.x + 'px', top: contextMenuPos.y + 'px' }"
- >
- <div class="context-menu-header" v-if="selectedText">
- <span class="selected-preview">"{{ selectedText.slice(0, 20) }}{{ selectedText.length > 20 ? '...' : '' }}"</span>
- </div>
- <div class="context-menu-section">标记为实体</div>
- <div class="context-menu-item" @click="markAsVariable('person')" :disabled="markingEntity">
- <span class="icon">👤</span>
- <span>人物</span>
- </div>
- <div class="context-menu-item" @click="markAsVariable('org')" :disabled="markingEntity">
- <span class="icon">🏢</span>
- <span>组织机构</span>
- </div>
- <div class="context-menu-item" @click="markAsVariable('location')" :disabled="markingEntity">
- <span class="icon">📍</span>
- <span>地点</span>
- </div>
- <div class="context-menu-item" @click="markAsVariable('date')" :disabled="markingEntity">
- <span class="icon">📅</span>
- <span>日期/时间</span>
- </div>
- <div class="context-menu-item" @click="markAsVariable('data')" :disabled="markingEntity">
- <span class="icon">📊</span>
- <span>数据/指标</span>
- </div>
- <div class="context-menu-item" @click="markAsVariable('concept')" :disabled="markingEntity">
- <span class="icon">💡</span>
- <span>概念/技术</span>
- </div>
- <div class="context-menu-item" @click="markAsVariable('entity')" :disabled="markingEntity">
- <span class="icon">🏷️</span>
- <span>其他实体</span>
- </div>
- <div class="context-menu-loading" v-if="markingEntity">
- <el-icon class="is-loading"><Loading /></el-icon>
- <span>标记中...</span>
- </div>
- </div>
- <!-- 添加来源文件对话框 -->
- <el-dialog v-model="showAddSourceDialog" title="添加来源文件定义" width="400">
- <el-form :model="newSourceFile" label-width="80px">
- <el-form-item label="文件别名" required>
- <el-input v-model="newSourceFile.alias" placeholder="如:可研批复" />
- </el-form-item>
- <el-form-item label="描述">
- <el-input v-model="newSourceFile.description" placeholder="文件描述" />
- </el-form-item>
- <el-form-item label="是否必需">
- <el-switch v-model="newSourceFile.required" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="showAddSourceDialog = false">取消</el-button>
- <el-button type="primary" @click="addSourceFile">添加</el-button>
- </template>
- </el-dialog>
- <!-- 添加/编辑变量对话框 -->
- <el-dialog v-model="showVariableDialog" :title="editingVariable ? '编辑变量' : '添加变量'" width="500">
- <el-form :model="variableForm" label-width="100px">
- <el-form-item label="变量名" required>
- <el-input v-model="variableForm.name" placeholder="程序用名,如 project_name" />
- </el-form-item>
- <el-form-item label="显示名称" required>
- <el-input v-model="variableForm.displayName" placeholder="用户可见名称" />
- </el-form-item>
- <el-form-item label="类别">
- <el-select v-model="variableForm.category" style="width: 100%">
- <el-option label="核心实体" value="entity" />
- <el-option label="概念/技术" value="concept" />
- <el-option label="数据/指标" value="data" />
- <el-option label="地点/组织" value="location" />
- <el-option label="资源模板" value="asset" />
- </el-select>
- </el-form-item>
- <el-form-item label="示例值">
- <el-input v-model="variableForm.exampleValue" placeholder="文档中的原始值" />
- </el-form-item>
- <el-form-item label="来源类型">
- <el-select v-model="variableForm.sourceType" style="width: 100%">
- <el-option label="从来源文件提取" value="document" />
- <el-option label="手动输入" value="manual" />
- <el-option label="引用其他变量" value="reference" />
- <el-option label="固定值" value="fixed" />
- </el-select>
- </el-form-item>
- <el-form-item label="来源文件" v-if="variableForm.sourceType === 'document'">
- <el-select v-model="variableForm.sourceFileAlias" style="width: 100%">
- <el-option
- v-for="sf in sourceFiles"
- :key="sf.id"
- :label="sf.alias"
- :value="sf.alias"
- />
- </el-select>
- </el-form-item>
- <el-form-item label="提取方式" v-if="variableForm.sourceType === 'document'">
- <el-select v-model="variableForm.extractType" style="width: 100%">
- <el-option label="直接提取" value="direct" />
- <el-option label="AI 字段提取" value="ai_extract" />
- <el-option label="AI 总结" value="ai_summarize" />
- </el-select>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="showVariableDialog = false">取消</el-button>
- <el-button
- v-if="editingVariable"
- type="danger"
- @click="deleteVariable"
- >
- 删除
- </el-button>
- <el-button type="primary" @click="saveVariable">保存</el-button>
- </template>
- </el-dialog>
- <!-- 知识图谱弹窗 -->
- <el-dialog v-model="showGraphModal" title="🔗 知识图谱" width="900">
- <div class="graph-container">
- <div class="graph-legend">
- <div class="legend-title">图例</div>
- <div class="legend-item">
- <span class="legend-dot entity"></span>
- <span>核心实体</span>
- </div>
- <div class="legend-item">
- <span class="legend-dot concept"></span>
- <span>概念/技术</span>
- </div>
- <div class="legend-item">
- <span class="legend-dot data"></span>
- <span>数据/指标</span>
- </div>
- <div class="legend-item">
- <span class="legend-dot location"></span>
- <span>地点/组织</span>
- </div>
- </div>
- <div class="graph-body">
- <div class="graph-placeholder">
- <el-icon size="64" color="#ccc"><Connection /></el-icon>
- <p>知识图谱可视化(开发中)</p>
- </div>
- </div>
- </div>
- </el-dialog>
- <!-- 实体编辑弹窗 -->
- <el-dialog v-model="showEntityEditModal" title="编辑实体" width="400">
- <div class="entity-edit-form" v-if="editingEntity">
- <div class="entity-edit-preview">
- <span class="preview-icon">{{ getEntityTypeIcon(editingEntity.type) }}</span>
- <span class="preview-text">"{{ editingEntity.text }}"</span>
- </div>
-
- <el-form label-width="80px">
- <el-form-item label="实体类型">
- <el-select v-model="editingEntity.type" style="width: 100%">
- <el-option label="👤 人物" value="PERSON" />
- <el-option label="🏢 组织机构" value="ORGANIZATION" />
- <el-option label="📍 地点" value="LOCATION" />
- <el-option label="📅 日期/时间" value="DATE" />
- <el-option label="📊 数据/指标" value="DATA" />
- <el-option label="💡 概念/技术" value="CONCEPT" />
- <el-option label="🏷️ 其他实体" value="ENTITY" />
- </el-select>
- </el-form-item>
- <el-form-item label="状态">
- <el-tag :type="editingEntity.confirmed ? 'success' : 'info'">
- {{ editingEntity.confirmed ? '已确认' : '待确认' }}
- </el-tag>
- </el-form-item>
- </el-form>
- </div>
- <template #footer>
- <el-button @click="showEntityEditModal = false">取消</el-button>
- <el-button
- type="primary"
- v-if="editingEntity && !editingEntity.confirmed"
- @click="confirmEditingEntity"
- >
- 确认实体
- </el-button>
- <el-button type="danger" @click="deleteEditingEntity">删除</el-button>
- <el-button type="primary" @click="saveEditingEntity">保存</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import { ref, reactive, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
- import { useRouter, useRoute } from 'vue-router'
- import {
- ArrowLeft, Clock, Share, Check, Plus, Delete, Connection, Refresh, Search, Loading
- } from '@element-plus/icons-vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { useTemplateStore } from '@/stores/template'
- import { documentApi, templateApi } from '@/api'
- const router = useRouter()
- const route = useRoute()
- const templateStore = useTemplateStore()
- // 从 URL 查询参数获取文档ID
- const currentDocumentId = ref(route.query.doc || null)
- // 当前选中的报告ID(用于标记左侧列表的选中状态)
- const currentReportId = ref(null)
- // 是否有当前激活的文档
- const hasActiveDocument = computed(() => !!currentDocumentId.value)
- // 欢迎页问候语
- const userName = computed(() => localStorage.getItem('username') || '用户')
- const greetingText = computed(() => {
- const hour = new Date().getHours()
- if (hour < 6) return '凌晨好'
- if (hour < 9) return '早上好'
- if (hour < 12) return '上午好'
- if (hour < 14) return '中午好'
- if (hour < 18) return '下午好'
- if (hour < 22) return '晚上好'
- return '夜深了'
- })
- const reportTitle = ref('')
- const titleMeasure = ref(null)
- const titleInputWidth = ref(120)
- const viewMode = ref('edit')
- const saved = ref(true)
- const editorRef = ref(null)
- const editorContentRef = ref(null)
- const loading = ref(false)
- const regenerating = ref(false)
- // 面板宽度(可拖拽调整)
- const leftPanelWidth = ref(300)
- const rightPanelWidth = ref(380)
- const isResizing = ref(false)
- const resizeType = ref('') // 'left' or 'right'
- // 开始拖拽左侧分隔条
- function startResizeLeft(e) {
- isResizing.value = true
- resizeType.value = 'left'
- document.addEventListener('mousemove', handleResize)
- document.addEventListener('mouseup', stopResize)
- document.body.style.cursor = 'col-resize'
- document.body.style.userSelect = 'none'
- }
- // 开始拖拽右侧分隔条
- function startResizeRight(e) {
- isResizing.value = true
- resizeType.value = 'right'
- document.addEventListener('mousemove', handleResize)
- document.addEventListener('mouseup', stopResize)
- document.body.style.cursor = 'col-resize'
- document.body.style.userSelect = 'none'
- }
- // 处理拖拽
- function handleResize(e) {
- if (!isResizing.value) return
-
- if (resizeType.value === 'left') {
- const newWidth = e.clientX
- leftPanelWidth.value = Math.max(240, Math.min(500, newWidth))
- } else if (resizeType.value === 'right') {
- const newWidth = window.innerWidth - e.clientX
- rightPanelWidth.value = Math.max(280, Math.min(500, newWidth))
- }
- }
- // 停止拖拽
- function stopResize() {
- isResizing.value = false
- resizeType.value = ''
- document.removeEventListener('mousemove', handleResize)
- document.removeEventListener('mouseup', stopResize)
- document.body.style.cursor = ''
- document.body.style.userSelect = ''
- }
- // 动态计算标题输入框宽度
- watch(reportTitle, () => {
- nextTick(() => {
- if (titleMeasure.value) {
- const measuredWidth = titleMeasure.value.offsetWidth + 30 // 额外边距
- titleInputWidth.value = Math.max(120, Math.min(400, measuredWidth))
- }
- })
- })
- // 左侧面板 Tab(默认显示报告列表)
- const leftPanelTab = ref('reports')
- // 我的报告列表
- const myReports = ref([])
- const loadingReports = ref(false)
- const reportSearchKeyword = ref('')
- // 筛选后的报告列表
- const filteredReports = computed(() => {
- if (!reportSearchKeyword.value) {
- return myReports.value
- }
- const keyword = reportSearchKeyword.value.toLowerCase()
- return myReports.value.filter(r =>
- r.name?.toLowerCase().includes(keyword)
- )
- })
- // 加载我的报告列表
- async function loadMyReports() {
- loadingReports.value = true
- try {
- const data = await templateApi.list(1, 50)
- myReports.value = data.records || data || []
- } catch (error) {
- console.warn('获取报告列表失败:', error)
- myReports.value = []
- } finally {
- loadingReports.value = false
- }
- }
- // 切换报告
- async function switchReport(report) {
- // 如果点击的是当前已选中的报告,则取消选中
- if (currentDocumentId.value && currentDocumentId.value === report.baseDocumentId) {
- unselectReport()
- return
- }
-
- // 如果是当前选中的空白报告(无 baseDocumentId),也取消选中
- if (!report.baseDocumentId && currentReportId.value === report.id) {
- unselectReport()
- return
- }
-
- // 更新当前报告ID(用于标记选中状态)
- currentReportId.value = report.id
-
- if (!report.baseDocumentId) {
- // 没有关联文档,进入空白编辑页面
- currentDocumentId.value = 'empty-' + report.id // 使用特殊标识表示空白文档
- reportTitle.value = report.name || '未命名报告'
-
- // 清空文档内容,显示空白提示
- blocks.value = []
- tocItems.value = []
- documentContent.value = emptyPlaceholder
- entities.value = []
- sourceFiles.value = []
- return
- }
-
- // 更新 URL(不刷新页面)
- const newUrl = new URL(window.location.href)
- newUrl.searchParams.set('doc', report.baseDocumentId)
- window.history.replaceState({}, '', newUrl.toString())
-
- // 更新当前文档ID
- currentDocumentId.value = report.baseDocumentId
-
- // 加载新文档
- await loadDocumentById(report.baseDocumentId)
-
- // 更新报告标题
- reportTitle.value = report.name || '未命名报告'
- }
- // 取消选中报告,回到初始状态
- function unselectReport() {
- // 清除 URL 参数
- const newUrl = new URL(window.location.href)
- newUrl.searchParams.delete('doc')
- window.history.replaceState({}, '', newUrl.toString())
-
- // 清除当前文档和报告
- currentDocumentId.value = null
- currentReportId.value = null
- reportTitle.value = '新建报告'
-
- // 清空文档内容
- blocks.value = []
- tocItems.value = []
- documentContent.value = emptyPlaceholder
- entities.value = []
- sourceFiles.value = []
-
- // 切换回报告 Tab
- leftPanelTab.value = 'reports'
- }
- // 根据文档ID加载文档
- async function loadDocumentById(documentId) {
- if (!documentId) return
-
- loading.value = true
- try {
- const [structuredDoc] = await Promise.all([
- documentApi.getStructured(documentId),
- loadToc(documentId)
- ])
-
- if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
- blocks.value = structuredDoc.blocks
- documentContent.value = renderStructuredDocument(structuredDoc)
- entities.value = extractEntitiesFromBlocks(structuredDoc.blocks)
- } else {
- blocks.value = []
- documentContent.value = emptyPlaceholder
- entities.value = []
- }
- } catch (error) {
- console.warn('加载文档失败:', error)
- blocks.value = []
- documentContent.value = emptyPlaceholder
- entities.value = []
- } finally {
- loading.value = false
- }
- }
- // 新建报告
- async function handleCreateReport() {
- try {
- const { value: reportName } = await ElMessageBox.prompt('请输入报告名称', '新建报告', {
- confirmButtonText: '创建',
- cancelButtonText: '取消',
- inputPattern: /\S+/,
- inputErrorMessage: '报告名称不能为空'
- })
-
- if (reportName) {
- const newReport = await templateApi.create({ name: reportName })
- ElMessage.success('报告创建成功')
- // 刷新报告列表
- await loadMyReports()
- // 如果新报告有文档,切换到该报告
- if (newReport && newReport.baseDocumentId) {
- await switchReport(newReport)
- }
- }
- } catch (error) {
- if (error !== 'cancel') {
- console.error('创建报告失败:', error)
- ElMessage.error('创建报告失败')
- }
- }
- }
- // 上传文件入口
- function handleUploadFile() {
- // 先选择一个报告,然后切换到资源 Tab
- if (myReports.value.length > 0) {
- leftPanelTab.value = 'reports'
- ElMessage.info('请先选择一个报告,然后在资源面板上传文件')
- } else {
- ElMessage.info('请先创建一个报告')
- }
- }
- // AI 辅助入口
- function handleAiAssist() {
- ElMessage.info('AI 辅助功能开发中...')
- }
- // 格式化报告时间
- function formatReportTime(dateStr) {
- if (!dateStr) return ''
- const date = new Date(dateStr)
- const now = new Date()
- const diffMs = now - date
- const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
-
- if (diffDays === 0) {
- return '今天'
- } else if (diffDays === 1) {
- return '昨天'
- } else if (diffDays < 7) {
- return `${diffDays}天前`
- } else {
- return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
- }
- }
- // 获取报告状态文本
- function getReportStatusText(status) {
- const statusMap = {
- 'draft': '草稿',
- 'published': '已发布',
- 'archived': '已归档',
- 'editing': '编辑中'
- }
- return statusMap[status] || '草稿'
- }
- // 来源文件(从 API 获取)
- const sourceFiles = ref([])
- const selectedFile = ref(null)
- const showAddSourceDialog = ref(false)
- const newSourceFile = reactive({
- alias: '',
- description: '',
- required: true
- })
- // 文档结构块(用于生成目录等)
- const blocks = ref([])
- // 目录数据(从 API 获取)
- const tocItems = ref([])
- // 加载文档目录
- async function loadToc(documentId) {
- try {
- const items = await documentApi.getToc(documentId)
- tocItems.value = items || []
- } catch (error) {
- console.warn('获取文档目录失败:', error)
- tocItems.value = []
- }
- }
- // 获取目录项的项目符号
- function getTocBullet(level) {
- const bullets = ['●', '○', '◦']
- return bullets[Math.min(level - 1, bullets.length - 1)]
- }
- // 规范化文本用于比较
- function normalizeForCompare(text) {
- if (!text) return ''
- return text
- .replace(/\s+/g, ' ') // 多个空格合并
- .replace(/[\u00A0\u202F\u2009]/g, ' ') // 特殊空格转普通空格
- .trim()
- }
- // 滚动到指定章节(只匹配 h1-h6 标题元素)
- function scrollToHeading(item) {
- const titleText = normalizeForCompare(item.title || item.text)
- if (!titleText) return
-
- // 在文档内容区域中查找标题元素
- const editorContent = document.querySelector('.editor-content')
- if (!editorContent) return
-
- // 查找 h1-h6 标题元素
- const headings = editorContent.querySelectorAll('h1, h2, h3, h4, h5, h6')
-
- for (const heading of headings) {
- const headingText = normalizeForCompare(heading.textContent)
- if (headingText === titleText) {
- heading.scrollIntoView({ behavior: 'smooth', block: 'start' })
- highlightElement(heading)
- return
- }
- }
-
- ElMessage.info(`章节「${titleText}」在当前文档中不存在`)
- }
- // 高亮元素
- function highlightElement(el) {
- el.classList.add('highlight-block')
- setTimeout(() => {
- el.classList.remove('highlight-block')
- }, 2000)
- }
- // 旧的滚动方法(用于 blockId)
- function scrollToBlock(blockId) {
- if (!blockId) return
-
- const blockEl = document.querySelector(`[data-block-id="${blockId}"]`)
- if (blockEl) {
- blockEl.scrollIntoView({ behavior: 'smooth', block: 'start' })
- // 高亮一下
- blockEl.classList.add('highlight-block')
- setTimeout(() => {
- blockEl.classList.remove('highlight-block')
- }, 2000)
- }
- }
- // 变量(从 API 获取)
- const variables = ref([])
- // 文档中的实体(从 blocks 的 elements 中提取)
- const entities = ref([])
- // 要素搜索和筛选
- const entitySearchKeyword = ref('')
- const entityTypeFilter = ref('')
- // 计算属性:按类型统计要素数量
- const entityTypeCounts = computed(() => {
- const counts = {}
- if (!entities.value || !Array.isArray(entities.value)) return counts
- entities.value.forEach(entity => {
- if (!entity) return
- const type = entity.type || 'default'
- counts[type] = (counts[type] || 0) + 1
- })
- return counts
- })
- // 实体显示数量限制(性能优化)
- const entityDisplayLimit = ref(50)
- const ENTITY_PAGE_SIZE = 50
- // 计算属性:筛选后的要素列表(全部)
- const allFilteredEntities = computed(() => {
- if (!entities.value || !Array.isArray(entities.value)) return []
-
- let result = entities.value.filter(e => e != null)
-
- // 按类型筛选
- if (entityTypeFilter.value) {
- result = result.filter(e => e.type === entityTypeFilter.value)
- }
-
- // 按关键词搜索
- if (entitySearchKeyword.value) {
- const keyword = entitySearchKeyword.value.toLowerCase()
- result = result.filter(e =>
- e.text?.toLowerCase()?.includes(keyword) ||
- e.type?.toLowerCase()?.includes(keyword)
- )
- }
-
- return result
- })
- // 计算属性:限制显示数量的要素列表
- const filteredEntities = computed(() => {
- const all = allFilteredEntities.value || []
- return all.slice(0, entityDisplayLimit.value)
- })
- // 是否还有更多实体可显示
- const hasMoreEntities = computed(() => {
- const all = allFilteredEntities.value || []
- return all.length > entityDisplayLimit.value
- })
- // 剩余未显示的实体数量
- const remainingEntitiesCount = computed(() => {
- const all = allFilteredEntities.value || []
- return Math.max(0, all.length - entityDisplayLimit.value)
- })
- // 加载更多实体
- function loadMoreEntities() {
- entityDisplayLimit.value += ENTITY_PAGE_SIZE
- }
- // 重置实体显示数量(筛选条件变化时)
- watch([entityTypeFilter, entitySearchKeyword], () => {
- entityDisplayLimit.value = ENTITY_PAGE_SIZE
- })
- // 切换类型筛选
- function toggleEntityTypeFilter(type) {
- if (entityTypeFilter.value === type) {
- entityTypeFilter.value = ''
- } else {
- entityTypeFilter.value = type
- }
- }
- // 获取实体类型名称(支持后端返回的英文类型)
- function getEntityTypeName(type) {
- const typeNames = {
- // 中文类型
- 'entity': '实体',
- 'concept': '概念',
- 'data': '数据',
- 'location': '地点',
- 'asset': '资产',
- 'person': '人物',
- 'org': '组织',
- 'date': '日期',
- 'product': '产品',
- 'event': '事件',
- 'law': '法规',
- 'default': '其他',
- // 后端返回的英文类型
- 'DOC_ID': '文档编号',
- 'ORG': '组织机构',
- 'ORGANIZATION': '组织机构',
- 'PERSON': '人物',
- 'LOCATION': '地点',
- 'LOC': '地点',
- 'DATE': '日期',
- 'TIME': '时间',
- 'MONEY': '金额',
- 'PERCENT': '百分比',
- 'PRODUCT': '产品',
- 'EVENT': '事件',
- 'LAW': '法规',
- 'WORK_OF_ART': '作品',
- 'LANGUAGE': '语言',
- 'NORP': '民族/宗教/政治团体',
- 'FAC': '设施',
- 'FACILITY': '设施',
- 'GPE': '地理政治实体',
- 'CARDINAL': '数量',
- 'ORDINAL': '序数',
- 'QUANTITY': '数量单位',
- 'TITLE': '职务/头衔',
- 'STANDARD': '标准规范',
- 'RATING': '评级',
- 'PERIOD': '时间段',
- 'SCORE': '评分',
- 'LEVEL': '等级',
- // 业务相关类型
- 'CERT': '证书/资质',
- 'NUMBER': '编号/数值',
- 'METHOD': '方法/流程',
- 'PROJECT': '项目',
- 'POLICY': '政策/制度',
- 'DEVICE': '设备',
- 'MATERIAL': '材料',
- 'TECHNOLOGY': '技术',
- 'REQUIREMENT': '要求',
- 'INDICATOR': '指标',
- 'RESULT': '结果',
- 'PROBLEM': '问题',
- 'SOLUTION': '解决方案',
- 'RISK': '风险',
- 'MEASURE': '措施',
- 'DEPARTMENT': '部门',
- 'ROLE': '角色',
- 'DOCUMENT': '文档',
- 'REGULATION': '法规',
- 'PROCEDURE': '程序',
- 'ACTIVITY': '活动',
- 'TASK': '任务',
- 'GOAL': '目标',
- 'RESOURCE': '资源',
- 'SYSTEM': '系统',
- 'AREA': '区域',
- 'EQUIPMENT': '设备',
- 'TOOL': '工具',
- 'SOFTWARE': '软件',
- 'DATA': '数据',
- 'RECORD': '记录',
- 'REPORT': '报告',
- 'PLAN': '计划',
- 'SCHEDULE': '日程',
- 'BUDGET': '预算',
- 'COST': '成本',
- 'UNIT': '单位',
- 'COMPANY': '公司',
- 'INSTITUTION': '机构'
- }
- const upperType = type?.toUpperCase()
- return typeNames[type] || typeNames[upperType] || type || '其他'
- }
- /**
- * 从结构化文档的 blocks 中提取所有实体
- */
- function extractEntitiesFromBlocks(blocks) {
- const entityList = []
- const entityMap = new Map() // 用于去重
-
- if (!blocks || !Array.isArray(blocks)) {
- return entityList
- }
-
- for (const block of blocks) {
- if (!block.elements || !Array.isArray(block.elements)) {
- continue
- }
-
- for (const element of block.elements) {
- if (element.type === 'entity' && element.entityId) {
- // 使用 entityId 去重
- if (!entityMap.has(element.entityId)) {
- entityMap.set(element.entityId, true)
- entityList.push({
- id: element.entityId,
- text: element.entityText || '',
- type: element.entityType || 'ENTITY',
- confirmed: element.confirmed || false
- })
- }
- }
- }
- }
-
- return entityList
- }
- // 加载模板数据
- onMounted(async () => {
- try {
- // 加载报告列表
- await loadMyReports()
-
- // 检查 URL 是否有指定文档ID
- const docId = route.query.doc
- if (docId) {
- // 只有 URL 明确指定了文档才加载
- currentDocumentId.value = docId
- await loadDocumentById(docId)
- // 找到对应的报告设置标题和ID
- const report = myReports.value.find(r => r.baseDocumentId === docId)
- if (report) {
- reportTitle.value = report.name || '未命名报告'
- currentReportId.value = report.id
- }
- } else {
- // 没有指定文档,显示欢迎页,不自动选中任何报告
- reportTitle.value = '新建报告'
- currentReportId.value = null
- documentContent.value = emptyPlaceholder
- }
- } catch (err) {
- console.error('初始化失败:', err)
- documentContent.value = emptyPlaceholder
- }
- })
- const showVariableDialog = ref(false)
- const showAddVariableDialog = ref(false)
- const editingVariable = ref(null)
- const variableForm = reactive({
- name: '',
- displayName: '',
- category: 'entity',
- exampleValue: '',
- sourceType: 'document',
- sourceFileAlias: '',
- extractType: 'direct'
- })
- // 右键菜单
- const contextMenuVisible = ref(false)
- const contextMenuPos = reactive({ x: 0, y: 0 })
- const selectedText = ref('')
- const selectionRange = ref(null)
- // 知识图谱
- const showGraphModal = ref(false)
- // 实体编辑弹窗
- const showEntityEditModal = ref(false)
- const editingEntity = ref(null)
- /**
- * 打开实体编辑弹窗
- */
- function openEntityEditModal(entity) {
- editingEntity.value = { ...entity }
- showEntityEditModal.value = true
- }
- /**
- * 确认编辑中的实体
- */
- async function confirmEditingEntity() {
- if (!editingEntity.value) return
- await handleConfirmEntity(editingEntity.value)
- editingEntity.value.confirmed = true
- }
- /**
- * 删除编辑中的实体
- */
- async function deleteEditingEntity() {
- if (!editingEntity.value) return
- await handleUnmarkEntity(editingEntity.value)
- showEntityEditModal.value = false
- editingEntity.value = null
- }
- /**
- * 保存编辑中的实体
- */
- async function saveEditingEntity() {
- if (!editingEntity.value) return
-
- // 查找原实体
- const idx = entities.value.findIndex(e => e.id === editingEntity.value.id)
- if (idx === -1) {
- showEntityEditModal.value = false
- return
- }
-
- const originalEntity = entities.value[idx]
-
- // 如果类型发生变化,调用更新API
- if (originalEntity.type !== editingEntity.value.type) {
- await handleUpdateEntityType(originalEntity, editingEntity.value.type)
- }
-
- showEntityEditModal.value = false
- editingEntity.value = null
- }
- /**
- * 全局函数:从文档内点击实体时调用
- * 注意:函数名与 ref 变量不同,避免冲突
- */
- window.handleEntityClick = function(event, entityId) {
- event.stopPropagation()
- const entity = entities.value.find(e => e.id === entityId)
- if (entity) {
- openEntityEditModal(entity)
- }
- }
- // 文档内容(从 API 获取或空白)
- const documentContent = ref('')
- // 监听文档内容变化,更新 DOM
- watch(documentContent, (newContent) => {
- nextTick(() => {
- if (editorContentRef.value) {
- editorContentRef.value.innerHTML = newContent || ''
- }
- })
- })
- // 空白模板时的占位提示 - V2 风格
- const emptyPlaceholder = `
- <div class="empty-editor-placeholder">
- <div class="empty-icon">📄</div>
- <h2>开始编辑您的报告</h2>
- <p class="empty-subtitle">当前报告还没有内容,您可以:</p>
- <div class="empty-actions">
- <div class="action-card">
- <span class="action-icon">📁</span>
- <span class="action-text">上传来源文件自动解析生成内容</span>
- </div>
- <div class="action-card">
- <span class="action-icon">✏️</span>
- <span class="action-text">直接在此处开始编辑</span>
- </div>
- <div class="action-card">
- <span class="action-icon">🤖</span>
- <span class="action-text">使用 AI 助手辅助撰写</span>
- </div>
- </div>
- <p class="empty-hint">💡 提示:选中文本后右键可将其标记为要素</p>
- </div>
- `
- /**
- * 渲染结构化文档(合并 blocks 和 images)
- * 根据 index 排序,将图片插入到正确的位置
- */
- function renderStructuredDocument(structuredDoc) {
- const blocks = structuredDoc.blocks || []
- const images = structuredDoc.images || []
- const tables = structuredDoc.tables || []
- const paragraphs = structuredDoc.paragraphs || []
-
- // 将所有元素合并
- const allElements = []
-
- // 从 blocks 中提取实体映射(按文本内容匹配)
- const entityMap = buildEntityMap(blocks)
-
- // 检查 paragraphs 是否有 runs(带格式信息)
- const hasParagraphsWithRuns = paragraphs.some(p => p.runs && p.runs.length > 0)
-
- if (hasParagraphsWithRuns) {
- // 使用 paragraphs 渲染(保留格式 + 合并实体高亮)
- paragraphs.forEach(para => {
- // 查找对应的块ID(通过 index 匹配)
- const matchingBlock = blocks.find(b => b.index === para.index)
- const blockId = matchingBlock?.id || matchingBlock?.blockId || `para_${para.index}`
- const content = renderParagraphWithRunsAndEntities(para, entityMap)
- allElements.push({
- type: 'paragraph',
- index: para.index,
- html: `<div data-block-id="${blockId}" class="doc-block">${content}</div>`
- })
- })
- } else if (blocks.length > 0) {
- // 回退到 blocks 渲染(带实体标记,但无格式)
- blocks.forEach(block => {
- if (block.type === 'page') return // 跳过根节点
- // 添加 data-block-id 属性以支持实体标记定位
- const blockId = block.id || block.blockId
- const content = block.markedHtml || block.html || block.plainText || ''
- allElements.push({
- type: 'block',
- index: block.index,
- html: `<div data-block-id="${blockId}" class="doc-block">${content}</div>`
- })
- })
- }
-
- // 添加图片(保持原始尺寸,不显示说明文字)
- images.forEach(img => {
- // 图片样式:保持原始尺寸,不强制居中
- const imgStyle = img.width && img.height
- ? `width:${img.width}px; height:${img.height}px;`
- : 'max-width: 100%; height: auto;'
-
- allElements.push({
- type: 'image',
- index: img.index,
- html: `<div class="doc-image" style="margin: 8px 0;">
- <img src="${img.url}" alt="${img.alt || '图片'}" style="${imgStyle}" />
- </div>`
- })
- })
-
- // 添加表格
- tables.forEach(table => {
- allElements.push({
- type: 'table',
- index: table.index,
- html: renderTable(table, entityMap)
- })
- })
-
- // 按 index 排序
- allElements.sort((a, b) => (a.index || 0) - (b.index || 0))
-
- // 合并 HTML
- return allElements.map(el => el.html).join('')
- }
- /**
- * 渲染表格
- */
- function renderTable(table, entityMap) {
- if (!table.rows || table.rows.length === 0) {
- return '<div class="doc-table-empty">空表格</div>'
- }
-
- let html = '<div class="doc-table-container"><table class="doc-table">'
-
- table.rows.forEach((row, rowIndex) => {
- html += '<tr>'
- row.forEach((cell, colIndex) => {
- const tag = rowIndex === 0 ? 'th' : 'td'
- const attrs = []
-
- if (cell.rowSpan && cell.rowSpan > 1) {
- attrs.push(`rowspan="${cell.rowSpan}"`)
- }
- if (cell.colSpan && cell.colSpan > 1) {
- attrs.push(`colspan="${cell.colSpan}"`)
- }
-
- // 单元格样式
- const styleAttrs = []
- if (cell.style) {
- if (cell.style.alignment) {
- const alignMap = { 'left': 'left', 'center': 'center', 'right': 'right', 'both': 'justify' }
- styleAttrs.push(`text-align:${alignMap[cell.style.alignment] || cell.style.alignment}`)
- }
- if (cell.style.backgroundColor) {
- styleAttrs.push(`background-color:#${cell.style.backgroundColor}`)
- }
- }
- if (styleAttrs.length > 0) {
- attrs.push(`style="${styleAttrs.join(';')}"`)
- }
-
- // 单元格内容(支持 runs 格式)
- let content = ''
- if (cell.runs && cell.runs.length > 0) {
- content = cell.runs.map(run => renderTextRunWithEntities(run, entityMap)).join('')
- } else {
- content = highlightEntitiesInText(cell.text || '', entityMap)
- }
-
- html += `<${tag} ${attrs.join(' ')}>${content}</${tag}>`
- })
- html += '</tr>'
- })
-
- html += '</table></div>'
- return html
- }
- /**
- * 从 blocks 中构建实体映射
- * 返回 { entityText: { entityId, entityType, confirmed } }
- */
- function buildEntityMap(blocks) {
- const entityMap = new Map()
-
- blocks.forEach(block => {
- if (!block.elements) return
-
- block.elements.forEach(el => {
- if (el.type === 'entity' && el.entityText) {
- // 使用实体文本作为 key(可能有多个相同文本的实体)
- if (!entityMap.has(el.entityText)) {
- entityMap.set(el.entityText, [])
- }
- entityMap.get(el.entityText).push({
- entityId: el.entityId,
- entityType: el.entityType,
- confirmed: el.confirmed
- })
- }
- })
- })
-
- return entityMap
- }
- /**
- * 渲染带格式和实体高亮的段落
- */
- function renderParagraphWithRunsAndEntities(para, entityMap) {
- if (!para.runs || para.runs.length === 0) {
- // 没有 runs,使用纯文本
- const content = highlightEntitiesInText(para.content || '', entityMap)
- return wrapWithParagraphTag(content, para.type, para.style)
- }
-
- // 渲染每个 run,同时应用实体高亮
- const runsHtml = para.runs.map(run => renderTextRunWithEntities(run, entityMap)).join('')
- return wrapWithParagraphTag(runsHtml, para.type, para.style)
- }
- /**
- * 渲染带格式的段落(使用 runs)- 保留兼容
- */
- function renderParagraphWithRuns(para) {
- if (!para.runs || para.runs.length === 0) {
- const content = escapeHtml(para.content || '').replace(/\n/g, '<br>')
- return wrapWithParagraphTag(content, para.type, para.style)
- }
-
- const runsHtml = para.runs.map(run => renderTextRun(run)).join('')
- return wrapWithParagraphTag(runsHtml, para.type, para.style)
- }
- /**
- * 渲染单个文本片段(Run)并高亮实体
- */
- function renderTextRunWithEntities(run, entityMap) {
- if (!run || !run.text) return ''
-
- // 先在文本中查找并高亮实体
- const highlightedText = highlightEntitiesInText(run.text, entityMap)
-
- // 如果文本被实体高亮处理过(包含 span 标签),需要特殊处理样式
- const hasEntityHighlight = highlightedText.includes('entity-highlight')
-
- // 构建样式
- const styles = buildRunStyles(run)
-
- // 如果没有样式,直接返回高亮后的文本
- if (styles.length === 0) {
- return highlightedText.replace(/\n/g, '<br>')
- }
-
- // 如果有实体高亮,需要用 span 包裹整体样式
- if (hasEntityHighlight) {
- return `<span style="${styles.join(';')}">${highlightedText.replace(/\n/g, '<br>')}</span>`
- }
-
- // 普通文本,处理换行并应用样式
- const text = escapeHtml(run.text).replace(/\n/g, '<br>')
-
- // 上下标特殊处理
- if (run.verticalAlign === 'superscript') {
- return `<sup style="${styles.join(';')}">${text}</sup>`
- } else if (run.verticalAlign === 'subscript') {
- return `<sub style="${styles.join(';')}">${text}</sub>`
- }
-
- return `<span style="${styles.join(';')}">${text}</span>`
- }
- /**
- * 在文本中查找并高亮实体
- */
- function highlightEntitiesInText(text, entityMap) {
- if (!text || !entityMap || entityMap.size === 0) {
- return escapeHtml(text || '')
- }
-
- // 按实体文本长度降序排序(优先匹配长的)
- const sortedEntities = Array.from(entityMap.keys()).sort((a, b) => b.length - a.length)
-
- let result = text
- const replacements = []
-
- // 找出所有需要替换的位置
- for (const entityText of sortedEntities) {
- const entities = entityMap.get(entityText)
- if (!entities || entities.length === 0) continue
-
- let searchStart = 0
- let entityIndex = 0
-
- while (true) {
- const pos = result.indexOf(entityText, searchStart)
- if (pos === -1) break
-
- // 获取对应的实体信息(循环使用)
- const entity = entities[entityIndex % entities.length]
-
- replacements.push({
- start: pos,
- end: pos + entityText.length,
- text: entityText,
- entity: entity
- })
-
- searchStart = pos + entityText.length
- entityIndex++
- }
- }
-
- // 按位置排序,从后往前替换(避免位置偏移)
- replacements.sort((a, b) => b.start - a.start)
-
- // 检查重叠,移除被包含的替换
- const finalReplacements = []
- for (const rep of replacements) {
- const hasOverlap = finalReplacements.some(
- existing => rep.start < existing.end && rep.end > existing.start
- )
- if (!hasOverlap) {
- finalReplacements.push(rep)
- }
- }
-
- // 执行替换
- for (const rep of finalReplacements) {
- const before = result.substring(0, rep.start)
- const after = result.substring(rep.end)
- const highlighted = renderEntityHighlight(rep.text, rep.entity)
- result = before + highlighted + after
- }
-
- // 对非实体部分进行 HTML 转义
- // 由于实体部分已经包含 HTML,需要分段处理
- return escapeNonEntityText(result)
- }
- /**
- * 转义非实体部分的文本
- */
- function escapeNonEntityText(text) {
- // 分割出实体标签和普通文本
- const parts = text.split(/(<span class="entity-highlight[^>]*>.*?<\/span>)/g)
-
- return parts.map(part => {
- if (part.startsWith('<span class="entity-highlight')) {
- return part // 保留实体标签
- }
- return escapeHtml(part) // 转义普通文本
- }).join('')
- }
- /**
- * 渲染实体高亮标签
- */
- function renderEntityHighlight(text, entity) {
- const cssClass = getEntityCssClass(entity.entityType)
- const confirmedMark = entity.confirmed ? ' ✓' : ''
-
- return `<span class="${cssClass}" ` +
- `data-entity-id="${entity.entityId || ''}" ` +
- `data-type="${entity.entityType || ''}" ` +
- `onclick="handleEntityClick(event,'${entity.entityId || ''}')" ` +
- `contenteditable="false">${escapeHtml(text)}${confirmedMark}</span>`
- }
- /**
- * 获取实体类型对应的 CSS 类
- */
- function getEntityCssClass(entityType) {
- const typeMap = {
- 'PERSON': 'entity-highlight person',
- 'ORG': 'entity-highlight org',
- 'ORGANIZATION': 'entity-highlight org',
- 'LOC': 'entity-highlight location',
- 'LOCATION': 'entity-highlight location',
- 'GPE': 'entity-highlight location',
- 'DATE': 'entity-highlight date',
- 'TIME': 'entity-highlight date',
- 'MONEY': 'entity-highlight data',
- 'NUMBER': 'entity-highlight data',
- 'PERCENT': 'entity-highlight data',
- 'DATA': 'entity-highlight data',
- 'CONCEPT': 'entity-highlight concept',
- 'PRODUCT': 'entity-highlight product',
- 'EVENT': 'entity-highlight event'
- }
- return typeMap[entityType?.toUpperCase()] || 'entity-highlight entity'
- }
- /**
- * 构建 Run 的 CSS 样式数组
- */
- function buildRunStyles(run) {
- const styles = []
-
- if (run.fontFamily) {
- styles.push(`font-family:${run.fontFamily}`)
- }
- if (run.fontSize && run.fontSize > 0) {
- styles.push(`font-size:${run.fontSize}pt`)
- }
- if (run.color) {
- const color = run.color.startsWith('#') ? run.color : `#${run.color}`
- styles.push(`color:${color}`)
- }
- if (run.highlightColor) {
- const bgColor = getHighlightColor(run.highlightColor)
- styles.push(`background-color:${bgColor}`)
- }
- if (run.bold) {
- styles.push('font-weight:bold')
- }
- if (run.italic) {
- styles.push('font-style:italic')
- }
-
- const textDecorations = []
- if (run.underline && run.underline !== 'none') {
- const underlineStyle = run.underline === 'double' ? 'double' :
- run.underline === 'wave' || run.underline === 'wavy' ? 'wavy' :
- run.underline === 'dotted' ? 'dotted' :
- run.underline === 'dashed' ? 'dashed' : 'solid'
- textDecorations.push(`underline ${underlineStyle}`)
- }
- if (run.strikeThrough) {
- textDecorations.push('line-through')
- }
- if (textDecorations.length > 0) {
- styles.push(`text-decoration:${textDecorations.join(' ')}`)
- }
-
- return styles
- }
- /**
- * 渲染单个文本片段(Run)- 保留兼容
- */
- function renderTextRun(run) {
- if (!run || !run.text) return ''
-
- // 转义 HTML 并将换行符转换为 <br>
- let text = escapeHtml(run.text).replace(/\n/g, '<br>')
- const styles = buildRunStyles(run)
-
- // 上下标
- if (run.verticalAlign === 'superscript') {
- return styles.length > 0 ? `<sup style="${styles.join(';')}">${text}</sup>` : `<sup>${text}</sup>`
- } else if (run.verticalAlign === 'subscript') {
- return styles.length > 0 ? `<sub style="${styles.join(';')}">${text}</sub>` : `<sub>${text}</sub>`
- }
-
- // 如果没有样式,直接返回文本
- if (styles.length === 0) {
- return text
- }
-
- return `<span style="${styles.join(';')}">${text}</span>`
- }
- /**
- * 获取高亮颜色对应的 CSS 颜色
- */
- function getHighlightColor(colorName) {
- const colors = {
- 'yellow': '#ffff00',
- 'green': '#00ff00',
- 'cyan': '#00ffff',
- 'magenta': '#ff00ff',
- 'blue': '#0000ff',
- 'red': '#ff0000',
- 'darkblue': '#000080',
- 'darkcyan': '#008080',
- 'darkgreen': '#008000',
- 'darkmagenta': '#800080',
- 'darkred': '#800000',
- 'darkyellow': '#808000',
- 'darkgray': '#808080',
- 'lightgray': '#c0c0c0',
- 'black': '#000000'
- }
- return colors[colorName.toLowerCase()] || colorName
- }
- /**
- * 用段落标签包裹内容
- */
- function wrapWithParagraphTag(content, type, style) {
- // 段落样式
- const styleAttrs = []
- if (style) {
- // 对齐方式
- if (style.alignment) {
- const alignMap = {
- 'left': 'left',
- 'center': 'center',
- 'right': 'right',
- 'both': 'justify', // 两端对齐
- 'justify': 'justify'
- }
- styleAttrs.push(`text-align:${alignMap[style.alignment] || style.alignment}`)
- }
-
- // 左缩进(twips -> pt,1 twip = 1/20 pt)
- if (style.indentLeft) {
- styleAttrs.push(`padding-left:${style.indentLeft / 20}pt`)
- }
-
- // 右缩进
- if (style.indentRight) {
- styleAttrs.push(`padding-right:${style.indentRight / 20}pt`)
- }
-
- // 首行缩进
- if (style.indentFirstLine) {
- styleAttrs.push(`text-indent:${style.indentFirstLine / 20}pt`)
- }
-
- // 悬挂缩进(负的首行缩进 + 增加左边距)
- if (style.indentHanging) {
- const hangingPt = style.indentHanging / 20
- styleAttrs.push(`text-indent:-${hangingPt}pt`)
- // 如果没有左缩进,需要增加左边距来补偿
- if (!style.indentLeft) {
- styleAttrs.push(`margin-left:${hangingPt}pt`)
- }
- }
-
- // 段前间距
- if (style.spacingBefore) {
- styleAttrs.push(`margin-top:${style.spacingBefore / 20}pt`)
- }
-
- // 段后间距
- if (style.spacingAfter) {
- styleAttrs.push(`margin-bottom:${style.spacingAfter / 20}pt`)
- }
-
- // 行距处理
- if (style.lineSpacing) {
- // getSpacingBetween 返回的是倍数(如 1.0, 1.5, 2.0)
- if (style.lineSpacing >= 1 && style.lineSpacing <= 5) {
- styleAttrs.push(`line-height:${style.lineSpacing}`)
- }
- } else if (style.lineSpacingValue && style.lineSpacingRule) {
- // 精确行距值处理
- const rule = style.lineSpacingRule.toLowerCase()
- if (rule === 'exact') {
- // 固定行距(twips -> pt)
- styleAttrs.push(`line-height:${style.lineSpacingValue / 20}pt`)
- } else if (rule === 'atleast' || rule === 'at_least') {
- // 最小行距
- styleAttrs.push(`min-height:${style.lineSpacingValue / 20}pt`)
- } else if (rule === 'auto') {
- // 倍数行距(240 twips = 1 倍行距)
- styleAttrs.push(`line-height:${style.lineSpacingValue / 240}`)
- }
- }
-
- // 字体信息(段落级别的默认字体)
- if (style.fontFamily) {
- styleAttrs.push(`font-family:${style.fontFamily}`)
- }
- if (style.fontSize) {
- styleAttrs.push(`font-size:${style.fontSize}pt`)
- }
- }
-
- const styleAttr = styleAttrs.length > 0 ? ` style="${styleAttrs.join(';')}"` : ''
-
- // 目录项特殊处理
- if (type === 'toc_item') {
- const pageNum = style?.tocPageNum || ''
- // 计算缩进级别(根据章节号判断)
- let level = 0
- const levelMatch = content.match(/^(\d+(?:\.\d+)*)/)
- if (levelMatch) {
- level = (levelMatch[1].match(/\./g) || []).length
- }
- const indentStyle = level > 0 ? ` style="padding-left:${level * 20}px"` : ''
- return `<div class="doc-toc-item"${indentStyle}><span class="toc-title">${content}</span><span class="toc-dots"></span><span class="toc-page">${pageNum}</span></div>`
- }
-
- switch (type) {
- case 'heading1':
- return `<h1${styleAttr}>${content}</h1>`
- case 'heading2':
- return `<h2${styleAttr}>${content}</h2>`
- case 'heading3':
- return `<h3${styleAttr}>${content}</h3>`
- case 'heading':
- return `<h2${styleAttr}>${content}</h2>`
- case 'toc':
- return `<div class="doc-toc-title"${styleAttr}>${content}</div>`
- case 'bullet':
- case 'list_item':
- return `<div class="doc-list-item bullet"${styleAttr}>${content}</div>`
- case 'ordered':
- return `<div class="doc-list-item ordered"${styleAttr}>${content}</div>`
- case 'quote':
- return `<blockquote${styleAttr}>${content}</blockquote>`
- case 'code':
- return `<pre><code>${content}</code></pre>`
- case 'title':
- return `<h1 class="doc-title"${styleAttr}>${content}</h1>`
- default:
- return `<p${styleAttr}>${content}</p>`
- }
- }
- /**
- * HTML 转义
- */
- function escapeHtml(text) {
- if (!text) return ''
- return text
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- }
- // 计算属性
- const groupedVariables = computed(() => {
- const groups = {}
- if (!variables.value || !Array.isArray(variables.value)) return groups
- variables.value.forEach(v => {
- if (!v) return
- const cat = v.category || 'other'
- if (!groups[cat]) groups[cat] = []
- groups[cat].push(v)
- })
- return groups
- })
- // 方法
- function goBack() {
- router.back()
- }
- function handleSave() {
- saved.value = true
- ElMessage.success('保存成功')
- }
- // 重新生成文档块结构
- async function handleRegenerateBlocks() {
- const baseDocumentId = templateStore.currentTemplate?.baseDocumentId
- if (!baseDocumentId) {
- ElMessage.warning('没有关联的示例文档')
- return
- }
- regenerating.value = true
- try {
- const result = await documentApi.regenerateBlocks(baseDocumentId)
- ElMessage.success(`重新生成成功: ${result.blockCount} 个文档块, ${result.entityCount} 个实体`)
-
- // 重新加载文档内容
- const structuredDoc = await documentApi.getStructured(baseDocumentId)
- if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
- blocks.value = structuredDoc.blocks // 更新 blocks
- documentContent.value = renderStructuredDocument(structuredDoc)
- // 重新提取实体
- entities.value = extractEntitiesFromBlocks(structuredDoc.blocks)
- }
- } catch (error) {
- console.error('重新生成失败:', error)
- ElMessage.error('重新生成失败: ' + (error.message || '未知错误'))
- } finally {
- regenerating.value = false
- }
- }
- function getFileIcon(file) {
- return '📄'
- }
- function selectFile(file) {
- selectedFile.value = file
- }
- async function removeSourceFile(file) {
- try {
- await templateStore.deleteSourceFile(file.id)
- sourceFiles.value = sourceFiles.value.filter(f => f.id !== file.id)
- ElMessage.success('删除成功')
- } catch (error) {
- ElMessage.error('删除失败: ' + error.message)
- }
- }
- async function addSourceFile() {
- if (!newSourceFile.alias) {
- ElMessage.warning('请输入文件别名')
- return
- }
- try {
- const sf = await templateStore.addSourceFile(templateId, newSourceFile)
- sourceFiles.value.push(sf)
- showAddSourceDialog.value = false
- Object.assign(newSourceFile, { alias: '', description: '', required: true })
- ElMessage.success('添加成功')
- } catch (error) {
- ElMessage.error('添加失败: ' + error.message)
- }
- }
- function getCategoryIcon(category) {
- const icons = {
- entity: '🏢',
- concept: '💡',
- data: '📊',
- location: '📍',
- asset: '📑'
- }
- return icons[category] || '📌'
- }
- function getCategoryColor(category) {
- const colors = {
- entity: '#1890ff',
- concept: '#722ed1',
- data: '#52c41a',
- location: '#faad14',
- asset: '#eb2f96'
- }
- return colors[category] || '#8c8c8c'
- }
- function getCategoryLabel(category) {
- const labels = {
- entity: '核心实体',
- concept: '概念/技术',
- data: '数据/指标',
- location: '地点/组织',
- asset: '资源模板'
- }
- return labels[category] || '其他'
- }
- /**
- * 根据实体类型获取图标
- */
- function getEntityTypeIcon(type) {
- const icons = {
- 'PERSON': '👤',
- 'ORGANIZATION': '🏢',
- 'ORG': '🏢',
- 'LOCATION': '📍',
- 'LOC': '📍',
- 'DATE': '📅',
- 'TIME': '⏰',
- 'PERIOD': '📆',
- 'MONEY': '💰',
- 'PERCENT': '📊',
- 'PRODUCT': '📦',
- 'EVENT': '📋',
- 'FACILITY': '🏭',
- 'FAC': '🏭',
- 'GPE': '🌍',
- 'LAW': '⚖️',
- 'WORK_OF_ART': '🎨',
- 'LANGUAGE': '🗣️',
- 'QUANTITY': '🔢',
- 'ORDINAL': '🔢',
- 'CARDINAL': '🔢',
- 'ENTITY': '🏷️',
- 'DOC_ID': '📄',
- 'NORP': '👥',
- 'TITLE': '🎖️',
- 'STANDARD': '📋',
- 'RATING': '⭐',
- 'SCORE': '💯',
- 'LEVEL': '📊',
- // 业务相关类型图标
- 'CERT': '📜',
- 'NUMBER': '🔢',
- 'METHOD': '⚙️',
- 'PROJECT': '📁',
- 'POLICY': '📑',
- 'DEVICE': '🔧',
- 'MATERIAL': '🧱',
- 'TECHNOLOGY': '💡',
- 'REQUIREMENT': '📝',
- 'INDICATOR': '📈',
- 'RESULT': '✅',
- 'PROBLEM': '⚠️',
- 'SOLUTION': '💡',
- 'RISK': '🚨',
- 'MEASURE': '📏',
- 'DEPARTMENT': '🏛️',
- 'ROLE': '👔',
- 'DOCUMENT': '📄',
- 'REGULATION': '⚖️',
- 'PROCEDURE': '📋',
- 'ACTIVITY': '🎯',
- 'TASK': '✔️',
- 'GOAL': '🎯',
- 'RESOURCE': '📦',
- 'SYSTEM': '🖥️',
- 'AREA': '📍',
- 'EQUIPMENT': '🔧',
- 'TOOL': '🛠️',
- 'SOFTWARE': '💻',
- 'DATA': '📊',
- 'RECORD': '📝',
- 'REPORT': '📊',
- 'PLAN': '📅',
- 'SCHEDULE': '📆',
- 'BUDGET': '💵',
- 'COST': '💰',
- 'UNIT': '📐',
- 'COMPANY': '🏢',
- 'INSTITUTION': '🏛️'
- }
- return icons[type?.toUpperCase()] || '🏷️'
- }
- /**
- * 根据实体类型获取样式类名
- */
- function getEntityTypeClass(type) {
- const typeMap = {
- 'PERSON': 'entity-person',
- 'ORGANIZATION': 'entity-org',
- 'LOCATION': 'entity-location',
- 'DATE': 'entity-date',
- 'TIME': 'entity-date',
- 'MONEY': 'entity-data',
- 'PERCENT': 'entity-data',
- 'PRODUCT': 'entity-product',
- 'EVENT': 'entity-event',
- 'FACILITY': 'entity-org',
- 'GPE': 'entity-location',
- 'LAW': 'entity-law'
- }
- return typeMap[type?.toUpperCase()] || 'entity-default'
- }
- /**
- * 滚动到文档中的指定实体
- */
- function scrollToEntity(entityId) {
- const editorEl = document.querySelector('.editor-content')
- if (!editorEl) return
-
- const entitySpan = editorEl.querySelector(`[data-entity-id="${entityId}"]`)
- if (entitySpan) {
- entitySpan.scrollIntoView({ behavior: 'smooth', block: 'center' })
- // 添加高亮闪烁效果
- entitySpan.classList.add('entity-highlight-flash')
- setTimeout(() => {
- entitySpan.classList.remove('entity-highlight-flash')
- }, 2000)
- }
- }
- function editVariable(variable) {
- editingVariable.value = variable
- Object.assign(variableForm, variable)
- showVariableDialog.value = true
- }
- async function saveVariable() {
- if (!variableForm.name || !variableForm.displayName) {
- ElMessage.warning('请填写必要字段')
- return
- }
- try {
- if (editingVariable.value) {
- // 更新
- const updated = await templateStore.updateVariable(editingVariable.value.id, variableForm)
- Object.assign(editingVariable.value, updated)
- ElMessage.success('更新成功')
- } else {
- // 新增
- const newVar = await templateStore.addVariable(templateId, variableForm)
- variables.value.push(newVar)
- ElMessage.success('添加成功')
- }
- showVariableDialog.value = false
- resetVariableForm()
- } catch (error) {
- ElMessage.error('保存失败: ' + error.message)
- }
- }
- async function deleteVariable() {
- if (editingVariable.value) {
- try {
- await templateStore.deleteVariable(editingVariable.value.id)
- variables.value = variables.value.filter(v => v.id !== editingVariable.value.id)
- showVariableDialog.value = false
- resetVariableForm()
- ElMessage.success('删除成功')
- } catch (error) {
- ElMessage.error('删除失败: ' + error.message)
- }
- }
- }
- function resetVariableForm() {
- editingVariable.value = null
- Object.assign(variableForm, {
- name: '',
- displayName: '',
- category: 'entity',
- exampleValue: '',
- sourceType: 'document',
- sourceFileAlias: '',
- extractType: 'direct'
- })
- }
- // 实体标记相关状态
- const markingEntity = ref(false)
- const selectionInfo = reactive({
- blockId: null,
- elementIndex: null,
- startOffset: null,
- endOffset: null,
- text: ''
- })
- /**
- * 处理右键菜单事件(阻止浏览器默认菜单)
- */
- function handleContextMenu(event) {
- const selection = window.getSelection()
- const text = selection.toString().trim()
- if (text && text.length > 0) {
- // 有选中文本时,显示自定义菜单
- event.preventDefault()
-
- selectedText.value = text
- selectionRange.value = selection.rangeCount > 0 ? selection.getRangeAt(0) : null
-
- // 尝试获取选中位置的块信息
- const blockInfo = getSelectionBlockInfo(selection)
- if (blockInfo) {
- Object.assign(selectionInfo, {
- blockId: blockInfo.blockId,
- elementIndex: blockInfo.elementIndex,
- startOffset: blockInfo.startOffset,
- endOffset: blockInfo.endOffset,
- text: text
- })
- } else {
- Object.assign(selectionInfo, {
- blockId: null,
- elementIndex: null,
- startOffset: null,
- endOffset: null,
- text: text
- })
- }
-
- // 计算菜单位置,确保不超出屏幕
- const menuWidth = 220
- const menuHeight = 350
- let x = event.clientX
- let y = event.clientY
-
- if (x + menuWidth > window.innerWidth) {
- x = window.innerWidth - menuWidth - 10
- }
- if (y + menuHeight > window.innerHeight) {
- y = window.innerHeight - menuHeight - 10
- }
-
- contextMenuPos.x = x
- contextMenuPos.y = y
- contextMenuVisible.value = true
- } else {
- // 没有选中文本时,允许浏览器默认右键菜单
- contextMenuVisible.value = false
- }
- }
- /**
- * 获取选中文本所在的块信息
- */
- function getSelectionBlockInfo(selection) {
- if (!selection.rangeCount) return null
-
- const range = selection.getRangeAt(0)
- let node = range.startContainer
-
- // 向上查找带有 data-block-id 的元素
- while (node && node !== document) {
- if (node.nodeType === Node.ELEMENT_NODE) {
- const blockId = node.getAttribute?.('data-block-id')
- if (blockId) {
- // 找到块,现在需要确定元素索引和偏移量
- const elementIndex = findElementIndex(node, range.startContainer)
- return {
- blockId,
- elementIndex: elementIndex,
- startOffset: range.startOffset,
- endOffset: range.endOffset
- }
- }
- }
- node = node.parentNode
- }
-
- return null
- }
- /**
- * 查找元素在块中的索引
- */
- function findElementIndex(blockElement, textNode) {
- // 简化实现:遍历块内的文本节点
- const walker = document.createTreeWalker(
- blockElement,
- NodeFilter.SHOW_TEXT,
- null,
- false
- )
-
- let index = 0
- let node
- while (node = walker.nextNode()) {
- if (node === textNode) {
- return index
- }
- index++
- }
-
- return 0
- }
- /**
- * 标记为实体(调用后端 API)
- */
- async function markAsVariable(entityType) {
- if (!selectedText.value) return
-
- const baseDocumentId = templateStore.currentTemplate?.baseDocumentId
- if (!baseDocumentId) {
- ElMessage.warning('没有关联的文档')
- contextMenuVisible.value = false
- return
- }
- // 映射前端类别到后端实体类型
- const entityTypeMap = {
- 'entity': 'ENTITY',
- 'concept': 'CONCEPT',
- 'data': 'DATA',
- 'location': 'LOCATION',
- 'asset': 'ASSET',
- 'person': 'PERSON',
- 'org': 'ORGANIZATION',
- 'date': 'DATE'
- }
-
- const backendEntityType = entityTypeMap[entityType] || entityType.toUpperCase()
-
- // 如果有完整的块信息,调用后端 API
- if (selectionInfo.blockId && selectionInfo.elementIndex !== null) {
- markingEntity.value = true
- try {
- const entityId = await documentApi.markEntity(baseDocumentId, selectionInfo.blockId, {
- elementIndex: selectionInfo.elementIndex,
- startOffset: selectionInfo.startOffset,
- endOffset: selectionInfo.endOffset,
- entityType: backendEntityType
- })
-
- // 添加到本地实体列表
- entities.value.push({
- id: entityId,
- text: selectedText.value,
- type: backendEntityType,
- confirmed: false
- })
-
- // 刷新文档内容以显示新标记
- await refreshDocumentContent()
-
- ElMessage.success('实体标记成功')
- } catch (error) {
- console.error('标记实体失败:', error)
- ElMessage.error('标记失败: ' + (error.message || '未知错误'))
- } finally {
- markingEntity.value = false
- }
- } else {
- // 没有块信息时,使用本地标记(临时方案)
- const newEntity = {
- id: 'local_' + Date.now(),
- text: selectedText.value,
- type: backendEntityType,
- confirmed: false,
- isLocal: true
- }
- entities.value.push(newEntity)
- ElMessage.success('实体已添加(本地)')
- }
- // 关闭菜单
- contextMenuVisible.value = false
- selectedText.value = ''
- }
- /**
- * 刷新文档内容
- */
- async function refreshDocumentContent() {
- const baseDocumentId = templateStore.currentTemplate?.baseDocumentId
- if (!baseDocumentId) return
-
- try {
- const structuredDoc = await documentApi.getStructured(baseDocumentId)
- if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
- blocks.value = structuredDoc.blocks
- documentContent.value = renderStructuredDocument(structuredDoc)
- entities.value = extractEntitiesFromBlocks(structuredDoc.blocks)
- }
- } catch (error) {
- console.error('刷新文档内容失败:', error)
- }
- }
- /**
- * 取消实体标记
- */
- async function handleUnmarkEntity(entity) {
- if (!entity || !entity.id) return
-
- // 本地实体直接删除
- if (entity.isLocal || entity.id.startsWith('local_')) {
- entities.value = entities.value.filter(e => e.id !== entity.id)
- ElMessage.success('已取消标记')
- return
- }
-
- const baseDocumentId = templateStore.currentTemplate?.baseDocumentId
- if (!baseDocumentId) return
-
- // 需要找到实体所在的块
- const blockId = findEntityBlockId(entity.id)
- if (!blockId) {
- ElMessage.warning('找不到实体所在的块')
- return
- }
-
- try {
- await ElMessageBox.confirm('确定要取消该实体的标记吗?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- })
-
- await documentApi.unmarkEntity(baseDocumentId, blockId, entity.id)
-
- // 刷新内容
- await refreshDocumentContent()
-
- ElMessage.success('已取消标记')
- } catch (error) {
- if (error !== 'cancel') {
- console.error('取消标记失败:', error)
- ElMessage.error('取消失败: ' + (error.message || '未知错误'))
- }
- }
- }
- /**
- * 确认实体
- */
- async function handleConfirmEntity(entity) {
- if (!entity || !entity.id) return
-
- const baseDocumentId = templateStore.currentTemplate?.baseDocumentId
- if (!baseDocumentId) return
-
- const blockId = findEntityBlockId(entity.id)
- if (!blockId) {
- // 本地实体直接标记确认
- entity.confirmed = true
- ElMessage.success('已确认')
- return
- }
-
- try {
- await documentApi.confirmEntity(baseDocumentId, blockId, entity.id)
-
- // 更新本地状态
- const idx = entities.value.findIndex(e => e.id === entity.id)
- if (idx !== -1) {
- entities.value[idx].confirmed = true
- }
-
- ElMessage.success('实体已确认')
- } catch (error) {
- console.error('确认实体失败:', error)
- ElMessage.error('确认失败: ' + (error.message || '未知错误'))
- }
- }
- /**
- * 更新实体类型
- */
- async function handleUpdateEntityType(entity, newType) {
- if (!entity || !entity.id) return
-
- const baseDocumentId = templateStore.currentTemplate?.baseDocumentId
- if (!baseDocumentId) return
-
- const blockId = findEntityBlockId(entity.id)
- if (!blockId) {
- // 本地实体直接更新
- entity.type = newType
- ElMessage.success('类型已更新')
- return
- }
-
- try {
- await documentApi.updateEntity(baseDocumentId, blockId, entity.id, newType)
-
- // 更新本地状态
- const idx = entities.value.findIndex(e => e.id === entity.id)
- if (idx !== -1) {
- entities.value[idx].type = newType
- }
-
- ElMessage.success('实体类型已更新')
- } catch (error) {
- console.error('更新实体类型失败:', error)
- ElMessage.error('更新失败: ' + (error.message || '未知错误'))
- }
- }
- /**
- * 从 blocks 中查找实体所在的块ID
- */
- function findEntityBlockId(entityId) {
- for (const block of blocks.value) {
- if (!block.elements) continue
- for (const el of block.elements) {
- if (el.type === 'entity' && el.entityId === entityId) {
- return block.id
- }
- }
- }
- return null
- }
- function handleFileUpload(response) {
- if (response.code === 200) {
- ElMessage.success('文件上传成功')
- // 可以在这里处理上传成功后的逻辑,如关联到项目
- }
- }
- // 点击其他地方关闭右键菜单
- function handleClickOutside(event) {
- if (contextMenuVisible.value && !event.target.closest('.context-menu')) {
- contextMenuVisible.value = false
- }
- }
- // ESC 键关闭右键菜单
- function handleKeyDown(event) {
- if (event.key === 'Escape' && contextMenuVisible.value) {
- contextMenuVisible.value = false
- }
- }
- onMounted(() => {
- document.addEventListener('click', handleClickOutside)
- document.addEventListener('keydown', handleKeyDown)
- })
- onUnmounted(() => {
- document.removeEventListener('click', handleClickOutside)
- document.removeEventListener('keydown', handleKeyDown)
- // 清理全局函数
- delete window.handleEntityClick
- })
- </script>
- <style lang="scss" scoped>
- // ==========================================
- // Editor 页面样式 - 参考 V2 原型设计
- // ==========================================
- .editor-page {
- height: calc(100vh - 56px);
- display: flex;
- flex-direction: column;
- background: var(--bg);
- }
- .editor-body {
- flex: 1;
- display: flex;
- overflow: hidden;
- }
- // ==========================================
- // 拖拽分隔条
- // ==========================================
- .resize-handle {
- width: 4px;
- background: transparent;
- cursor: col-resize;
- flex-shrink: 0;
- position: relative;
- z-index: 10;
- transition: background 0.2s;
-
- &:hover, &:active {
- background: var(--primary);
- }
-
- &::before {
- content: '';
- position: absolute;
- top: 0;
- bottom: 0;
- left: -3px;
- right: -3px;
- }
- }
- // ==========================================
- // 左侧面板 - V2 风格
- // ==========================================
- .left-panel {
- background: var(--white);
- border-right: 1px solid var(--border);
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
- min-width: 240px;
- max-width: 500px;
- overflow: hidden;
- // Tab 导航 - V2 风格(圆角填充)
- .panel-tabs {
- display: flex;
- gap: 6px;
- padding: 10px 12px;
- border-bottom: 1px solid var(--border);
- background: var(--white);
- overflow: hidden;
-
- .panel-tab {
- padding: 6px 10px;
- font-size: 12px;
- font-weight: 600;
- text-align: center;
- cursor: pointer;
- color: var(--text-2);
- border-radius: 10px;
- border: 1px solid transparent;
- transition: all 0.2s;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- white-space: nowrap;
- flex-shrink: 0;
-
- &:hover {
- color: var(--primary);
- background: var(--primary-light);
- }
-
- &.active {
- background: var(--primary);
- color: #fff;
- border-color: rgba(0, 0, 0, 0.04);
- box-shadow: var(--shadow-md);
- }
-
- .tab-count {
- font-size: 10px;
- font-weight: 500;
- background: rgba(255, 255, 255, 0.2);
- padding: 1px 6px;
- border-radius: 10px;
- color: inherit;
- }
-
- &:not(.active) .tab-count {
- background: var(--bg);
- color: var(--text-3);
- }
- }
- }
-
- // Tab 滑入动画 - 简化为淡入淡出
- .tab-slide-enter-active {
- transition: opacity 0.25s ease-out;
- }
-
- .tab-slide-leave-active {
- transition: opacity 0.15s ease-in;
- }
-
- .tab-slide-enter-from,
- .tab-slide-leave-to {
- opacity: 0;
- }
- .panel-header {
- padding: 14px 16px;
- border-bottom: 1px solid var(--border);
- font-size: 13px;
- font-weight: 600;
- display: flex;
- justify-content: space-between;
- align-items: center;
- .file-count {
- font-size: 12px;
- color: var(--text-3);
- font-weight: normal;
- }
- }
- .panel-body {
- flex: 1;
- overflow-y: auto;
- padding: 12px;
- min-height: 0;
-
- &.toc-panel {
- padding: 8px;
- }
-
- &.reports-panel {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- }
-
- // ==========================================
- // 我的报告面板 - V2 风格
- // ==========================================
- .new-report-btn {
- width: 100%;
- border-radius: var(--radius-md);
- height: 40px;
- font-size: 14px;
- font-weight: 600;
- background: var(--primary-gradient);
- border: none;
-
- &:hover {
- box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
- }
- }
-
- // 新建报告下拉菜单样式
- .new-report-menu {
- padding: 4px 0;
-
- .menu-item {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- padding: 12px 14px;
- cursor: pointer;
- border-radius: var(--radius-sm);
- transition: all 0.2s;
-
- &:hover {
- background: var(--bg);
- }
-
- .menu-icon {
- font-size: 24px;
- flex-shrink: 0;
- line-height: 1;
- }
-
- .menu-content {
- flex: 1;
- min-width: 0;
- }
-
- .menu-title {
- font-size: 14px;
- font-weight: 500;
- color: var(--text-1);
- margin-bottom: 4px;
- }
-
- .menu-desc {
- font-size: 12px;
- color: var(--text-3);
- line-height: 1.4;
- }
- }
- }
-
- .report-search {
- :deep(.el-input__wrapper) {
- border-radius: 18px;
- background: var(--bg);
- box-shadow: none;
- border: 1px solid var(--border);
-
- &:hover, &.is-focus {
- border-color: var(--primary);
- background: var(--white);
- }
- }
- }
-
- .report-list {
- display: flex;
- flex-direction: column;
- gap: 10px;
- flex: 1;
- overflow-y: auto;
- padding: 4px 0;
-
- .report-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 12px;
- background: var(--white);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.2s;
-
- &:hover {
- border-color: var(--primary);
- background: var(--primary-light);
- transform: translateY(-1px);
- box-shadow: var(--shadow-sm);
- }
-
- &.active {
- border-color: var(--primary);
- background: var(--primary-light);
- box-shadow: var(--shadow-sm);
-
- &::before {
- content: '';
- position: absolute;
- left: 0;
- top: 50%;
- transform: translateY(-50%);
- width: 3px;
- height: 18px;
- background: var(--primary);
- border-radius: 0 2px 2px 0;
- }
-
- .report-name {
- color: var(--primary);
- }
- }
-
- .report-icon {
- width: 40px;
- height: 40px;
- border-radius: var(--radius-sm);
- background: var(--bg);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 18px;
- flex-shrink: 0;
- }
-
- .report-info {
- flex: 1;
- min-width: 0;
-
- .report-name {
- font-weight: 600;
- font-size: 13px;
- color: var(--text-1);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- margin-bottom: 4px;
- }
-
- .report-meta {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 11px;
- color: var(--text-3);
-
- .report-status {
- padding: 2px 8px;
- border-radius: 4px;
- background: var(--bg);
- font-weight: 500;
-
- &.draft { color: var(--text-3); }
- &.editing { color: var(--primary); background: var(--primary-light); }
- &.published { color: var(--success); background: #f6ffed; }
- &.archived { color: var(--text-3); }
- }
- }
- }
- }
- }
-
- .report-empty, .report-loading {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px 20px;
- color: var(--text-3);
-
- .empty-icon {
- font-size: 48px;
- margin-bottom: 16px;
- opacity: 0.4;
- }
-
- .empty-text {
- font-size: 14px;
- font-weight: 500;
- color: var(--text-2);
- margin-bottom: 6px;
- }
-
- .empty-hint {
- font-size: 12px;
- }
- }
-
- .report-loading {
- flex-direction: row;
- gap: 10px;
- padding: 24px;
- }
-
- // ==========================================
- // 目录列表 - V2 风格
- // ==========================================
- .toc-list {
- .toc-item {
- display: flex;
- align-items: flex-start;
- padding: 10px 12px;
- border-radius: var(--radius-sm);
- cursor: pointer;
- transition: all 0.2s;
- font-size: 13px;
- line-height: 1.5;
-
- &:hover {
- background: var(--primary-light);
- }
-
- .toc-bullet {
- flex-shrink: 0;
- width: 14px;
- color: var(--primary);
- font-size: 8px;
- margin-top: 5px;
- }
-
- .toc-text {
- flex: 1;
- color: var(--text-1);
- word-break: break-word;
- }
-
- .toc-page {
- flex-shrink: 0;
- margin-left: 8px;
- color: var(--text-3);
- font-size: 11px;
- }
-
- // 层级缩进
- &.toc-level-1 {
- padding-left: 12px;
- font-weight: 600;
- .toc-bullet { font-size: 10px; }
- }
-
- &.toc-level-2 {
- padding-left: 28px;
- font-weight: 500;
- }
-
- &.toc-level-3 {
- padding-left: 44px;
- font-size: 12px;
- color: var(--text-2);
- }
- }
- }
-
- .toc-empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px 20px;
- color: var(--text-3);
-
- .empty-icon {
- font-size: 48px;
- margin-bottom: 16px;
- opacity: 0.4;
- }
-
- .empty-text {
- font-size: 14px;
- color: var(--text-2);
- margin-bottom: 6px;
- }
-
- .empty-hint {
- font-size: 12px;
- }
- }
- }
- // ==========================================
- // 上传区 - V2 风格
- // ==========================================
- .upload-zone {
- border: 2px dashed var(--border);
- border-radius: var(--radius-lg);
- margin-bottom: 16px;
- height: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--white);
- transition: all 0.2s;
- &:hover {
- border-color: var(--primary);
- background: var(--primary-light);
- }
- :deep(.el-upload-dragger) {
- padding: 0 12px;
- border: none;
- background: transparent;
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .upload-content {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .upload-icon {
- font-size: 18px;
- }
- .upload-text {
- font-size: 14px;
- font-weight: 600;
- color: var(--text-1);
- }
- .upload-hint {
- display: block;
- font-size: 11px;
- color: var(--text-3);
- margin-top: 8px;
- text-align: center;
- }
- }
- .file-list {
- margin-bottom: 16px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- // ==========================================
- // 文件项 - V2 风格
- // ==========================================
- .file-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 12px;
- background: var(--white);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.2s;
- position: relative;
- &:hover {
- border-color: var(--primary);
- background: var(--primary-light);
- }
-
- &.active {
- border-color: var(--primary);
- background: var(--primary-light);
- }
- .file-icon {
- width: 40px;
- height: 40px;
- border-radius: var(--radius-sm);
- display: flex;
- align-items: center;
- justify-content: center;
- color: #fff;
- font-weight: 700;
- font-size: 13px;
- flex-shrink: 0;
-
- &.pdf { background: #ff6b6b; }
- &.docx, &.doc { background: #4dabf7; }
- &.xlsx, &.xls { background: #73d13d; }
- &.md { background: #9254de; }
- &.default { background: var(--text-3); }
- }
- .file-info {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- .file-name {
- font-size: 13px;
- font-weight: 600;
- color: var(--text-1);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .file-meta {
- font-size: 11px;
- color: var(--text-3);
- margin-top: 4px;
- .required {
- color: var(--danger);
- }
- }
- }
-
- .file-status {
- font-size: 11px;
- white-space: nowrap;
-
- &.parsing { color: var(--primary); }
- &.done { color: var(--success); }
- }
- }
- .add-source-btn {
- width: 100%;
- border-radius: var(--radius-md);
- }
- // ==========================================
- // 中间面板 - V2 风格
- // ==========================================
- .center-panel {
- flex: 1;
- display: flex;
- flex-direction: column;
- background: var(--white);
- overflow: hidden;
- border-radius: var(--radius-md);
- margin: 0 8px;
- box-shadow: var(--shadow-sm);
- // ==========================================
- // 欢迎页 - V2 风格
- // ==========================================
- .welcome-page {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--white);
-
- .welcome-content {
- text-align: center;
- max-width: 600px;
- padding: 48px;
- }
-
- .welcome-logo {
- width: 80px;
- height: 80px;
- margin: 0 auto 32px;
- background: linear-gradient(135deg, var(--primary) 0%, #69c0ff 100%);
- border-radius: 16px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 40px;
- font-weight: 700;
- color: white;
- box-shadow: 0 12px 32px rgba(24, 144, 255, 0.3);
- }
-
- .welcome {
- h1 {
- font-size: 28px;
- font-weight: 700;
- color: var(--text-1);
- margin-bottom: 12px;
- line-height: 1.4;
-
- span {
- display: block;
- font-size: 20px;
- font-weight: 500;
- background: var(--ai-gradient);
- background-clip: text;
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- margin-top: 8px;
- }
- }
-
- p {
- font-size: 15px;
- color: var(--text-3);
- line-height: 1.6;
- }
- }
- }
- // ==========================================
- // 编辑器标题栏 - V2 风格(集成工具栏)
- // ==========================================
- .editor-title-bar {
- padding: 12px 20px;
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: center;
- gap: 16px;
- background: var(--white);
- flex-shrink: 0;
-
- // 左侧:标题和保存状态
- .title-section {
- display: flex;
- align-items: center;
- gap: 10px;
- flex: 1;
- min-width: 0;
- }
- .title-input-wrapper {
- position: relative;
- display: inline-block;
- min-width: 150px;
- max-width: 300px;
-
- .title-input {
- width: 100%;
- :deep(.el-input__wrapper) {
- box-shadow: none;
- background: transparent;
- border-radius: var(--radius-sm);
- padding: 0 8px;
- &:hover {
- background: var(--bg);
- }
-
- &.is-focus {
- background: var(--white);
- box-shadow: 0 0 0 2px var(--primary-light);
- }
- }
-
- :deep(.el-input__inner) {
- font-size: 15px;
- font-weight: 600;
- color: var(--text-1);
- }
- }
-
- .title-measure {
- position: absolute;
- visibility: hidden;
- white-space: nowrap;
- font-size: 15px;
- font-weight: 600;
- padding: 0 8px;
- pointer-events: none;
- }
- }
- .save-status {
- display: flex;
- align-items: center;
- gap: 4px;
- color: var(--success);
- font-size: 12px;
- white-space: nowrap;
- }
-
- // 中间:视图切换
- .view-toggle {
- display: flex;
- align-items: center;
-
- :deep(.el-radio-group) {
- .el-radio-button__inner {
- padding: 6px 12px;
- font-size: 12px;
- }
- }
- }
-
- // 右侧:操作按钮
- .toolbar-actions {
- display: flex;
- gap: 8px;
- align-items: center;
- flex-shrink: 0;
-
- :deep(.el-button) {
- border-radius: var(--radius-sm);
-
- &:not(.el-button--primary) {
- border-color: var(--border);
-
- &:hover {
- border-color: var(--primary);
- color: var(--primary);
- background: var(--primary-light);
- }
- }
-
- &.el-button--primary {
- background: var(--primary-gradient);
- border: none;
-
- &:hover {
- box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
- }
- }
- }
-
- :deep(.el-divider--vertical) {
- height: 20px;
- margin: 0 4px;
- }
- }
- }
- // ==========================================
- // 编辑器滚动区 - V2 风格
- // ==========================================
- .editor-scroll {
- flex: 1;
- overflow-y: auto;
- padding: 40px 48px;
- background: var(--white);
- }
- .editor-content {
- max-width: 1000px;
- margin: 0 auto;
- outline: none;
-
- // 文档块样式
- :deep(.doc-block) {
- position: relative;
- transition: background-color 0.2s;
-
- &:hover {
- background-color: rgba(24, 144, 255, 0.02);
- }
-
- // 被选中时的样式
- &.selected {
- background-color: rgba(24, 144, 255, 0.08);
- outline: 1px dashed var(--primary);
- }
- }
- :deep(h1) {
- font-size: 24px;
- font-weight: 700;
- margin-bottom: 24px;
- }
- :deep(h2) {
- font-size: 18px;
- font-weight: 600;
- margin: 28px 0 16px;
- }
- :deep(p) {
- margin-bottom: 12px;
- line-height: 1.6;
- }
- :deep(ul) {
- margin-bottom: 16px;
- padding-left: 24px;
- li {
- margin-bottom: 8px;
- }
- }
-
- // 目录样式
- :deep(.doc-toc-title) {
- font-size: 18pt;
- font-weight: bold;
- text-align: center;
- margin: 20px 0 16px;
- }
-
- :deep(.doc-toc-item) {
- display: flex;
- align-items: baseline;
- padding: 6px 0;
- line-height: 1.6;
- cursor: pointer;
- transition: background-color 0.2s;
-
- &:hover {
- background-color: #f5f5f5;
- }
-
- .toc-title {
- flex-shrink: 0;
- white-space: nowrap;
- }
-
- .toc-dots {
- flex: 1;
- border-bottom: 1px dotted #999;
- margin: 0 8px;
- min-width: 20px;
- height: 0.6em;
- }
-
- .toc-page {
- flex-shrink: 0;
- color: #666;
- min-width: 20px;
- text-align: right;
- }
- }
-
- // 表格样式
- :deep(.doc-table-container) {
- margin: 16px 0;
- overflow-x: auto;
- }
-
- :deep(.doc-table) {
- width: 100%;
- border-collapse: collapse;
- font-size: 14px;
-
- th, td {
- border: 1px solid #ddd;
- padding: 8px 12px;
- text-align: left;
- vertical-align: top;
- line-height: 1.5;
- }
-
- th {
- background-color: #f5f5f5;
- font-weight: bold;
- }
-
- tr:nth-child(even) td {
- background-color: #fafafa;
- }
-
- tr:hover td {
- background-color: #f0f7ff;
- }
- }
-
- :deep(.doc-table-empty) {
- padding: 20px;
- text-align: center;
- color: #999;
- border: 1px dashed #ddd;
- margin: 16px 0;
- }
-
- // 列表项样式
- :deep(.doc-list-item) {
- position: relative;
- margin-bottom: 8px;
- line-height: 1.6;
-
- &.bullet {
- padding-left: 1.5em;
- &::before {
- content: '•';
- position: absolute;
- left: 0;
- }
- }
-
- &.ordered {
- padding-left: 2em;
- counter-increment: doc-list;
- &::before {
- content: counter(doc-list) '.';
- position: absolute;
- left: 0;
- }
- }
- }
-
- // 重置列表计数器
- :deep(p + .doc-list-item.ordered:first-of-type),
- :deep(.doc-list-item.bullet + .doc-list-item.ordered) {
- counter-reset: doc-list;
- }
-
- // 块引用样式
- :deep(blockquote) {
- margin: 16px 0;
- padding: 12px 20px;
- border-left: 4px solid #ddd;
- background: #f9f9f9;
- color: #666;
- }
-
- // 代码块样式
- :deep(pre) {
- margin: 16px 0;
- padding: 16px;
- background: #f5f5f5;
- border-radius: 4px;
- overflow-x: auto;
-
- code {
- font-family: 'Consolas', 'Monaco', monospace;
- font-size: 13px;
- }
- }
-
- // 实体高亮样式 - 匹配原型 UI(边框 + 背景)
- :deep(.entity-highlight) {
- display: inline;
- padding: 2px 8px;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.2s;
- font-weight: 500;
- border: 1px solid #1890ff;
- color: #1890ff;
- background: rgba(24, 144, 255, 0.1);
-
- &:hover {
- background: #1890ff;
- color: white;
- }
-
- // 实体类型颜色
- &.entity {
- border-color: #1890ff;
- color: #1890ff;
- background: rgba(24, 144, 255, 0.1);
- &:hover { background: #1890ff; color: white; }
- }
-
- &.concept {
- border-color: #722ed1;
- color: #722ed1;
- background: rgba(114, 46, 209, 0.1);
- &:hover { background: #722ed1; color: white; }
- }
-
- &.data {
- border-color: #52c41a;
- color: #52c41a;
- background: rgba(82, 196, 26, 0.1);
- &:hover { background: #52c41a; color: white; }
- }
-
- &.location {
- border-color: #faad14;
- color: #d48806;
- background: rgba(250, 173, 20, 0.1);
- &:hover { background: #faad14; color: white; }
- }
-
- &.asset {
- border-color: #eb2f96;
- color: #eb2f96;
- background: rgba(235, 47, 150, 0.1);
- &:hover { background: #eb2f96; color: white; }
- }
-
- &.person {
- border-color: #1890ff;
- color: #1890ff;
- background: rgba(24, 144, 255, 0.1);
- &:hover { background: #1890ff; color: white; }
- }
-
- &.org {
- border-color: #722ed1;
- color: #722ed1;
- background: rgba(114, 46, 209, 0.1);
- &:hover { background: #722ed1; color: white; }
- }
-
- &.date {
- border-color: #13c2c2;
- color: #13c2c2;
- background: rgba(19, 194, 194, 0.1);
- &:hover { background: #13c2c2; color: white; }
- }
-
- &.product {
- border-color: #eb2f96;
- color: #eb2f96;
- background: rgba(235, 47, 150, 0.1);
- &:hover { background: #eb2f96; color: white; }
- }
-
- &.event {
- border-color: #fa8c16;
- color: #fa8c16;
- background: rgba(250, 140, 22, 0.1);
- &:hover { background: #fa8c16; color: white; }
- }
-
- &.law {
- border-color: #2f54eb;
- color: #2f54eb;
- background: rgba(47, 84, 235, 0.1);
- &:hover { background: #2f54eb; color: white; }
- }
- }
- }
- }
- // ==========================================
- // 右侧面板 - V2 风格
- // ==========================================
- .right-panel {
- background: var(--white);
- border-left: 1px solid var(--border);
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
- min-width: 280px;
- max-width: 500px;
- overflow: hidden;
-
- // 右侧面板分为上下两部分
- .element-section {
- flex: 4;
- overflow-y: auto;
- min-height: 0;
- }
-
- .ai-assistant {
- flex: 6;
- overflow-y: auto;
- min-height: 0;
- display: flex;
- flex-direction: column;
- }
- }
- // ==========================================
- // 要素管理区 - V2 风格
- // ==========================================
- .element-section {
- padding: 16px;
- border-bottom: 1px dashed var(--border);
-
- // 模块标题样式 - V2 风格
- .module-title {
- display: flex;
- align-items: center;
- gap: 10px;
- font-size: 15px;
- font-weight: 700;
- color: var(--text-1);
- margin-bottom: 14px;
-
- .module-icon {
- width: 36px;
- height: 36px;
- border-radius: 8px;
- background: var(--primary-gradient);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 18px;
- color: white;
- box-shadow: var(--shadow-md);
- }
- }
- .element-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12px;
- .element-title {
- font-size: 13px;
- font-weight: 600;
- display: flex;
- align-items: center;
- gap: 6px;
- .element-count {
- font-size: 11px;
- color: var(--text-3);
- font-weight: normal;
- }
- }
-
- // 要素 Tab 切换 - V2 风格
- .element-tabs {
- display: flex;
- gap: 8px;
-
- .element-tab {
- padding: 6px 12px;
- border-radius: 12px;
- background: transparent;
- border: 1px solid transparent;
- font-size: 13px;
- cursor: pointer;
- color: var(--text-2);
- transition: all 0.2s;
-
- &:hover {
- background: var(--bg);
- }
-
- &.active {
- background: var(--primary);
- color: #fff;
- border-color: rgba(0, 0, 0, 0.04);
- box-shadow: var(--shadow-md);
- }
- }
- }
- }
- .element-filter {
- padding: 0 0 12px;
-
- .entity-search {
- margin-bottom: 12px;
-
- :deep(.el-input__wrapper) {
- border-radius: 18px;
- background: var(--bg);
- box-shadow: none;
- border: 1px solid var(--border);
-
- &:hover, &.is-focus {
- border-color: var(--primary);
- background: var(--white);
- }
- }
- }
-
- .entity-type-filter {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
-
- .filter-tag {
- cursor: pointer;
- transition: all 0.2s;
- border-radius: 12px;
- font-size: 11px;
-
- &:hover {
- border-color: var(--primary);
- color: var(--primary);
- }
-
- &.active {
- background: var(--primary);
- color: white;
- border-color: var(--primary);
- }
-
- &.clear {
- background: transparent;
- border-style: dashed;
- color: var(--text-3);
-
- &:hover {
- border-color: var(--danger);
- color: var(--danger);
- }
- }
- }
- }
- }
- .element-body {
- padding: 0;
- }
- // 要素标签容器 - V2 风格
- .element-tags-wrap {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- max-height: 200px;
- overflow-y: auto;
- padding-right: 4px;
- padding-bottom: 16px;
-
- &::-webkit-scrollbar {
- width: 4px;
- }
-
- &::-webkit-scrollbar-track {
- background: var(--bg);
- border-radius: 2px;
- }
-
- &::-webkit-scrollbar-thumb {
- background: var(--border);
- border-radius: 2px;
-
- &:hover {
- background: var(--text-3);
- }
- }
- }
-
- .load-more-wrap {
- width: 100%;
- text-align: center;
- padding: 10px 0;
- margin-top: 8px;
- border-top: 1px dashed var(--border);
- }
-
- // ==========================================
- // 要素标签样式 - V2 风格
- // ==========================================
- .var-tag {
- height: 28px;
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 0 12px;
- border-radius: 2px;
- font-size: 12px;
- cursor: pointer;
- transition: all 0.2s;
- background: var(--bg);
- border: 1px solid var(--border);
- user-select: none;
-
- &:hover {
- border-color: var(--primary);
- background: var(--primary-light);
- transform: translateY(-1px);
- }
-
- &:active {
- cursor: grabbing;
- }
-
- .tag-icon {
- font-size: 12px;
- }
-
- .tag-name {
- max-width: 120px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-weight: 500;
- line-height: 28px;
- }
-
- .tag-status {
- color: #52c41a;
- font-size: 10px;
- }
-
- // 动态要素样式(圆角)
- &.dynamic {
- border-radius: 14px;
- }
-
- // 静态要素样式(微圆角)
- &.static {
- border-radius: 2px;
- }
-
- // 已确认状态
- &.confirmed {
- background: rgba(82, 196, 26, 0.1);
- border-color: #52c41a;
-
- .tag-name {
- color: #389e0d;
- }
- }
-
- // 实体类型样式 - 左边框颜色区分
- &.entity-person, &.entity {
- border-left: 3px solid var(--primary);
- }
- &.entity-org, &.concept {
- border-left: 3px solid #722ed1;
- }
- &.entity-location, &.location {
- border-left: 3px solid var(--warning);
- }
- &.entity-date {
- border-left: 3px solid #13c2c2;
- }
- &.entity-data, &.data {
- border-left: 3px solid var(--success);
- }
- &.entity-product, &.asset {
- border-left: 3px solid #eb2f96;
- }
- &.entity-event {
- border-left: 3px solid #fa8c16;
- }
- &.entity-law {
- border-left: 3px solid #2f54eb;
- }
- &.entity-default {
- border-left: 3px solid #8c8c8c;
- }
- }
- .element-hint {
- font-size: 12px;
- color: var(--text-3);
- text-align: center;
- padding: 24px;
- }
- }
- // 实体高亮闪烁效果
- @keyframes entity-flash {
- 0%, 100% { background-color: inherit; }
- 50% { background-color: #ffe58f; }
- }
- .entity-highlight-flash {
- animation: entity-flash 0.5s ease-in-out 3;
- }
- // 实体编辑弹窗样式
- .entity-edit-form {
- .entity-edit-preview {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- padding: 16px;
- background: var(--primary-light);
- border: 1px dashed var(--primary);
- border-radius: 8px;
- margin-bottom: 20px;
-
- .preview-icon {
- font-size: 24px;
- }
-
- .preview-text {
- font-size: 16px;
- font-weight: 600;
- color: var(--primary);
- }
- }
- }
- .category-section {
- padding: 12px 16px;
- border-bottom: 1px solid var(--border);
- .category-header {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 12px;
- font-weight: 600;
- margin-bottom: 10px;
- .category-dot {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- }
- .category-count {
- color: var(--text-3);
- font-weight: normal;
- background: var(--bg);
- padding: 2px 8px;
- border-radius: 10px;
- }
- }
- .category-items {
- .category-item {
- display: flex;
- justify-content: space-between;
- padding: 8px 12px;
- background: var(--bg);
- border-radius: 6px;
- margin-bottom: 6px;
- cursor: pointer;
- font-size: 12px;
- transition: all 0.2s;
- &:hover {
- background: var(--primary-light);
- }
- .item-value {
- color: var(--text-3);
- }
- }
- }
- }
- // ==========================================
- // 右键菜单 - V2 风格
- // ==========================================
- .context-menu {
- position: fixed;
- min-width: 180px;
- background: var(--white);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-lg);
- z-index: 3000;
- overflow: hidden;
- .context-menu-header {
- padding: 12px 14px;
- background: var(--bg);
- border-bottom: 1px solid var(--border);
-
- .selected-preview {
- font-size: 12px;
- color: var(--primary);
- font-weight: 600;
- max-width: 150px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- }
-
- .context-menu-section {
- padding: 8px 14px 4px;
- font-size: 10px;
- color: var(--text-3);
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
- .context-menu-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 14px;
- font-size: 13px;
- cursor: pointer;
- transition: all 0.15s;
- color: var(--text-1);
- &:hover {
- background: var(--primary-light);
- color: var(--primary);
- }
-
- &[disabled="true"] {
- opacity: 0.5;
- pointer-events: none;
- }
- .icon {
- font-size: 14px;
- width: 20px;
- text-align: center;
- }
-
- .shortcut {
- margin-left: auto;
- font-size: 11px;
- color: var(--text-3);
- }
- }
-
- .context-menu-divider {
- height: 1px;
- background: var(--border);
- margin: 4px 0;
- }
-
- .context-menu-loading {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 12px;
- color: var(--primary);
- font-size: 12px;
- border-top: 1px solid var(--border);
- background: var(--bg);
- }
- }
- // ==========================================
- // 实体弹出框样式 - V2 风格
- // ==========================================
- .entity-popover {
- .entity-popover-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 10px;
-
- .entity-text {
- font-weight: 600;
- font-size: 14px;
- max-width: 140px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: var(--text-1);
- }
- }
-
- .entity-popover-type {
- font-size: 12px;
- color: var(--text-2);
- margin-bottom: 14px;
- padding: 4px 8px;
- background: var(--bg);
- border-radius: 4px;
- display: inline-block;
- }
-
- .entity-popover-actions {
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
-
- :deep(.el-button) {
- border-radius: var(--radius-sm);
- }
- }
- }
- // ==========================================
- // 知识图谱容器 - V2 风格
- // ==========================================
- .graph-container {
- height: 500px;
- position: relative;
- background: linear-gradient(45deg, #f8f9fa 25%, transparent 25%),
- linear-gradient(-45deg, #f8f9fa 25%, transparent 25%),
- linear-gradient(45deg, transparent 75%, #f8f9fa 75%),
- linear-gradient(-45deg, transparent 75%, #f8f8f8 75%);
- background-size: 20px 20px;
- background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
- border-radius: var(--radius-md);
- .graph-legend {
- position: absolute;
- top: 16px;
- left: 16px;
- background: var(--white);
- border-radius: var(--radius-md);
- padding: 14px 18px;
- box-shadow: var(--shadow-md);
- .legend-title {
- font-size: 12px;
- font-weight: 600;
- margin-bottom: 10px;
- color: var(--text-1);
- }
- .legend-item {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 12px;
- color: var(--text-2);
- margin-bottom: 6px;
-
- &:last-child {
- margin-bottom: 0;
- }
- }
- .legend-dot {
- width: 12px;
- height: 12px;
- border-radius: 50%;
- &.core, &.entity { background: var(--primary); }
- &.concept { background: #722ed1; }
- &.data { background: var(--success); }
- &.location { background: var(--warning); }
- }
- }
- .graph-body {
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- .graph-placeholder {
- text-align: center;
- color: var(--text-3);
-
- .placeholder-icon {
- font-size: 48px;
- margin-bottom: 16px;
- opacity: 0.5;
- }
- p {
- margin-top: 12px;
- font-size: 14px;
- }
- }
- }
- }
- // ==========================================
- // 空白编辑器占位提示样式 - V2 风格
- // ==========================================
- :deep(.empty-editor-placeholder) {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 80px 40px;
- text-align: center;
- min-height: 400px;
- .empty-icon {
- font-size: 64px;
- margin-bottom: 24px;
- opacity: 0.8;
- }
- h2 {
- font-size: 24px;
- font-weight: 600;
- margin-bottom: 12px;
- color: var(--text-1);
- }
- .empty-subtitle {
- font-size: 15px;
- color: var(--text-3);
- margin-bottom: 32px;
- }
- .empty-actions {
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-bottom: 32px;
- width: 100%;
- max-width: 400px;
- }
- .action-card {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 16px 20px;
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.2s;
- text-align: left;
- &:hover {
- border-color: var(--primary);
- background: var(--primary-light);
- transform: translateX(4px);
- }
- .action-icon {
- font-size: 24px;
- flex-shrink: 0;
- }
- .action-text {
- font-size: 14px;
- color: var(--text-1);
- font-weight: 500;
- }
- }
- .empty-hint {
- font-size: 13px;
- color: var(--text-3);
- padding: 12px 20px;
- background: var(--bg);
- border-radius: var(--radius-md);
- border-left: 3px solid var(--primary);
- }
- }
- // 高亮块动画
- .highlight-block {
- animation: highlight-pulse 2s ease-out;
- }
- @keyframes highlight-pulse {
- 0% {
- background: rgba(24, 144, 255, 0.3);
- box-shadow: 0 0 0 4px rgba(24, 144, 255, 0.2);
- }
- 100% {
- background: transparent;
- box-shadow: none;
- }
- }
- </style>
|