| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728 |
- <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 === 'projects' }"
- @click="leftPanelTab = 'projects'"
- >
- 📄 项目
- <span class="tab-count">{{ projects.length }}</span>
- </div>
- <transition name="tab-slide">
- <div
- v-if="hasActiveProject"
- class="panel-tab"
- :class="{ active: leftPanelTab === 'attachments' }"
- @click="leftPanelTab = 'attachments'"
- >
- 📁 附件
- <span class="tab-count">{{ attachments.length }}</span>
- </div>
- </transition>
- <transition name="tab-slide">
- <div
- v-if="hasActiveProject"
- class="panel-tab"
- :class="{ active: leftPanelTab === 'rules' }"
- @click="leftPanelTab = 'rules'"
- >
- ⚙️ 规则
- <span class="tab-count">{{ rules.length }}</span>
- </div>
- </transition>
- </div>
- <!-- 项目列表面板 -->
- <div class="panel-body reports-panel" v-show="leftPanelTab === 'projects'">
- <el-button
- class="new-report-btn"
- type="primary"
- :icon="Plus"
- @click="showNewProjectDialog = true"
- >
- 新建项目
- </el-button>
- <el-input
- v-model="projectSearchKeyword"
- placeholder="搜索项目..."
- :prefix-icon="Search"
- clearable
- class="report-search"
- />
- <div class="report-list" v-if="filteredProjects.length > 0">
- <div
- v-for="project in filteredProjects"
- :key="project.id"
- class="report-item"
- :class="{ active: currentProjectId === project.id }"
- @click="switchProject(project)"
- >
- <div class="report-icon">📄</div>
- <div class="report-info">
- <div class="report-name">{{ project.title }}</div>
- <div class="report-meta">
- <span class="report-time">{{ formatTime(project.updatedAt || project.createdAt) }}</span>
- <span class="report-status" :class="project.status">
- {{ getStatusText(project.status) }}
- </span>
- </div>
- </div>
- <el-dropdown trigger="click" @command="(cmd) => handleProjectCommand(cmd, project)" @click.stop>
- <el-button class="report-delete-btn" circle size="small">
- <el-icon><MoreFilled /></el-icon>
- </el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item command="copy">复制项目</el-dropdown-item>
- <el-dropdown-item command="archive">归档</el-dropdown-item>
- <el-dropdown-item command="delete" divided>删除</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </div>
- </div>
-
- <div class="report-empty" v-else-if="!loadingProjects">
- <div class="empty-icon">📄</div>
- <div class="empty-text">{{ projectSearchKeyword ? '未找到匹配的项目' : '暂无项目' }}</div>
- <div class="empty-hint">点击上方按钮创建新项目</div>
- </div>
- <div class="report-loading" v-if="loadingProjects">
- <el-icon class="is-loading"><Loading /></el-icon>
- <span>加载中...</span>
- </div>
- </div>
- <!-- 附件面板 -->
- <div class="panel-body" v-show="leftPanelTab === 'attachments'">
- <el-upload
- class="upload-zone"
- drag
- :auto-upload="false"
- :on-change="handleAttachmentUpload"
- :show-file-list="false"
- accept=".pdf,.doc,.docx,.xls,.xlsx"
- >
- <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="att in attachments"
- :key="att.id"
- class="file-item"
- :class="{ active: selectedAttachment?.id === att.id }"
- @click="selectAttachment(att)"
- >
- <span class="file-icon">{{ getFileIcon(att) }}</span>
- <div class="file-info">
- <div class="file-name">{{ att.displayName }}</div>
- <div class="file-meta">
- <span>{{ att.entityCount || 0 }} 个实体</span>
- </div>
- </div>
- <el-button
- size="small"
- :icon="Delete"
- circle
- @click.stop="removeAttachment(att)"
- />
- </div>
- </div>
- </div>
- <!-- 规则面板 -->
- <div class="panel-body" v-show="leftPanelTab === 'rules'">
- <el-button
- class="new-report-btn"
- :icon="Plus"
- @click="showNewRuleDialog = true"
- >
- 添加规则
- </el-button>
- <el-button
- v-if="rules.length > 0"
- type="success"
- size="small"
- style="margin: 8px 0; width: 100%;"
- @click="handleBatchExecuteRules"
- :loading="executingRules"
- >
- 批量执行规则
- </el-button>
- <div class="file-list">
- <div
- v-for="rule in rules"
- :key="rule.id"
- class="file-item"
- >
- <span class="file-icon">⚙️</span>
- <div class="file-info">
- <div class="file-name">{{ rule.ruleName }}</div>
- <div class="file-meta">
- <el-tag size="small" :type="rule.lastRunStatus === 'success' ? 'success' : rule.lastRunStatus === 'failed' ? 'danger' : 'info'">
- {{ rule.ruleType }}
- </el-tag>
- </div>
- </div>
- <el-button size="small" type="primary" text @click.stop="handleExecuteRule(rule)" title="执行">▶</el-button>
- <el-button size="small" :icon="Delete" circle @click.stop="handleDeleteRule(rule)" />
- </div>
- </div>
- </div>
- </div>
- <!-- 左侧拖拽分隔条 -->
- <div class="resize-handle left-resize" @mousedown="startResizeLeft"></div>
- <!-- 中间主区域 -->
- <div class="center-panel">
- <!-- 欢迎页 -->
- <div class="welcome-page" v-if="!hasActiveProject">
- <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">
- <el-input v-model="projectTitle" class="title-input" placeholder="请输入项目标题" style="width: 300px;" />
- <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="document">📄 文档</el-radio-button>
- <el-radio-button value="elements">📝 要素</el-radio-button>
- <el-radio-button value="entities">🏷️ 实体</el-radio-button>
- </el-radio-group>
- </div>
- <div class="toolbar-actions">
- <el-button size="small" @click="handleCopyProject" :icon="CopyDocument">复制</el-button>
- <el-divider direction="vertical" />
- <el-button size="small" type="primary" :icon="Check" @click="handleSave">保存</el-button>
- </div>
- </div>
- <div class="editor-scroll" ref="editorRef" v-loading="loading" element-loading-text="正在加载项目...">
- <!-- 文档视图 -->
- <div class="document-view" v-if="viewMode === 'document'">
- <!-- 工具栏 -->
- <div class="doc-toolbar">
- <div class="doc-toolbar-left" v-if="attachments.length > 0">
- <span class="doc-att-label">附件:</span>
- <el-select v-model="docAttachmentId" placeholder="选择附件" size="small" style="width: 240px;" @change="loadDocContent">
- <el-option v-for="att in attachments" :key="att.id" :label="att.displayName" :value="att.id" />
- </el-select>
- </div>
- <div class="doc-toolbar-right">
- <el-switch v-model="highlightEnabled" active-text="要素高亮" inactive-text="" size="small" style="margin-right: 12px;" @change="renderDocHtml" />
- <el-tag v-if="highlightEnabled" size="small" type="info">{{ elementHighlightCount }} 处高亮</el-tag>
- </div>
- </div>
- <!-- 文档渲染区域(可编辑) -->
- <div
- class="doc-paper"
- v-if="docHtml"
- :style="docPaperStyle"
- contenteditable="true"
- spellcheck="false"
- v-html="docHtml"
- @input="onDocInput"
- @click="onDocClick"
- ref="docPaperRef"
- ></div>
- <!-- 无内容提示 -->
- <div class="doc-empty" v-else-if="!docLoading && docAttachmentId">
- <el-empty description="该附件暂无解析内容" />
- </div>
- <div class="doc-empty" v-else-if="!docLoading && attachments.length === 0">
- <el-empty description="暂无附件,请先在左侧上传文档" />
- </div>
- <div class="doc-empty" v-else-if="!docLoading && !docAttachmentId">
- <el-empty description="请选择一个附件查看文档内容" />
- </div>
- <div class="doc-loading" v-if="docLoading">
- <el-icon class="is-loading" :size="24"><Loading /></el-icon>
- <span>正在加载文档内容...</span>
- </div>
- </div>
- <!-- 要素高亮弹出框 -->
- <div
- v-if="highlightPopover.visible"
- class="element-popover"
- :style="{ top: highlightPopover.y + 'px', left: highlightPopover.x + 'px' }"
- >
- <div class="popover-header">
- <span class="popover-label">{{ highlightPopover.elementName }}</span>
- <el-tag size="small">{{ highlightPopover.elementKey }}</el-tag>
- </div>
- <div class="popover-body">
- <div class="popover-field">
- <span class="popover-field-label">当前值:</span>
- <el-input
- v-model="highlightPopover.currentValue"
- size="small"
- placeholder="输入要素值"
- @keyup.enter="savePopoverValue"
- />
- </div>
- <div class="popover-field" v-if="highlightPopover.originalValue">
- <span class="popover-field-label">原始值:</span>
- <span class="popover-original">{{ highlightPopover.originalValue }}</span>
- </div>
- </div>
- <div class="popover-footer">
- <el-button size="small" @click="highlightPopover.visible = false">关闭</el-button>
- <el-button size="small" type="primary" @click="savePopoverValue">保存</el-button>
- </div>
- </div>
- <!-- 要素视图 -->
- <div class="elements-view" v-if="viewMode === 'elements'">
- <div class="elements-grid">
- <div class="element-card" v-for="elem in elements" :key="elem.id">
- <div class="element-card-header">
- <span class="element-label">{{ elem.elementName }}</span>
- <el-tag size="small" :type="elem.dataType === 'text' ? '' : 'warning'">{{ elem.dataType || '文本' }}</el-tag>
- </div>
- <div class="element-card-body">
- <div class="element-value-row" v-for="val in getElementValues(elem.elementKey)" :key="val.valueId">
- <el-input v-model="val.valueText" placeholder="暂无值" size="small" @change="onValueChange(val)" />
- <div class="value-meta" v-if="val.fillSource">
- <span class="original-label">来源:</span>
- <span class="original-value">{{ val.fillSource }}</span>
- </div>
- <div class="value-status">
- <el-tag v-if="val.isFilled" type="success" size="small">已填充</el-tag>
- <el-tag v-else type="info" size="small">待填充</el-tag>
- </div>
- </div>
- <div class="element-empty" v-if="getElementValues(elem.elementKey).length === 0">
- <span class="empty-hint">暂无值,请通过规则填充或手动输入</span>
- </div>
- </div>
- </div>
- </div>
- <div class="elements-empty" v-if="elements.length === 0">
- <el-empty description="暂无要素定义">
- <el-button type="primary" :icon="Plus" @click="showAddElementDialog = true">添加要素</el-button>
- </el-empty>
- </div>
- </div>
- <!-- 实体视图 -->
- <div class="entities-view" v-if="viewMode === 'entities'">
- <div class="entity-filter-bar">
- <el-input v-model="entitySearchKeyword" placeholder="搜索实体..." :prefix-icon="Search" clearable style="width: 300px;" />
- <el-select v-model="entityTypeFilter" placeholder="实体类型" clearable style="width: 150px; margin-left: 12px;">
- <el-option label="全部" value="" />
- <el-option label="组织机构" value="ORG" />
- <el-option label="人物" value="PERSON" />
- <el-option label="日期" value="DATE" />
- <el-option label="编号" value="CODE" />
- <el-option label="数值" value="NUMBER" />
- </el-select>
- </div>
- <el-table :data="filteredEntities" style="width: 100%" stripe max-height="calc(100vh - 220px)">
- <el-table-column prop="entityText" label="实体文本" min-width="200" />
- <el-table-column prop="entityType" label="类型" width="120">
- <template #default="{ row }">
- <el-tag size="small">{{ getEntityTypeName(row.entityType) }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="businessLabel" label="业务标签" width="150" />
- <el-table-column prop="confidence" label="置信度" width="100">
- <template #default="{ row }">
- <span v-if="row.confidence">{{ (row.confidence * 100).toFixed(0) }}%</span>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column prop="sourceAttachment" label="来源附件" width="150" />
- </el-table>
- <div class="entities-empty" v-if="entities.length === 0">
- <el-empty description="暂无实体,请先上传附件并解析" />
- </div>
- </div>
- </div>
- </template>
- </div>
- <!-- 右侧拖拽分隔条 -->
- <div v-if="hasActiveProject" class="resize-handle right-resize" @mousedown="startResizeRight"></div>
- <!-- 右侧面板 -->
- <div v-if="hasActiveProject" class="right-panel" :style="{ width: rightPanelWidth + 'px' }">
- <div class="element-section">
- <div class="element-header">
- <span class="element-title">📊 项目概览</span>
- </div>
- <div class="element-body">
- <div class="overview-stats">
- <div class="stat-item"><span class="stat-label">要素数</span><span class="stat-value">{{ elements.length }}</span></div>
- <div class="stat-item"><span class="stat-label">已填充</span><span class="stat-value filled">{{ filledValueCount }}</span></div>
- <div class="stat-item"><span class="stat-label">附件数</span><span class="stat-value">{{ attachments.length }}</span></div>
- <div class="stat-item"><span class="stat-label">实体数</span><span class="stat-value">{{ entities.length }}</span></div>
- <div class="stat-item"><span class="stat-label">规则数</span><span class="stat-value">{{ rules.length }}</span></div>
- </div>
- </div>
- </div>
- <div class="element-section">
- <div class="element-header">
- <span class="element-title">🏷️ 要素列表 <span class="element-count">({{ elements.length }})</span></span>
- <el-button size="small" type="primary" :icon="Plus" @click="showAddElementDialog = true">添加</el-button>
- </div>
- <div class="element-body">
- <div class="element-tags-wrap" v-if="elements.length > 0">
- <div v-for="elem in elements" :key="elem.id" class="var-tag confirmed" :title="elem.elementName">
- <span class="tag-name">{{ elem.elementName }}</span>
- <span class="tag-status" v-if="hasFilledValue(elem.elementKey)">✓</span>
- </div>
- </div>
- <div class="element-hint" v-else>暂无要素,点击添加按钮创建</div>
- </div>
- </div>
- <div class="element-section" v-if="entities.length > 0">
- <div class="element-header">
- <span class="element-title">🔍 实体摘要 <span class="element-count">({{ entities.length }})</span></span>
- </div>
- <div class="element-body">
- <div class="element-tags-wrap">
- <div v-for="entity in entities.slice(0, 20)" :key="entity.id" class="var-tag" :class="getEntityTypeClass(entity.entityType)" :title="entity.entityText">
- <span class="tag-icon">{{ getEntityTypeIcon(entity.entityType) }}</span>
- <span class="tag-name">{{ entity.entityText }}</span>
- </div>
- <div v-if="entities.length > 20" class="element-hint" style="margin-top: 8px;">还有 {{ entities.length - 20 }} 个实体...</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 新建项目对话框 -->
- <el-dialog v-model="showNewProjectDialog" title="新建项目" width="460" :close-on-click-modal="false">
- <el-form :model="newProjectForm" label-width="80px">
- <el-form-item label="项目名称" required>
- <el-input v-model="newProjectForm.title" placeholder="请输入项目名称" maxlength="100" show-word-limit />
- </el-form-item>
- <el-form-item label="项目描述">
- <el-input v-model="newProjectForm.description" type="textarea" :rows="3" placeholder="项目描述(可选)" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="showNewProjectDialog = false">取消</el-button>
- <el-button type="primary" @click="handleCreateProject" :disabled="!newProjectForm.title.trim()" :loading="creatingProject">创建</el-button>
- </template>
- </el-dialog>
- <!-- 添加要素对话框 -->
- <el-dialog v-model="showAddElementDialog" title="添加要素" width="500">
- <el-form :model="newElementForm" label-width="100px">
- <el-form-item label="要素名称" required>
- <el-input v-model="newElementForm.elementName" placeholder="如:项目编号" />
- </el-form-item>
- <el-form-item label="要素标识" required>
- <el-input v-model="newElementForm.elementKey" placeholder="如:basicInfo.projectCode" />
- </el-form-item>
- <el-form-item label="数据类型">
- <el-select v-model="newElementForm.dataType" style="width: 100%">
- <el-option label="文本" value="text" />
- <el-option label="数字" value="number" />
- <el-option label="日期" value="date" />
- <el-option label="金额" value="money" />
- </el-select>
- </el-form-item>
- <el-form-item label="描述">
- <el-input v-model="newElementForm.description" type="textarea" :rows="2" placeholder="要素描述" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="showAddElementDialog = false">取消</el-button>
- <el-button type="primary" @click="handleAddElement" :disabled="!newElementForm.elementName || !newElementForm.elementKey">添加</el-button>
- </template>
- </el-dialog>
- <!-- 添加规则对话框 -->
- <el-dialog v-model="showNewRuleDialog" title="添加规则" width="500">
- <el-form :model="newRuleForm" label-width="100px">
- <el-form-item label="规则名称" required>
- <el-input v-model="newRuleForm.ruleName" placeholder="如:项目编号-直接引用实体" />
- </el-form-item>
- <el-form-item label="规则类型" required>
- <el-select v-model="newRuleForm.ruleType" style="width: 100%">
- <el-option label="直接引用实体" value="direct_entity" />
- <el-option label="AI 提取" value="ai_extract" />
- <el-option label="固定值" value="fixed_value" />
- <el-option label="计算公式" value="formula" />
- </el-select>
- </el-form-item>
- <el-form-item label="目标要素">
- <el-select v-model="newRuleForm.targetElementKey" style="width: 100%" placeholder="选择要填充的要素">
- <el-option v-for="elem in elements" :key="elem.elementKey" :label="elem.elementName" :value="elem.elementKey" />
- </el-select>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="showNewRuleDialog = false">取消</el-button>
- <el-button type="primary" @click="handleCreateRule" :disabled="!newRuleForm.ruleName">创建</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import { ref, reactive, computed, onMounted } from 'vue'
- import { useRouter, useRoute } from 'vue-router'
- import { Plus, Delete, Search, Loading, Check, CopyDocument, MoreFilled } from '@element-plus/icons-vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { projectApi, elementApi, valueApi, attachmentApi, entityApi, ruleApi } from '@/api'
- const router = useRouter()
- const route = useRoute()
- const currentProjectId = ref(null)
- const hasActiveProject = computed(() => !!currentProjectId.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 projectTitle = ref('')
- const viewMode = ref('document')
- const saved = ref(true)
- const editorRef = ref(null)
- const loading = ref(false)
- const leftPanelWidth = ref(300)
- const rightPanelWidth = ref(340)
- const isResizing = ref(false)
- const resizeType = ref('')
- const leftPanelTab = ref('projects')
- const projects = ref([])
- const loadingProjects = ref(false)
- const projectSearchKeyword = ref('')
- const elements = ref([])
- const values = ref([])
- const attachments = ref([])
- const entities = ref([])
- const rules = ref([])
- const selectedAttachment = ref(null)
- const entitySearchKeyword = ref('')
- const entityTypeFilter = ref('')
- const executingRules = ref(false)
- // 文档预览状态
- const docAttachmentId = ref(null)
- const docContent = ref(null)
- const docLoading = ref(false)
- const docHtml = ref('')
- const docPaperRef = ref(null)
- const highlightEnabled = ref(true)
- const elementHighlightCount = ref(0)
- const highlightPopover = reactive({
- visible: false, x: 0, y: 0,
- elementKey: '', fullElementKey: '', elementName: '', currentValue: '', originalValue: '', valueId: null
- })
- const showNewProjectDialog = ref(false)
- const showAddElementDialog = ref(false)
- const showNewRuleDialog = ref(false)
- const creatingProject = ref(false)
- const newProjectForm = reactive({ title: '', description: '' })
- const newElementForm = reactive({ elementName: '', elementKey: '', dataType: 'text', description: '' })
- const newRuleForm = reactive({ ruleName: '', ruleType: 'direct_entity', targetElementKey: '' })
- const filteredProjects = computed(() => {
- if (!projectSearchKeyword.value) return projects.value
- const kw = projectSearchKeyword.value.toLowerCase()
- return projects.value.filter(p => p.title?.toLowerCase().includes(kw))
- })
- const filteredEntities = computed(() => {
- let result = entities.value || []
- if (entityTypeFilter.value) result = result.filter(e => e.entityType === entityTypeFilter.value)
- if (entitySearchKeyword.value) {
- const kw = entitySearchKeyword.value.toLowerCase()
- result = result.filter(e => e.entityText?.toLowerCase().includes(kw) || e.businessLabel?.toLowerCase().includes(kw))
- }
- return result
- })
- const filledValueCount = computed(() => values.value.filter(v => v.isFilled).length)
- // 面板拖拽
- function startResizeLeft() {
- 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() {
- 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') leftPanelWidth.value = Math.max(240, Math.min(500, e.clientX))
- else if (resizeType.value === 'right') rightPanelWidth.value = Math.max(280, Math.min(500, window.innerWidth - e.clientX))
- }
- function stopResize() {
- isResizing.value = false; resizeType.value = ''
- document.removeEventListener('mousemove', handleResize)
- document.removeEventListener('mouseup', stopResize)
- document.body.style.cursor = ''; document.body.style.userSelect = ''
- }
- // 项目操作
- async function loadProjects() {
- loadingProjects.value = true
- try {
- const data = await projectApi.list({ page: 1, size: 50 })
- projects.value = data?.records || data || []
- } catch (error) {
- console.warn('获取项目列表失败:', error)
- projects.value = []
- } finally { loadingProjects.value = false }
- }
- async function switchProject(project) {
- if (currentProjectId.value === project.id) { unselectProject(); return }
- currentProjectId.value = project.id
- projectTitle.value = project.title || ''
- leftPanelTab.value = 'projects'
- await loadProjectData(project.id)
- }
- function unselectProject() {
- currentProjectId.value = null; projectTitle.value = ''
- elements.value = []; values.value = []; attachments.value = []; entities.value = []; rules.value = []
- docAttachmentId.value = null; docContent.value = null
- leftPanelTab.value = 'projects'
- }
- async function loadProjectData(projectId) {
- loading.value = true
- try {
- const [elemData, valData, attData, entData, ruleData] = await Promise.all([
- elementApi.list(projectId).catch(() => []),
- valueApi.list(projectId).catch(() => []),
- attachmentApi.list(projectId).catch(() => []),
- entityApi.listByProject(projectId).catch(() => []),
- ruleApi.list(projectId).catch(() => [])
- ])
- elements.value = elemData || []; values.value = valData || []
- attachments.value = attData || []; entities.value = entData || []; rules.value = ruleData || []
- // 自动加载第一个附件的文档内容
- if (attachments.value.length > 0) {
- docAttachmentId.value = attachments.value[0].id
- loadDocContent(attachments.value[0].id)
- } else {
- docAttachmentId.value = null
- docContent.value = null
- }
- } catch (error) { console.error('加载项目数据失败:', error) }
- finally { loading.value = false }
- }
- async function handleCreateProject() {
- if (!newProjectForm.title.trim()) return
- creatingProject.value = true
- try {
- const project = await projectApi.create({ title: newProjectForm.title.trim(), description: newProjectForm.description })
- showNewProjectDialog.value = false; newProjectForm.title = ''; newProjectForm.description = ''
- await loadProjects()
- if (project) await switchProject(project)
- ElMessage.success('项目创建成功')
- } catch (error) { ElMessage.error('创建失败: ' + error.message) }
- finally { creatingProject.value = false }
- }
- async function handleProjectCommand(cmd, project) {
- switch (cmd) {
- case 'copy':
- try { await projectApi.copy(project.id); await loadProjects(); ElMessage.success('项目复制成功') }
- catch (e) { ElMessage.error('复制失败: ' + e.message) }
- break
- case 'archive':
- try { await projectApi.archive(project.id); await loadProjects(); ElMessage.success('项目已归档') }
- catch (e) { ElMessage.error('归档失败: ' + e.message) }
- break
- case 'delete':
- try {
- await ElMessageBox.confirm(`确定要删除项目「${project.title}」吗?`, '删除确认', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' })
- await projectApi.delete(project.id)
- if (currentProjectId.value === project.id) unselectProject()
- await loadProjects(); ElMessage.success('项目已删除')
- } catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message) }
- break
- }
- }
- async function handleCopyProject() {
- if (!currentProjectId.value) return
- try {
- const copied = await projectApi.copy(currentProjectId.value)
- await loadProjects(); ElMessage.success('项目复制成功')
- if (copied) await switchProject(copied)
- } catch (e) { ElMessage.error('复制失败: ' + e.message) }
- }
- function handleSave() { saved.value = true; ElMessage.success('保存成功') }
- // ==================== 文档预览 + 可编辑 + 要素高亮 ====================
- async function loadDocContent(attId) {
- if (!attId) return
- docLoading.value = true
- docContent.value = null
- docHtml.value = ''
- try {
- const data = await attachmentApi.getDocContent(attId)
- docContent.value = data
- renderDocHtml()
- } catch (e) {
- console.warn('加载文档内容失败:', e)
- docContent.value = null
- docHtml.value = ''
- } finally {
- docLoading.value = false
- }
- }
- const docPaperStyle = computed(() => {
- const page = docContent.value?.page
- if (!page) return {}
- return {
- maxWidth: `${page.widthMm * 3.78}px`,
- paddingTop: `${page.marginTopMm * 3.78}px`,
- paddingBottom: `${page.marginBottomMm * 3.78}px`,
- paddingLeft: `${page.marginLeftMm * 3.78}px`,
- paddingRight: `${page.marginRightMm * 3.78}px`,
- }
- })
- // 从值的 elementKey 中提取不含项目前缀的 key
- // 例如 "PRJ-2024-001:basicInfo.projectCode" -> "basicInfo.projectCode"
- function stripValueKeyPrefix(valueElementKey) {
- const idx = valueElementKey?.indexOf(':')
- return idx >= 0 ? valueElementKey.substring(idx + 1) : valueElementKey
- }
- // 构建要素值映射表,分为长文本和短文本两类
- function buildElementValueMap() {
- const longTexts = [] // paragraph/table 类型的长文本要素
- const shortTexts = [] // text 类型的短文本要素
- const colors = [
- '#fff3cd', '#cce5ff', '#d4edda', '#f8d7da', '#e2d5f1',
- '#d1ecf1', '#ffeeba', '#c3e6cb', '#f5c6cb', '#d6d8db'
- ]
- let colorIdx = 0
- for (const elem of elements.value) {
- const elemValues = values.value.filter(v => stripValueKeyPrefix(v.elementKey) === elem.elementKey)
- const elemType = elem.elementType || 'text'
- for (const val of elemValues) {
- const text = val.valueText
- if (!text || text.length < 2) continue
- const entry = {
- text,
- elementKey: elem.elementKey,
- fullElementKey: val.elementKey,
- elementName: elem.elementName,
- valueId: val.valueId,
- elemType,
- color: colors[colorIdx % colors.length]
- }
- // paragraph/table 类型或多行文本视为长文本
- if (elemType === 'paragraph' || elemType === 'table' || text.includes('\n') || text.length > 100) {
- longTexts.push(entry)
- } else {
- shortTexts.push(entry)
- }
- }
- colorIdx++
- }
- // 长文本按长度降序
- longTexts.sort((a, b) => b.text.length - a.text.length)
- // 短文本按长度降序
- shortTexts.sort((a, b) => b.text.length - a.text.length)
- return { longTexts, shortTexts }
- }
- // 将文档 blocks 渲染为 HTML 字符串(含要素高亮)
- function renderDocHtml() {
- if (!docContent.value?.blocks) { docHtml.value = ''; return }
- const blocks = docContent.value.blocks
- const { longTexts, shortTexts } = highlightEnabled.value ? buildElementValueMap() : { longTexts: [], shortTexts: [] }
- let highlightCount = 0
- const parts = []
- // 预处理:为每个长文本要素,将其 valueText 按行拆分为句子集合
- // 用于判断某个 block 的文本是否属于某个长文本要素
- const longTextLines = longTexts.map(lt => ({
- ...lt,
- lines: new Set(lt.text.split('\n').map(l => l.trim()).filter(l => l.length > 0))
- }))
- // 收集被长文本高亮覆盖的 block IDs,这些 block 内的短文本不再单独高亮
- const longHighlightedBlockIds = new Set()
- // 第一遍:确定哪些 block 属于长文本要素
- for (const block of blocks) {
- const blockText = getBlockPlainText(block)
- if (!blockText) continue
- for (const lt of longTextLines) {
- if (lt.lines.has(blockText)) {
- longHighlightedBlockIds.add(block.id)
- break
- }
- }
- }
- // 第二遍:渲染,连续属于同一长文本要素的 block 合并到一个边框内
- let currentLongKey = null // 当前正在收集的长文本要素 key
- let currentLongMatch = null // 当前长文本要素匹配信息
- let longGroupHtml = '' // 当前长文本分组的 HTML 累积
- function flushLongGroup() {
- if (currentLongKey && longGroupHtml) {
- const borderColor = darkenColor(currentLongMatch.color)
- parts.push(`<div class="elem-highlight-wrap" data-elem-key="${currentLongMatch.elementKey}" data-value-id="${currentLongMatch.valueId || ''}" title="${escapeAttr(currentLongMatch.elementName)}" style="border:2px solid ${borderColor};border-radius:4px;padding:6px 8px;margin:4px 0;cursor:pointer;">${longGroupHtml}</div>`)
- highlightCount++
- }
- currentLongKey = null
- currentLongMatch = null
- longGroupHtml = ''
- }
- for (const block of blocks) {
- if (block.type === 'table') {
- flushLongGroup()
- const tableMatch = findTableLongTextMatch(block, longTextLines)
- parts.push(renderTableHtml(block, tableMatch))
- if (tableMatch) highlightCount++
- } else {
- const blockText = getBlockPlainText(block)
- const longMatch = findBlockLongTextMatch(blockText, longTextLines)
- if (longMatch) {
- // 如果和当前分组是同一个要素,继续累积
- if (currentLongKey === longMatch.elementKey) {
- longGroupHtml += renderBlockHtml(block, [], null, () => {})
- } else {
- // 不同要素,先 flush 上一组,再开始新组
- flushLongGroup()
- currentLongKey = longMatch.elementKey
- currentLongMatch = longMatch
- longGroupHtml = renderBlockHtml(block, [], null, () => {})
- }
- } else {
- // 非长文本 block,先 flush 再正常渲染
- flushLongGroup()
- parts.push(renderBlockHtml(block, shortTexts, null, (n) => { highlightCount += n }))
- }
- }
- }
- flushLongGroup() // flush 最后一组
- elementHighlightCount.value = highlightCount
- docHtml.value = parts.join('')
- }
- // 获取 block 的纯文本
- function getBlockPlainText(block) {
- if (!block.runs) return ''
- return block.runs.map(r => r.text).join('').trim()
- }
- // 查找 block 文本是否匹配某个长文本要素
- function findBlockLongTextMatch(blockText, longTextLines) {
- if (!blockText) return null
- for (const lt of longTextLines) {
- if (lt.lines.has(blockText)) return lt
- }
- return null
- }
- // 查找表格是否匹配某个长文本要素(通过表格第一行文本匹配)
- function findTableLongTextMatch(block, longTextLines) {
- if (!block.table?.data?.length) return null
- const firstRowText = block.table.data[0].map(c => c.text).join(' | ')
- for (const lt of longTextLines) {
- if (lt.text.includes(firstRowText)) return lt
- }
- return null
- }
- function renderBlockHtml(block, shortMap, longMatch, countFn) {
- const tag = getBlockTag(block.type)
- const cls = `doc-block doc-${block.type}`
- const styleStr = buildStyleStr(block.style)
- const styleAttr = styleStr ? ` style="${styleStr}"` : ''
- let inner = ''
- // 图片
- if (block.images?.length > 0) {
- for (const img of block.images) {
- const imgStyle = []
- if (img.widthInch) imgStyle.push(`width:${img.widthInch}in`)
- if (img.heightInch) imgStyle.push(`height:${img.heightInch}in`)
- imgStyle.push('max-width:100%', 'display:block', 'margin:8px auto')
- inner += `<img src="${img.src}" style="${imgStyle.join(';')}" class="doc-inline-image" contenteditable="false" />`
- }
- }
- // Runs
- if (block.runs) {
- let runsHtml = ''
- for (const run of block.runs) {
- const text = escapeHtml(run.text)
- const rs = buildRunStyleStr(run)
- runsHtml += rs ? `<span style="${rs}">${text}</span>` : text
- }
- // 长文本高亮:不在 runs 层面处理,在外层 block 包裹
- if (longMatch) {
- countFn(1)
- } else if (shortMap.length > 0 && runsHtml.length > 0) {
- // 短文本高亮:只替换 HTML 标签外的纯文本部分,避免破坏已有标签
- let count = 0
- for (const em of shortMap) {
- const escaped = escapeHtml(em.text)
- const hl = `<span class="elem-highlight" data-elem-key="${em.elementKey}" data-value-id="${em.valueId || ''}" style="border:1.5px solid ${darkenColor(em.color)};border-radius:3px;padding:0 2px;cursor:pointer;" contenteditable="false" title="${escapeAttr(em.elementName)}">${escaped}</span>`
- const newHtml = replaceTextOutsideTags(runsHtml, escaped, hl)
- if (newHtml !== runsHtml) {
- runsHtml = newHtml
- count++
- }
- }
- if (count > 0) countFn(count)
- }
- inner += runsHtml
- }
- if (!inner) inner = ' '
- return `<${tag} class="${cls}"${styleAttr} data-block-id="${block.id}">${inner}</${tag}>`
- }
- function renderTableHtml(block, longMatch) {
- const t = block.table
- if (!t?.data) return ''
- let html = `<table class="doc-table" data-block-id="${block.id}">`
- for (let ri = 0; ri < t.data.length; ri++) {
- html += '<tr>'
- for (const cell of t.data[ri]) {
- const cs = cell.colspan > 1 ? ` colspan="${cell.colspan}"` : ''
- html += `<td class="doc-table-cell"${cs}>${escapeHtml(cell.text)}</td>`
- }
- html += '</tr>'
- }
- html += '</table>'
- if (longMatch) {
- const borderColor = darkenColor(longMatch.color)
- html = `<div class="elem-highlight-wrap" data-elem-key="${longMatch.elementKey}" data-value-id="${longMatch.valueId || ''}" title="${escapeAttr(longMatch.elementName)}" style="border:2px solid ${borderColor};border-radius:4px;padding:6px;margin:4px auto;cursor:pointer;display:flex;justify-content:center;">${html}</div>`
- }
- return html
- }
- function getBlockTag(type) {
- if (type === 'heading1') return 'h1'
- if (type === 'heading2') return 'h2'
- if (type === 'heading3') return 'h3'
- if (type?.startsWith('toc')) return 'div'
- return 'p'
- }
- function buildStyleStr(style) {
- if (!style) return ''
- const parts = []
- if (style.alignment) {
- const map = { left: 'left', center: 'center', right: 'right', justify: 'justify', both: 'justify' }
- parts.push(`text-align:${map[style.alignment] || style.alignment}`)
- }
- if (style.indentLeft) parts.push(`padding-left:${style.indentLeft / 914400}in`)
- if (style.indentRight) parts.push(`padding-right:${style.indentRight / 914400}in`)
- if (style.indentFirstLine) parts.push(`text-indent:${style.indentFirstLine / 914400}in`)
- if (style.indentHanging) parts.push(`text-indent:-${style.indentHanging / 914400}in`)
- if (style.spacingBefore) parts.push(`margin-top:${style.spacingBefore / 914400}in`)
- if (style.spacingAfter) parts.push(`margin-bottom:${style.spacingAfter / 914400}in`)
- if (style.lineSpacing && style.lineSpacing >= 1 && style.lineSpacing <= 5) parts.push(`line-height:${style.lineSpacing}`)
- return parts.join(';')
- }
- function buildRunStyleStr(run) {
- const parts = []
- if (run.fontFamily) parts.push(`font-family:${run.fontFamily}`)
- if (run.fontSize) parts.push(`font-size:${run.fontSize}pt`)
- if (run.bold) parts.push('font-weight:bold')
- if (run.italic) parts.push('font-style:italic')
- if (run.color) parts.push(`color:${run.color.startsWith('#') ? run.color : '#' + run.color}`)
- if (run.underline) parts.push('text-decoration:underline')
- if (run.strikeThrough) parts.push('text-decoration:line-through')
- return parts.join(';')
- }
- function escapeHtml(text) {
- if (!text) return ''
- return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
- }
- function escapeAttr(text) {
- if (!text) return ''
- return text.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''')
- }
- // 只替换 HTML 标签外部的纯文本中的目标字符串,避免破坏已有标签和属性
- function replaceTextOutsideTags(html, search, replacement) {
- // 将 HTML 拆分为:标签部分 和 文本部分
- // 正则匹配所有 HTML 标签(包括自闭合标签)
- const parts = html.split(/(<[^>]+>)/g)
- let replaced = false
- for (let i = 0; i < parts.length; i++) {
- // 奇数索引是标签,偶数索引是文本
- if (i % 2 === 0 && parts[i].includes(search)) {
- parts[i] = parts[i].split(search).join(replacement)
- replaced = true
- }
- }
- return replaced ? parts.join('') : html
- }
- function darkenColor(hex) {
- // 简单加深颜色用于下边框
- const map = {
- '#fff3cd': '#e0a800', '#cce5ff': '#3d8bfd', '#d4edda': '#28a745',
- '#f8d7da': '#dc3545', '#e2d5f1': '#6f42c1', '#d1ecf1': '#17a2b8',
- '#ffeeba': '#d39e00', '#c3e6cb': '#1e7e34', '#f5c6cb': '#c82333',
- '#d6d8db': '#6c757d'
- }
- return map[hex] || '#999'
- }
- // 文档编辑事件
- function onDocInput() {
- saved.value = false
- }
- // 点击文档中的高亮要素
- function onDocClick(e) {
- const target = e.target.closest('.elem-highlight') || e.target.closest('.elem-highlight-wrap') || e.target.closest('.elem-highlight-table')
- if (!target) {
- highlightPopover.visible = false
- return
- }
- const elemKey = target.dataset.elemKey
- const valueId = target.dataset.valueId
- const elem = elements.value.find(el => el.elementKey === elemKey)
- const val = values.value.find(v => String(v.valueId) === String(valueId)) ||
- values.value.find(v => stripValueKeyPrefix(v.elementKey) === elemKey)
- if (!elem) return
- const rect = target.getBoundingClientRect()
- const scrollEl = editorRef.value
- const scrollRect = scrollEl?.getBoundingClientRect() || { top: 0, left: 0 }
- highlightPopover.elementKey = elemKey
- highlightPopover.fullElementKey = val?.elementKey || ''
- highlightPopover.elementName = elem.elementName
- highlightPopover.currentValue = val?.valueText || ''
- highlightPopover.originalValue = ''
- highlightPopover.valueId = val?.valueId || null
- highlightPopover.x = rect.left - scrollRect.left + scrollEl.scrollLeft
- highlightPopover.y = rect.bottom - scrollRect.top + scrollEl.scrollTop + 4
- highlightPopover.visible = true
- }
- async function savePopoverValue() {
- if (!highlightPopover.elementKey || !currentProjectId.value) {
- ElMessage.warning('无法保存:未找到对应的值记录')
- return
- }
- try {
- // API: PUT /projects/{projectId}/values/{elementKey} with { valueText }
- const apiKey = highlightPopover.fullElementKey || highlightPopover.elementKey
- const result = await valueApi.update(currentProjectId.value, apiKey, { valueText: highlightPopover.currentValue })
- // 更新本地数据
- const val = values.value.find(v => stripValueKeyPrefix(v.elementKey) === highlightPopover.elementKey)
- if (val) {
- val.valueText = highlightPopover.currentValue
- val.isFilled = !!highlightPopover.currentValue
- }
- highlightPopover.visible = false
- renderDocHtml()
- ElMessage.success('要素值已更新')
- } catch (e) {
- ElMessage.error('保存失败: ' + e.message)
- }
- }
- // 要素/值
- function getElementValues(elementKey) { return values.value.filter(v => stripValueKeyPrefix(v.elementKey) === elementKey) }
- function hasFilledValue(elementKey) { return values.value.some(v => stripValueKeyPrefix(v.elementKey) === elementKey && v.isFilled) }
- function onValueChange(val) { saved.value = false; val.isModified = true }
- async function handleAddElement() {
- if (!newElementForm.elementName || !newElementForm.elementKey) return
- try {
- const elem = await elementApi.add(currentProjectId.value, { ...newElementForm })
- elements.value.push(elem); showAddElementDialog.value = false
- Object.assign(newElementForm, { elementName: '', elementKey: '', dataType: 'text', description: '' })
- ElMessage.success('要素添加成功')
- } catch (e) { ElMessage.error('添加失败: ' + e.message) }
- }
- // 附件
- async function handleAttachmentUpload(file) {
- if (!currentProjectId.value) return
- try {
- const att = await attachmentApi.upload(currentProjectId.value, file.raw, file.name)
- attachments.value.push(att); ElMessage.success('附件上传成功')
- } catch (e) { ElMessage.error('上传失败: ' + e.message) }
- }
- function selectAttachment(att) { selectedAttachment.value = att }
- async function removeAttachment(att) {
- try {
- await ElMessageBox.confirm(`确定删除附件「${att.displayName}」?`, '删除确认', { type: 'warning' })
- await attachmentApi.delete(att.id)
- attachments.value = attachments.value.filter(a => a.id !== att.id); ElMessage.success('附件已删除')
- } catch (e) { if (e !== 'cancel') ElMessage.error('删除失败') }
- }
- function getFileIcon(att) {
- const name = att.displayName || ''
- if (name.endsWith('.pdf')) return '📕'
- if (name.endsWith('.doc') || name.endsWith('.docx')) return '📘'
- if (name.endsWith('.xls') || name.endsWith('.xlsx')) return '📗'
- return '📄'
- }
- // 规则
- async function handleCreateRule() {
- if (!newRuleForm.ruleName) return
- try {
- const rule = await ruleApi.create(currentProjectId.value, { ...newRuleForm })
- rules.value.push(rule); showNewRuleDialog.value = false
- Object.assign(newRuleForm, { ruleName: '', ruleType: 'direct_entity', targetElementKey: '' })
- ElMessage.success('规则创建成功')
- } catch (e) { ElMessage.error('创建失败: ' + e.message) }
- }
- async function handleDeleteRule(rule) {
- try {
- await ElMessageBox.confirm(`确定删除规则「${rule.ruleName}」?`, '删除确认', { type: 'warning' })
- await ruleApi.delete(rule.id); rules.value = rules.value.filter(r => r.id !== rule.id); ElMessage.success('规则已删除')
- } catch (e) { if (e !== 'cancel') ElMessage.error('删除失败') }
- }
- async function handleExecuteRule(rule) {
- try { await ruleApi.execute(rule.id); ElMessage.success(`规则「${rule.ruleName}」执行成功`); await loadProjectData(currentProjectId.value) }
- catch (e) { ElMessage.error('执行失败: ' + e.message) }
- }
- async function handleBatchExecuteRules() {
- if (!currentProjectId.value) return
- executingRules.value = true
- try { await ruleApi.batchExecute(currentProjectId.value); ElMessage.success('批量执行完成'); await loadProjectData(currentProjectId.value) }
- catch (e) { ElMessage.error('执行失败: ' + e.message) }
- finally { executingRules.value = false }
- }
- // 实体工具
- function getEntityTypeName(type) {
- const map = { 'ORG': '组织机构', 'PERSON': '人物', 'DATE': '日期', 'CODE': '编号', 'NUMBER': '数值', 'LOCATION': '地点', 'MONEY': '金额', 'PERCENT': '百分比' }
- return map[type] || type || '其他'
- }
- function getEntityTypeIcon(type) {
- const map = { 'ORG': '🏢', 'PERSON': '👤', 'DATE': '📅', 'CODE': '🔢', 'NUMBER': '📊', 'LOCATION': '📍', 'MONEY': '💰', 'PERCENT': '📊' }
- return map[type] || '🏷️'
- }
- function getEntityTypeClass(type) {
- const map = { 'ORG': 'entity-org', 'PERSON': 'entity-person', 'DATE': 'entity-date', 'CODE': 'entity-data', 'NUMBER': 'entity-data', 'LOCATION': 'entity-location' }
- return map[type] || 'entity-default'
- }
- // 工具函数
- function formatTime(dateStr) {
- if (!dateStr) return ''
- const date = new Date(dateStr); const now = new Date()
- const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24))
- if (diffDays === 0) return '今天'
- if (diffDays === 1) return '昨天'
- if (diffDays < 7) return `${diffDays}天前`
- return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
- }
- function getStatusText(status) {
- const map = { 'draft': '草稿', 'active': '进行中', 'archived': '已归档', 'completed': '已完成' }
- return map[status] || '草稿'
- }
- onMounted(async () => {
- await loadProjects()
- const pid = route.query.project
- if (pid) {
- const p = projects.value.find(p => String(p.id) === String(pid))
- if (p) await switchProject(p)
- }
- })
- </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);
- }
- }
-
-
- .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;
- position: relative;
-
- &: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-delete-btn {
- opacity: 0;
- flex-shrink: 0;
- transition: opacity 0.2s;
- border: none;
- background: transparent;
- color: var(--text-3);
-
- &:hover {
- color: var(--danger);
- background: rgba(255, 77, 79, 0.1);
- }
- }
-
- &:hover .report-delete-btn {
- opacity: 1;
- }
- }
- }
-
- .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; }
- }
-
- // 未确认的 AI 建议(文档中虚线样式)
- &.ai-suggestion-pending {
- border-style: dashed;
- opacity: 0.9;
- }
-
- // 点击 AI 建议后,文档中该要素的「待确认」高亮
- &.entity-pending-confirm {
- box-shadow: 0 0 0 2px #1890ff;
- opacity: 1;
- }
- }
- }
- }
- // ==========================================
- // 右侧面板 - 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;
- }
- }
-
- .header-actions {
- display: flex;
- gap: 4px;
-
- .el-button {
- padding: 4px 8px;
- font-size: 12px;
- }
- }
- }
-
- // AI 建议区块特殊样式
- &.ai-section {
- background: var(--bg);
- border-bottom: none;
-
- .element-header {
- .element-title {
- color: var(--text-2);
- }
- }
-
- .element-option.ai-highlight-option {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px 16px;
- font-size: 12px;
- color: var(--text-2);
- .option-label {
- flex: 1;
- }
- }
-
- .element-tags-wrap {
- max-height: 300px;
- }
- }
-
- // 要素 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;
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
- // 要素标签容器 - 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);
- }
- }
- }
-
- // ==========================================
- // 要素标签样式 - 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;
- }
-
- .tag-action {
- color: var(--primary);
- font-size: 14px;
- font-weight: bold;
- margin-left: 2px;
- }
-
- // 已确认的要素
- &.confirmed {
- background: var(--white);
- border-color: var(--primary);
-
- .tag-name {
- color: var(--text-1);
- }
- }
-
- // AI 建议的要素(虚线边框、淡色)
- &.ai-suggestion {
- background: transparent;
- border-style: dashed;
- border-color: var(--border);
- opacity: 0.85;
-
- .tag-name {
- color: var(--text-2);
- }
-
- &:hover {
- opacity: 1;
- border-color: var(--primary);
- border-style: solid;
- background: var(--primary-light);
-
- .tag-action {
- transform: scale(1.2);
- }
- }
- }
-
- // 动态要素样式(圆角)
- &.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;
- }
-
- // 当前正在确认的 AI 建议 tag
- &.is-pending {
- border-color: var(--primary);
- background: var(--primary-light);
- border-style: solid;
- }
- }
-
- // AI 建议确认栏已移至「+」按钮的悬浮框内,此处样式仅作保留注释
- .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);
- position: relative;
- &: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;
- flex-shrink: 0;
- }
-
- .shortcut {
- margin-left: auto;
- font-size: 11px;
- color: var(--text-3);
- }
-
- .submenu-arrow {
- margin-left: auto;
- font-size: 14px;
- color: var(--text-3);
- }
-
- &.has-submenu {
- &:hover .submenu-arrow {
- color: var(--primary);
- }
- }
- }
-
- // 子菜单
- .context-submenu {
- position: absolute;
- left: 100%;
- top: 0;
- min-width: 150px;
- background: var(--white);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-lg);
- overflow: hidden;
-
- .context-menu-item {
- padding: 8px 12px;
- font-size: 12px;
- gap: 8px;
-
- .icon {
- font-size: 12px;
- width: 16px;
- }
- }
- }
-
- .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;
- }
- }
- // ==========================================
- // 报告要素管理弹窗样式
- // ==========================================
- .elements-modal {
- :deep(.el-dialog__header) {
- padding: 16px 20px;
- border-bottom: 1px solid var(--border);
- margin-right: 0;
- }
-
- :deep(.el-dialog__body) {
- padding: 0;
- }
-
- :deep(.el-dialog__footer) {
- padding: 12px 20px;
- border-top: 1px solid var(--border);
- }
- }
- .elements-modal-content {
- .elements-search {
- display: flex;
- align-items: center;
- gap: 16px;
- padding: 16px 20px;
- border-bottom: 1px solid var(--border);
- background: var(--bg);
-
- .el-input {
- max-width: 300px;
- }
- }
-
- .elements-table-wrap {
- padding: 0;
-
- :deep(.el-table) {
- .element-name {
- font-weight: 500;
- color: var(--text-1);
- }
-
- .element-desc {
- color: var(--text-3);
- font-size: 12px;
- }
-
- .original-value {
- color: var(--text-2);
- font-size: 12px;
- }
-
- .element-source {
- color: var(--primary);
- font-size: 12px;
- }
-
- .el-input__wrapper {
- box-shadow: none;
- background: var(--bg);
- border-radius: var(--radius-sm);
-
- &:hover, &.is-focus {
- background: var(--white);
- box-shadow: 0 0 0 1px var(--primary);
- }
- }
- }
- }
-
- .elements-pagination {
- display: flex;
- justify-content: flex-end;
- padding: 12px 20px;
- border-top: 1px solid var(--border);
- }
- }
- // ==========================================
- // 新建报告对话框样式
- // ==========================================
- .new-report-dialog {
- :deep(.el-dialog__header) {
- padding: 16px 20px;
- border-bottom: 1px solid var(--border);
- margin-right: 0;
- }
-
- :deep(.el-dialog__body) {
- padding: 20px;
- }
-
- :deep(.el-dialog__footer) {
- padding: 12px 20px;
- border-top: 1px solid var(--border);
- }
- }
- .new-report-form {
- .section-label {
- font-size: 13px;
- font-weight: 500;
- color: var(--text-2);
- margin-bottom: 10px;
- }
-
- .create-type-section {
- margin-bottom: 20px;
- }
-
- .create-type-options {
- display: flex;
- gap: 12px;
- }
-
- .type-option {
- flex: 1;
- display: flex;
- align-items: flex-start;
- gap: 12px;
- padding: 14px;
- background: var(--bg);
- border: 2px solid var(--border);
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.2s;
- position: relative;
-
- &:hover {
- border-color: var(--primary-light);
- background: var(--white);
- }
-
- &.active {
- border-color: var(--primary);
- background: var(--primary-light);
-
- .option-title {
- color: var(--primary);
- }
- }
-
- .option-icon {
- font-size: 24px;
- flex-shrink: 0;
- line-height: 1;
- }
-
- .option-content {
- flex: 1;
- min-width: 0;
- }
-
- .option-title {
- font-size: 14px;
- font-weight: 600;
- color: var(--text-1);
- margin-bottom: 4px;
- }
-
- .option-desc {
- font-size: 12px;
- color: var(--text-3);
- line-height: 1.4;
- }
-
- .option-check {
- position: absolute;
- top: 8px;
- right: 8px;
- width: 20px;
- height: 20px;
- background: var(--primary);
- color: white;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- font-weight: bold;
- }
- }
-
- .name-input-section {
- margin-bottom: 20px;
-
- :deep(.el-input__wrapper) {
- border-radius: var(--radius-sm);
- }
- }
-
- .upload-section {
- .report-upload-area {
- :deep(.el-upload) {
- width: 100%;
- }
-
- :deep(.el-upload-dragger) {
- width: 100%;
- height: auto;
- padding: 24px;
- border-radius: var(--radius-md);
- border: 2px dashed var(--border);
- background: var(--bg);
-
- &:hover {
- border-color: var(--primary);
- background: var(--primary-light);
- }
- }
-
- .upload-content {
- text-align: center;
- }
-
- .upload-icon {
- font-size: 32px;
- color: var(--text-3);
- margin-bottom: 8px;
- }
-
- .upload-text {
- font-size: 14px;
- color: var(--text-2);
- margin-bottom: 4px;
-
- em {
- color: var(--primary);
- font-style: normal;
- }
- }
-
- .upload-hint {
- font-size: 12px;
- color: var(--text-3);
- }
- }
- }
- }
- // ==========================================
- // 要素视图
- // ==========================================
- .elements-view {
- padding: 24px;
-
- .elements-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
- gap: 16px;
- }
-
- .element-card {
- background: var(--white);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- overflow: hidden;
- transition: all 0.2s;
-
- &:hover {
- border-color: var(--primary);
- box-shadow: var(--shadow-sm);
- }
-
- .element-card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 16px;
- background: var(--bg);
- border-bottom: 1px solid var(--border);
-
- .element-label {
- font-weight: 600;
- font-size: 14px;
- color: var(--text-1);
- }
- }
-
- .element-card-body {
- padding: 12px 16px;
-
- .element-value-row {
- margin-bottom: 8px;
-
- .value-meta {
- margin-top: 4px;
- font-size: 11px;
- color: var(--text-3);
-
- .original-label {
- margin-right: 4px;
- }
- .original-value {
- color: var(--text-2);
- }
- }
-
- .value-status {
- margin-top: 4px;
- }
- }
-
- .element-empty {
- padding: 16px 0;
- text-align: center;
-
- .empty-hint {
- font-size: 12px;
- color: var(--text-3);
- }
- }
- }
- }
-
- .elements-empty {
- padding: 60px 20px;
- text-align: center;
- }
- }
- // ==========================================
- // 实体视图
- // ==========================================
- .entities-view {
- padding: 24px;
-
- .entity-filter-bar {
- display: flex;
- align-items: center;
- margin-bottom: 16px;
- }
-
- .entities-empty {
- padding: 60px 20px;
- text-align: center;
- }
- }
- // ==========================================
- // 项目概览统计
- // ==========================================
- .overview-stats {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 12px;
-
- .stat-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 12px 8px;
- background: var(--bg);
- border-radius: var(--radius-sm);
-
- .stat-label {
- font-size: 11px;
- color: var(--text-3);
- margin-bottom: 4px;
- }
-
- .stat-value {
- font-size: 20px;
- font-weight: 700;
- color: var(--text-1);
-
- &.filled {
- color: var(--success);
- }
- }
- }
- }
- // ==========================================
- // 文档视图 - Word 排版还原 + 可编辑 + 要素高亮
- // ==========================================
- .document-view {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 20px;
- background: #e8e8e8;
- min-height: 100%;
- position: relative;
- .doc-toolbar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 16px;
- padding: 8px 16px;
- background: var(--white);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-sm);
- width: 100%;
- max-width: 820px;
- .doc-toolbar-left, .doc-toolbar-right {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .doc-att-label {
- font-size: 13px;
- font-weight: 600;
- color: var(--text-2);
- white-space: nowrap;
- }
- }
- .doc-paper {
- background: #fff;
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
- border-radius: 2px;
- width: 100%;
- max-width: 820px;
- min-height: 1100px;
- padding: 96px 90px;
- font-family: 'Times New Roman', 'SimSun', '宋体', serif;
- font-size: 12pt;
- line-height: 1.6;
- color: #000;
- word-wrap: break-word;
- overflow-wrap: break-word;
- }
- .doc-block {
- margin: 0;
- padding: 0;
- min-height: 1em;
- }
- // 标题样式
- h1.doc-block {
- font-size: 18pt;
- font-weight: bold;
- margin: 16px 0 10px;
- line-height: 1.4;
- }
- h2.doc-block {
- font-size: 16pt;
- font-weight: bold;
- margin: 14px 0 8px;
- line-height: 1.4;
- }
- h3.doc-block {
- font-size: 14pt;
- font-weight: bold;
- margin: 12px 0 6px;
- line-height: 1.4;
- }
- // 段落
- p.doc-block {
- margin: 0 0 2px;
- text-align: justify;
- }
- // 目录
- .doc-toc1, .doc-toc2, .doc-toc3 {
- font-size: 14pt;
- margin: 2px 0;
- cursor: default;
- }
- .doc-toc2 {
- padding-left: 24px;
- font-size: 12pt;
- }
- .doc-toc3 {
- padding-left: 48px;
- font-size: 11pt;
- }
- // 空段落
- p.doc-paragraph:empty::after,
- p.doc-block:empty::after {
- content: '\00a0';
- }
- // 内联图片
- .doc-inline-image {
- max-width: 100%;
- height: auto;
- display: block;
- margin: 8px auto;
- }
- // 表格
- .doc-table {
- width: 100%;
- border-collapse: collapse;
- margin: 12px 0;
- font-size: 11pt;
- .doc-table-cell {
- border: 1px solid #000;
- padding: 6px 8px;
- vertical-align: top;
- line-height: 1.4;
- }
- tr:first-child .doc-table-cell {
- font-weight: bold;
- background: #f0f0f0;
- }
- }
- // 空状态 & 加载
- .doc-empty, .doc-loading {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 80px 20px;
- width: 100%;
- max-width: 820px;
- background: var(--white);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-sm);
- }
- .doc-loading {
- flex-direction: row;
- gap: 12px;
- padding: 40px;
- font-size: 14px;
- color: var(--text-2);
- }
- }
- // ==========================================
- // 要素高亮弹出框
- // ==========================================
- .element-popover {
- position: absolute;
- z-index: 1000;
- background: #fff;
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
- width: 320px;
- overflow: hidden;
- .popover-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 14px;
- background: var(--bg);
- border-bottom: 1px solid var(--border);
- .popover-label {
- font-weight: 600;
- font-size: 14px;
- color: var(--text-1);
- }
- }
- .popover-body {
- padding: 12px 14px;
- .popover-field {
- margin-bottom: 10px;
- &:last-child {
- margin-bottom: 0;
- }
- .popover-field-label {
- display: block;
- font-size: 12px;
- color: var(--text-3);
- margin-bottom: 4px;
- }
- .popover-original {
- font-size: 13px;
- color: var(--text-2);
- background: var(--bg);
- padding: 4px 8px;
- border-radius: var(--radius-sm);
- display: inline-block;
- }
- }
- }
- .popover-footer {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- padding: 8px 14px;
- border-top: 1px solid var(--border);
- background: var(--bg);
- }
- }
- // 可编辑文档纸张的光标和选区样式
- .doc-paper[contenteditable="true"] {
- outline: none;
- cursor: text;
- &:focus {
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15), 0 0 0 2px rgba(24, 144, 255, 0.2);
- }
- }
- // 高亮边框样式
- .elem-highlight {
- transition: border-color 0.2s, box-shadow 0.2s;
- &:hover {
- box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.4);
- border-color: #1890ff !important;
- }
- }
- .elem-highlight-long {
- display: inline !important;
- transition: border-color 0.2s, box-shadow 0.2s;
- &:hover {
- box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.4);
- border-color: #1890ff !important;
- }
- }
- .elem-highlight-wrap {
- position: relative;
- transition: border-color 0.2s, box-shadow 0.2s;
- &:hover {
- border-color: #1890ff !important;
- box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.4);
- }
- .doc-table {
- margin: 0 auto;
- }
- }
- </style>
|