灵越智报_完整交互版_v2.html 255 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>灵越智报 - 智能报告生成平台</title>
  7. <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
  8. <!-- 引入 iconfont 项目(已替换) -->
  9. <link rel="stylesheet" href="http://at.alicdn.com/t/c/font_2630279_pgouo6fulk.css">
  10. <style>
  11. :root {
  12. --primary: #1890ff;
  13. --primary-dark: #096dd9;
  14. --primary-light: #e6f7ff;
  15. --primary-gradient: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
  16. --white: #ffffff;
  17. --bg: #f5f7fa;
  18. --border: #e8e8e8;
  19. --text1: #262626;
  20. --text2: #595959;
  21. --text3: #8c8c8c;
  22. --success: #52c41a;
  23. --warning: #faad14;
  24. --danger: #ff4d4f;
  25. --ai-gradient: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
  26. --data-gradient: linear-gradient(135deg, #52c41a 0%, #13c2c2 100%);
  27. }
  28. /* JavaScript for syncing editor title was moved out of the style block to avoid invalid CSS. */
  29. * { margin: 0; padding: 0; box-sizing: border-box; }
  30. body { font-family: 'Noto Sans SC', sans-serif; background: var(--bg); color: var(--text1); font-size: 14px; line-height: 1.6; height: 100vh; overflow: hidden; }
  31. /* 全局头部 */
  32. .header { position: fixed; top: 0; left: 0; right: 0; height: 56px; background: var(--white); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 20px; z-index: 1000; }
  33. .header-left { display: flex; align-items: center; gap: 20px; }
  34. .logo { display: flex; align-items: center; gap: 8px; font-size: 17px; font-weight: 600; color: var(--primary); cursor: pointer; }
  35. .logo-icon { width: 32px; height: 32px; background: var(--primary-gradient); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; font-size: 18px; }
  36. .search-box { position: relative; width: 320px; }
  37. .search-input { width: 100%; height: 36px; padding: 0 14px 0 36px; border: 1px solid var(--border); border-radius: 18px; font-size: 13px; background: var(--bg); outline: none; transition: all 0.2s; }
  38. .search-input:focus { background: var(--white); border-color: var(--primary); }
  39. .search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: #8c8c8c; font-size: 14px; }
  40. .header-right { display: flex; align-items: center; gap: 10px; }
  41. .hd-btn { position: relative; width: 36px; height: 36px; border: none; background: transparent; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; color: var(--text2); transition: all 0.2s; }
  42. .hd-btn:hover { background: var(--bg); color: var(--primary); }
  43. .badge { position: absolute; top: 2px; right: 2px; min-width: 16px; height: 16px; background: var(--danger); color: white; border-radius: 8px; font-size: 10px; display: flex; align-items: center; justify-content: center; }
  44. .user-menu { display: flex; align-items: center; gap: 6px; padding: 4px 8px 4px 4px; border-radius: 20px; cursor: pointer; }
  45. .user-menu:hover { background: var(--bg); }
  46. .avatar { width: 30px; height: 30px; border-radius: 50%; background: var(--primary-gradient); color: white; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 12px; }
  47. .user-name { font-size: 13px; font-weight: 500; }
  48. /* 侧边栏 */
  49. .sidebar { position: fixed; top: 56px; left: 0; width: 240px; height: calc(100vh - 56px); background: var(--white); border-right: none; display: flex; flex-direction: column; transition: width 0.3s; z-index: 900; overflow-y: auto; }
  50. .sidebar.hidden { display: none; }
  51. .sidebar-menu { flex: 1; padding: 10px; overflow-y: auto; }
  52. .menu-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 8px; cursor: pointer; transition: all 0.2s; margin-bottom: 2px; position: relative; }
  53. .menu-item:hover { background: var(--primary-light); color: var(--primary); }
  54. .menu-item.active { background: var(--primary-light); color: var(--primary); }
  55. .menu-item.active::before { content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 3px; height: 18px; background: var(--primary); border-radius: 0 2px 2px 0; }
  56. .menu-icon { font-size: 16px; }
  57. .menu-text { font-size: 13px; font-weight: 500; }
  58. .menu-badge { min-width: 18px; height: 18px; padding: 0 5px; background: var(--primary); color: white; border-radius: 9px; font-size: 10px; display: flex; align-items: center; justify-content: center; }
  59. /* 主内容 */
  60. .main { margin-left: 300px; margin-top: 56px; height: calc(100vh - 56px); overflow-y: auto; display:flex; flex-direction:column; }
  61. .page { display: none; padding: 20px; }
  62. .page.active { display: block; }
  63. /* 通用组件 */
  64. .card { background: var(--white); border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
  65. .btn { display: inline-flex; align-items: center; gap: 4px; padding: 7px 14px; border: 1px solid var(--border); background: var(--white); border-radius: 6px; cursor: pointer; font-size: 12px; transition: all 0.2s; }
  66. .btn:hover { border-color: var(--primary); color: var(--primary); }
  67. .btn-primary { background: var(--primary-gradient); color: white; border: none; }
  68. .btn-primary:hover { box-shadow: 0 4px 12px rgba(24,144,255,0.4); }
  69. /* 首页样式 */
  70. .welcome h1 { font-size: 24px; font-weight: 600; margin-bottom: 4px; }
  71. .welcome h1 span { background: var(--ai-gradient); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
  72. .welcome p { color: var(--text2); }
  73. .stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin: 20px 0; }
  74. .stat-card { padding: 18px; cursor: pointer; transition: all 0.3s; }
  75. .stat-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0,0,0,0.1); }
  76. .stat-icon { width: 40px; height: 40px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 20px; margin-bottom: 10px; }
  77. .stat-icon.blue { background: linear-gradient(135deg, #e6f7ff, #bae7ff); }
  78. .stat-icon.purple { background: linear-gradient(135deg, #f9f0ff, #efdbff); }
  79. .stat-icon.green { background: linear-gradient(135deg, #f6ffed, #d9f7be); }
  80. .stat-icon.orange { background: linear-gradient(135deg, #fff7e6, #ffe7ba); }
  81. .stat-value { font-size: 26px; font-weight: 700; }
  82. .stat-label { font-size: 13px; color: var(--text2); margin-bottom: 6px; }
  83. .stat-trend { font-size: 12px; }
  84. .stat-trend.up { color: var(--success); }
  85. /* AI对话入口 */
  86. .ai-card { padding: 20px; margin-bottom: 20px; }
  87. .ai-welcome { background: linear-gradient(135deg, #f0f7ff, #f5f0ff); border-radius: 10px; padding: 16px; margin-bottom: 16px; }
  88. .ai-avatar { width: 44px; height: 44px; background: var(--ai-gradient); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 22px; margin-bottom: 10px; }
  89. .ai-title { font-size: 14px; font-weight: 600; margin-bottom: 8px; }
  90. .ai-list { list-style: none; margin-bottom: 10px; }
  91. .ai-list li { color: var(--text2); padding: 3px 0; font-size: 13px; }
  92. .ai-list li::before { content: '•'; color: var(--primary); margin-right: 8px; }
  93. .ai-tip { font-size: 12px; color: var(--text3); padding: 8px 12px; background: rgba(255,255,255,0.7); border-radius: 6px; border-left: 3px solid var(--primary); }
  94. .ai-input-wrap { position: relative; margin-bottom: 14px; }
  95. .ai-input { width: 100%; height: 46px; padding: 0 100px 0 18px; border: 2px solid var(--border); border-radius: 23px; font-size: 14px; outline: none; transition: all 0.3s; }
  96. .ai-input:focus { border-color: var(--primary); }
  97. .ai-input-btns { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); display: flex; gap: 4px; }
  98. .ai-input-btn { width: 32px; height: 32px; border: none; background: transparent; border-radius: 50%; cursor: pointer; font-size: 16px; color: var(--text3); }
  99. .ai-input-btn:hover { background: var(--bg); color: var(--primary); }
  100. .ai-input-btn.send { background: var(--primary-gradient); color: white; display: none; }
  101. .ai-input-btn.send.show { display: flex; align-items: center; justify-content: center; }
  102. .thinking-modes { display: flex; gap: 8px; flex-wrap: wrap; }
  103. .mode-btn { padding: 6px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 18px; font-size: 12px; cursor: pointer; }
  104. .mode-btn:hover { border-color: var(--primary); background: var(--primary-light); }
  105. .mode-btn.active { background: var(--primary-light); border-color: var(--primary); color: var(--primary); }
  106. /* 模板区 */
  107. .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
  108. .section-title { font-size: 15px; font-weight: 600; }
  109. .section-link { font-size: 13px; color: var(--primary); cursor: pointer; }
  110. .tpl-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: 14px; }
  111. .tpl-card { overflow: hidden; cursor: pointer; transition: all 0.3s; }
  112. .tpl-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0,0,0,0.1); }
  113. .tpl-preview { height: 100px; background: linear-gradient(135deg, #f5f7fa, #e8ecf0); display: flex; align-items: center; justify-content: center; font-size: 36px; }
  114. .tpl-info { padding: 14px; }
  115. .tpl-name { font-weight: 600; margin-bottom: 6px; }
  116. .tpl-meta { display: flex; gap: 12px; font-size: 11px; color: var(--text3); margin-bottom: 10px; }
  117. .tpl-tags { display: flex; gap: 4px; margin-bottom: 10px; }
  118. .tpl-tag { padding: 2px 6px; background: var(--primary-light); color: var(--primary); border-radius: 3px; font-size: 10px; }
  119. .tpl-tag.hot { background: #fff1f0; color: var(--danger); }
  120. .tpl-btn { width: 100%; padding: 8px; background: var(--primary-gradient); color: white; border: none; border-radius: 6px; font-size: 12px; cursor: pointer; }
  121. .quick-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
  122. .quick-action { display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px dashed var(--border); border-radius: 10px; cursor: pointer; }
  123. .quick-action:hover { border-color: var(--primary); background: var(--primary-light); }
  124. .quick-action-icon { width: 40px; height: 40px; background: var(--bg); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 20px; }
  125. /* 编辑器专用样式 */
  126. .editor-page { display: none; position: fixed; top: 56px; left: 0; right: 0; bottom: 0; background: var(--bg); z-index: 800; overflow: hidden; }
  127. .editor-page.active { display: block; }
  128. /* 编辑器工具栏 */
  129. .editor-toolbar { height: 56px; background: var(--white); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 16px; gap: 16px; flex-shrink: 0; }
  130. .back-btn { display: flex; align-items: center; gap: 4px; padding: 8px 12px; border: none; background: transparent; border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--text2); }
  131. .back-btn:hover { background: var(--bg); color: var(--primary); }
  132. .report-title-input { border: none; background: transparent; font-size: 15px; font-weight: 600; padding: 8px 12px; border-radius: 6px; min-width: 300px; outline: none; }
  133. .report-title-input:hover { background: var(--bg); }
  134. .report-title-input:focus { background: var(--white); box-shadow: 0 0 0 2px var(--primary-light); }
  135. .save-status { display: flex; align-items: center; gap: 4px; font-size: 13px; color: var(--success); }
  136. .toolbar-right { margin-left: auto; display: flex; gap: 10px; align-items: center; }
  137. .toolbar-btn { display: flex; align-items: center; gap: 6px; padding: 8px 16px; border: 1px solid var(--border); background: var(--white); border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--text1); transition: all 0.2s; }
  138. .toolbar-btn:hover { border-color: var(--primary); color: var(--primary); background: var(--primary-light); }
  139. .toolbar-btn.primary { background: var(--primary); color: white; border: none; }
  140. .toolbar-btn.primary:hover { background: var(--primary-dark); }
  141. .toolbar-btn .icon { font-size: 14px; }
  142. .toolbar-divider { width: 1px; height: 24px; background: var(--border); }
  143. /* 编辑器主体 */
  144. .editor-body { flex: 1; display: flex; overflow: hidden; min-height: 0; }
  145. /* 当编辑器激活时,使用三列布局:左侧资源、中心编辑、右侧AI助手(与示例一致) */
  146. .editor-page.active .editor-body {
  147. display: grid;
  148. grid-template-columns: 300px 1fr 300px;
  149. gap: 16px;
  150. padding: 16px;
  151. /* ensure each column stretches to the full grid height */
  152. align-items: stretch;
  153. align-content: stretch;
  154. box-sizing: border-box;
  155. height: calc(100vh - 56px);
  156. /* ensure grid row(s) fill available space so children can stretch */
  157. grid-auto-rows: 1fr;
  158. }
  159. /* 左侧项目文件面板 */
  160. .left-panel { width: 300px; background: var(--white); border-right: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; overflow: hidden; height: 100%; }
  161. .panel-header { padding: 14px 16px; border-bottom: 1px solid var(--border); font-size: 13px; font-weight: 600; color: var(--text1); display: flex; align-items: center; justify-content: space-between; }
  162. .panel-header-tabs .tabs-left { display:flex; gap:8px; align-items:center; }
  163. .panel-tab { padding:8px 14px; border-radius:12px; border:1px solid transparent; background:transparent; cursor:pointer; font-weight:700; font-size:14px; }
  164. .panel-tab.active { background: var(--primary); color: #fff; border-color: rgba(0,0,0,0.04); box-shadow: 0 6px 18px rgba(17,24,39,0.06); }
  165. .panel-header .file-count { margin-left:8px; font-weight:500; color:var(--text3); }
  166. .file-count { font-size: 12px; color: var(--text3); font-weight: normal; }
  167. .panel-body { flex: 1; padding: 12px; display: flex; flex-direction: column; min-height: 0; }
  168. /* docs / recent split: top 40% docs (scrollable), bottom 60% recent (scrollable) */
  169. .docs-area { flex: 4; overflow-y: auto; min-height: 0; }
  170. .recent-area { flex: 6; overflow-y: auto; min-height: 0; margin-top: 8px; }
  171. /* 上传区 */
  172. .upload-zone { border: 2px dashed var(--border); border-radius: 12px; height:40px; padding: 0 12px; text-align: center; cursor: pointer; margin-bottom: 16px; transition: all 0.18s; display:flex; align-items:center; justify-content:center; background: var(--white); }
  173. .upload-zone:hover { border-color: var(--primary); background: var(--primary-light); }
  174. /* 文本式添加资源(居中、大按钮感) */
  175. .upload-zone .add-resource-text { font-size:16px; font-weight:600; color:var(--text1); padding:0 12px; line-height:40px; }
  176. .upload-hint { font-size: 11px; color: var(--text3); display:block; margin-top:8px; text-align:center; }
  177. /* 左侧面板折叠按钮 */
  178. .panel-toggle-btn { background: transparent; border: 1px solid var(--border); border-radius: 8px; padding:6px 8px; cursor:pointer; font-size:14px; color:var(--text2); }
  179. .panel-toggle-btn:hover { border-color: var(--primary); color:var(--primary); }
  180. /* 小浮动展开按钮(当面板折叠时显示) */
  181. .resource-expand-btn { position: fixed; left: 8px; top: 100px; width:40px; height:40px; border-radius:8px; background:var(--white); border:1px solid var(--border); display:flex; align-items:center; justify-content:center; z-index:1400; box-shadow:0 6px 18px rgba(0,0,0,0.08); cursor:pointer; }
  182. /* 在正文中隐藏来源徽章(仅编辑区不显示来源文件标识) */
  183. .editor-content .source-badge { display: none !important; }
  184. /* 新:左侧文档列表样式(图片示例风格) - 内部旧 tab 样式已清理 */
  185. .doc-section-header { display:flex; align-items:center; justify-content:space-between; padding:6px 4px 10px; font-size:13px; font-weight:600; color:var(--text1); }
  186. .badge-count { background: var(--bg); padding:2px 8px; border-radius:12px; font-size:12px; color:var(--primary); }
  187. .doc-list { display:flex; flex-direction:column; gap:10px; padding:4px 0 12px; }
  188. .doc-card { display:flex; gap:10px; align-items:center; padding:10px; background:var(--white); border-radius:10px; border:1px solid var(--border); cursor:pointer; }
  189. .doc-card .doc-thumb { width:40px; height:40px; border-radius:6px; background:var(--bg); display:flex; align-items:center; justify-content:center; font-size:18px; color:var(--text3); }
  190. /* file badge styles for attachment icons (PDF/DOC/XLS/MD) */
  191. .file-badge { display:inline-flex; width:40px; height:40px; border-radius:8px; align-items:center; justify-content:center; color:#fff; font-weight:700; font-size:13px; flex:0 0 40px; }
  192. .file-badge.pdf { background: #ff6b6b; }
  193. .file-badge.doc { background: #4dabf7; }
  194. .file-badge.xls { background: #73d13d; }
  195. .file-badge.md { background: #9254de; }
  196. .doc-card .doc-meta { display:flex; flex-direction:column; }
  197. .doc-card .doc-title { font-weight:600; font-size:13px; color:var(--text1); }
  198. .doc-card .doc-time { font-size:11px; color:var(--text3); margin-top:4px; }
  199. .recent-section { margin-top:12px; padding-top:8px; border-top:1px dashed var(--border); }
  200. .recent-list { display:flex; flex-direction:column; padding-top:8px; }
  201. .recent-item { padding:8px 10px; background:transparent; border:none; font-size:12px; color:var(--text2); border-radius:4px; transition: all 0.2s ease; }
  202. .recent-item:hover { background:#f5f5f5; }
  203. /* 文件预览模态(大弹窗) */
  204. .file-preview-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center; z-index: 1600; }
  205. .file-preview-modal { width: 90%; max-width: 1000px; max-height: 90vh; overflow: auto; background: var(--white); border-radius: 14px; box-shadow: 0 30px 80px rgba(10,30,60,0.35); padding: 20px; position: relative; }
  206. .file-preview-modal .modal-header { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px; }
  207. .file-preview-modal .modal-title { font-weight:700; font-size:16px; color:var(--text1); }
  208. .file-preview-modal .modal-body { color:var(--text1); line-height:1.7; white-space:pre-wrap; font-size:14px; }
  209. .file-preview-close { position:absolute; right:12px; top:12px; background:transparent;border:none;font-size:18px;cursor:pointer;color:var(--text3); }
  210. /* 文件项操作按钮(悬浮显示) */
  211. .file-item { position: relative; }
  212. .file-actions { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); display: none; gap:6px; align-items:center; }
  213. .file-item:hover .file-actions { display: flex; }
  214. .file-actions .action-btn { background: var(--white); border: 1px solid var(--border); padding:6px; border-radius:6px; font-size:13px; cursor:pointer; display:flex; align-items:center; justify-content:center; width:34px; height:34px; box-shadow: 0 2px 6px rgba(0,0,0,0.06); }
  215. .file-actions .action-btn:hover { border-color: var(--primary); color: var(--primary); }
  216. /* 解析过程弹出框(更接近示例:大圆角、渐变背景、分段进度) */
  217. .parsing-popover { position: fixed; width: 360px; background: linear-gradient(135deg, #f7fbff 0%, #e6f7ff 100%); border: 1px solid rgba(24,144,255,0.12); border-radius: 14px; box-shadow: 0 18px 48px rgba(15,35,70,0.18); z-index: 1500; padding: 14px; }
  218. .parsing-popover .title { font-weight:700; margin-bottom:10px; color:var(--text1); display:flex; align-items:center; justify-content:space-between; }
  219. .parsing-popover .parsing-progress { display:flex; gap:6px; margin-bottom:12px; }
  220. .parsing-popover .parsing-progress .seg { flex:1; height:8px; background: rgba(255,255,255,0.6); border-radius:6px; transition: all 0.4s; box-shadow: inset 0 -2px 4px rgba(0,0,0,0.03); }
  221. .parsing-popover .parsing-progress .seg.active { background: linear-gradient(90deg, rgba(24,144,255,0.95), rgba(58,150,255,0.8)); box-shadow: 0 6px 14px rgba(24,144,255,0.12); transform: translateY(-2px); }
  222. .parsing-popover .steps-list { display:flex; flex-direction:column; gap:8px; margin-bottom:8px; }
  223. .parsing-popover .step { display:flex; align-items:center; gap:12px; padding:10px 12px; border-radius:10px; background: rgba(255,255,255,0.6); }
  224. .parsing-popover .step .dot { width:14px;height:14px;border-radius:50%;background:var(--bg); border:2px solid rgba(0,0,0,0.06); flex-shrink:0; }
  225. .parsing-popover .step.completed { background: rgba(246,255,238,0.9); }
  226. .parsing-popover .step.completed .dot { background: var(--success); border-color: transparent; box-shadow: 0 6px 20px rgba(82,200,26,0.12); }
  227. .parsing-popover .step.active { background: rgba(240,248,255,0.95); }
  228. .parsing-popover .step.active .dot { background: var(--primary); border-color: transparent; box-shadow: 0 6px 20px rgba(24,144,255,0.12); }
  229. .parsing-popover .step.pending { background: rgba(250,250,250,0.9); }
  230. .parsing-popover .step.pending .dot { background: #f0f0f0; border-color: rgba(0,0,0,0.06); }
  231. .parsing-popover .step .label { font-size:13px; color:var(--text1); }
  232. .parsing-popover .close-btn { position:absolute; right:10px; top:8px; cursor:pointer; font-size:14px; color:var(--text3); }
  233. /* 文件分组 */
  234. .file-group { margin-bottom: 16px; }
  235. .file-group-header { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text3); margin-bottom: 8px; padding: 0 4px; }
  236. .file-group-header .count { background: var(--bg); padding: 1px 6px; border-radius: 8px; font-size: 10px; }
  237. /* 文件项 */
  238. .file-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: var(--white); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: all 0.2s; }
  239. .file-item:hover { border-color: var(--primary); background: var(--primary-light); }
  240. .file-item.active { border-color: var(--primary); background: var(--primary-light); }
  241. .file-icon { font-size: 28px; flex-shrink: 0; }
  242. .file-icon.pdf { color: #ff4d4f; }
  243. .file-icon.docx { color: #1890ff; }
  244. .file-icon.xlsx { color: #52c41a; }
  245. .file-icon.md { color: #8c8c8c; }
  246. .file-info { flex: 1; min-width: 0; }
  247. .file-name { font-size: 12px; font-weight: 500; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  248. .file-meta { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--text3); }
  249. .file-status { font-size: 11px; white-space: nowrap; }
  250. .file-status.parsing { color: var(--primary); }
  251. .file-status.done { color: var(--success); }
  252. /* 中间编辑区 */
  253. .center-panel { flex: 1; display: flex; flex-direction: column; background: var(--white); overflow: hidden; min-height: 0; height: 100%; }
  254. /* 编辑器顶部标题栏 */
  255. .editor-title-bar { padding: 16px 24px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; }
  256. .editor-main-title { font-size: 18px; font-weight: 600; flex: 1; cursor: pointer; }
  257. /* 调整:圆角 4px,固定高度 24px,水平内边距保持 8px,垂直居中 */
  258. .report-status { display:inline-block; margin-left:0; padding:0 8px; height:24px; line-height:24px; background:var(--primary-light); color:var(--primary); border-radius:4px; font-size:12px; font-weight:600; }
  259. /* Icon hover tooltip */
  260. .icon-tooltip {
  261. position: fixed;
  262. z-index: 4000;
  263. background: rgba(0,0,0,0.78);
  264. color: #fff;
  265. padding: 6px 8px;
  266. border-radius: 6px;
  267. font-size: 12px;
  268. pointer-events: none;
  269. white-space: nowrap;
  270. opacity: 0;
  271. transition: opacity 120ms ease;
  272. }
  273. .view-toggle { display: flex; align-items: center; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
  274. .view-btn { padding: 8px 16px; border: none; background: var(--white); font-size: 13px; cursor: pointer; color: var(--text2); display: flex; align-items: center; gap: 6px; transition: all 0.2s; }
  275. .view-btn:first-child { border-right: 1px solid var(--border); }
  276. .view-btn:hover { background: var(--bg); }
  277. .view-btn.active { background: var(--primary-light); color: var(--primary); font-weight: 500; }
  278. .graph-btn { width: 36px; height: 36px; border: 1px solid var(--border); background: var(--white); border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; margin-left: 8px; transition: all 0.2s; }
  279. .graph-btn:hover { border-color: var(--primary); background: var(--primary-light); }
  280. .graph-btn.active { border-color: var(--primary); background: var(--primary-light); color: var(--primary); }
  281. /* 编辑器内容区 */
  282. .editor-scroll { flex: 1; overflow-y: auto; padding: 40px 48px; }
  283. .editor-content { max-width: 1000px; margin: 0 auto; }
  284. .editor-content h1 { font-size: 24px; font-weight: 700; margin-bottom: 24px; color: var(--text1); }
  285. .editor-content h2 { font-size: 18px; font-weight: 600; margin: 28px 0 16px; color: var(--text1); }
  286. .editor-content h3 { font-size: 15px; font-weight: 600; margin: 20px 0 12px; color: var(--text1); }
  287. .editor-content p { margin-bottom: 16px; line-height: 1.8; color: var(--text1); }
  288. /* 实体高亮标记 */
  289. .entity-highlight { display: inline; padding: 2px 6px; border: 1px solid var(--primary); border-radius: 4px; color: var(--primary); background: var(--primary-light); cursor: pointer; transition: all 0.2s; position: relative; }
  290. .entity-highlight:hover { background: var(--primary); color: white; }
  291. /* 不同类型实体的颜色样式,与右侧栏element-tag保持一致 */
  292. .entity-highlight.entity { border-color: var(--primary); background: var(--primary-light); color: var(--primary); }
  293. .entity-highlight.entity:hover { background: var(--primary); color: white; }
  294. .entity-highlight.concept { border-color: #722ed1; background: #f9f0ff; color: #722ed1; }
  295. .entity-highlight.concept:hover { background: #722ed1; color: white; }
  296. .entity-highlight.data { border-color: var(--success); background: #f6ffed; color: var(--success); }
  297. .entity-highlight.data:hover { background: var(--success); color: white; }
  298. .entity-highlight.location { border-color: var(--warning); background: #fff7e6; color: var(--warning); }
  299. .entity-highlight.location:hover { background: var(--warning); color: white; }
  300. .entity-highlight.asset { border-color: #eb2f96; background: #fff0f6; color: #eb2f96; }
  301. .entity-highlight.asset:hover { background: #eb2f96; color: white; }
  302. /* 来源标识小徽章(AI / 附件 / 人工) */
  303. .source-badge { display:inline-flex; align-items:center; justify-content:center; font-size:10px; padding:2px 6px; margin-left:6px; border-radius:10px; border:1px solid rgba(0,0,0,0.06); background:#fff; color:var(--text2); cursor:pointer; box-shadow:0 1px 2px rgba(0,0,0,0.04); }
  304. .source-badge.ai { background: #fff7e6; color: var(--warning); border-color: rgba(255, 216, 128, 0.4); }
  305. .source-badge.file { background: #f6ffed; color: var(--success); border-color: rgba(166, 230, 149, 0.45); }
  306. .source-badge.manual { background: #f0f5ff; color: var(--primary); border-color: rgba(173, 199, 255, 0.45); }
  307. .source-badge:hover { transform: translateY(-1px); }
  308. /* AI优化建议卡片 */
  309. .ai-suggestion-card { background: #fffbf0; border: 1px solid #ffe7ba; border-radius: 10px; padding: 16px; margin: 16px 0; }
  310. .ai-suggestion-header { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
  311. .ai-suggestion-title { font-size: 13px; font-weight: 600; color: var(--warning); }
  312. .ai-suggestion-content { font-size: 12px; line-height: 1.7; color: var(--text1); margin-bottom: 12px; }
  313. .ai-suggestion-list { list-style: none; margin-bottom: 12px; }
  314. .ai-suggestion-list li { padding: 4px 0; font-size: 12px; color: var(--text2); }
  315. .ai-suggestion-list li::before { content: '•'; color: var(--primary); margin-right: 8px; }
  316. .ai-suggestion-actions { display: flex; gap: 8px; }
  317. /* 数据表格 */
  318. .data-table-card { background: var(--white); border: 1px solid var(--border); border-radius: 10px; margin: 16px 0; overflow: hidden; }
  319. .data-table-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border); }
  320. .data-table-title { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; }
  321. .data-table-source { font-size: 11px; color: var(--text3); }
  322. .data-table { width: 100%; border-collapse: collapse; }
  323. .data-table th { background: var(--bg); padding: 10px 16px; text-align: left; font-size: 12px; font-weight: 600; color: var(--text2); border-bottom: 1px solid var(--border); }
  324. .data-table td { padding: 10px 16px; font-size: 13px; border-bottom: 1px solid var(--border); }
  325. .data-table tr:last-child td { border-bottom: none; }
  326. .data-table tr:hover td { background: var(--primary-light); }
  327. /* 数据引用卡片 */
  328. .data-reference-card { background: linear-gradient(135deg, #f0f9ff, #e6f7ff); border: 1px solid #bae7ff; border-radius: 10px; padding: 16px; margin: 16px 0; }
  329. .reference-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
  330. .reference-icon { font-size: 16px; }
  331. .reference-title { font-size: 13px; font-weight: 600; color: var(--primary); }
  332. .reference-content { display: flex; flex-direction: column; gap: 6px; }
  333. .reference-item { display: flex; align-items: center; gap: 8px; font-size: 12px; }
  334. .reference-label { color: var(--text3); font-weight: 500; min-width: 70px; }
  335. .reference-value { color: var(--text1); }
  336. .reference-value.success { color: var(--success); font-weight: 600; }
  337. /* 竞争分析卡片 */
  338. .competition-card { background: linear-gradient(135deg, #fffbe6, #fff7e6); border: 1px solid #ffe7ba; border-radius: 10px; padding: 16px; margin: 16px 0; }
  339. .competition-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
  340. .competition-icon { font-size: 16px; }
  341. .competition-title { font-size: 13px; font-weight: 600; color: var(--warning); }
  342. .competition-content { display: flex; flex-direction: column; gap: 10px; }
  343. .competition-item { padding: 12px; background: rgba(255,255,255,0.7); border-radius: 8px; border-left: 3px solid var(--warning); }
  344. .competitor-name { font-weight: 600; margin-bottom: 4px; }
  345. .competitor-share { font-size: 12px; color: var(--text2); margin-bottom: 2px; }
  346. .competitor-strength { font-size: 12px; color: var(--text3); }
  347. /* AI生成内容卡片 */
  348. .ai-generated-card { background: linear-gradient(135deg, #f9f0ff, #f5f0ff); border: 1px solid #efdbff; border-radius: 10px; padding: 16px; margin: 16px 0; }
  349. .ai-generated-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
  350. .ai-generated-icon { font-size: 16px; }
  351. .ai-generated-title { font-size: 13px; font-weight: 600; color: var(--primary); }
  352. .ai-generated-content { font-size: 13px; line-height: 1.6; }
  353. .ai-generated-content ul { margin: 8px 0; padding-left: 20px; }
  354. .ai-generated-content li { margin: 4px 0; }
  355. .ai-generated-actions { display: flex; gap: 8px; margin-top: 12px; }
  356. /* 右侧AI助手面板 */
  357. .right-panel { width: 300px; background: var(--white); border-left: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; overflow: hidden; height: 100%; }
  358. /* split right panel: top = report elements (~40%), bottom = AI assistant (~60%) */
  359. .right-panel .element-section { flex: 4; overflow-y: auto; min-height: 0; }
  360. .right-panel .ai-assistant { flex: 6; overflow-y: auto; min-height: 0; display: flex; flex-direction: column; }
  361. /* 要素管理区 */
  362. .element-section { padding: 16px; border-bottom: 1px dashed var(--border); padding-bottom: 16px !important; }
  363. .element-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
  364. .element-title { font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
  365. .element-count { font-size: 11px; color: var(--text3); font-weight: normal; }
  366. /* 要素标签容器 */
  367. .element-tags-wrap { display: flex; flex-wrap: wrap; gap: 8px; max-height: 200px; overflow-y: auto; padding-right: 4px; padding-bottom: 16px; }
  368. .element-tags-wrap::-webkit-scrollbar { width: 4px; }
  369. .element-tags-wrap::-webkit-scrollbar-track { background: var(--bg); border-radius: 2px; }
  370. .element-tags-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
  371. .element-tags-wrap::-webkit-scrollbar-thumb:hover { background: var(--text3); }
  372. /* 要素标签样式 */
  373. .element-tag { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 16px; font-size: 12px; cursor: grab; transition: all 0.2s; user-select: none; }
  374. .element-tag:hover { border-color: var(--primary); background: var(--primary-light); transform: translateY(-1px); }
  375. .element-tag:active { cursor: grabbing; }
  376. .element-tag.dragging { opacity: 0.5; }
  377. .element-tag .tag-icon { font-size: 12px; }
  378. .element-tag .tag-name { font-weight: 500; }
  379. .element-tag.entity { border-left: 3px solid var(--primary); }
  380. .element-tag.concept { border-left: 3px solid #722ed1; }
  381. /* Tabs for elements */
  382. .element-tabs { display:flex; gap:8px; }
  383. .element-tab { padding:6px 12px; border-radius:12px; background:transparent; border:1px solid transparent; font-size:13px; cursor:pointer; color:var(--text2); }
  384. .element-tab.active { background: var(--primary); color:#fff; border-color: rgba(0,0,0,0.04); box-shadow: 0 6px 18px rgba(17,24,39,0.06); }
  385. /* Tag shape / size rules */
  386. .element-tag { height:28px; padding:0 12px; border-radius:2px; display:inline-flex; align-items:center; }
  387. .element-tag .tag-name { line-height:28px; }
  388. .element-tag.dynamic { border-radius:14px; } /* rounded for dynamic */
  389. .element-tag.static { border-radius:2px; } /* slightly rounded for static */
  390. /* module title style (icon + text), placed above header */
  391. .module-title { display:flex; align-items:center; gap:10px; font-size:15px; font-weight:700; color:var(--text1); padding:0; margin-bottom:10px; }
  392. .module-title .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: 0 6px 18px rgba(0,0,0,0.06); }
  393. .element-tag.data { border-left: 3px solid var(--success); }
  394. .element-tag.location { border-left: 3px solid var(--warning); }
  395. .element-tag.asset { border-left: 3px solid #eb2f96; }
  396. .element-hint { font-size: 11px; color: var(--text3); margin-top: 10px; text-align: center; }
  397. /* 要素详情弹出框 */
  398. .element-popover { position: fixed; width: 280px; background: var(--white); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); z-index: 2000; display: none; }
  399. .element-popover.show { display: block; }
  400. .popover-header { padding: 12px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
  401. .popover-icon { width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; }
  402. .popover-icon.entity { background: linear-gradient(135deg, #e6f7ff, #bae7ff); }
  403. .popover-icon.data { background: linear-gradient(135deg, #f6ffed, #d9f7be); }
  404. .popover-icon.location { background: linear-gradient(135deg, #fff7e6, #ffe7ba); }
  405. .popover-icon.asset { background: linear-gradient(135deg, #fff0f6, #ffd6e7); }
  406. .popover-title { font-size: 14px; font-weight: 600; flex: 1; }
  407. .popover-close { width: 24px; height: 24px; border: none; background: var(--bg); border-radius: 50%; cursor: pointer; font-size: 12px; }
  408. .popover-close:hover { background: var(--danger); color: white; }
  409. .popover-body { padding: 14px; }
  410. .popover-section { margin-bottom: 10px; }
  411. .popover-section:last-child { margin-bottom: 0; }
  412. .popover-label { font-size: 10px; color: var(--text3); margin-bottom: 4px; text-transform: uppercase; }
  413. .popover-value { font-size: 12px; color: var(--text1); }
  414. .popover-relations { display: flex; flex-wrap: wrap; gap: 6px; }
  415. .popover-relation { padding: 4px 8px; background: var(--bg); border-radius: 4px; font-size: 11px; cursor: pointer; }
  416. .popover-relation:hover { background: var(--primary-light); color: var(--primary); }
  417. .popover-actions { display: flex; gap: 8px; margin-top: 12px; }
  418. .popover-actions .btn { flex: 1; justify-content: center; font-size: 12px; }
  419. /* AI助手区 */
  420. .ai-assistant { position: relative; flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0; }
  421. .ai-header { padding: 12px 16px; border-bottom: none; display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
  422. .ai-avatar-sm { width: 36px; height: 36px; background: var(--ai-gradient); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; }
  423. .ai-info { flex: 1; }
  424. .ai-name { font-size: 15px; font-weight: 600; }
  425. .ai-status { font-size: 11px; color: var(--success); }
  426. /* Hide AI subtitle and tabs per user request */
  427. .ai-info .ai-status { display: none; }
  428. .ai-tabs { display: none !important; }
  429. /* AI Tab切换 */
  430. .ai-tabs { display: flex; border-bottom: 1px solid var(--border); flex-shrink: 0; }
  431. .ai-tab { flex: 1; padding: 10px; text-align: center; font-size: 12px; cursor: pointer; color: var(--text2); border-bottom: 2px solid transparent; transition: all 0.2s; display: flex; align-items: center; justify-content: center; gap: 4px; }
  432. .ai-tab:hover { color: var(--primary); }
  433. .ai-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
  434. /* AI消息区 */
  435. /* 消息区:为固定在底部的输入区预留底部空间,避免遮挡最后一条消息 */
  436. .ai-messages { flex: 1; overflow-y: auto; padding: 16px; padding-bottom: 96px; min-height: 0; }
  437. .msg { display: flex; gap: 10px; margin-bottom: 16px; }
  438. .msg.user { flex-direction: row-reverse; }
  439. .msg-avatar { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0; }
  440. .msg.ai .msg-avatar { background: var(--ai-gradient); color: white; }
  441. .msg.user .msg-avatar { background: var(--primary); color: white; }
  442. .msg-bubble { max-width: 85%; padding: 10px 14px; border-radius: 12px; font-size: 13px; line-height: 1.6; }
  443. .msg.ai .msg-bubble { background: var(--bg); border-radius: 4px 12px 12px 12px; }
  444. .msg.user .msg-bubble { background: var(--primary); color: white; border-radius: 12px 4px 12px 12px; }
  445. /* AI输入区 */
  446. /* 输入区固定在 AI 面板底部,不随消息滚动 */
  447. .ai-input-area { padding: 12px 16px; border-top: none; background: var(--white); flex-shrink: 0; position: absolute; left: 0; right: 0; bottom: 0; z-index: 4; }
  448. .ai-input-box { background: var(--bg); border: 1px solid var(--border); border-radius: 20px; transition: all 0.2s; }
  449. .ai-input-box:focus-within { border-color: var(--primary); background: var(--white); box-shadow: 0 0 0 3px rgba(24,144,255,0.1); }
  450. .ai-input-box textarea { width: 100%; border: none; background: transparent; resize: none; outline: none; font-size: 13px; line-height: 1.5; padding: 10px 16px; min-height: 40px; max-height: 80px; font-family: inherit; border-radius: 20px; display: block; }
  451. .ai-input-box textarea::placeholder { color: var(--text3); font-size: 14px; }
  452. /* 输入区内布局:左侧图标、中心输入、右侧操作(语音/发送) */
  453. /* Cursor-like AI input: larger rounded, subtle shadow, inline icons */
  454. .ai-input-inner { display:flex; align-items:center; gap:8px; }
  455. .ai-input-left, .ai-input-right { display:flex; gap:6px; align-items:center; flex-shrink:0; }
  456. .ai-input-box { flex:1; background:var(--white); border:1px solid var(--border); border-radius:8px; padding:8px 10px; box-shadow: 0 6px 18px rgba(16,24,40,0.06); display:flex; flex-direction:column; gap:6px; }
  457. .ai-input-label { font-size:12px; color:var(--text3); }
  458. .ai-input-row { display:flex; flex-direction:column; gap:6px; }
  459. .ai-input-row .ai-input-top { display:flex; align-items:center; gap:8px; }
  460. .ai-input-row textarea { flex:1; width:100%; border:none; outline:none; resize:none; font-size:14px; color:var(--text1); background:transparent; line-height:1.4; padding:6px 12px; min-height:20px; max-height:160px; }
  461. .ai-input-toolbar { display:flex; align-items:center; justify-content:space-between; gap:8px; height:28px; }
  462. .ai-input-toolbar .left, .ai-input-toolbar .right { display:flex; gap:12px; align-items:center; }
  463. .ai-input-box textarea::placeholder { color:var(--text3); }
  464. .ai-input-left .ai-icon-btn, .ai-input-right .ai-icon-btn { width:24px; height:24px; border-radius:4px; border:none; background:transparent; display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:12px; color:#8c8c8c; padding:0; transition: all 0.2s ease; }
  465. .ai-input-left .ai-icon-btn:hover, .ai-input-right .ai-icon-btn:hover { background:#f0f0f0; border-color:transparent; color:#555555; }
  466. .ai-send-btn { width:24px; height:24px; border-radius:4px; border:none; background:#f0f0f0; display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:12px; color:#555555; transition: all 0.18s; box-shadow:none; padding:0; }
  467. .ai-send-btn:hover { background:#1890ff; color:#ffffff; }
  468. .ai-send-btn.active { background: var(--primary); color: white; box-shadow: 0 6px 18px rgba(24,144,255,0.16); transform: translateY(-1px); }
  469. .ai-input-caption { font-size:13px; color:var(--text3); margin-bottom:6px; padding-left:6px; }
  470. .ai-input-caption .ai-highlight { color: var(--primary); font-weight:600; margin-left:6px; }
  471. .ai-input-hint { display: flex; align-items: center; justify-content: space-between; padding: 8px 4px 0; font-size: 10px; color: var(--text3); }
  472. /* 动画 */
  473. @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
  474. .parsing-anim { animation: pulse 1.5s infinite; }
  475. /* 可编辑内容区 */
  476. .editor-content[contenteditable="true"] { outline: none; }
  477. .editor-content[contenteditable="true"]:focus { background: #fafbfc; box-shadow: inset 0 0 0 2px var(--primary-light); }
  478. .editor-content[contenteditable="true"] ::selection { background: rgba(24,144,255,0.3); border-radius: 2px; }
  479. /* 文本编辑选中效果 */
  480. .editor-content[contenteditable="true"]:focus p { position: relative; }
  481. .editor-content[contenteditable="true"]:focus p::before {
  482. content: '';
  483. position: absolute;
  484. left: -20px;
  485. top: 0;
  486. bottom: 0;
  487. width: 3px;
  488. background: var(--primary);
  489. border-radius: 2px;
  490. opacity: 0;
  491. transition: opacity 0.2s;
  492. }
  493. .editor-content[contenteditable="true"]:focus p:hover::before {
  494. opacity: 0.5;
  495. }
  496. .editor-content[contenteditable="true"]:focus p.selected::before {
  497. opacity: 1;
  498. }
  499. /* 右键菜单 */
  500. .context-menu { position: fixed; min-width: 180px; background: var(--white); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); z-index: 3000; display: none; overflow: hidden; }
  501. .context-menu.show { display: block; }
  502. .context-menu-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; font-size: 13px; cursor: pointer; transition: all 0.15s; }
  503. .context-menu-item:hover { background: var(--primary-light); color: var(--primary); }
  504. .context-menu-item .icon { font-size: 14px; width: 20px; text-align: center; }
  505. .context-menu-item .shortcut { margin-left: auto; font-size: 11px; color: var(--text3); }
  506. .context-menu-divider { height: 1px; background: var(--border); margin: 4px 0; }
  507. /* 数据关系表弹窗 */
  508. .data-relation-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1300; }
  509. .data-relation-modal.show { display: flex; }
  510. .data-relation-card { width: 600px; max-width: 90vw; background: var(--white); border-radius: 16px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }
  511. .data-relation-header { padding: 20px; background: linear-gradient(135deg, #f0f7ff, #f5f0ff); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 15px; }
  512. .data-relation-icon { width: 48px; height: 48px; background: var(--white); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
  513. .data-relation-title { flex: 1; }
  514. .data-relation-title h3 { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
  515. .data-relation-title span { font-size: 12px; color: var(--text3); }
  516. .data-relation-close { width: 32px; height: 32px; border: none; background: rgba(0,0,0,0.05); border-radius: 50%; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
  517. .data-relation-close:hover { background: var(--danger); color: white; }
  518. .data-relation-body { padding: 20px; max-height: 60vh; overflow-y: auto; }
  519. .relation-section { margin-bottom: 24px; }
  520. .relation-section:last-child { margin-bottom: 0; }
  521. .relation-label { font-size: 14px; font-weight: 600; margin-bottom: 12px; color: var(--text1); display: flex; align-items: center; gap: 6px; }
  522. .relation-table { width: 100%; border-collapse: collapse; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
  523. .relation-table th { background: var(--bg); padding: 12px; text-align: left; font-size: 12px; font-weight: 600; color: var(--text2); border-bottom: 1px solid var(--border); }
  524. .relation-table td { padding: 12px; border-bottom: 1px solid var(--border); font-size: 13px; }
  525. .relation-table tr:last-child td { border-bottom: none; }
  526. .relation-table tr:hover td { background: var(--primary-light); }
  527. .relation-input { width: 100%; padding: 6px 8px; border: 1px solid var(--border); border-radius: 4px; font-size: 12px; outline: none; }
  528. .relation-input:focus { border-color: var(--primary); }
  529. .relation-tags { display: flex; flex-wrap: wrap; gap: 8px; }
  530. .relation-tag { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; background: var(--bg); border: 1px solid var(--border); border-radius: 12px; font-size: 11px; cursor: pointer; transition: all 0.2s; }
  531. .relation-tag:hover { background: var(--primary-light); border-color: var(--primary); }
  532. .data-relation-footer { padding: 16px 20px; background: var(--bg); border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; }
  533. /* 拖拽提示 */
  534. .editor-content.drag-over { background: linear-gradient(135deg, rgba(24,144,255,0.05), rgba(24,144,255,0.1)); }
  535. .editor-content.drag-over::after { content: '释放鼠标插入要素'; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 12px 24px; background: var(--primary); color: white; border-radius: 8px; font-size: 14px; z-index: 100; }
  536. /* 报告要素弹窗样式 */
  537. .report-elements-modal { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; z-index: 2200; }
  538. .report-elements-modal.show { display: flex; }
  539. .report-elements-card { width: 960px; max-width: 96vw; max-height: 80vh; background: var(--white); border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow: hidden; display:flex; flex-direction:column; }
  540. .report-elements-header { padding: 12px 16px; border-bottom: 1px solid var(--border); display:flex; align-items:center; gap:12px; }
  541. .report-elements-title { font-size: 16px; font-weight:700; flex:1; }
  542. .report-elements-body { padding: 12px 16px; overflow:auto; flex:1; }
  543. .report-elements-footer { padding: 12px 16px; border-top:1px solid var(--border); display:flex; gap:8px; justify-content:flex-end; background:var(--bg); }
  544. .elements-table { width:100%; border-collapse: collapse; }
  545. .elements-table th, .elements-table td { padding:8px 10px; text-align:left; border-bottom:1px solid var(--border); font-size:13px; vertical-align:middle; }
  546. .elements-table th { background: var(--bg); font-weight:600; font-size:12px; color:var(--text2); }
  547. .elements-search { display:flex; gap:8px; align-items:center; }
  548. .elements-search input { padding:8px 10px; border:1px solid var(--border); border-radius:6px; width:260px; }
  549. .elements-actions .icon-btn { padding: 0; border-radius: 4px; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; }
  550. /* pager styles */
  551. .pager { display:flex; gap:6px; align-items:center; }
  552. .pager-btn { padding:6px 10px; border:1px solid var(--border); background:var(--white); border-radius:6px; cursor:pointer; font-size:13px; }
  553. .pager-btn.active { background:var(--primary); color:white; border-color:var(--primary); }
  554. .page-size-select { padding:6px 8px; border:1px solid var(--border); border-radius:6px; background:var(--white); }
  555. /* Toast容器 */
  556. .toast-container { position: fixed; top: 70px; right: 24px; z-index: 3000; display: flex; flex-direction: column; gap: 8px; }
  557. .toast { display: flex; align-items: center; gap: 10px; padding: 12px 18px; background: var(--white); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); transform: translateX(120%); transition: transform 0.3s; min-width: 240px; }
  558. .toast.show { transform: translateX(0); }
  559. .toast.success { border-left: 4px solid var(--success); }
  560. .toast.error { border-left: 4px solid var(--danger); }
  561. .toast.warning { border-left: 4px solid var(--warning); }
  562. .toast.info { border-left: 4px solid var(--primary); }
  563. /* 通知面板 */
  564. .notif-panel { position: fixed; top: 56px; right: 0; width: 360px; height: calc(100vh - 56px); background: var(--white); box-shadow: -4px 0 16px rgba(0,0,0,0.1); transform: translateX(100%); transition: transform 0.3s; z-index: 999; display: flex; flex-direction: column; }
  565. /* 遮罩 */
  566. .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.3); z-index: 998; display: none; }
  567. .kg-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1199; display: none; }
  568. /* 导出菜单 */
  569. .export-menu { position: fixed; background: var(--white); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); min-width: 180px; display: none; z-index: 2001; overflow: hidden; }
  570. /* 要素关系图谱弹窗 */
  571. .knowledge-graph-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 1200px; height: 80%; max-height: 700px; background: var(--white); border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); display: none; flex-direction: column; z-index: 1200; overflow: hidden; }
  572. .knowledge-graph-modal.show { display: flex; }
  573. .kg-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: linear-gradient(135deg, #f0f7ff, #f5f0ff); }
  574. .kg-title { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 600; color: var(--primary); }
  575. .kg-icon { font-size: 20px; }
  576. .kg-controls { display: flex; align-items: center; gap: 12px; }
  577. .kg-view-toggle { display: flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
  578. .kg-view-btn { padding: 6px 12px; border: none; background: var(--white); font-size: 12px; cursor: pointer; color: var(--text2); display: flex; align-items: center; gap: 4px; transition: all 0.2s; }
  579. .kg-view-btn:first-child { border-right: 1px solid var(--border); }
  580. .kg-view-btn:hover { background: var(--bg); }
  581. .kg-view-btn.active { background: var(--primary-light); color: var(--primary); }
  582. .kg-close { width: 28px; height: 28px; border: none; background: rgba(0,0,0,0.05); border-radius: 50%; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
  583. .kg-close:hover { background: var(--danger); color: white; }
  584. .kg-content { flex: 1; overflow: hidden; }
  585. /* 图谱视图 */
  586. .kg-graph-view { height: 100%; display: flex; flex-direction: column; }
  587. .graph-canvas { flex: 1; 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; }
  588. .graph-node { position: absolute; width: 120px; padding: 12px; background: var(--white); border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); cursor: pointer; transition: all 0.3s; border: 2px solid transparent; text-align: center; }
  589. .graph-node:hover { transform: scale(1.05); box-shadow: 0 8px 24px rgba(0,0,0,0.2); }
  590. .graph-node.core { border-color: var(--primary); background: linear-gradient(135deg, #f0f7ff, #e6f7ff); }
  591. .graph-node.concept { border-color: #722ed1; background: linear-gradient(135deg, #f9f0ff, #efdbff); }
  592. .graph-node.data { border-color: var(--success); background: linear-gradient(135deg, #f6ffed, #d9f7be); }
  593. .graph-node.location { border-color: var(--warning); background: linear-gradient(135deg, #fff7e6, #ffe7ba); }
  594. .graph-node.highlighted { border-color: var(--danger); box-shadow: 0 0 20px rgba(255,77,79,0.4); transform: scale(1.1); }
  595. .node-icon { font-size: 24px; margin-bottom: 6px; }
  596. .node-label { font-size: 12px; font-weight: 600; margin-bottom: 4px; }
  597. .node-type { font-size: 10px; color: var(--text3); }
  598. .graph-lines { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
  599. .graph-legend { padding: 12px 16px; background: var(--bg); border-top: 1px solid var(--border); display: flex; gap: 16px; justify-content: center; }
  600. .legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text2); }
  601. .legend-dot { width: 12px; height: 12px; border-radius: 50%; }
  602. .legend-dot.core { background: var(--primary); }
  603. .legend-dot.concept { background: #722ed1; }
  604. .legend-dot.data { background: var(--success); }
  605. .legend-dot.location { background: var(--warning); }
  606. /* 列表视图 */
  607. .kg-list-view { height: 100%; display: flex; flex-direction: column; }
  608. .list-search { padding: 16px; border-bottom: 1px solid var(--border); }
  609. .list-search-input { width: 100%; padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; font-size: 13px; outline: none; }
  610. .list-search-input:focus { border-color: var(--primary); }
  611. .entity-categories { flex: 1; overflow-y: auto; }
  612. .category-section { margin-bottom: 16px; }
  613. .category-header { padding: 12px 16px; background: var(--bg); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--text1); }
  614. .category-icon { font-size: 16px; }
  615. .entity-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border); cursor: pointer; transition: all 0.2s; }
  616. .entity-item:hover { background: var(--primary-light); }
  617. .entity-item:last-child { border-bottom: none; }
  618. .entity-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; }
  619. .entity-info { flex: 1; }
  620. .entity-name { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
  621. .entity-meta { font-size: 11px; color: var(--text3); }
  622. .entity-actions { display: flex; gap: 4px; }
  623. .entity-action-btn { width: 24px; height: 24px; border: none; background: transparent; border-radius: 4px; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; }
  624. .entity-action-btn:hover { background: var(--bg); }
  625. /* 确认对话框 */
  626. .confirm-dialog { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1400; opacity: 0; transition: opacity 0.3s; }
  627. .confirm-dialog.show { opacity: 1; }
  628. .confirm-content { background: var(--white); border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); min-width: 400px; max-width: 500px; overflow: hidden; transform: scale(0.9); transition: transform 0.3s; }
  629. .confirm-dialog.show .confirm-content { transform: scale(1); }
  630. .confirm-header { padding: 20px; background: linear-gradient(135deg, #f0f7ff, #f5f0ff); border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
  631. .confirm-header h3 { font-size: 16px; font-weight: 600; margin: 0; }
  632. .confirm-close { width: 32px; height: 32px; border: none; background: rgba(0,0,0,0.05); border-radius: 50%; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
  633. .confirm-close:hover { background: var(--danger); color: white; }
  634. .confirm-body { padding: 20px; font-size: 14px; line-height: 1.6; color: var(--text1); }
  635. .confirm-footer { padding: 16px 20px; background: var(--bg); border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; }
  636. /* 响应式 */
  637. @media (max-width: 1024px) {
  638. .sidebar { width: 200px; }
  639. .main { margin-left: 200px; }
  640. .editor-body { flex-direction: column; }
  641. .left-panel, .right-panel { width: 100%; height: auto; max-height: 300px; }
  642. .knowledge-graph-modal { width: 100%; }
  643. .data-relation-modal { width: 95%; }
  644. .confirm-content { min-width: 320px; }
  645. }
  646. </style>
  647. <script>
  648. // === 标题同步:将正文 H1 的文字同步到编辑器顶部小标题与输入中 ===
  649. function syncEditorTitleFromContent() {
  650. try {
  651. const h1 = document.querySelector('.editor-content h1');
  652. const toolbarTitle = document.querySelector('.editor-main-title');
  653. const titleInput = document.querySelector('.report-title-input');
  654. if (h1 && toolbarTitle) {
  655. const txt = h1.textContent.trim();
  656. toolbarTitle.textContent = txt;
  657. if (titleInput) titleInput.value = txt;
  658. }
  659. } catch (e) {
  660. console.warn('syncEditorTitleFromContent error', e);
  661. }
  662. }
  663. // 同步一次并监听内容变化(以 H1 变动为触发器)
  664. document.addEventListener('DOMContentLoaded', function() {
  665. syncEditorTitleFromContent();
  666. const h1 = document.querySelector('.editor-content h1');
  667. if (h1) {
  668. const ro = new MutationObserver(() => syncEditorTitleFromContent());
  669. ro.observe(h1, { characterData: true, childList: true, subtree: true });
  670. }
  671. });
  672. </script>
  673. </head>
  674. <body>
  675. <!-- 全局头部 -->
  676. <header class="topbar" id="topbar">
  677. <div class="topbar-left">
  678. <div class="logo">灵</div>
  679. <div class="brand">灵越智报</div>
  680. </div>
  681. <div class="topbar-right">
  682. <button class="icon-btn" title="搜索"><i class="iconfont icon-SEARCH"></i></button>
  683. <button class="icon-btn notif-btn" title="通知"><i class="iconfont icon-MESSAGE_NOTIFICATION_L"></i><span class="badge">3</span></button>
  684. <div class="avatar">张</div>
  685. <div class="username">张三</div>
  686. </div>
  687. </header>
  688. <style>
  689. .topbar {
  690. height: 56px;
  691. display: flex;
  692. align-items: center;
  693. justify-content: space-between;
  694. padding: 0 18px;
  695. background: var(--white);
  696. border-bottom: 1px solid var(--border);
  697. gap: 12px;
  698. position: sticky;
  699. top: 0;
  700. z-index: 50;
  701. }
  702. .topbar-left { display:flex;align-items:center;gap:10px;min-width:180px; }
  703. .logo {
  704. width:36px;height:36px;border-radius:8px;background:linear-gradient(135deg,var(--primary) 0%, #69c0ff 100%);display:flex;align-items:center;justify-content:center;color:white;font-weight:700;font-size:18px;
  705. }
  706. .brand { font-weight:600;color:var(--text); font-size:16px; }
  707. .topbar-center { display:none; }
  708. .topbar-right { display:flex; align-items:center; gap:8px; min-width:auto; justify-content:flex-end; }
  709. .topbar-right .notif-btn { margin-right: 8px; }
  710. .avatar { width:28px;height:28px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px; margin-right: 0; }
  711. .username { color:var(--text); font-size:13px; margin-left: -4px; }
  712. .icon-btn {
  713. background: transparent;
  714. border: none;
  715. cursor: pointer;
  716. font-size: 16px;
  717. position: relative;
  718. color: #8c8c8c;
  719. width: 24px;
  720. height: 24px;
  721. display: flex;
  722. align-items: center;
  723. justify-content: center;
  724. border-radius: 4px;
  725. transition: all 0.2s ease;
  726. }
  727. .icon-btn:hover {
  728. background: #f0f0f0;
  729. color: #555555;
  730. }
  731. .icon-btn .badge { position:absolute; top:-6px; right:-6px; background:var(--danger); color:white; border-radius:10px; font-size:11px; padding:2px 6px; }
  732. @media (max-width: 900px) {
  733. .brand { display:none; }
  734. }
  735. </style>
  736. <!-- 侧边栏 -->
  737. <aside class="sidebar" id="sidebar">
  738. <nav class="sidebar-menu">
  739. <!-- 报告编辑入口已移除,页面打开时直接进入编辑器 -->
  740. </nav>
  741. </aside>
  742. <!-- 主内容区 -->
  743. <main class="main" id="mainContent" style="display:none;">
  744. <!-- 首页 -->
  745. <div class="page" id="page-home">
  746. <div class="welcome">
  747. <h1>早上好,张三!<span>智能报告,洞察未来。</span></h1>
  748. <p>今天是个创作的好日子,开始您的智能报告之旅吧</p>
  749. </div>
  750. <div class="stats-grid">
  751. <div class="stat-card card" onclick="void(0)">
  752. <div class="stat-icon blue">📄</div>
  753. <div class="stat-value">12</div>
  754. <div class="stat-label">我的报告</div>
  755. <div class="stat-trend up">↑ 3 本周新增</div>
  756. </div>
  757. <div class="stat-card card" onclick="void(0)">
  758. <div class="stat-icon purple">🎨</div>
  759. <div class="stat-value">15</div>
  760. <div class="stat-label">可用模板</div>
  761. <div class="stat-trend up">↑ 2 新增</div>
  762. </div>
  763. <div class="stat-card card">
  764. <div class="stat-icon green">📚</div>
  765. <div class="stat-value">48</div>
  766. <div class="stat-label">知识文档</div>
  767. <div class="stat-trend">📁 1.2GB</div>
  768. </div>
  769. <div class="stat-card card" onclick="toggleFab()">
  770. <div class="stat-icon orange">💰</div>
  771. <div class="stat-value">¥127.50</div>
  772. <div class="stat-label">本月消耗</div>
  773. <div class="stat-trend">↓ 12%</div>
  774. </div>
  775. </div>
  776. <!-- AI对话入口 -->
  777. <div class="ai-card card">
  778. <div class="ai-welcome">
  779. <div class="ai-avatar">🤖</div>
  780. <div class="ai-title">你好!我是灵越AI助手,可以帮你:</div>
  781. <ul class="ai-list">
  782. <li>快速生成各类报告</li>
  783. <li>分析和解读数据</li>
  784. <li>回答业务相关问题</li>
  785. </ul>
  786. <div class="ai-tip">试试输入:"帮我生成一份智慧园区建设项目可行性研究报告"</div>
  787. </div>
  788. <div class="ai-input-wrap">
  789. <input type="text" class="ai-input" placeholder="输入您的需求,或 @知识库 引用资料..." id="homeAiInput" oninput="toggleSendBtn()">
  790. <div class="ai-input-btns">
  791. <button class="ai-input-btn">🎤</button>
  792. <button class="ai-input-btn">📎</button>
  793. <button class="ai-input-btn send" id="homeSendBtn" onclick="handleHomeAi()">➤</button>
  794. </div>
  795. </div>
  796. <div class="thinking-modes">
  797. <div class="mode-btn active" onclick="setMode(this)">🧠 深度思考</div>
  798. <div class="mode-btn" onclick="setMode(this)">⚡ 快速回答</div>
  799. <div class="mode-btn" onclick="setMode(this)">🌐 联网搜索</div>
  800. <div class="mode-btn" onclick="setMode(this)">📊 数据分析</div>
  801. </div>
  802. </div>
  803. <!-- 模板推荐区 -->
  804. <div class="section-header">
  805. <h2 class="section-title">📋 推荐模板</h2>
  806. <span class="section-link" onclick="void(0)">查看全部 →</span>
  807. </div>
  808. <div class="tpl-grid">
  809. <div class="tpl-card card" onclick="void(0)">
  810. <div class="tpl-preview">📊</div>
  811. <div class="tpl-info">
  812. <div class="tpl-name">市场分析报告</div>
  813. <div class="tpl-meta"><span>📊 128次</span><span>⭐ 4.8</span></div>
  814. <div class="tpl-tags"><span class="tpl-tag">官方</span><span class="tpl-tag hot">热门</span></div>
  815. <button class="tpl-btn">使用此模板</button>
  816. </div>
  817. </div>
  818. <div class="tpl-card card" onclick="void(0)">
  819. <div class="tpl-preview">🏢</div>
  820. <div class="tpl-info">
  821. <div class="tpl-name">可行性研究报告</div>
  822. <div class="tpl-meta"><span>📊 96次</span><span>⭐ 4.9</span></div>
  823. <div class="tpl-tags"><span class="tpl-tag">官方</span><span class="tpl-tag hot">热门</span></div>
  824. <button class="tpl-btn">使用此模板</button>
  825. </div>
  826. </div>
  827. <div class="tpl-card card" onclick="void(0)">
  828. <div class="tpl-preview">📅</div>
  829. <div class="tpl-info">
  830. <div class="tpl-name">项目周报</div>
  831. <div class="tpl-meta"><span>📊 256次</span><span>⭐ 4.9</span></div>
  832. <div class="tpl-tags"><span class="tpl-tag">官方</span></div>
  833. <button class="tpl-btn">使用此模板</button>
  834. </div>
  835. </div>
  836. </div>
  837. <div class="quick-actions">
  838. <div class="quick-action" onclick="showToast('上传模板', 'info')">
  839. <div class="quick-action-icon">📤</div>
  840. <span>上传新模板</span>
  841. </div>
  842. <div class="quick-action" onclick="showToast('创建模板', 'info')">
  843. <div class="quick-action-icon">🛠️</div>
  844. <span>创建新模板</span>
  845. </div>
  846. </div>
  847. </div>
  848. <!-- 报告记录页 -->
  849. <div class="page" id="page-reports">
  850. <h2>📋 报告记录</h2>
  851. <div style="display:flex;gap:12px;margin:16px 0;">
  852. <select style="padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:13px;background:var(--white);"><option>全部状态</option><option>初稿</option><option>审核中</option><option>已定稿</option></select>
  853. <input type="text" placeholder="🔍 搜索报告..." style="flex:1;max-width:280px;padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:13px;">
  854. <div style="margin-left:auto;display:flex;gap:6px;">
  855. <button class="btn" style="background:var(--primary-light);border-color:var(--primary);color:var(--primary)">全部</button>
  856. <button class="btn">本周</button>
  857. <button class="btn">本月</button>
  858. </div>
  859. </div>
  860. <div style="display:flex;flex-direction:column;gap:12px;">
  861. <div class="card" style="padding:18px;">
  862. <div style="display:flex;align-items:center;gap:12px;margin-bottom:10px;">
  863. <span style="font-size:22px;">📄</span>
  864. <span style="flex:1;font-size:15px;font-weight:600;">智慧园区建设项目可行性研究报告</span>
  865. <span style="padding:4px 10px;background:#f6ffed;color:var(--success);border-radius:12px;font-size:11px;">已定稿</span>
  866. </div>
  867. <div style="display:flex;gap:20px;font-size:12px;color:var(--text2);margin-bottom:12px;">
  868. <span>📅 2025-12-30</span><span>👤 张三</span><span>🏢 华南事业部</span>
  869. </div>
  870. <div style="display:flex;gap:8px;">
  871. <button class="btn btn-primary" onclick="void(0)">查看</button>
  872. <button class="btn" onclick="void(0)">编辑</button>
  873. <button class="btn" onclick="showToast('导出PDF成功', 'success')">导出</button>
  874. </div>
  875. </div>
  876. <div class="card" style="padding:18px;">
  877. <div style="display:flex;align-items:center;gap:12px;margin-bottom:10px;">
  878. <span style="font-size:22px;">📄</span>
  879. <span style="flex:1;font-size:15px;font-weight:600;">Q4市场分析报告</span>
  880. <span style="padding:4px 10px;background:#fffbe6;color:var(--warning);border-radius:12px;font-size:11px;">审核中</span>
  881. </div>
  882. <div style="display:flex;gap:20px;font-size:12px;color:var(--text2);margin-bottom:12px;">
  883. <span>📅 2025-12-28</span><span>👤 张三</span>
  884. </div>
  885. <div style="display:flex;gap:8px;">
  886. <button class="btn btn-primary" onclick="void(0)">查看</button>
  887. <button class="btn" onclick="showToast('已发送催办', 'success')">催办</button>
  888. </div>
  889. </div>
  890. </div>
  891. </div>
  892. <!-- 模板管理页 -->
  893. <div class="page" id="page-templates">
  894. <h2>🎨 模板管理</h2>
  895. <div style="display:flex;gap:12px;margin:16px 0;">
  896. <input type="text" placeholder="🔍 搜索模板..." style="width:280px;padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:13px;">
  897. <div style="display:flex;gap:6px;">
  898. <span style="padding:8px 16px;background:var(--primary);color:white;border-radius:18px;font-size:12px;cursor:pointer;">全部</span>
  899. <span style="padding:8px 16px;background:var(--bg);border-radius:18px;font-size:12px;cursor:pointer;" onclick="showToast('官方模板', 'info')">官方模板</span>
  900. <span style="padding:8px 16px;background:var(--bg);border-radius:18px;font-size:12px;cursor:pointer;" onclick="showToast('我的模板', 'info')">我的模板</span>
  901. </div>
  902. <button class="btn btn-primary" style="margin-left:auto;">➕ 创建模板</button>
  903. </div>
  904. <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;">
  905. <div class="tpl-card card" onclick="void(0)"><div class="tpl-preview">📊</div><div class="tpl-info"><div class="tpl-name">市场分析报告</div><div class="tpl-meta"><span>📊 128次</span></div><div class="tpl-tags"><span class="tpl-tag">官方</span><span class="tpl-tag hot">热门</span></div><button class="tpl-btn">使用</button></div></div>
  906. <div class="tpl-card card" onclick="void(0)"><div class="tpl-preview">🏢</div><div class="tpl-info"><div class="tpl-name">可行性研究报告</div><div class="tpl-meta"><span>📊 96次</span></div><div class="tpl-tags"><span class="tpl-tag">官方</span></div><button class="tpl-btn">使用</button></div></div>
  907. <div class="tpl-card card" onclick="void(0)"><div class="tpl-preview">📅</div><div class="tpl-info"><div class="tpl-name">项目周报</div><div class="tpl-meta"><span>📊 256次</span></div><div class="tpl-tags"><span class="tpl-tag">官方</span></div><button class="tpl-btn">使用</button></div></div>
  908. <div class="tpl-card card" onclick="void(0)"><div class="tpl-preview">💼</div><div class="tpl-info"><div class="tpl-name">尽职调查报告</div><div class="tpl-meta"><span>📊 45次</span></div><div class="tpl-tags"><span class="tpl-tag">行业</span></div><button class="tpl-btn">使用</button></div></div>
  909. </div>
  910. </div>
  911. <!-- 数据源管理页 -->
  912. <div class="page" id="page-datasources">
  913. <h2>🔗 数据源管理</h2>
  914. <div style="display:flex;gap:12px;margin:16px 0;">
  915. <input type="text" placeholder="🔍 搜索数据源..." style="width:280px;padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:13px;">
  916. <button class="btn btn-primary" style="margin-left:auto;">➕ 添加数据源</button>
  917. </div>
  918. <div style="display:flex;flex-direction:column;gap:12px;">
  919. <div class="card" style="padding:18px;">
  920. <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
  921. <div style="width:44px;height:44px;background:var(--primary-light);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px;">🗄️</div>
  922. <div style="flex:1;"><div style="font-size:15px;font-weight:600;">销售数据库</div><div style="font-size:12px;color:var(--text3);">MySQL · db.company.com:3306</div></div>
  923. <div style="font-size:12px;color:var(--success);">● 已连接</div>
  924. </div>
  925. <div style="display:flex;gap:8px;"><button class="btn">测试连接</button><button class="btn">同步数据</button><button class="btn">查看数据表</button></div>
  926. </div>
  927. <div class="card" style="padding:18px;">
  928. <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
  929. <div style="width:44px;height:44px;background:#f6ffed;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px;">🌐</div>
  930. <div style="flex:1;"><div style="font-size:15px;font-weight:600;">市场数据API</div><div style="font-size:12px;color:var(--text3);">REST API · api.marketdata.com</div></div>
  931. <div style="font-size:12px;color:var(--success);">● 已连接</div>
  932. </div>
  933. <div style="display:flex;gap:8px;"><button class="btn">测试接口</button><button class="btn">查看文档</button></div>
  934. </div>
  935. </div>
  936. </div>
  937. </main>
  938. <!-- 编辑器页面 -->
  939. <div class="editor-page active" id="page-editor">
  940. <!-- 编辑器主体 -->
  941. <div class="editor-body">
  942. <!-- 左侧项目文件面板 -->
  943. <div class="left-panel">
  944. <div class="panel-header panel-header-tabs">
  945. <div class="tabs-left">
  946. <button class="panel-tab active" id="tabDocsTop" onclick="switchLeftTab('docs')">我的文档</button>
  947. <button class="panel-tab" id="tabFilesTop" onclick="switchLeftTab('files')">我的附件</button>
  948. </div>
  949. <div style="display:flex;align-items:center;gap:8px;">
  950. <button class="panel-toggle-btn" title="折叠资源面板" onclick="toggleResourcePanel(this)" style="margin-left:8px;">❮</button>
  951. </div>
  952. </div>
  953. <div class="panel-body">
  954. <!-- 内部旧 tabs 已移除 -->
  955. <div class="doc-section" id="leftDocsSection">
  956. <div class="doc-section-header" id="reportsHeader">
  957. <div id="reportsTitle">报告记录 <span id="reportsCount" style="font-weight:600">· 3</span></div>
  958. <div style="display:flex;align-items:center;gap:8px;">
  959. <button class="icon-btn" id="leftSearchBtn" title="搜索"><i class="iconfont icon-SEARCH"></i></button>
  960. <button class="icon-btn" id="newReportBtn" title="新建报告"><i class="iconfont icon-CREATE"></i></button>
  961. </div>
  962. </div>
  963. <div class="docs-area">
  964. <div class="doc-list">
  965. <div class="doc-card file-item" onclick="showFilePreview(this)" data-content="智慧园区建设项目可行性研究报告 原文预览..." data-name="智慧园区建设项目可行性研究报告">
  966. <div class="doc-thumb">📄</div>
  967. <div class="file-actions">
  968. <button class="action-btn" onclick="downloadReport(this)" title="下载">⬇️</button>
  969. <button class="action-btn" onclick="archiveReport(this)" title="归档">🗂️</button>
  970. <button class="action-btn" onclick="deleteReport(this)" title="删除">🗑️</button>
  971. </div>
  972. <div class="doc-meta">
  973. <div class="doc-title">智慧园区建设项目可行性研究报告</div>
  974. <div class="doc-time">2025/12/30 00:00:00</div>
  975. </div>
  976. </div>
  977. <div class="doc-card file-item" onclick="showFilePreview(this)" data-content="华东市场分析 原文预览..." data-name="华东市场分析">
  978. <div class="doc-thumb">📄</div>
  979. <div class="file-actions">
  980. <button class="action-btn" onclick="downloadReport(this)" title="下载">⬇️</button>
  981. <button class="action-btn" onclick="archiveReport(this)" title="归档">🗂️</button>
  982. <button class="action-btn" onclick="deleteReport(this)" title="删除">🗑️</button>
  983. </div>
  984. <div class="doc-meta">
  985. <div class="doc-title">华东市场分析</div>
  986. <div class="doc-time">2026/1/26 01:51:44</div>
  987. </div>
  988. </div>
  989. <div class="doc-card file-item" onclick="showFilePreview(this)" data-content="季度销售简报 原文预览..." data-name="季度销售简报">
  990. <div class="doc-thumb">📄</div>
  991. <div class="file-actions">
  992. <button class="action-btn" onclick="downloadReport(this)" title="下载">⬇️</button>
  993. <button class="action-btn" onclick="archiveReport(this)" title="归档">🗂️</button>
  994. <button class="action-btn" onclick="deleteReport(this)" title="删除">🗑️</button>
  995. </div>
  996. <div class="doc-meta">
  997. <div class="doc-title">季度销售简报</div>
  998. <div class="doc-time">2026/1/24 13:51:44</div>
  999. </div>
  1000. </div>
  1001. </div>
  1002. </div>
  1003. <div class="recent-area">
  1004. <div class="recent-section">
  1005. <div class="section-title small">最近操作</div>
  1006. <div class="recent-list" id="recentList">
  1007. <div class="recent-item" title="AI 已生成报告初稿:季度销售简报..."><span class="recent-text">AI 已生成报告初稿:季度销售简报...</span></div>
  1008. <div class="recent-item" title="文件 '市场调研数据.pdf' 解析完成"><span class="recent-text">文件 '市场调研数据.pdf' 解析完成</span></div>
  1009. <div class="recent-item" title="附件 '财务预测表.xlsx' 解析完成"><span class="recent-text">附件 '财务预测表.xlsx' 解析完成</span></div>
  1010. <div class="recent-item" title="用户 张三 上传文件 '公司报表.xlsx'"><span class="recent-text">用户 张三 上传文件 '公司报表.xlsx'</span></div>
  1011. </div>
  1012. </div>
  1013. </div>
  1014. </div>
  1015. <div class="doc-section" id="leftFilesSection" style="display:none;">
  1016. <div class="doc-section-header">
  1017. <div>我的附件</div>
  1018. <div class="badge-count" id="leftFilesCount">0</div>
  1019. </div>
  1020. <div class="doc-list">
  1021. <!-- Attachments will be listed here -->
  1022. </div>
  1023. </div>
  1024. </div>
  1025. </div>
  1026. <!-- 中间编辑区 -->
  1027. <div class="center-panel">
  1028. <div class="editor-title-bar" style="display:flex;justify-content:space-between;align-items:center;gap:12px;">
  1029. <div style="display:flex;align-items:center;gap:12px;">
  1030. <div class="editor-main-title" id="editorMainTitle" role="button" tabindex="0" title="报告标题">智慧园区建设项目可行性研究报告</div>
  1031. <span id="reportStatusTag" class="report-status" title="点击切换状态">草稿</span>
  1032. </div>
  1033. <div class="editor-actions" style="display:flex;align-items:center;gap:8px;">
  1034. <button class="icon-btn" id="toggleViewBtn" title="切换:原文/标记" onclick="toggleView()"><i class="iconfont icon-BIG_PROMOTION"></i></button>
  1035. <button class="icon-btn" id="reportElementsTopBtn" title="报告要素" onclick="openReportElementsModal()"><i class="iconfont icon-COMPONENTS"></i></button>
  1036. <button class="icon-btn" id="exportBtn" title="导出" onclick="showExportMenu(this)"><i class="iconfont icon-UPLOAD"></i></button>
  1037. <button class="icon-btn" id="moreBtn" title="更多" onclick="showMoreMenu(this)">⋯</button>
  1038. </div>
  1039. </div>
  1040. <div class="editor-scroll">
  1041. <!-- 原文视图 -->
  1042. <div class="editor-content" id="contentOriginal" contenteditable="true" oncontextmenu="showContextMenu(event)">
  1043. <h1>智慧园区建设项目可行性研究报告</h1>
  1044. <h2>一、项目背景</h2>
  1045. <p>随着数字经济的快速发展,智慧园区已成为推动产业升级和城市现代化的重要载体。本项目旨在构建集智能化管理、低碳绿色、产业协同于一体的新型智慧园区。</p>
  1046. <h3>1.1 行业现状</h3>
  1047. <p>根据最新市场调研数据显示,2024年中国智慧园区市场规模已达到1,789亿元,同比增长18%,预计2025年将突破2,100亿元。</p>
  1048. <!-- 数据表格 -->
  1049. <div class="data-table-card">
  1050. <div class="data-table-header">
  1051. <div class="data-table-title">
  1052. <span>📊</span>
  1053. <span>市场规模数据</span>
  1054. </div>
  1055. <div class="data-table-source">来源: 市场调研数据.pdf</div>
  1056. </div>
  1057. <table class="data-table">
  1058. <thead>
  1059. <tr>
  1060. <th>年份</th>
  1061. <th>市场规模(亿元)</th>
  1062. <th>同比增长</th>
  1063. </tr>
  1064. </thead>
  1065. <tbody>
  1066. <tr><td>2022</td><td>1,280</td><td>15.2%</td></tr>
  1067. <tr><td>2023</td><td>1,516</td><td>18.4%</td></tr>
  1068. <tr><td>2024</td><td>1,789</td><td>18.0%</td></tr>
  1069. <tr><td>2025E</td><td>2,100</td><td>17.4%</td></tr>
  1070. </tbody>
  1071. </table>
  1072. </div>
  1073. <h2>二、项目概述</h2>
  1074. <p>本项目位于华南地区核心区域,规划总面积约50万平方米,预计总投资12.5亿元。项目将分三期建设,首期重点打造智能制造产业集群和数字服务中心。</p>
  1075. <h3>2.1 项目定位</h3>
  1076. <p>本项目致力于打造粤港澳大湾区最具代表性的智慧园区标杆项目,通过引入先进的信息技术和管理理念,实现产业数字化转型和高质量发展。</p>
  1077. <h3>2.2 建设内容</h3>
  1078. <p>项目建设内容主要包括智慧基础设施建设、产业服务平台搭建、数字化管理平台开发等三大方面,总建筑面积约35万平方米。</p>
  1079. <h2>三、投资估算</h2>
  1080. <p>项目总投资为12.5亿元,其中建设投资10.2亿元,流动资金2.3亿元。资金来源包括政府投资4亿元、企业自筹6.5亿元、银行贷款2亿元。</p>
  1081. <h2>四、市场分析</h2>
  1082. <p>当前智慧园区市场需求旺盛,随着产业数字化转型的加速推进,预计未来5年内智慧园区市场规模将保持20%以上的年增长率。</p>
  1083. <h2>五、经济效益分析</h2>
  1084. <p>项目建设期预计3年,运营期20年。项目建成后预计年营业收入8.6亿元,年利润总额2.4亿元,投资回收期约为6.2年。</p>
  1085. </div>
  1086. <!-- 标记视图 -->
  1087. <div class="editor-content" id="contentMarked" style="display:none;" contenteditable="true" oncontextmenu="showContextMenu(event)">
  1088. <h1>智慧园区建设项目可行性研究报告</h1>
  1089. <h2>一、项目背景</h2>
  1090. <p>随着<span class="entity-highlight" onclick="showDataRelationModal('数字经济', '产业趋势', 'concept', '技术方案说明.pdf')" contenteditable="false">数字经济</span>的快速发展,<span class="entity-highlight" onclick="showDataRelationModal('智慧园区', '核心实体', 'entity', '项目可行性研究报告.docx')" contenteditable="false">智慧园区</span>已成为推动<span class="entity-highlight" onclick="showDataRelationModal('产业升级', '概念', 'concept', '项目可行性研究报告.docx')" contenteditable="false">产业升级</span>和<span class="entity-highlight" onclick="showDataRelationModal('城市现代化', '概念', 'concept', '项目可行性研究报告.docx')" contenteditable="false">城市现代化</span>的重要载体。本项目旨在构建集<span class="entity-highlight" onclick="showDataRelationModal('智能化管理', '技术', 'concept', '技术方案说明.pdf')" contenteditable="false">智能化管理</span>、<span class="entity-highlight" onclick="showDataRelationModal('低碳绿色', '概念', 'concept', '项目可行性研究报告.docx')" contenteditable="false">低碳绿色</span>、<span class="entity-highlight" onclick="showDataRelationModal('产业协同', '模式', 'concept', '项目可行性研究报告.docx')" contenteditable="false">产业协同</span>于一体的新型智慧园区。</p>
  1091. <!-- AI优化建议卡片 -->
  1092. <div class="ai-suggestion-card" id="aiSuggestionCard" contenteditable="false">
  1093. <div class="ai-suggestion-header">
  1094. <span class="ai-suggestion-icon">💡</span>
  1095. <span class="ai-suggestion-title">AI 优化建议</span>
  1096. </div>
  1097. <div class="ai-suggestion-content">
  1098. 此处可补充具体的政策文件引用,增强论述的权威性。已从《市场调研数据.pdf》中提取到《"十四五"数字经济发展规划》等相关政策信息。
  1099. </div>
  1100. <div class="ai-suggestion-actions">
  1101. <button class="suggest-btn accept" onclick="acceptSuggestion()">✓ 采纳建议</button>
  1102. <button class="suggest-btn ignore" onclick="ignoreSuggestion()">✕ 忽略</button>
  1103. </div>
  1104. </div>
  1105. <h3>1.1 行业现状</h3>
  1106. <p>根据最新市场调研数据显示,2024年中国<span class="entity-highlight" onclick="showDataRelationModal('智慧园区', '核心实体', 'entity', '市场调研数据.pdf')" contenteditable="false">智慧园区</span>市场规模已达到<span class="entity-highlight" onclick="showDataRelationModal('1,789亿元', '市场规模数据', 'data', '市场调研数据.pdf')" contenteditable="false">1,789亿元</span>,同比增长<span class="entity-highlight" onclick="showDataRelationModal('18%', '增长率数据', 'data', '市场调研数据.pdf')" contenteditable="false">18%</span>,预计2025年将突破<span class="entity-highlight" onclick="showDataRelationModal('2,100亿元', '预测数据', 'data', '市场调研数据.pdf')" contenteditable="false">2,100亿元</span>。</p>
  1107. <!-- 数据表格 -->
  1108. <div class="data-table-card" contenteditable="false">
  1109. <div class="data-table-header">
  1110. <div class="data-table-title">
  1111. <span>📊</span>
  1112. <span>市场规模数据</span>
  1113. </div>
  1114. <div class="data-table-source">来源: 市场调研数据.pdf</div>
  1115. </div>
  1116. <table class="data-table">
  1117. <thead>
  1118. <tr>
  1119. <th>年份</th>
  1120. <th>市场规模(亿元)</th>
  1121. <th>同比增长</th>
  1122. </tr>
  1123. </thead>
  1124. <tbody>
  1125. <tr><td>2022</td><td><span class="entity-highlight" onclick="showDataRelationModal('1,280亿元', '历史数据', 'data', '市场调研数据.pdf')" contenteditable="false">1,280</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('15.2%', '增长率', 'data', '市场调研数据.pdf')" contenteditable="false">15.2%</span></td></tr>
  1126. <tr><td>2023</td><td><span class="entity-highlight" onclick="showDataRelationModal('1,516亿元', '历史数据', 'data', '市场调研数据.pdf')" contenteditable="false">1,516</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('18.4%', '增长率', 'data', '市场调研数据.pdf')" contenteditable="false">18.4%</span></td></tr>
  1127. <tr><td>2024</td><td><span class="entity-highlight" onclick="showDataRelationModal('1,789亿元', '当前数据', 'data', '市场调研数据.pdf')" contenteditable="false">1,789</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('18.0%', '增长率', 'data', '市场调研数据.pdf')" contenteditable="false">18.0%</span></td></tr>
  1128. <tr><td><span class="entity-highlight" onclick="showDataRelationModal('2025E', '预测年份', 'data', '市场调研数据.pdf')" contenteditable="false">2025E</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('2,100亿元', '预测数据', 'data', '市场调研数据.pdf')" contenteditable="false">2,100</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('17.4%', '预测增长率', 'data', '市场调研数据.pdf')" contenteditable="false">17.4%</span></td></tr>
  1129. </tbody>
  1130. </table>
  1131. </div>
  1132. <h2>二、项目概述</h2>
  1133. <p>本项目位于<span class="entity-highlight" onclick="showDataRelationModal('华南地区', '地理位置', 'location', '项目可行性研究报告.docx')" contenteditable="false">华南地区</span>核心区域,规划总面积约<span class="entity-highlight" onclick="showDataRelationModal('50万平方米', '面积数据', 'data', '项目可行性研究报告.docx')" contenteditable="false">50万平方米</span>,预计总投资<span class="entity-highlight" onclick="showDataRelationModal('12.5亿元', '投资额', 'data', '项目可行性研究报告.docx')" contenteditable="false">12.5亿元</span>。项目将分<span class="entity-highlight" onclick="showDataRelationModal('三期', '建设周期', 'data', '项目可行性研究报告.docx')" contenteditable="false">三期</span>建设,首期重点打造<span class="entity-highlight" onclick="showDataRelationModal('智能制造产业集群', '产业类型', 'concept', '项目可行性研究报告.docx')" contenteditable="false">智能制造产业集群</span>和<span class="entity-highlight" onclick="showDataRelationModal('数字服务中心', '基础设施', 'concept', '项目可行性研究报告.docx')" contenteditable="false">数字服务中心</span>。</p>
  1134. <h3>2.1 项目定位</h3>
  1135. <p>本项目致力于打造<span class="entity-highlight" onclick="showDataRelationModal('粤港澳大湾区', '区域', 'location', '项目可行性研究报告.docx')" contenteditable="false">粤港澳大湾区</span>最具代表性的<span class="entity-highlight" onclick="showDataRelationModal('智慧园区标杆项目', '项目类型', 'concept', '项目可行性研究报告.docx')" contenteditable="false">智慧园区标杆项目</span>,通过引入先进的信息技术和管理理念,实现<span class="entity-highlight" onclick="showDataRelationModal('产业数字化转型', '转型目标', 'concept', '项目可行性研究报告.docx')" contenteditable="false">产业数字化转型</span>和<span class="entity-highlight" onclick="showDataRelationModal('高质量发展', '发展目标', 'concept', '项目可行性研究报告.docx')" contenteditable="false">高质量发展</span>。</p>
  1136. <h3>2.2 建设内容</h3>
  1137. <p>项目建设内容主要包括<span class="entity-highlight" onclick="showDataRelationModal('智慧基础设施', '建设内容', 'concept', '项目可行性研究报告.docx')" contenteditable="false">智慧基础设施</span>建设、<span class="entity-highlight" onclick="showDataRelationModal('产业服务平台', '建设内容', 'concept', '项目可行性研究报告.docx')" contenteditable="false">产业服务平台</span>搭建、<span class="entity-highlight" onclick="showDataRelationModal('数字化管理平台', '建设内容', 'concept', '项目可行性研究报告.docx')" contenteditable="false">数字化管理平台</span>开发等三大方面,总建筑面积约<span class="entity-highlight" onclick="showDataRelationModal('35万平方米', '建筑面积', 'data', '项目可行性研究报告.docx')" contenteditable="false">35万平方米</span>。</p>
  1138. <!-- 数据引用卡片 -->
  1139. <div class="data-reference-card" contenteditable="false">
  1140. <div class="reference-header">
  1141. <span class="reference-icon">🔗</span>
  1142. <span class="reference-title">数据引用验证</span>
  1143. </div>
  1144. <div class="reference-content">
  1145. <div class="reference-item">
  1146. <span class="reference-label">来源文档:</span>
  1147. <span class="reference-value">技术方案说明.pdf (第3页)</span>
  1148. </div>
  1149. <div class="reference-item">
  1150. <span class="reference-label">验证状态:</span>
  1151. <span class="reference-value success">✓ 已验证</span>
  1152. </div>
  1153. <div class="reference-item">
  1154. <span class="reference-label">置信度:</span>
  1155. <span class="reference-value">95.8%</span>
  1156. </div>
  1157. </div>
  1158. </div>
  1159. <h2>三、投资估算</h2>
  1160. <p>项目总投资为<span class="entity-highlight" onclick="showDataRelationModal('12.5亿元', '总投资', 'data', '财务预测表.xlsx')" contenteditable="false">12.5亿元</span>,其中建设投资<span class="entity-highlight" onclick="showDataRelationModal('10.2亿元', '建设投资', 'data', '财务预测表.xlsx')" contenteditable="false">10.2亿元</span>,流动资金<span class="entity-highlight" onclick="showDataRelationModal('2.3亿元', '流动资金', 'data', '财务预测表.xlsx')" contenteditable="false">2.3亿元</span>。资金来源包括政府投资<span class="entity-highlight" onclick="showDataRelationModal('4亿元', '政府投资', 'data', '财务预测表.xlsx')" contenteditable="false">4亿元</span>、企业自筹<span class="entity-highlight" onclick="showDataRelationModal('6.5亿元', '自筹资金', 'data', '财务预测表.xlsx')" contenteditable="false">6.5亿元</span>、银行贷款<span class="entity-highlight" onclick="showDataRelationModal('2亿元', '贷款资金', 'data', '财务预测表.xlsx')" contenteditable="false">2亿元</span>。</p>
  1161. <h2>四、市场分析</h2>
  1162. <p>当前<span class="entity-highlight" onclick="showDataRelationModal('智慧园区', '产品', 'entity', '市场调研数据.pdf')" contenteditable="false">智慧园区</span>市场需求旺盛,随着<span class="entity-highlight" onclick="showDataRelationModal('产业数字化转型', '趋势', 'concept', '市场调研数据.pdf')" contenteditable="false">产业数字化转型</span>的加速推进,预计未来<span class="entity-highlight" onclick="showDataRelationModal('5年', '时间周期', 'data', '市场调研数据.pdf')" contenteditable="false">5年</span>内智慧园区市场规模将保持<span class="entity-highlight" onclick="showDataRelationModal('20%', '增长率预测', 'data', '市场调研数据.pdf')" contenteditable="false">20%</span>以上的年增长率。</p>
  1163. <!-- 竞争分析卡片 -->
  1164. <div class="competition-card" contenteditable="false">
  1165. <div class="competition-header">
  1166. <span class="competition-icon">🏆</span>
  1167. <span class="competition-title">竞争格局分析</span>
  1168. </div>
  1169. <div class="competition-content">
  1170. <div class="competition-item">
  1171. <div class="competitor-name">领先企业A</div>
  1172. <div class="competitor-share">市场份额: 28%</div>
  1173. <div class="competitor-strength">优势: 技术领先</div>
  1174. </div>
  1175. <div class="competition-item">
  1176. <div class="competitor-name">新兴企业B</div>
  1177. <div class="competitor-share">市场份额: 18%</div>
  1178. <div class="competitor-strength">优势: 服务创新</div>
  1179. </div>
  1180. <div class="competition-item">
  1181. <div class="competitor-name">本项目</div>
  1182. <div class="competitor-share">目标份额: 15%</div>
  1183. <div class="competitor-strength">优势: 区域特色</div>
  1184. </div>
  1185. </div>
  1186. </div>
  1187. <h2>五、经济效益分析</h2>
  1188. <p>项目建设期预计<span class="entity-highlight" onclick="showDataRelationModal('3年', '建设期', 'data', '财务预测表.xlsx')" contenteditable="false">3年</span>,运营期<span class="entity-highlight" onclick="showDataRelationModal('20年', '运营期', 'data', '财务预测表.xlsx')" contenteditable="false">20年</span>。项目建成后预计年营业收入<span class="entity-highlight" onclick="showDataRelationModal('8.6亿元', '年收入', 'data', '财务预测表.xlsx')" contenteditable="false">8.6亿元</span>,年利润总额<span class="entity-highlight" onclick="showDataRelationModal('2.4亿元', '年利润', 'data', '财务预测表.xlsx')" contenteditable="false">2.4亿元</span>,投资回收期约为<span class="entity-highlight" onclick="showDataRelationModal('6.2年', '回收期', 'data', '财务预测表.xlsx')" contenteditable="false">6.2年</span>。</p>
  1189. <!-- 财务预测表格 -->
  1190. <div class="data-table-card" contenteditable="false">
  1191. <div class="data-table-header">
  1192. <div class="data-table-title">
  1193. <span>💰</span>
  1194. <span>财务预测数据</span>
  1195. </div>
  1196. <div class="data-table-source">来源: 财务预测表.xlsx</div>
  1197. </div>
  1198. <table class="data-table">
  1199. <thead>
  1200. <tr>
  1201. <th>年份</th>
  1202. <th>营业收入(亿元)</th>
  1203. <th>利润总额(亿元)</th>
  1204. <th>投资回收</th>
  1205. </tr>
  1206. </thead>
  1207. <tbody>
  1208. <tr><td>第1年</td><td><span class="entity-highlight" onclick="showDataRelationModal('6.2亿元', '第1年收入', 'data', '财务预测表.xlsx')" contenteditable="false">6.2</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('1.8亿元', '第1年利润', 'data', '财务预测表.xlsx')" contenteditable="false">1.8</span></td><td>运营初期</td></tr>
  1209. <tr><td>第2年</td><td><span class="entity-highlight" onclick="showDataRelationModal('7.4亿元', '第2年收入', 'data', '财务预测表.xlsx')" contenteditable="false">7.4</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('2.1亿元', '第2年利润', 'data', '财务预测表.xlsx')" contenteditable="false">2.1</span></td><td>快速增长</td></tr>
  1210. <tr><td>第3年</td><td><span class="entity-highlight" onclick="showDataRelationModal('8.6亿元', '第3年收入', 'data', '财务预测表.xlsx')" contenteditable="false">8.6</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('2.4亿元', '第3年利润', 'data', '财务预测表.xlsx')" contenteditable="false">2.4</span></td><td>达产年份</td></tr>
  1211. </tbody>
  1212. </table>
  1213. </div>
  1214. <!-- AI生成内容建议 -->
  1215. <div class="ai-generated-card" contenteditable="false">
  1216. <div class="ai-generated-header">
  1217. <span class="ai-generated-icon">✨</span>
  1218. <span class="ai-generated-title">AI 生成内容</span>
  1219. </div>
  1220. <div class="ai-generated-content">
  1221. <p><strong>风险分析:</strong></p>
  1222. <ul>
  1223. <li>技术风险:新兴技术应用的不确定性</li>
  1224. <li>市场风险:需求变化和竞争加剧</li>
  1225. <li>运营风险:团队建设和管理挑战</li>
  1226. <li>财务风险:资金链和投资回收压力</li>
  1227. </ul>
  1228. <div class="ai-generated-actions">
  1229. <button class="btn btn-primary" onclick="insertGeneratedContent()">➕ 应用内容</button>
  1230. <button class="btn" onclick="showToast('已忽略AI建议', 'info')">✕ 忽略</button>
  1231. </div>
  1232. </div>
  1233. </div>
  1234. </div>
  1235. </div>
  1236. </div>
  1237. <!-- 右侧AI助手面板 -->
  1238. <div class="right-panel">
  1239. <!-- 报告要素区(上半) -->
  1240. <div class="element-section">
  1241. <div class="module-title" role="button" aria-label="打开报告要素列表">
  1242. <div class="module-icon">📋</div>
  1243. <div class="module-text">报告要素</div>
  1244. <div class="elements-actions" style="display:flex;align-items:center;gap:8px;margin-left:8px;">
  1245. <!-- report elements opener moved to editor actions -->
  1246. </div>
  1247. </div>
  1248. <div class="element-header" style="display:flex;align-items:center;justify-content:space-between;padding-top:4px;border-top:1px solid transparent;">
  1249. <div class="element-tabs" style="display:flex;align-items:center;gap:8px;">
  1250. <div class="element-tab active" id="tabDynamic" onclick="switchElementTab('dynamic')">动态要素</div>
  1251. <div class="element-tab" id="tabStatic" onclick="switchElementTab('static')">静态要素</div>
  1252. </div>
  1253. <div>
  1254. <button class="icon-btn" id="elementSearchBtn" title="搜索" style="margin-left:8px;"><i class="iconfont icon-SEARCH"></i></button>
  1255. </div>
  1256. </div>
  1257. <!-- 要素标签容器:动态 / 静态 两个面板,使用 tab 切换 -->
  1258. <div id="dynamicTags" class="element-tags-wrap">
  1259. <!-- 动态要素(AI 规则计算产生)示例 -->
  1260. <span class="element-tag dynamic" draggable="true" ondragstart="handleTagDragStart(event, '市场上升信号')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'dynamic1')">
  1261. <span class="tag-icon">📈</span>
  1262. <span class="tag-name">市场上升信号</span>
  1263. </span>
  1264. <span class="element-tag dynamic" draggable="true" ondragstart="handleTagDragStart(event, '增长偏好')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'dynamic2')">
  1265. <span class="tag-icon">⚡</span>
  1266. <span class="tag-name">增长偏好</span>
  1267. </span>
  1268. </div>
  1269. <div id="staticTags" class="element-tags-wrap" style="display:none;">
  1270. <!-- 静态要素:直接来自原文采集(保留原有示例) -->
  1271. <span class="element-tag static entity" draggable="true" ondragstart="handleTagDragStart(event, '智慧园区')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'smartpark')">
  1272. <span class="tag-icon">🏢</span>
  1273. <span class="tag-name">智慧园区</span>
  1274. </span>
  1275. <span class="element-tag static concept" draggable="true" ondragstart="handleTagDragStart(event, '产业升级')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'upgrade')">
  1276. <span class="tag-icon">📈</span>
  1277. <span class="tag-name">产业升级</span>
  1278. </span>
  1279. <span class="element-tag static concept" draggable="true" ondragstart="handleTagDragStart(event, '城市现代化')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'modern')">
  1280. <span class="tag-icon">🌆</span>
  1281. <span class="tag-name">城市现代化</span>
  1282. </span>
  1283. <span class="element-tag static concept" draggable="true" ondragstart="handleTagDragStart(event, '智能化管理')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'ai')">
  1284. <span class="tag-icon">🤖</span>
  1285. <span class="tag-name">智能化管理</span>
  1286. </span>
  1287. <span class="element-tag static data" draggable="true" ondragstart="handleTagDragStart(event, '1,789亿元')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'data1')">
  1288. <span class="tag-icon">💰</span>
  1289. <span class="tag-name">1,789亿元</span>
  1290. </span>
  1291. <span class="element-tag static data" draggable="true" ondragstart="handleTagDragStart(event, '18%')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'data2')">
  1292. <span class="tag-icon">📊</span>
  1293. <span class="tag-name">18%</span>
  1294. </span>
  1295. </div>
  1296. </div>
  1297. <!-- 要素详情弹出框 -->
  1298. <div class="element-popover" id="elementPopover">
  1299. <div class="popover-header">
  1300. <div class="popover-icon entity" id="popoverIcon">🏢</div>
  1301. <div class="popover-title" id="popoverTitle">智慧园区</div>
  1302. <button class="popover-close" onclick="hideTagPopover()">×</button>
  1303. </div>
  1304. <div class="popover-body">
  1305. <div class="popover-section">
  1306. <div class="popover-label">类型</div>
  1307. <div class="popover-value" id="popoverType">核心实体</div>
  1308. </div>
  1309. <div class="popover-section">
  1310. <div class="popover-label">来源</div>
  1311. <div class="popover-value" id="popoverSource">项目可行性研究报告.docx</div>
  1312. </div>
  1313. <div class="popover-section">
  1314. <div class="popover-label">关联要素</div>
  1315. <div class="popover-relations" id="popoverRelations">
  1316. <span class="popover-relation">→ 产业升级</span>
  1317. <span class="popover-relation">→ 城市现代化</span>
  1318. </div>
  1319. </div>
  1320. <div class="popover-actions">
  1321. <button class="btn" onclick="showToast('已定位到文档', 'info');hideTagPopover();">📍 定位</button>
  1322. <button class="btn btn-primary" onclick="insertTagToEditor();hideTagPopover();">➕ 插入</button>
  1323. </div>
  1324. </div>
  1325. </div>
  1326. <!-- AI助手区 -->
  1327. <div class="ai-assistant">
  1328. <div class="ai-header">
  1329. <div class="ai-avatar-sm">🤖</div>
  1330. <div class="ai-info">
  1331. <div class="ai-name">AI助手</div>
  1332. <div class="ai-status">● 已加载项目上下文</div>
  1333. </div>
  1334. </div>
  1335. <!-- AI Tab切换 -->
  1336. <div class="ai-tabs">
  1337. <div class="ai-tab active" onclick="switchAiTab(this, 'chat')">💬 对话</div>
  1338. <div class="ai-tab" onclick="switchAiTab(this, 'suggest')">💡 建议</div>
  1339. <div class="ai-tab" onclick="switchAiTab(this, 'memory')">🧠 记忆</div>
  1340. </div>
  1341. <!-- AI消息区 -->
  1342. <div class="ai-messages" id="aiMessages">
  1343. <div class="msg ai">
  1344. <div class="msg-avatar">🤖</div>
  1345. <div class="msg-bubble">您好!我已分析上传的5份文档,构建了项目知识图谱。有什么可以帮您的?</div>
  1346. </div>
  1347. <div class="msg user">
  1348. <div class="msg-avatar">张</div>
  1349. <div class="msg-bubble">帮我补充市场分析部分的竞争格局内容</div>
  1350. </div>
  1351. <div class="msg ai">
  1352. <div class="msg-avatar">🤖</div>
  1353. <div class="msg-bubble">好的,我已从《市场调研数据.pdf》中提取了竞争格局相关数据。建议如下:</div>
  1354. </div>
  1355. <!-- AI输入区 -->
  1356. <div class="ai-input-area">
  1357. <div class="ai-input-inner">
  1358. <div class="ai-input-box">
  1359. <div class="ai-input-row">
  1360. <div class="ai-input-top">
  1361. <textarea id="aiTextarea" placeholder="发消息给AI助手" rows="1" onkeydown="handleAiKey(event)" oninput="autoResizeTextarea(this)"></textarea>
  1362. </div>
  1363. <div class="ai-input-toolbar">
  1364. <div class="left">
  1365. <!-- 1: 自定义图标(icon-CREATE) -->
  1366. <button class="ai-icon-btn" id="aiIconCreateBtn" title="上传"><i class="iconfont icon-CREATE"></i></button>
  1367. <!-- 2: 将原来的 '@' 替换为 icon-a-SYMBOLS_ -->
  1368. <button class="ai-icon-btn" title="@" onclick="showToast('@', 'info')"><i class="iconfont icon-a-SYMBOLS_"></i></button>
  1369. <!-- 3: 将 '联网' 替换为 icon-hulianwangoff -->
  1370. <button class="ai-icon-btn" title="联网" onclick="showToast('联网搜索(示意)', 'info')"><i class="iconfont icon-hulianwangoff"></i></button>
  1371. </div>
  1372. <div class="right">
  1373. <!-- 4: 将语音输入图标替换为 icon-01 -->
  1374. <button class="ai-icon-btn" title="语音输入" onclick="showToast('语音输入(示意)', 'info')"><i class="iconfont icon-01"></i></button>
  1375. <button class="ai-send-btn" id="aiSendBtn" title="发送" onclick="sendAiMsg()">⬆</button>
  1376. </div>
  1377. </div>
  1378. </div>
  1379. </div>
  1380. </div>
  1381. <!-- hint removed as requested -->
  1382. </div>
  1383. </div>
  1384. </div>
  1385. </div>
  1386. </div>
  1387. <!-- 报告要素弹窗 -->
  1388. <div class="report-elements-modal" id="reportElementsModal" aria-hidden="true">
  1389. <div class="report-elements-card" role="dialog" aria-modal="true" aria-labelledby="reportElementsTitle">
  1390. <div class="report-elements-header">
  1391. <div class="report-elements-title" id="reportElementsTitle">报告要素</div>
  1392. <div class="elements-search">
  1393. <input type="text" id="elementsSearchInput" placeholder="搜索要素名称 / 类型..." />
  1394. </div>
  1395. </div>
  1396. <div class="report-elements-body">
  1397. <table class="elements-table" id="elementsTable">
  1398. <thead>
  1399. <tr>
  1400. <th style="width:14%;">名称</th>
  1401. <th style="width:18%;">描述</th>
  1402. <th style="width:8%;">类型</th>
  1403. <th style="width:10%;">要素类型</th>
  1404. <th style="width:12%;">原值</th>
  1405. <th style="width:12%;">新值</th>
  1406. <th style="width:12%;">填充源</th>
  1407. <th style="width:8%;">操作</th>
  1408. </tr>
  1409. </thead>
  1410. <tbody id="elementsTbody">
  1411. <!-- 动态生成 -->
  1412. </tbody>
  1413. </table>
  1414. </div>
  1415. <div class="report-elements-footer">
  1416. <div style="margin-right:auto;display:flex;gap:12px;align-items:center;">
  1417. <!-- 翻页控件已移除:改由分页数字按钮显示(默认显示最多 5 个页码) -->
  1418. <div class="pagination" id="elementsPaginationInfo" style="font-size:12px;color:var(--text3);"></div>
  1419. <div style="display:flex;align-items:center;gap:8px;margin-left:12px;">
  1420. <label style="font-size:12px;color:var(--text3);">每页显示:</label>
  1421. <select id="elementsPageSize" class="page-size-select">
  1422. <option value="10">10</option>
  1423. <option value="20">20</option>
  1424. <option value="50">50</option>
  1425. </select>
  1426. </div>
  1427. </div>
  1428. <button class="btn" onclick="closeReportElementsModal()">取消</button>
  1429. <button class="btn btn-primary" id="saveElementsBtn">保存全部</button>
  1430. </div>
  1431. </div>
  1432. </div>
  1433. <!-- 右键菜单 -->
  1434. <div class="context-menu" id="contextMenu">
  1435. <div class="context-menu-item" onclick="execContextAction('copy')">
  1436. <span class="icon">📋</span>
  1437. <span>复制</span>
  1438. <span class="shortcut">Ctrl+C</span>
  1439. </div>
  1440. <div class="context-menu-item" onclick="execContextAction('cut')">
  1441. <span class="icon">✂️</span>
  1442. <span>剪切</span>
  1443. <span class="shortcut">Ctrl+X</span>
  1444. </div>
  1445. <div class="context-menu-item" onclick="execContextAction('paste')">
  1446. <span class="icon">📄</span>
  1447. <span>粘贴</span>
  1448. <span class="shortcut">Ctrl+V</span>
  1449. </div>
  1450. <div class="context-menu-divider"></div>
  1451. <div class="context-menu-item" onclick="execContextAction('polish')">
  1452. <span class="icon">✨</span>
  1453. <span>AI 润色</span>
  1454. </div>
  1455. <div class="context-menu-item" onclick="execContextAction('spell')">
  1456. <span class="icon">📝</span>
  1457. <span>检查拼写</span>
  1458. </div>
  1459. <div class="context-menu-divider"></div>
  1460. <div class="context-menu-item" onclick="execContextAction('mark')">
  1461. <span class="icon">🏷️</span>
  1462. <span>标记为要素</span>
  1463. </div>
  1464. <div class="context-menu-item" onclick="execContextAction('quote')">
  1465. <span class="icon">💬</span>
  1466. <span>引用到AI助手</span>
  1467. </div>
  1468. </div>
  1469. <!-- 数据关系表弹窗 -->
  1470. <div class="data-relation-modal" id="dataRelationModal">
  1471. <div class="data-relation-card">
  1472. <div class="data-relation-header">
  1473. <div class="data-relation-icon" id="relationIcon">🏷️</div>
  1474. <div class="data-relation-title">
  1475. <h3 id="relationEntityName">标签数据关系</h3>
  1476. <span id="relationEntityType">实体类型 · 数据来源</span>
  1477. </div>
  1478. <button class="data-relation-close" onclick="closeDataRelationModal()">×</button>
  1479. </div>
  1480. <div class="data-relation-body">
  1481. <div class="relation-section">
  1482. <div class="relation-label">📊 数据关系表</div>
  1483. <table class="relation-table">
  1484. <thead>
  1485. <tr>
  1486. <th style="width:25%">属性</th>
  1487. <th style="width:30%">原始值</th>
  1488. <th style="width:30%">当前标签值</th>
  1489. <th style="width:15%">操作</th>
  1490. </tr>
  1491. </thead>
  1492. <tbody id="relationTableBody">
  1493. <!-- 动态生成 -->
  1494. </tbody>
  1495. </table>
  1496. </div>
  1497. <div class="relation-section">
  1498. <div class="relation-label">🔗 关联要素</div>
  1499. <div class="relation-tags" id="relationTags">
  1500. <!-- 动态生成关联标签 -->
  1501. </div>
  1502. </div>
  1503. </div>
  1504. <div class="data-relation-footer">
  1505. <button class="btn" onclick="showToast('已删除标记', 'info');closeDataRelationModal();">🗑️ 删除标记</button>
  1506. <button class="btn" onclick="closeDataRelationModal()">取消</button>
  1507. <button class="btn btn-primary" onclick="saveDataRelationChanges()">保存更改</button>
  1508. </div>
  1509. </div>
  1510. </div>
  1511. <!-- 导出菜单 -->
  1512. <div class="export-menu" id="exportMenu" style="position:fixed;background:var(--white);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,0.15);min-width:180px;display:none;z-index:2001;overflow:hidden;">
  1513. <div style="padding:12px 16px;cursor:pointer;font-size:13px;display:flex;align-items:center;gap:10px;transition:background 0.15s;" onmouseover="this.style.background='var(--primary-light)'" onmouseout="this.style.background=''" onclick="showToast('导出PDF成功', 'success');hideExportMenu()">
  1514. <span>📕</span><span>导出为 PDF</span>
  1515. </div>
  1516. <div style="padding:12px 16px;cursor:pointer;font-size:13px;display:flex;align-items:center;gap:10px;transition:background 0.15s;" onmouseover="this.style.background='var(--primary-light)'" onmouseout="this.style.background=''" onclick="showToast('导出Word成功', 'success');hideExportMenu()">
  1517. <span>📘</span><span>导出为 Word</span>
  1518. </div>
  1519. </div>
  1520. <!-- 要素关系图谱弹窗 -->
  1521. <div class="knowledge-graph-modal" id="knowledgeGraphModal">
  1522. <div class="kg-header">
  1523. <div class="kg-title">
  1524. <span class="kg-icon">🔗</span>
  1525. <span>要素关系图谱</span>
  1526. </div>
  1527. <div class="kg-controls">
  1528. <div class="kg-view-toggle">
  1529. <button class="kg-view-btn active" onclick="switchGraphView('graph')" id="graphViewBtn">
  1530. <span>🕸️</span>
  1531. <span>图谱视图</span>
  1532. </button>
  1533. <button class="kg-view-btn" onclick="switchGraphView('list')" id="listViewBtn">
  1534. <span>📋</span>
  1535. <span>列表视图</span>
  1536. </button>
  1537. </div>
  1538. <button class="kg-close" onclick="closeKnowledgeGraph()">×</button>
  1539. </div>
  1540. </div>
  1541. <div class="kg-content">
  1542. <!-- 图谱视图 -->
  1543. <div class="kg-graph-view" id="graphView">
  1544. <div class="graph-canvas" id="graphCanvas">
  1545. <div class="graph-node core" style="left: 50%; top: 50%; transform: translate(-50%, -50%);" data-entity="智慧园区">
  1546. <div class="node-icon">🏢</div>
  1547. <div class="node-label">智慧园区</div>
  1548. <div class="node-type">核心实体</div>
  1549. </div>
  1550. <div class="graph-node concept" style="left: 25%; top: 30%;" data-entity="产业升级">
  1551. <div class="node-icon">📈</div>
  1552. <div class="node-label">产业升级</div>
  1553. <div class="node-type">概念</div>
  1554. </div>
  1555. <div class="graph-node concept" style="left: 75%; top: 30%;" data-entity="城市现代化">
  1556. <div class="node-icon">🌆</div>
  1557. <div class="node-label">城市现代化</div>
  1558. <div class="node-type">概念</div>
  1559. </div>
  1560. <div class="graph-node concept" style="left: 15%; top: 70%;" data-entity="智能化管理">
  1561. <div class="node-icon">🤖</div>
  1562. <div class="node-label">智能化管理</div>
  1563. <div class="node-type">技术</div>
  1564. </div>
  1565. <div class="graph-node data" style="left: 40%; top: 20%;" data-entity="1,789亿元">
  1566. <div class="node-icon">💰</div>
  1567. <div class="node-label">1,789亿元</div>
  1568. <div class="node-type">市场规模</div>
  1569. </div>
  1570. <div class="graph-node data" style="left: 60%; top: 80%;" data-entity="12.5亿元">
  1571. <div class="node-icon">💵</div>
  1572. <div class="node-label">12.5亿元</div>
  1573. <div class="node-type">投资额</div>
  1574. </div>
  1575. <div class="graph-node location" style="left: 85%; top: 70%;" data-entity="华南地区">
  1576. <div class="node-icon">📍</div>
  1577. <div class="node-label">华南地区</div>
  1578. <div class="node-type">地理位置</div>
  1579. </div>
  1580. <!-- 关系连线 -->
  1581. <svg class="graph-lines" width="100%" height="100%">
  1582. <defs>
  1583. <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
  1584. <polygon points="0 0, 10 3.5, 0 7" fill="#1890ff" opacity="0.6"/>
  1585. </marker>
  1586. </defs>
  1587. <!-- 核心关系 -->
  1588. <line x1="50%" y1="50%" x2="25%" y2="30%" stroke="#1890ff" stroke-width="2" opacity="0.6" marker-end="url(#arrowhead)"/>
  1589. <line x1="50%" y1="50%" x2="75%" y2="30%" stroke="#1890ff" stroke-width="2" opacity="0.6" marker-end="url(#arrowhead)"/>
  1590. <line x1="50%" y1="50%" x2="15%" y2="70%" stroke="#1890ff" stroke-width="2" opacity="0.6" marker-end="url(#arrowhead)"/>
  1591. <line x1="50%" y1="50%" x2="40%" y2="20%" stroke="#1890ff" stroke-width="2" opacity="0.6" marker-end="url(#arrowhead)"/>
  1592. <line x1="50%" y1="50%" x2="60%" y2="80%" stroke="#1890ff" stroke-width="2" opacity="0.6" marker-end="url(#arrowhead)"/>
  1593. <line x1="50%" y1="50%" x2="85%" y2="70%" stroke="#1890ff" stroke-width="2" opacity="0.6" marker-end="url(#arrowhead)"/>
  1594. </svg>
  1595. </div>
  1596. <div class="graph-legend">
  1597. <div class="legend-item"><span class="legend-dot core"></span><span>核心实体</span></div>
  1598. <div class="legend-item"><span class="legend-dot concept"></span><span>概念</span></div>
  1599. <div class="legend-item"><span class="legend-dot data"></span><span>数据</span></div>
  1600. <div class="legend-item"><span class="legend-dot location"></span><span>地点</span></div>
  1601. </div>
  1602. </div>
  1603. <!-- 列表视图 -->
  1604. <div class="kg-list-view" id="listView" style="display: none;">
  1605. <div class="list-search">
  1606. <input type="text" placeholder="🔍 搜索要素..." class="list-search-input" oninput="filterEntities(this.value)">
  1607. </div>
  1608. <div class="entity-categories">
  1609. <div class="category-section">
  1610. <div class="category-header">
  1611. <span class="category-icon">🏢</span>
  1612. <span class="category-title">核心实体 (1)</span>
  1613. </div>
  1614. <div class="entity-items">
  1615. <div class="entity-item" onclick="highlightEntity('智慧园区')">
  1616. <div class="entity-icon">🏢</div>
  1617. <div class="entity-info">
  1618. <div class="entity-name">智慧园区</div>
  1619. <div class="entity-meta">6个关联 • 3个来源文档</div>
  1620. </div>
  1621. <div class="entity-actions">
  1622. <button class="entity-action-btn" onclick="locateEntity('智慧园区')">📍</button>
  1623. <button class="entity-action-btn" onclick="editEntity('智慧园区')">✏️</button>
  1624. </div>
  1625. </div>
  1626. </div>
  1627. </div>
  1628. <div class="category-section">
  1629. <div class="category-header">
  1630. <span class="category-icon">💡</span>
  1631. <span class="category-title">概念 (4)</span>
  1632. </div>
  1633. <div class="entity-items">
  1634. <div class="entity-item" onclick="highlightEntity('产业升级')">
  1635. <div class="entity-icon">📈</div>
  1636. <div class="entity-info">
  1637. <div class="entity-name">产业升级</div>
  1638. <div class="entity-meta">2个关联 • 2个来源文档</div>
  1639. </div>
  1640. <div class="entity-actions">
  1641. <button class="entity-action-btn" onclick="locateEntity('产业升级')">📍</button>
  1642. <button class="entity-action-btn" onclick="editEntity('产业升级')">✏️</button>
  1643. </div>
  1644. </div>
  1645. <div class="entity-item" onclick="highlightEntity('城市现代化')">
  1646. <div class="entity-icon">🌆</div>
  1647. <div class="entity-info">
  1648. <div class="entity-name">城市现代化</div>
  1649. <div class="entity-meta">2个关联 • 1个来源文档</div>
  1650. </div>
  1651. <div class="entity-actions">
  1652. <button class="entity-action-btn" onclick="locateEntity('城市现代化')">📍</button>
  1653. <button class="entity-action-btn" onclick="editEntity('城市现代化')">✏️</button>
  1654. </div>
  1655. </div>
  1656. <div class="entity-item" onclick="highlightEntity('智能化管理')">
  1657. <div class="entity-icon">🤖</div>
  1658. <div class="entity-info">
  1659. <div class="entity-name">智能化管理</div>
  1660. <div class="entity-meta">1个关联 • 1个来源文档</div>
  1661. </div>
  1662. <div class="entity-actions">
  1663. <button class="entity-action-btn" onclick="locateEntity('智能化管理')">📍</button>
  1664. <button class="entity-action-btn" onclick="editEntity('智能化管理')">✏️</button>
  1665. </div>
  1666. </div>
  1667. <div class="entity-item" onclick="highlightEntity('低碳绿色')">
  1668. <div class="entity-icon">🌱</div>
  1669. <div class="entity-info">
  1670. <div class="entity-name">低碳绿色</div>
  1671. <div class="entity-meta">1个关联 • 1个来源文档</div>
  1672. </div>
  1673. <div class="entity-actions">
  1674. <button class="entity-action-btn" onclick="locateEntity('低碳绿色')">📍</button>
  1675. <button class="entity-action-btn" onclick="editEntity('低碳绿色')">✏️</button>
  1676. </div>
  1677. </div>
  1678. </div>
  1679. </div>
  1680. <div class="category-section">
  1681. <div class="category-header">
  1682. <span class="category-icon">📊</span>
  1683. <span class="category-title">数据 (8)</span>
  1684. </div>
  1685. <div class="entity-items">
  1686. <div class="entity-item" onclick="highlightEntity('1,789亿元')">
  1687. <div class="entity-icon">💰</div>
  1688. <div class="entity-info">
  1689. <div class="entity-name">1,789亿元</div>
  1690. <div class="entity-meta">市场规模数据 • 1个来源</div>
  1691. </div>
  1692. <div class="entity-actions">
  1693. <button class="entity-action-btn" onclick="locateEntity('1,789亿元')">📍</button>
  1694. <button class="entity-action-btn" onclick="editEntity('1,789亿元')">✏️</button>
  1695. </div>
  1696. </div>
  1697. <div class="entity-item" onclick="highlightEntity('18%')">
  1698. <div class="entity-icon">📊</div>
  1699. <div class="entity-info">
  1700. <div class="entity-name">18%</div>
  1701. <div class="entity-meta">增长率数据 • 1个来源</div>
  1702. </div>
  1703. <div class="entity-actions">
  1704. <button class="entity-action-btn" onclick="locateEntity('18%')">📍</button>
  1705. <button class="entity-action-btn" onclick="editEntity('18%')">✏️</button>
  1706. </div>
  1707. </div>
  1708. <div class="entity-item" onclick="highlightEntity('50万平方米')">
  1709. <div class="entity-icon">📐</div>
  1710. <div class="entity-info">
  1711. <div class="entity-name">50万平方米</div>
  1712. <div class="entity-meta">面积数据 • 1个来源</div>
  1713. </div>
  1714. <div class="entity-actions">
  1715. <button class="entity-action-btn" onclick="locateEntity('50万平方米')">📍</button>
  1716. <button class="entity-action-btn" onclick="editEntity('50万平方米')">✏️</button>
  1717. </div>
  1718. </div>
  1719. <div class="entity-item" onclick="highlightEntity('12.5亿元')">
  1720. <div class="entity-icon">💵</div>
  1721. <div class="entity-info">
  1722. <div class="entity-name">12.5亿元</div>
  1723. <div class="entity-meta">投资额数据 • 1个来源</div>
  1724. </div>
  1725. <div class="entity-actions">
  1726. <button class="entity-action-btn" onclick="locateEntity('12.5亿元')">📍</button>
  1727. <button class="entity-action-btn" onclick="editEntity('12.5亿元')">✏️</button>
  1728. </div>
  1729. </div>
  1730. </div>
  1731. </div>
  1732. <div class="category-section">
  1733. <div class="category-header">
  1734. <span class="category-icon">📍</span>
  1735. <span class="category-title">地点 (1)</span>
  1736. </div>
  1737. <div class="entity-items">
  1738. <div class="entity-item" onclick="highlightEntity('华南地区')">
  1739. <div class="entity-icon">📍</div>
  1740. <div class="entity-info">
  1741. <div class="entity-name">华南地区</div>
  1742. <div class="entity-meta">地理位置 • 1个来源</div>
  1743. </div>
  1744. <div class="entity-actions">
  1745. <button class="entity-action-btn" onclick="locateEntity('华南地区')">📍</button>
  1746. <button class="entity-action-btn" onclick="editEntity('华南地区')">✏️</button>
  1747. </div>
  1748. </div>
  1749. </div>
  1750. </div>
  1751. </div>
  1752. </div>
  1753. </div>
  1754. </div>
  1755. <!-- FAB资源监控 -->
  1756. <div style="position:fixed;bottom:24px;right:24px;z-index:1000;" id="fabContainer">
  1757. <div id="fabPanel" style="position:absolute;bottom:60px;right:0;width:260px;background:var(--white);border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.15);display:none;overflow:hidden;">
  1758. <div style="padding:14px 16px;background:linear-gradient(135deg, #52c41a 0%, #13c2c2 100%);color:white;font-weight:600;font-size:13px;">📊 资源监控</div>
  1759. <div style="padding:14px 16px;">
  1760. <div style="margin-bottom:12px;">
  1761. <div style="display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px;"><span>Token 消耗</span><span style="font-weight:500;">15.6K / 20K</span></div>
  1762. <div style="height:6px;background:var(--bg);border-radius:3px;"><div style="height:100%;width:78%;background:var(--warning);border-radius:3px;"></div></div>
  1763. </div>
  1764. <div style="margin-bottom:12px;">
  1765. <div style="display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px;"><span>GPU 显存</span><span style="font-weight:500;">3.6G / 8G</span></div>
  1766. <div style="height:6px;background:var(--bg);border-radius:3px;"><div style="height:100%;width:45%;background:var(--success);border-radius:3px;"></div></div>
  1767. </div>
  1768. <div style="display:flex;justify-content:space-between;padding:10px;background:var(--bg);border-radius:8px;">
  1769. <div style="text-align:center;"><div style="font-size:16px;font-weight:600;color:var(--primary);">¥3.12</div><div style="font-size:10px;color:var(--text3);">本次会话</div></div>
  1770. <div style="text-align:center;"><div style="font-size:16px;font-weight:600;color:var(--primary);">¥127.50</div><div style="font-size:10px;color:var(--text3);">本月累计</div></div>
  1771. </div>
  1772. </div>
  1773. </div>
  1774. <button id="fabBtn" style="width:50px;height:50px;border-radius:50%;background:linear-gradient(135deg, #52c41a 0%, #13c2c2 100%);border:none;cursor:grab;display:flex;align-items:center;justify-content:center;font-size:20px;color:white;box-shadow:0 6px 20px rgba(82,196,26,0.35);">📊</button>
  1775. </div>
  1776. <script>
  1777. (function(){
  1778. const container = document.getElementById('fabContainer');
  1779. const btn = document.getElementById('fabBtn');
  1780. if (!container || !btn) return;
  1781. let dragging = false;
  1782. let moved = false;
  1783. let startX = 0, startY = 0, origLeft = 0, origTop = 0;
  1784. // 默认位置:固定在右下角(24px 间距)
  1785. container.style.right = '24px';
  1786. container.style.bottom = '24px';
  1787. container.style.left = '';
  1788. container.style.top = '';
  1789. function start(e) {
  1790. const evt = e.touches ? e.touches[0] : e;
  1791. dragging = true;
  1792. moved = false;
  1793. startX = evt.clientX;
  1794. startY = evt.clientY;
  1795. const rect = container.getBoundingClientRect();
  1796. origLeft = rect.left;
  1797. origTop = rect.top;
  1798. document.addEventListener('mousemove', onMove);
  1799. document.addEventListener('mouseup', end);
  1800. document.addEventListener('touchmove', onMove, { passive: false });
  1801. document.addEventListener('touchend', end);
  1802. container.style.transition = 'none';
  1803. btn.style.cursor = 'grabbing';
  1804. e.preventDefault();
  1805. }
  1806. function onMove(e) {
  1807. if (!dragging) return;
  1808. const evt = e.touches ? e.touches[0] : e;
  1809. const dx = evt.clientX - startX;
  1810. const dy = evt.clientY - startY;
  1811. if (Math.abs(dx) > 4 || Math.abs(dy) > 4) moved = true;
  1812. const vw = window.innerWidth, vh = window.innerHeight;
  1813. const rect = container.getBoundingClientRect();
  1814. const w = rect.width, h = rect.height;
  1815. let newLeft = origLeft + dx;
  1816. let newTop = origTop + dy;
  1817. newLeft = Math.max(8, Math.min(vw - w - 8, newLeft));
  1818. newTop = Math.max(8, Math.min(vh - h - 8, newTop));
  1819. container.style.left = newLeft + 'px';
  1820. container.style.top = newTop + 'px';
  1821. container.style.right = '';
  1822. container.style.bottom = '';
  1823. e.preventDefault();
  1824. }
  1825. function end() {
  1826. if (!dragging) return;
  1827. dragging = false;
  1828. document.removeEventListener('mousemove', onMove);
  1829. document.removeEventListener('mouseup', end);
  1830. document.removeEventListener('touchmove', onMove);
  1831. document.removeEventListener('touchend', end);
  1832. container.style.transition = '';
  1833. btn.style.cursor = 'grab';
  1834. // 不持久化位置:拖动仅在当前会话有效,刷新/重新登录后恢复到默认右下角
  1835. // if moved, prevent the immediate click from toggling the panel
  1836. if (moved) {
  1837. const preventer = function(ev) { ev.stopImmediatePropagation(); ev.preventDefault(); btn.removeEventListener('click', preventer, true); };
  1838. btn.addEventListener('click', preventer, true);
  1839. }
  1840. }
  1841. // replace inline onclick by controlled click handler
  1842. btn.addEventListener('click', function(e){
  1843. if (dragging || moved) { e.preventDefault(); e.stopPropagation(); return; }
  1844. try { toggleFab(); } catch (err) { console.warn('toggleFab not available', err); }
  1845. });
  1846. // init and ensure position stays in viewport on resize
  1847. window.addEventListener('resize', function(){
  1848. try {
  1849. const rect = container.getBoundingClientRect();
  1850. const vw = window.innerWidth, vh = window.innerHeight;
  1851. let left = rect.left, top = rect.top;
  1852. const w = rect.width, h = rect.height;
  1853. if (left > vw - 8) left = Math.max(8, vw - w - 8);
  1854. if (top > vh - 8) top = Math.max(8, vh - h - 8);
  1855. container.style.left = left + 'px';
  1856. container.style.top = top + 'px';
  1857. } catch (e) {}
  1858. });
  1859. btn.addEventListener('mousedown', start);
  1860. btn.addEventListener('touchstart', start, { passive: false });
  1861. loadPos();
  1862. })();
  1863. </script>
  1864. <!-- 通知面板 -->
  1865. <div class="notif-panel" id="notifPanel">
  1866. <div style="padding:18px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;">
  1867. <span style="font-size:15px;font-weight:600;">消息通知</span>
  1868. <span style="font-size:12px;color:var(--primary);cursor:pointer;" onclick="showToast('已全部标为已读', 'success')">全部已读</span>
  1869. </div>
  1870. <div style="flex:1;overflow-y:auto;">
  1871. <div style="padding:14px 18px;border-bottom:1px solid var(--border);background:var(--primary-light);cursor:pointer;" onclick="void(0)">
  1872. <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
  1873. <span style="width:22px;height:22px;background:#f6ffed;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;">✅</span>
  1874. <span style="flex:1;font-weight:500;font-size:13px;">文档解析完成</span>
  1875. <span style="font-size:10px;color:var(--text3);">刚刚</span>
  1876. </div>
  1877. <div style="font-size:12px;color:var(--text2);">《市场调研数据.pdf》已解析完成,提取到35个实体</div>
  1878. </div>
  1879. <div style="padding:14px 18px;border-bottom:1px solid var(--border);cursor:pointer;">
  1880. <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
  1881. <span style="width:22px;height:22px;background:#fffbe6;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;">💬</span>
  1882. <span style="flex:1;font-weight:500;font-size:13px;">李四评论了您的报告</span>
  1883. <span style="font-size:10px;color:var(--text3);">2小时前</span>
  1884. </div>
  1885. <div style="font-size:12px;color:var(--text2);">建议补充竞争格局分析...</div>
  1886. </div>
  1887. </div>
  1888. </div>
  1889. <!-- Toast容器 -->
  1890. <div class="toast-container" id="toastContainer"></div>
  1891. <!-- 遮罩 -->
  1892. <div class="overlay" id="overlay" onclick="closeAll()"></div>
  1893. <!-- 要素关系图谱遮罩 -->
  1894. <div class="kg-overlay" id="kgOverlay" onclick="closeKnowledgeGraph()"></div>
  1895. <script>
  1896. // === 导航 ===
  1897. document.addEventListener('DOMContentLoaded', function(){
  1898. try {
  1899. const ta = document.getElementById('aiTextarea');
  1900. const send = document.getElementById('aiSendBtn');
  1901. if (ta && send) {
  1902. const update = () => {
  1903. if (ta.value.trim().length > 0) send.classList.add('active');
  1904. else send.classList.remove('active');
  1905. };
  1906. ta.addEventListener('input', update);
  1907. update();
  1908. }
  1909. } catch (e) { console.warn('ai input init err', e); }
  1910. });
  1911. function navTo(page) {
  1912. document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  1913. document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('active'));
  1914. document.getElementById('page-' + page)?.classList.add('active');
  1915. document.querySelector('[data-page="' + page + '"]')?.classList.add('active');
  1916. document.getElementById('page-editor').classList.remove('active');
  1917. document.getElementById('sidebar').classList.remove('hidden');
  1918. document.getElementById('mainContent').style.display = '';
  1919. closeAll();
  1920. }
  1921. function goHome() { navTo('home'); }
  1922. // === 编辑器 ===
  1923. function openEditor() {
  1924. document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  1925. document.getElementById('page-editor').classList.add('active');
  1926. document.getElementById('sidebar').classList.add('hidden');
  1927. document.getElementById('mainContent').style.display = 'none';
  1928. closeAll();
  1929. }
  1930. function closeEditor() { navTo('home'); }
  1931. // === 通知 ===
  1932. function toggleNotif() {
  1933. const panel = document.getElementById('notifPanel');
  1934. const overlay = document.getElementById('overlay');
  1935. const isOpen = panel.style.transform === 'translateX(0%)';
  1936. panel.style.transform = isOpen ? 'translateX(100%)' : 'translateX(0%)';
  1937. overlay.style.display = isOpen ? 'none' : 'block';
  1938. }
  1939. // === FAB ===
  1940. function toggleFab() {
  1941. const panel = document.getElementById('fabPanel');
  1942. panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
  1943. }
  1944. // === 思考模式 ===
  1945. function setMode(el) {
  1946. document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
  1947. el.classList.add('active');
  1948. showToast('已切换到 ' + el.textContent.trim(), 'success');
  1949. }
  1950. // === 首页AI输入 ===
  1951. function toggleSendBtn() {
  1952. const input = document.getElementById('homeAiInput');
  1953. const btn = document.getElementById('homeSendBtn');
  1954. btn.classList.toggle('show', input.value.trim().length > 0);
  1955. }
  1956. function handleHomeAi() {
  1957. const input = document.getElementById('homeAiInput');
  1958. if (input.value.trim()) {
  1959. showToast('AI正在处理...', 'info');
  1960. setTimeout(() => openEditor(), 1000);
  1961. input.value = '';
  1962. document.getElementById('homeSendBtn').classList.remove('show');
  1963. }
  1964. }
  1965. // === 要素管理 ===
  1966. const tagData = {
  1967. smartpark: { icon: '🏢', type: 'entity', name: '智慧园区', typeText: '核心实体', source: '项目可行性研究报告.docx', relations: ['产业升级', '城市现代化', '智能化管理'] },
  1968. upgrade: { icon: '📈', type: 'concept', name: '产业升级', typeText: '概念', source: '项目可行性研究报告.docx', relations: ['智慧园区', '城市现代化'] },
  1969. modern: { icon: '🌆', type: 'concept', name: '城市现代化', typeText: '概念', source: '项目可行性研究报告.docx', relations: ['智慧园区', '产业升级'] },
  1970. ai: { icon: '🤖', type: 'concept', name: '智能化管理', typeText: '技术', source: '技术方案说明.pdf', relations: ['智慧园区', '低碳绿色'] },
  1971. green: { icon: '🌱', type: 'concept', name: '低碳绿色', typeText: '概念', source: '项目可行性研究报告.docx', relations: ['智慧园区'] },
  1972. location: { icon: '📍', type: 'location', name: '华南地区', typeText: '地点', source: '项目可行性研究报告.docx', relations: ['智慧园区', '50万m²'] },
  1973. data1: { icon: '💰', type: 'data', name: '1,789亿元', typeText: '市场规模数据', source: '市场调研数据.pdf', relations: ['智慧园区', '18%'] },
  1974. data2: { icon: '📊', type: 'data', name: '18%', typeText: '增长率数据', source: '市场调研数据.pdf', relations: ['1,789亿元'] },
  1975. data3: { icon: '📐', type: 'data', name: '50万平方米', typeText: '面积数据', source: '项目可行性研究报告.docx', relations: ['华南地区'] },
  1976. data4: { icon: '💵', type: 'data', name: '12.5亿元', typeText: '投资额', source: '财务预测表.xlsx', relations: ['智慧园区'] },
  1977. chart: { icon: '📊', type: 'asset', name: '趋势图', typeText: '图表资产', source: '资产库', relations: ['柱状图'] },
  1978. template: { icon: '📝', type: 'asset', name: '结论模板', typeText: '文本模板', source: '资产库', relations: ['结论'] }
  1979. };
  1980. let currentTagName = '';
  1981. function showTagPopover(event, tagId) {
  1982. event.stopPropagation();
  1983. const popover = document.getElementById('elementPopover');
  1984. const data = tagData[tagId];
  1985. if (!data) return;
  1986. currentTagName = data.name;
  1987. document.getElementById('popoverIcon').className = 'popover-icon ' + data.type;
  1988. document.getElementById('popoverIcon').textContent = data.icon;
  1989. document.getElementById('popoverTitle').textContent = data.name;
  1990. document.getElementById('popoverType').textContent = data.typeText;
  1991. document.getElementById('popoverSource').textContent = data.source;
  1992. const relationsEl = document.getElementById('popoverRelations');
  1993. relationsEl.innerHTML = data.relations.map(r =>
  1994. `<span class="popover-relation" onclick="showToast('跳转到: ${r}', 'info')">${r}</span>`
  1995. ).join('');
  1996. const rect = event.currentTarget.getBoundingClientRect();
  1997. popover.style.top = Math.min(rect.bottom + 8, window.innerHeight - 300) + 'px';
  1998. popover.style.left = Math.min(rect.left, window.innerWidth - 300) + 'px';
  1999. popover.classList.add('show');
  2000. }
  2001. function hideTagPopover() {
  2002. document.getElementById('elementPopover').classList.remove('show');
  2003. }
  2004. // === Sync tags from editor content to right-panel lists ===
  2005. function sanitizeId(name) {
  2006. return 'tag_' + name.replace(/\s+/g, '_').replace(/[^\w\-]/g, '').toLowerCase();
  2007. }
  2008. function rgbToHex(rgb) {
  2009. if (!rgb) return '';
  2010. const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
  2011. if (!m) return '';
  2012. const r = parseInt(m[1]), g = parseInt(m[2]), b = parseInt(m[3]);
  2013. return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
  2014. }
  2015. function isColorRed(rgb) {
  2016. if (!rgb) return false;
  2017. const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
  2018. if (!m) return false;
  2019. const r = parseInt(m[1]), g = parseInt(m[2]), b = parseInt(m[3]);
  2020. return (r > 200 && g < 130 && b < 130);
  2021. }
  2022. function syncTagsFromContent() {
  2023. try {
  2024. console.debug('[debug] syncTagsFromContent start');
  2025. const nodes = document.querySelectorAll('.editor-content .entity-highlight');
  2026. const dynContainer = document.getElementById('dynamicTags');
  2027. const statContainer = document.getElementById('staticTags');
  2028. if (!dynContainer || !statContainer) return;
  2029. // clear containers
  2030. dynContainer.innerHTML = '';
  2031. statContainer.innerHTML = '';
  2032. nodes.forEach(node => {
  2033. try {
  2034. // extract visible text only (exclude badge/span text)
  2035. let rawText = '';
  2036. node.childNodes.forEach(n => {
  2037. if (n.nodeType === Node.TEXT_NODE) rawText += n.textContent;
  2038. else if (n.nodeType === Node.ELEMENT_NODE && n.classList && n.classList.contains('tag-name')) rawText += n.textContent;
  2039. });
  2040. rawText = (rawText || node.textContent || '').trim();
  2041. if (!rawText) {
  2042. console.debug('[debug] syncTagsFromContent: skipping empty node', node);
  2043. return;
  2044. }
  2045. const computed = window.getComputedStyle(node);
  2046. let bg = computed.backgroundColor || '';
  2047. const border = computed.borderColor || '';
  2048. const color = computed.color || '';
  2049. console.debug('[debug] tag', rawText, { background: bg, border: border, color: color });
  2050. // prefer background if not transparent
  2051. if (!bg || bg === 'transparent' || bg.indexOf('0, 0, 0, 0') !== -1) {
  2052. bg = border || color || '';
  2053. }
  2054. // Improved dynamic detection:
  2055. // 1) explicit data-source indicating AI/generated
  2056. // 2) presence of a .source-badge.ai inside the node
  2057. // 3) existing color-based heuristic (fallback)
  2058. let isDynamic = false;
  2059. try {
  2060. const ds = (node.dataset && node.dataset.source) ? String(node.dataset.source) : '';
  2061. const category = (node.dataset && node.dataset.category) ? String(node.dataset.category) : '';
  2062. if (ds && /ai|生成|AI/i.test(ds)) {
  2063. isDynamic = true;
  2064. } else if (category && /市场规模数据|data|AI/i.test(category)) {
  2065. isDynamic = true;
  2066. } else if (node.querySelector && node.querySelector('.source-badge.ai')) {
  2067. isDynamic = true;
  2068. } else {
  2069. isDynamic = isColorRed(bg) || isColorRed(border) || /#ff4d4f/i.test((bg || '') + (border || ''));
  2070. }
  2071. } catch (sigErr) {
  2072. console.warn('dynamic detection fallback err', sigErr);
  2073. isDynamic = isColorRed(bg) || isColorRed(border) || /#ff4d4f/i.test((bg || '') + (border || ''));
  2074. }
  2075. console.debug('[debug] determined isDynamic=', isDynamic, 'for', rawText);
  2076. const id = sanitizeId(rawText);
  2077. if (!tagData[id]) {
  2078. tagData[id] = { icon: '🏷️', type: 'entity', name: rawText, typeText: '来源: 文本', source: '文档', relations: [] };
  2079. }
  2080. const span = document.createElement('span');
  2081. span.className = 'element-tag ' + (isDynamic ? 'dynamic' : 'static') + ' mapped';
  2082. span.setAttribute('draggable', 'true');
  2083. span.onclick = function (e) { e.stopPropagation(); showTagPopover(e, id); };
  2084. span.ondragstart = (e) => handleTagDragStart(e, rawText);
  2085. span.ondragend = (e) => handleTagDragEnd(e);
  2086. // apply color styling to match source highlight (if available)
  2087. const fill = rgbToHex(bg);
  2088. if (fill) {
  2089. span.style.background = fill;
  2090. // choose light or dark text based on luminance
  2091. const hex = fill.replace('#','');
  2092. const r = parseInt(hex.substring(0,2),16), g = parseInt(hex.substring(2,4),16), b = parseInt(hex.substring(4,6),16);
  2093. const luminance = (0.299*r + 0.587*g + 0.114*b);
  2094. span.style.color = luminance > 160 ? '#000' : '#fff';
  2095. span.style.border = '1px solid rgba(0,0,0,0.06)';
  2096. }
  2097. span.innerHTML = `<span class="tag-icon">${tagData[id].icon || '🏷️'}</span><span class="tag-name">${rawText}</span>`;
  2098. if (isDynamic) {
  2099. dynContainer.appendChild(span);
  2100. }
  2101. else statContainer.appendChild(span);
  2102. } catch (e) { console.warn('syncTagsFromContent node err', e); }
  2103. });
  2104. // post-process: relocate any static tags that are actually red in the editor
  2105. try { relocateRedStaticTags(dynContainer, statContainer); } catch (e) { console.warn('relocateRedStaticTags err', e); }
  2106. console.debug('[debug] syncTagsFromContent done', { dynamicCount: dynContainer.children.length, staticCount: statContainer.children.length });
  2107. } catch (e) { console.warn('syncTagsFromContent err', e); }
  2108. }
  2109. // Move any tags currently in static container that are red in the editor into dynamic container
  2110. function relocateRedStaticTags(dynContainer, statContainer) {
  2111. try {
  2112. const staticChildren = Array.from(statContainer.children);
  2113. if (staticChildren.length === 0) return;
  2114. const editorNodes = Array.from(document.querySelectorAll('.editor-content .entity-highlight'));
  2115. staticChildren.forEach(child => {
  2116. try {
  2117. const nameEl = child.querySelector('.tag-name');
  2118. const tagName = nameEl ? nameEl.textContent.trim() : (child.textContent || '').trim();
  2119. if (!tagName) return;
  2120. // find corresponding editor node by exact text match (prefer full match)
  2121. const sourceNode = editorNodes.find(n => {
  2122. const text = (n.textContent || '').trim();
  2123. return text === tagName || text.includes(tagName);
  2124. });
  2125. if (!sourceNode) return;
  2126. const cs = window.getComputedStyle(sourceNode);
  2127. const bg = cs.backgroundColor || cs.borderColor || cs.color || '';
  2128. if (isColorRed(bg)) {
  2129. // move element
  2130. child.classList.remove('static');
  2131. child.classList.add('dynamic');
  2132. dynContainer.appendChild(child);
  2133. console.debug('[debug] relocated tag to dynamic:', tagName);
  2134. }
  2135. } catch (e) { console.warn('relocate child err', e); }
  2136. });
  2137. } catch (e) { console.warn('relocateRedStaticTags overall err', e); }
  2138. }
  2139. // observe editor content changes and sync tags (debounced)
  2140. (function() {
  2141. let t = null;
  2142. document.addEventListener('selectionchange', () => { /* no-op keep event loop active */ });
  2143. document.addEventListener('DOMContentLoaded', () => {
  2144. document.querySelectorAll('.editor-content').forEach(editor => {
  2145. try {
  2146. const mo = new MutationObserver(() => {
  2147. clearTimeout(t);
  2148. t = setTimeout(() => {
  2149. try { syncTagsFromContent(); } catch(e) { console.warn('debounced sync err', e); }
  2150. }, 150);
  2151. });
  2152. mo.observe(editor, { characterData: true, childList: true, subtree: true });
  2153. } catch (e) { console.warn('attach mutation observer err', e); }
  2154. });
  2155. // initial sync after load
  2156. try { syncTagsFromContent(); } catch(e) { console.warn('initial syncTagsFromContent err', e); }
  2157. // ensure tabs trigger a sync when clicked
  2158. try {
  2159. const td = document.getElementById('tabDynamic');
  2160. const ts = document.getElementById('tabStatic');
  2161. if (td) { td.addEventListener('click', () => { try { syncTagsFromContent(); } catch(e){} }); }
  2162. if (ts) { ts.addEventListener('click', () => { try { syncTagsFromContent(); } catch(e){} }); }
  2163. } catch (e) { console.warn('attach tab click sync err', e); }
  2164. });
  2165. })();
  2166. // 切换动态/静态要素 Tab
  2167. function switchElementTab(tab) {
  2168. try {
  2169. const tabDyn = document.getElementById('tabDynamic');
  2170. const tabStat = document.getElementById('tabStatic');
  2171. const dyn = document.getElementById('dynamicTags');
  2172. const stat = document.getElementById('staticTags');
  2173. if (tab === 'dynamic') {
  2174. if (tabDyn) tabDyn.classList.add('active');
  2175. if (tabStat) tabStat.classList.remove('active');
  2176. if (dyn) dyn.style.display = '';
  2177. if (stat) stat.style.display = 'none';
  2178. try { syncTagsFromContent(); } catch(e){ console.warn('sync on tab switch err', e); }
  2179. } else {
  2180. if (tabDyn) tabDyn.classList.remove('active');
  2181. if (tabStat) tabStat.classList.add('active');
  2182. if (dyn) dyn.style.display = 'none';
  2183. if (stat) stat.style.display = '';
  2184. try { syncTagsFromContent(); } catch(e){ console.warn('sync on tab switch err', e); }
  2185. }
  2186. } catch (e) { console.warn('switchElementTab err', e); }
  2187. }
  2188. function insertTagToEditor() {
  2189. if (currentTagName) {
  2190. showToast('已插入要素: ' + currentTagName, 'success');
  2191. }
  2192. }
  2193. document.addEventListener('click', function(e) {
  2194. if (!e.target.closest('.element-popover') && !e.target.closest('.element-tag')) {
  2195. hideTagPopover();
  2196. }
  2197. });
  2198. // === 标签拖拽功能 ===
  2199. function handleTagDragStart(event, tagName) {
  2200. currentTagName = tagName;
  2201. event.currentTarget.classList.add('dragging');
  2202. event.dataTransfer.setData('text/plain', tagName);
  2203. event.dataTransfer.effectAllowed = 'copy';
  2204. setTimeout(() => {
  2205. document.querySelectorAll('.editor-content').forEach(el => {
  2206. el.classList.add('drag-over');
  2207. });
  2208. }, 0);
  2209. }
  2210. function handleTagDragEnd(event) {
  2211. event.currentTarget.classList.remove('dragging');
  2212. document.querySelectorAll('.editor-content').forEach(el => {
  2213. el.classList.remove('drag-over');
  2214. });
  2215. }
  2216. // 编辑区放置事件
  2217. document.addEventListener('DOMContentLoaded', function() {
  2218. const editorContents = document.querySelectorAll('.editor-content');
  2219. editorContents.forEach(editor => {
  2220. editor.addEventListener('dragover', function(e) {
  2221. e.preventDefault();
  2222. e.dataTransfer.dropEffect = 'copy';
  2223. });
  2224. editor.addEventListener('drop', function(e) {
  2225. e.preventDefault();
  2226. const tagName = e.dataTransfer.getData('text/plain');
  2227. if (tagName) {
  2228. showToast('✓ 已插入要素: ' + tagName, 'success');
  2229. }
  2230. this.classList.remove('drag-over');
  2231. });
  2232. editor.addEventListener('dragleave', function(e) {
  2233. if (!this.contains(e.relatedTarget)) {
  2234. this.classList.remove('drag-over');
  2235. }
  2236. });
  2237. });
  2238. });
  2239. // === 右键菜单 ===
  2240. let selectedText = '';
  2241. let selectedRange = null;
  2242. function showContextMenu(event) {
  2243. const selection = window.getSelection();
  2244. selectedText = selection.toString().trim();
  2245. selectedRange = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
  2246. if (selectedText.length > 0) {
  2247. event.preventDefault();
  2248. const menu = document.getElementById('contextMenu');
  2249. menu.style.top = event.clientY + 'px';
  2250. menu.style.left = event.clientX + 'px';
  2251. menu.classList.add('show');
  2252. }
  2253. }
  2254. function hideContextMenu() {
  2255. document.getElementById('contextMenu').classList.remove('show');
  2256. }
  2257. function execContextAction(action) {
  2258. hideContextMenu();
  2259. switch(action) {
  2260. case 'copy':
  2261. document.execCommand('copy');
  2262. showToast('已复制到剪贴板', 'success');
  2263. break;
  2264. case 'cut':
  2265. document.execCommand('cut');
  2266. showToast('已剪切', 'success');
  2267. break;
  2268. case 'paste':
  2269. document.execCommand('paste');
  2270. break;
  2271. case 'polish':
  2272. showAiPolishConfirm();
  2273. break;
  2274. case 'spell':
  2275. showSpellCheckConfirm();
  2276. break;
  2277. case 'mark':
  2278. markAsEntity();
  2279. break;
  2280. case 'quote':
  2281. const aiMessages = document.getElementById('aiMessages');
  2282. const quotedText = selectedText.length > 50 ? selectedText.substring(0, 50) + '...' : selectedText;
  2283. aiMessages.innerHTML += `
  2284. <div class="msg user">
  2285. <div class="msg-avatar">张</div>
  2286. <div class="msg-bubble">
  2287. <div style="padding:8px;background:rgba(255,255,255,0.5);border-radius:6px;margin-bottom:8px;font-size:12px;border-left:3px solid rgba(255,255,255,0.8);">
  2288. 📝 引用: "${quotedText}"
  2289. </div>
  2290. 请基于这段内容帮我分析
  2291. </div>
  2292. </div>
  2293. `;
  2294. aiMessages.scrollTop = aiMessages.scrollHeight;
  2295. showToast('已引用到AI助手', 'success');
  2296. break;
  2297. }
  2298. }
  2299. // === AI润色确认 ===
  2300. function showAiPolishConfirm() {
  2301. const polishedText = `经过AI润色优化:${selectedText.replace(/的/g, '的').replace(/了/g, '了').replace(/和/g, '与')}`;
  2302. showConfirmDialog(
  2303. 'AI润色确认',
  2304. `原文: "${selectedText}"<br><br>润色后: "${polishedText}"`,
  2305. () => applyTextReplacement(polishedText),
  2306. () => showToast('已取消润色', 'info')
  2307. );
  2308. }
  2309. // === 拼写检查确认 ===
  2310. function showSpellCheckConfirm() {
  2311. // 模拟拼写检查结果
  2312. const correctedText = selectedText.replace(/园区/g, '园区').replace(/智慧/g, '智慧');
  2313. const hasErrors = correctedText !== selectedText;
  2314. if (hasErrors) {
  2315. showConfirmDialog(
  2316. '拼写检查结果',
  2317. `发现拼写问题:<br>原文: "${selectedText}"<br>更正后: "${correctedText}"`,
  2318. () => applyTextReplacement(correctedText),
  2319. () => showToast('已忽略拼写建议', 'info')
  2320. );
  2321. } else {
  2322. showToast('✓ 拼写检查完成,未发现错误', 'success');
  2323. }
  2324. }
  2325. // === 标记为要素 ===
  2326. function markAsEntity() {
  2327. if (!selectedRange) return;
  2328. const span = document.createElement('span');
  2329. span.className = 'entity-highlight entity'; // 默认标记为entity类型
  2330. span.contentEditable = 'false';
  2331. // 标记来源为人工
  2332. span.dataset.source = '人工标记';
  2333. span.dataset.category = 'entity';
  2334. span.onclick = function(e) { e.stopPropagation(); showDataRelationModal(selectedText, '自定义要素', 'entity', span.dataset.source); };
  2335. span.textContent = selectedText;
  2336. // 添加来源徽章
  2337. const badge = document.createElement('span');
  2338. badge.className = 'source-badge manual';
  2339. badge.title = '来源:人工标记(点击查看详情)';
  2340. badge.textContent = '人工';
  2341. badge.onclick = function(e) { e.stopPropagation(); showDataRelationModal(selectedText, '自定义要素', 'entity', span.dataset.source); };
  2342. span.appendChild(badge);
  2343. selectedRange.deleteContents();
  2344. selectedRange.insertNode(span);
  2345. showToast(`✓ 已将"${selectedText}"标记为要素`, 'success');
  2346. }
  2347. // === 初始化:为已有实体标注添加来源徽章和类型样式(从 onclick 参数或 data-source 推断)===
  2348. function initializeSourceBadges() {
  2349. const nodes = document.querySelectorAll('.entity-highlight');
  2350. nodes.forEach(node => {
  2351. try {
  2352. // 如果已经有 badge,就跳过
  2353. if (node.querySelector && node.querySelector('.source-badge')) return;
  2354. // 优先读取 data-source 和 data-category
  2355. let source = node.dataset && node.dataset.source;
  2356. let category = node.dataset && node.dataset.category;
  2357. // 如果没有 data-source或data-category,从 onclick 属性解析参数
  2358. if ((!source || !category) && node.getAttribute('onclick')) {
  2359. const onclick = node.getAttribute('onclick');
  2360. // 解析 showDataRelationModal('name','type','category','sourceFile')
  2361. const m = onclick.match(/showDataRelationModal\(([^)]+)\)/);
  2362. if (m && m[1]) {
  2363. const parts = m[1].split(',').map(s => s.trim());
  2364. if (parts.length >= 4) {
  2365. if (!category) category = parts[2].replace(/^['"]|['"]$/g, '');
  2366. if (!source) source = parts[3].replace(/^['"]|['"]$/g, '');
  2367. }
  2368. }
  2369. }
  2370. // 为元素添加类型类名(如果还没有的话)
  2371. if (category && !node.classList.contains(category)) {
  2372. node.classList.add(category);
  2373. }
  2374. // 标记类和徽章文字
  2375. let cls = 'file', txt = '文件';
  2376. if (!source) { cls = 'manual'; txt = '人工'; source = '未知'; }
  2377. else if (/ai/i.test(source) || /生成/.test(source) || source === 'AI') { cls = 'ai'; txt = 'AI'; }
  2378. else if (/人工|人工标记/.test(source)) { cls = 'manual'; txt = '人工'; }
  2379. else { cls = 'file'; txt = source.split('/').pop(); if (txt.length > 10) txt = txt.slice(0,10) + '…'; }
  2380. // 保存到dataset
  2381. node.dataset.source = source;
  2382. node.dataset.category = category || 'entity';
  2383. const badge = document.createElement('span');
  2384. badge.className = `source-badge ${cls}`;
  2385. badge.title = `来源:${source}`;
  2386. badge.textContent = txt;
  2387. badge.onclick = function(e) { e.stopPropagation(); showDataRelationModal(node.textContent, '自定义要素', category || 'entity', source); };
  2388. node.appendChild(badge);
  2389. } catch (err) {
  2390. console.error('initializeSourceBadges error', err);
  2391. }
  2392. });
  2393. }
  2394. // === 通用确认对话框 ===
  2395. function showConfirmDialog(title, content, onConfirm, onCancel) {
  2396. // 创建确认对话框
  2397. const dialog = document.createElement('div');
  2398. dialog.className = 'confirm-dialog';
  2399. dialog.innerHTML = `
  2400. <div class="confirm-content">
  2401. <div class="confirm-header">
  2402. <h3>${title}</h3>
  2403. <button class="confirm-close" onclick="closeConfirmDialog(this)">×</button>
  2404. </div>
  2405. <div class="confirm-body">${content}</div>
  2406. <div class="confirm-footer">
  2407. <button class="btn" onclick="closeConfirmDialog(this); ${onCancel ? 'setTimeout(() => {' + onCancel.toString().replace(/^function\s*\(\)\s*\{/, '').replace(/}$/, '') + '}, 100)' : ''}">取消</button>
  2408. <button class="btn btn-primary" onclick="closeConfirmDialog(this); ${onConfirm ? 'setTimeout(() => {' + onConfirm.toString().replace(/^function\s*\(\)\s*\{/, '').replace(/}$/, '') + '}, 100)' : ''}">确认应用</button>
  2409. </div>
  2410. </div>
  2411. `;
  2412. document.body.appendChild(dialog);
  2413. setTimeout(() => dialog.classList.add('show'), 10);
  2414. }
  2415. function closeConfirmDialog(btn) {
  2416. const dialog = btn.closest('.confirm-dialog');
  2417. dialog.classList.remove('show');
  2418. setTimeout(() => dialog.remove(), 300);
  2419. }
  2420. // === 文本替换功能 ===
  2421. function applyTextReplacement(newText) {
  2422. if (selectedRange) {
  2423. selectedRange.deleteContents();
  2424. selectedRange.insertNode(document.createTextNode(newText));
  2425. showToast('✓ 内容已更新', 'success');
  2426. }
  2427. }
  2428. // 点击其他地方关闭右键菜单
  2429. document.addEventListener('click', function(e) {
  2430. if (!e.target.closest('.context-menu')) {
  2431. hideContextMenu();
  2432. }
  2433. });
  2434. // === 实体标签编辑弹窗 ===
  2435. const entityEditData = {
  2436. smartpark: {
  2437. icon: '🏢', name: '智慧园区', type: 'entity', typeText: '核心实体',
  2438. source: '项目可行性研究报告.docx', originalValue: '智慧园区',
  2439. relations: ['产业升级', '城市现代化', '智能化管理', '1,789亿元']
  2440. }
  2441. };
  2442. let currentEditEntityId = null;
  2443. function showEntityEditModal(event, entityId) {
  2444. event.preventDefault();
  2445. event.stopPropagation();
  2446. const data = entityEditData[entityId];
  2447. if (!data) return;
  2448. currentEditEntityId = entityId;
  2449. document.getElementById('editEntityIcon').textContent = data.icon;
  2450. document.getElementById('editEntityName').textContent = data.name;
  2451. document.getElementById('editEntityType').textContent = data.typeText + ' · 来自 ' + data.source;
  2452. document.getElementById('editEntityValue').value = data.name;
  2453. document.getElementById('editEntityCategory').value = data.type;
  2454. const relationsEl = document.getElementById('editEntityRelations');
  2455. relationsEl.innerHTML = data.relations.map(r =>
  2456. `<span class="popover-relation" onclick="showToast('跳转到: ${r}', 'info')">${r}</span>`
  2457. ).join('') + `<span style="padding:3px 8px;background:var(--primary-light);border-radius:4px;font-size:11px;color:var(--primary);cursor:pointer;" onclick="showToast('添加关联', 'info')">+ 添加</span>`;
  2458. document.getElementById('entityEditModal').classList.add('show');
  2459. }
  2460. function closeEntityEditModal() {
  2461. document.getElementById('entityEditModal').classList.remove('show');
  2462. currentEditEntityId = null;
  2463. }
  2464. function saveEntityEdit() {
  2465. const newValue = document.getElementById('editEntityValue').value;
  2466. const newCategory = document.getElementById('editEntityCategory').value;
  2467. if (currentEditEntityId && entityEditData[currentEditEntityId]) {
  2468. entityEditData[currentEditEntityId].name = newValue;
  2469. entityEditData[currentEditEntityId].type = newCategory;
  2470. showToast('✓ 标签已更新: ' + newValue, 'success');
  2471. }
  2472. closeEntityEditModal();
  2473. }
  2474. // === AI对话 ===
  2475. function handleAiKey(e) {
  2476. if (e.key === 'Enter' && !e.shiftKey) {
  2477. e.preventDefault();
  2478. sendAiMsg();
  2479. }
  2480. }
  2481. function autoResizeTextarea(el) {
  2482. el.style.height = 'auto';
  2483. el.style.height = Math.min(el.scrollHeight, 80) + 'px';
  2484. }
  2485. function sendAiMsg() {
  2486. const textarea = document.getElementById('aiTextarea');
  2487. const msg = textarea.value.trim();
  2488. if (!msg) return;
  2489. const container = document.getElementById('aiMessages');
  2490. container.innerHTML += '<div class="msg user"><div class="msg-avatar">张</div><div class="msg-bubble">' + msg + '</div></div>';
  2491. textarea.value = '';
  2492. textarea.style.height = 'auto';
  2493. container.scrollTop = container.scrollHeight;
  2494. setTimeout(() => {
  2495. container.innerHTML += '<div class="msg ai"><div class="msg-avatar">🤖</div><div class="msg-bubble">好的,我来帮您处理这个请求。基于已解析的文档,我找到了相关内容可以补充到报告中。</div></div>';
  2496. container.scrollTop = container.scrollHeight;
  2497. }, 1000);
  2498. }
  2499. // === 视图切换 ===
  2500. function switchView(view) {
  2501. const originalBtn = document.getElementById('viewOriginal');
  2502. const markedBtn = document.getElementById('viewMarked');
  2503. const originalContent = document.getElementById('contentOriginal');
  2504. const markedContent = document.getElementById('contentMarked');
  2505. if (view === 'original') {
  2506. if (originalBtn) originalBtn.classList.add('active');
  2507. if (markedBtn) markedBtn.classList.remove('active');
  2508. if (originalContent) originalContent.style.display = 'block';
  2509. if (markedContent) markedContent.style.display = 'none';
  2510. showToast('已切换到原文视图 - 显示纯净文档内容', 'info');
  2511. } else {
  2512. if (originalBtn) originalBtn.classList.remove('active');
  2513. if (markedBtn) markedBtn.classList.add('active');
  2514. if (originalContent) originalContent.style.display = 'none';
  2515. if (markedContent) markedContent.style.display = 'block';
  2516. showToast('已切换到标记视图 - 显示AI提取的实体标记和分析内容', 'info');
  2517. }
  2518. }
  2519. // === 文件高亮 ===
  2520. function highlightFile(el) {
  2521. document.querySelectorAll('.file-item').forEach(f => f.classList.remove('active'));
  2522. el.classList.add('active');
  2523. }
  2524. // === 上传模拟 ===
  2525. function simulateUpload() {
  2526. showToast('请选择要上传的文件', 'info');
  2527. }
  2528. // === 导出菜单 ===
  2529. function showExportMenu(btn) {
  2530. const menu = document.getElementById('exportMenu');
  2531. const rect = btn.getBoundingClientRect();
  2532. menu.style.top = (rect.bottom + 8) + 'px';
  2533. menu.style.right = (window.innerWidth - rect.right) + 'px';
  2534. menu.style.display = 'block';
  2535. }
  2536. function hideExportMenu() {
  2537. document.getElementById('exportMenu').style.display = 'none';
  2538. }
  2539. document.addEventListener('click', function(e) {
  2540. if (!e.target.closest('[onclick*="showExportMenu"]') && !e.target.closest('#exportMenu')) {
  2541. hideExportMenu();
  2542. }
  2543. });
  2544. // === AI Tab切换 ===
  2545. function switchAiTab(el, tab) {
  2546. document.querySelectorAll('.ai-tab').forEach(t => t.classList.remove('active'));
  2547. el.classList.add('active');
  2548. showToast('切换到 ' + el.textContent.trim(), 'info');
  2549. }
  2550. // === AI建议 ===
  2551. function acceptSuggestion() {
  2552. try {
  2553. const aiMessages = document.getElementById('aiMessages');
  2554. if (aiMessages) {
  2555. aiMessages.innerHTML += `<div class="msg user"><div class="msg-avatar">张</div><div class="msg-bubble">✓ 我已采纳建议</div></div>`;
  2556. aiMessages.innerHTML += `<div class="msg ai"><div class="msg-avatar">🤖</div><div class="msg-bubble">已根据您的采纳更新内容并插入到文档中。</div></div>`;
  2557. aiMessages.scrollTop = aiMessages.scrollHeight;
  2558. }
  2559. showToast && showToast('✓ 已采纳建议,已发送对话通知', 'success');
  2560. const card = document.getElementById('aiSuggestionCard');
  2561. if (card) try { card.remove(); } catch (e) {}
  2562. } catch (e) { console.warn('acceptSuggestion err', e); }
  2563. }
  2564. function ignoreSuggestion() {
  2565. try {
  2566. const aiMessages = document.getElementById('aiMessages');
  2567. if (aiMessages) {
  2568. aiMessages.innerHTML += `<div class="msg user"><div class="msg-avatar">张</div><div class="msg-bubble">✕ 我已忽略该建议</div></div>`;
  2569. aiMessages.innerHTML += `<div class="msg ai"><div class="msg-avatar">🤖</div><div class="msg-bubble">已忽略建议。如需恢复,请在“建议”中查找历史记录。</div></div>`;
  2570. aiMessages.scrollTop = aiMessages.scrollHeight;
  2571. }
  2572. showToast && showToast('已忽略建议(已转为对话)', 'info');
  2573. const card = document.getElementById('aiSuggestionCard');
  2574. if (card) try { card.remove(); } catch (e) {}
  2575. } catch (e) { console.warn('ignoreSuggestion err', e); }
  2576. }
  2577. function acceptContentSuggestion() {
  2578. try {
  2579. const aiMessages = document.getElementById('aiMessages');
  2580. if (aiMessages) {
  2581. aiMessages.innerHTML += `<div class="msg user"><div class="msg-avatar">张</div><div class="msg-bubble">✓ 请添加建议内容</div></div>`;
  2582. aiMessages.innerHTML += `<div class="msg ai"><div class="msg-avatar">🤖</div><div class="msg-bubble">已插入竞争格局分析章节,您可以在文档中查看并编辑。</div></div>`;
  2583. aiMessages.scrollTop = aiMessages.scrollHeight;
  2584. }
  2585. showToast && showToast('✓ 已添加建议内容(对话形式)', 'success');
  2586. } catch (e) { console.warn('acceptContentSuggestion err', e); }
  2587. }
  2588. function insertGeneratedContent() {
  2589. try {
  2590. const aiMessages = document.getElementById('aiMessages');
  2591. if (aiMessages) {
  2592. aiMessages.innerHTML += `<div class="msg user"><div class="msg-avatar">张</div><div class="msg-bubble">➕ 插入 AI 生成内容</div></div>`;
  2593. aiMessages.innerHTML += `<div class="msg ai"><div class="msg-avatar">🤖</div><div class="msg-bubble">已将 AI 生成的风险分析章节插入到报告中。</div></div>`;
  2594. aiMessages.scrollTop = aiMessages.scrollHeight;
  2595. }
  2596. showToast && showToast('✓ 已插入 AI 生成内容(对话形式)', 'success');
  2597. const card = document.querySelector('.ai-generated-card');
  2598. if (card) try { card.remove(); } catch (e) {}
  2599. } catch (e) { console.warn('insertGeneratedContent err', e); }
  2600. }
  2601. // 将 AI 建议卡片移动到 AI 聊天区(以对话形式呈现),避免页面布局错位
  2602. (function(){
  2603. function moveCardToChat(cardEl) {
  2604. if (!cardEl) return false;
  2605. const aiMessages = document.getElementById('aiMessages');
  2606. if (!aiMessages) return false;
  2607. const title = cardEl.querySelector('.ai-suggestion-title') ? cardEl.querySelector('.ai-suggestion-title').textContent : '';
  2608. const contentEl = cardEl.querySelector('.ai-suggestion-content') || cardEl;
  2609. const contentHtml = contentEl.innerHTML || cardEl.innerHTML;
  2610. const wrapper = document.createElement('div');
  2611. wrapper.className = 'msg ai';
  2612. wrapper.innerHTML = `<div class="msg-avatar">🤖</div><div class="msg-bubble"><strong>${title}</strong><div style="margin-top:8px;">${contentHtml}</div></div>`;
  2613. aiMessages.appendChild(wrapper);
  2614. aiMessages.scrollTop = aiMessages.scrollHeight;
  2615. return true;
  2616. }
  2617. document.addEventListener('DOMContentLoaded', function(){
  2618. try {
  2619. const suggest = document.getElementById('aiSuggestionCard');
  2620. if (suggest) {
  2621. moveCardToChat(suggest);
  2622. try { suggest.remove(); } catch(e){}
  2623. }
  2624. const gen = document.querySelector('.ai-generated-card');
  2625. if (gen) {
  2626. moveCardToChat(gen);
  2627. try { gen.remove(); } catch(e){}
  2628. }
  2629. } catch (e) { console.warn('move AI suggestion to chat err', e); }
  2630. });
  2631. })();
  2632. // === Toast ===
  2633. function showToast(msg, type) {
  2634. const box = document.getElementById('toastContainer');
  2635. const icons = { success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️' };
  2636. const colors = { success: '#f6ffed', error: '#fff1f0', info: 'var(--primary-light)', warning: '#fffbe6' };
  2637. const textColors = { success: 'var(--success)', error: 'var(--danger)', info: 'var(--primary)', warning: 'var(--warning)' };
  2638. const toast = document.createElement('div');
  2639. toast.className = `toast show ${type}`;
  2640. toast.innerHTML = '<span style="width:24px;height:24px;border-radius:50%;background:' + colors[type] + ';color:' + textColors[type] + ';display:flex;align-items:center;justify-content:center;font-size:12px;">' + icons[type] + '</span><span style="font-size:13px;margin-left:10px;">' + msg + '</span>';
  2641. box.appendChild(toast);
  2642. setTimeout(() => {
  2643. toast.classList.remove('show');
  2644. setTimeout(() => toast.remove(), 300);
  2645. }, 3000);
  2646. }
  2647. // === 关闭所有 ===
  2648. function closeAll() {
  2649. document.getElementById('notifPanel').style.transform = 'translateX(100%)';
  2650. document.getElementById('overlay').style.display = 'none';
  2651. document.getElementById('fabPanel').style.display = 'none';
  2652. }
  2653. // === 数据关系表弹窗 ===
  2654. let currentEntityData = {};
  2655. function showDataRelationModal(entityName, entityType, entityCategory, sourceFile) {
  2656. currentEntityData = {
  2657. name: entityName,
  2658. type: entityType,
  2659. category: entityCategory,
  2660. source: sourceFile,
  2661. originalValue: entityName,
  2662. currentValue: entityName
  2663. };
  2664. // 设置弹窗内容
  2665. document.getElementById('relationEntityName').textContent = entityName;
  2666. document.getElementById('relationEntityType').textContent = entityType + ' · 来自 ' + (sourceFile || '未知来源');
  2667. // 动态创建或更新来源信息区域(如果 modal HTML 中不存在)
  2668. let sourceInfoEl = document.getElementById('relationSourceInfo');
  2669. if (!sourceInfoEl) {
  2670. const titleEl = document.querySelector('.data-relation-title');
  2671. if (titleEl) {
  2672. sourceInfoEl = document.createElement('div');
  2673. sourceInfoEl.id = 'relationSourceInfo';
  2674. sourceInfoEl.className = 'relation-source';
  2675. sourceInfoEl.style = 'margin-top:6px;font-size:12px;color:var(--text3);';
  2676. titleEl.appendChild(sourceInfoEl);
  2677. }
  2678. }
  2679. if (sourceInfoEl) {
  2680. const safeSource = sourceFile || '未知来源';
  2681. sourceInfoEl.innerHTML = `来源:<a href="#" id="relationSourceLink" onclick="openSourceFile(event, '${safeSource.replace(/'/g, "\\'")}')">查看来源文件</a>`;
  2682. }
  2683. // 设置图标
  2684. const iconEl = document.getElementById('relationIcon');
  2685. const icons = { entity: '🏢', concept: '💡', data: '📊', location: '📍' };
  2686. iconEl.textContent = icons[entityCategory] || '🏷️';
  2687. // 生成关系表
  2688. generateRelationTable(entityName, entityType, entityCategory, sourceFile);
  2689. // 显示弹窗
  2690. document.getElementById('dataRelationModal').classList.add('show');
  2691. }
  2692. function generateRelationTable(entityName, entityType, entityCategory, sourceFile) {
  2693. const tableBody = document.getElementById('relationTableBody');
  2694. const relationTags = document.getElementById('relationTags');
  2695. // 模拟关系数据
  2696. const relationData = {
  2697. '智慧园区': [
  2698. { property: '实体名称', original: '智慧园区', current: '智慧园区', canEdit: true },
  2699. { property: '实体类型', original: '核心实体', current: '核心实体', canEdit: false },
  2700. { property: '数据来源', original: '项目可行性研究报告.docx', current: '项目可行性研究报告.docx', canEdit: false },
  2701. { property: '出现次数', original: '8次', current: '8次', canEdit: false }
  2702. ],
  2703. '产业升级': [
  2704. { property: '概念名称', original: '产业升级', current: '产业升级', canEdit: true },
  2705. { property: '概念类型', original: '发展概念', current: '发展概念', canEdit: false },
  2706. { property: '数据来源', original: '项目可行性研究报告.docx', current: '项目可行性研究报告.docx', canEdit: false },
  2707. { property: '关联强度', original: '高', current: '高', canEdit: false }
  2708. ],
  2709. '1,789亿元': [
  2710. { property: '数值', original: '1,789亿元', current: '1,789亿元', canEdit: true },
  2711. { property: '数据类型', original: '市场规模', current: '市场规模', canEdit: false },
  2712. { property: '数据来源', original: '市场调研数据.pdf', current: '市场调研数据.pdf', canEdit: false },
  2713. { property: '置信度', original: '95%', current: '95%', canEdit: false }
  2714. ]
  2715. };
  2716. const data = relationData[entityName] ? relationData[entityName].slice() : [
  2717. { property: '标签名称', original: entityName, current: entityName, canEdit: true },
  2718. { property: '标签类型', original: entityType, current: entityType, canEdit: false },
  2719. { property: '数据来源', original: sourceFile || '未知来源', current: sourceFile || '未知来源', canEdit: false }
  2720. ];
  2721. // 确保展示来源文件(优先使用传入的 sourceFile)
  2722. const hasSourceRow = data.some(r => /来源|数据来源/.test(r.property));
  2723. if (!hasSourceRow) {
  2724. data.unshift({ property: '来源文件', original: sourceFile || '未知来源', current: sourceFile || '未知来源', canEdit: false });
  2725. } else if (sourceFile) {
  2726. data.forEach(r => {
  2727. if (/来源|数据来源/.test(r.property)) {
  2728. r.original = sourceFile;
  2729. r.current = sourceFile;
  2730. }
  2731. });
  2732. }
  2733. tableBody.innerHTML = data.map(row => `
  2734. <tr>
  2735. <td>${row.property}</td>
  2736. <td class="original">${row.original}</td>
  2737. <td class="current">
  2738. ${row.canEdit ?
  2739. `<input type="text" class="relation-input" value="${row.current}" onchange="updateEntityValue('${row.property}', this.value)">` :
  2740. row.current
  2741. }
  2742. </td>
  2743. <td>
  2744. ${row.canEdit ? '<button class="btn" onclick="resetEntityValue(this)" style="font-size:11px;padding:4px 8px;">重置</button>' : '-'}
  2745. </td>
  2746. </tr>
  2747. `).join('');
  2748. // 生成关联标签
  2749. const relatedEntities = ['产业升级', '城市现代化', '智能化管理', '1,789亿元', '华南地区'];
  2750. relationTags.innerHTML = relatedEntities.map(entity => `
  2751. <span class="relation-tag" onclick="showDataRelationModal('${entity}', '关联实体', 'entity', '关联文档')">${entity}</span>
  2752. `).join('');
  2753. }
  2754. function updateEntityValue(property, newValue) {
  2755. currentEntityData.currentValue = newValue;
  2756. showToast(`已更新 ${property}: ${newValue}`, 'success');
  2757. }
  2758. function resetEntityValue(btn) {
  2759. const input = btn.closest('tr').querySelector('.relation-input');
  2760. input.value = currentEntityData.originalValue;
  2761. currentEntityData.currentValue = currentEntityData.originalValue;
  2762. showToast('已重置为原始值', 'info');
  2763. }
  2764. function saveDataRelationChanges() {
  2765. if (currentEntityData.currentValue !== currentEntityData.originalValue) {
  2766. showToast(`标签已更新: ${currentEntityData.originalValue} → ${currentEntityData.currentValue}`, 'success');
  2767. // 这里可以添加实际的保存逻辑
  2768. }
  2769. closeDataRelationModal();
  2770. }
  2771. function closeDataRelationModal() {
  2772. document.getElementById('dataRelationModal').classList.remove('show');
  2773. }
  2774. // === 要素关系图谱 ===
  2775. function toggleKnowledgeGraph() {
  2776. const modal = document.getElementById('knowledgeGraphModal');
  2777. const overlay = document.getElementById('kgOverlay');
  2778. const btn = document.getElementById('graphBtn');
  2779. const isOpen = modal.classList.contains('show');
  2780. if (isOpen) {
  2781. modal.classList.remove('show');
  2782. overlay.style.display = 'none';
  2783. if (btn) btn.classList.remove('active');
  2784. } else {
  2785. modal.classList.add('show');
  2786. overlay.style.display = 'block';
  2787. if (btn) btn.classList.add('active');
  2788. showToast('已打开要素关系图谱', 'info');
  2789. }
  2790. }
  2791. function closeKnowledgeGraph() {
  2792. const modal = document.getElementById('knowledgeGraphModal');
  2793. const overlay = document.getElementById('kgOverlay');
  2794. const btn = document.getElementById('graphBtn');
  2795. modal.classList.remove('show');
  2796. overlay.style.display = 'none';
  2797. if (btn) btn.classList.remove('active');
  2798. }
  2799. function switchGraphView(view) {
  2800. const graphView = document.getElementById('graphView');
  2801. const listView = document.getElementById('listView');
  2802. const graphBtn = document.getElementById('graphViewBtn');
  2803. const listBtn = document.getElementById('listViewBtn');
  2804. if (view === 'graph') {
  2805. graphView.style.display = 'block';
  2806. listView.style.display = 'none';
  2807. graphBtn.classList.add('active');
  2808. listBtn.classList.remove('active');
  2809. showToast('切换到图谱视图', 'info');
  2810. } else {
  2811. graphView.style.display = 'none';
  2812. listView.style.display = 'block';
  2813. graphBtn.classList.remove('active');
  2814. listBtn.classList.add('active');
  2815. showToast('切换到列表视图', 'info');
  2816. }
  2817. }
  2818. function filterEntities(query) {
  2819. const items = document.querySelectorAll('.entity-item');
  2820. const lowerQuery = query.toLowerCase();
  2821. items.forEach(item => {
  2822. const name = item.querySelector('.entity-name').textContent.toLowerCase();
  2823. if (name.includes(lowerQuery) || query === '') {
  2824. item.style.display = 'flex';
  2825. } else {
  2826. item.style.display = 'none';
  2827. }
  2828. });
  2829. }
  2830. function highlightEntity(entityName) {
  2831. // 高亮图谱中的节点
  2832. document.querySelectorAll('.graph-node').forEach(node => {
  2833. node.classList.remove('highlighted');
  2834. if (node.dataset.entity === entityName) {
  2835. node.classList.add('highlighted');
  2836. node.scrollIntoView({ behavior: 'smooth', block: 'center' });
  2837. }
  2838. });
  2839. showToast('已高亮要素: ' + entityName, 'info');
  2840. }
  2841. function locateEntity(entityName) {
  2842. closeKnowledgeGraph();
  2843. // 切换到标记视图
  2844. switchView('marked');
  2845. // 滚动到对应位置(这里可以根据实际情况调整)
  2846. showToast('已定位到要素: ' + entityName, 'success');
  2847. }
  2848. function editEntity(entityName) {
  2849. showToast('编辑要素: ' + entityName, 'info');
  2850. // 这里可以打开要素编辑弹窗
  2851. }
  2852. // 图谱节点点击事件
  2853. document.addEventListener('DOMContentLoaded', function() {
  2854. document.querySelectorAll('.graph-node').forEach(node => {
  2855. node.addEventListener('click', function() {
  2856. const entityName = this.dataset.entity;
  2857. highlightEntity(entityName);
  2858. });
  2859. });
  2860. });
  2861. // 页面加载后初始化来源徽章
  2862. document.addEventListener('DOMContentLoaded', function() {
  2863. try {
  2864. if (typeof initializeSourceBadges === 'function') initializeSourceBadges();
  2865. } catch (e) {
  2866. console.warn('initializeSourceBadges error', e);
  2867. }
  2868. });
  2869. // === 段落选中与交互控制 ===
  2870. // 将光标所在段落标记为 selected,仅高亮该段
  2871. function updateSelectedParagraph() {
  2872. try {
  2873. const sel = window.getSelection();
  2874. if (!sel || sel.rangeCount === 0) return;
  2875. const anchor = sel.anchorNode;
  2876. if (!anchor) return;
  2877. // 找到包含光标的段落元素(p 或 div 作段落)
  2878. let paragraph = null;
  2879. if (anchor.nodeType === Node.TEXT_NODE) paragraph = anchor.parentElement.closest('p, div');
  2880. else if (anchor.nodeType === Node.ELEMENT_NODE) paragraph = anchor.closest('p, div');
  2881. // 仅在编辑区内生效
  2882. document.querySelectorAll('.editor-content p, .editor-content div').forEach(el => el.classList.remove('selected'));
  2883. if (paragraph && paragraph.closest('.editor-content')) {
  2884. paragraph.classList.add('selected');
  2885. }
  2886. } catch (e) {
  2887. console.warn('updateSelectedParagraph error', e);
  2888. }
  2889. }
  2890. // 监听选择变化、点击、键盘输入以更新段落选中状态
  2891. document.addEventListener('selectionchange', () => {
  2892. const active = document.activeElement;
  2893. if (active && active.classList && active.classList.contains('editor-content')) {
  2894. updateSelectedParagraph();
  2895. }
  2896. });
  2897. // 点击编辑区也触发更新(兼容鼠标点击)
  2898. document.querySelectorAll('.editor-content').forEach(editor => {
  2899. editor.addEventListener('click', updateSelectedParagraph);
  2900. editor.addEventListener('keyup', updateSelectedParagraph);
  2901. editor.addEventListener('input', updateSelectedParagraph);
  2902. });
  2903. // 点击页面其它区域时清除段落选中样式
  2904. document.addEventListener('click', (e) => {
  2905. if (!e.target.closest('.editor-content')) {
  2906. document.querySelectorAll('.editor-content p.selected, .editor-content div.selected').forEach(p => p.classList.remove('selected'));
  2907. }
  2908. });
  2909. // === AI润色/拼写确认与文本替换(右键) ===
  2910. // showAiPolishConfirm / showSpellCheckConfirm / applyTextReplacement 已定义于右键逻辑区域
  2911. // === 初始化 ===
  2912. setTimeout(() => showToast('欢迎使用灵越智报平台 🎉', 'success'), 500);
  2913. // 更新欢迎语
  2914. (function() {
  2915. const hour = new Date().getHours();
  2916. let greeting = '早上好';
  2917. if (hour >= 12 && hour < 18) greeting = '下午好';
  2918. else if (hour >= 18) greeting = '晚上好';
  2919. const el = document.querySelector('.welcome h1');
  2920. if (el) el.innerHTML = greeting + ',张三!<span>智能报告,洞察未来。</span>';
  2921. })();
  2922. // 模拟解析进度
  2923. let progress1 = 65;
  2924. setInterval(() => {
  2925. progress1 += Math.random() * 3;
  2926. if (progress1 >= 100) {
  2927. progress1 = 100;
  2928. const el = document.querySelector('#parsingFile1 .file-status');
  2929. if (el) {
  2930. el.textContent = '✓ 已完成';
  2931. el.className = 'file-status done';
  2932. }
  2933. } else {
  2934. const el = document.querySelector('#parsingFile1 .file-status');
  2935. if (el && el.classList.contains('parsing')) {
  2936. el.textContent = '📊 解析中 ' + Math.floor(progress1) + '%';
  2937. }
  2938. }
  2939. }, 2000);
  2940. // === 文件项交互(悬浮图标、解析弹窗、查看/引用/删除) ===
  2941. function initializeFileActions() {
  2942. document.querySelectorAll('.file-item').forEach(item => {
  2943. try {
  2944. const statusEl = item.querySelector('.file-status');
  2945. const statusParsing = statusEl && statusEl.classList.contains('parsing');
  2946. const statusDone = statusEl && statusEl.classList.contains('done');
  2947. // store demo content for viewing/引用
  2948. if (!item.dataset.content) {
  2949. item.dataset.content = `【${item.querySelector('.file-name') ? item.querySelector('.file-name').textContent : '文档'}】原文预览:这是用于演示的文档内容片段。`;
  2950. }
  2951. // create actions container
  2952. let actions = item.querySelector('.file-actions');
  2953. if (!actions) {
  2954. actions = document.createElement('div');
  2955. actions.className = 'file-actions';
  2956. item.appendChild(actions);
  2957. }
  2958. actions.innerHTML = '';
  2959. if (statusParsing) {
  2960. // parsing: show delete icon on hover
  2961. const delBtn = document.createElement('div');
  2962. delBtn.className = 'action-btn';
  2963. delBtn.title = '删除';
  2964. delBtn.innerHTML = '🗑️';
  2965. delBtn.onclick = (e) => { e.stopPropagation(); deleteFile(item); };
  2966. actions.appendChild(delBtn);
  2967. // clicking the file shows parsing popover
  2968. item.onclick = (e) => { e.stopPropagation(); showParsingPopover(item); };
  2969. } else {
  2970. // done: show view, quote, delete
  2971. const viewBtn = document.createElement('div');
  2972. viewBtn.className = 'action-btn';
  2973. viewBtn.title = '查看';
  2974. viewBtn.innerHTML = '👁️';
  2975. viewBtn.onclick = (e) => { e.stopPropagation(); showFilePreview(item); };
  2976. actions.appendChild(viewBtn);
  2977. const quoteBtn = document.createElement('div');
  2978. quoteBtn.className = 'action-btn';
  2979. quoteBtn.title = '引用到AI助手';
  2980. quoteBtn.innerHTML = '🔗';
  2981. quoteBtn.onclick = (e) => { e.stopPropagation(); quoteFileToAI(item); };
  2982. actions.appendChild(quoteBtn);
  2983. const delBtn = document.createElement('div');
  2984. delBtn.className = 'action-btn';
  2985. delBtn.title = '删除';
  2986. delBtn.innerHTML = '🗑️';
  2987. delBtn.onclick = (e) => { e.stopPropagation(); deleteFile(item); };
  2988. actions.appendChild(delBtn);
  2989. // clicking file selects it (preview)
  2990. item.onclick = (e) => { e.stopPropagation(); showFilePreview(item); };
  2991. }
  2992. } catch (err) { console.error('init file action', err); }
  2993. });
  2994. }
  2995. // 左侧 tabs 切换
  2996. function switchLeftTab(tab) {
  2997. const docs = document.getElementById('leftDocsSection');
  2998. const files = document.getElementById('leftFilesSection');
  2999. // safely toggle primary tab elements (some templates may use different ids)
  3000. const tabDocsEl = document.getElementById('tabDocs');
  3001. const tabFilesEl = document.getElementById('tabFiles');
  3002. if (tabDocsEl) tabDocsEl.classList.toggle('active', tab === 'docs');
  3003. if (tabFilesEl) tabFilesEl.classList.toggle('active', tab === 'files');
  3004. // sync top header tabs if present (these IDs exist in the DOM)
  3005. const topDocs = document.getElementById('tabDocsTop');
  3006. const topFiles = document.getElementById('tabFilesTop');
  3007. if (topDocs) topDocs.classList.toggle('active', tab === 'docs');
  3008. if (topFiles) topFiles.classList.toggle('active', tab === 'files');
  3009. // Keep the left panel split into two parts:
  3010. // - top: dynamic area (.docs-area) that shows either "我的文档" or "我的附件"
  3011. // - bottom: .recent-area (always visible)
  3012. const docsArea = docs ? docs.querySelector('.docs-area') : null;
  3013. const recentArea = docs ? docs.querySelector('.recent-area') : null;
  3014. // Ensure the leftDocsSection is visible (we render top content into docsArea)
  3015. if (docs) docs.style.display = '';
  3016. if (files) files.style.display = 'none'; // keep files section unused visually
  3017. if (recentArea) recentArea.style.display = '';
  3018. if (tab === 'docs') {
  3019. // show documents list in the top area
  3020. if (docsArea) {
  3021. docsArea.style.display = '';
  3022. // re-render / sort documents if function exists
  3023. try { if (typeof updateReportsCountAndSort === 'function') updateReportsCountAndSort(); } catch (e) { console.warn('updateReportsCountAndSort err', e); }
  3024. // restore header title to "报告记录"
  3025. try {
  3026. const reportsTitleEl = document.getElementById('reportsTitle');
  3027. const reportsCountEl = document.getElementById('reportsCount');
  3028. if (reportsTitleEl && reportsTitleEl.firstChild) reportsTitleEl.firstChild.nodeValue = '报告记录 ';
  3029. if (reportsCountEl) {
  3030. // updateReportsCountAndSort already sets count, but ensure display format
  3031. reportsCountEl.textContent = reportsCountEl.textContent || '· ' + (document.querySelectorAll('#leftDocsSection .doc-card').length || 0);
  3032. }
  3033. } catch (ee) { console.warn('restore reports title err', ee); }
  3034. }
  3035. } else {
  3036. // show attachments list in the same top area
  3037. if (docsArea) {
  3038. docsArea.style.display = '';
  3039. try { if (typeof renderAttachments === 'function') renderAttachments(); } catch (e) { console.warn('renderAttachments err', e); }
  3040. }
  3041. }
  3042. }
  3043. // 更新报告计数并按时间倒序排列列表
  3044. function updateReportsCountAndSort() {
  3045. try {
  3046. const list = document.querySelector('#leftDocsSection .doc-list');
  3047. if (!list) return;
  3048. const items = Array.from(list.querySelectorAll('.doc-card'));
  3049. // parse date from .doc-time like "2026/1/26 11:51:44"
  3050. items.sort((a, b) => {
  3051. const ta = a.querySelector('.doc-time') ? new Date(a.querySelector('.doc-time').textContent.replace(/-/g, '/')) : new Date(0);
  3052. const tb = b.querySelector('.doc-time') ? new Date(b.querySelector('.doc-time').textContent.replace(/-/g, '/')) : new Date(0);
  3053. return tb - ta;
  3054. });
  3055. // re-append in sorted order
  3056. items.forEach(it => list.appendChild(it));
  3057. // update count
  3058. const count = items.length;
  3059. const rc = document.getElementById('reportsCount');
  3060. if (rc) rc.textContent = '· ' + count;
  3061. const leftBadge = document.getElementById('leftDocsCount');
  3062. if (leftBadge) leftBadge.textContent = count;
  3063. } catch (e) { console.warn('updateReportsCountAndSort err', e); }
  3064. }
  3065. document.addEventListener('DOMContentLoaded', function() {
  3066. try { updateReportsCountAndSort(); } catch (e) {}
  3067. // wire up search/new buttons (placeholders)
  3068. const searchBtn = document.getElementById('leftSearchBtn');
  3069. if (searchBtn) searchBtn.onclick = () => { const q = prompt('搜索报告:'); if (q) alert('搜索: ' + q); };
  3070. const newBtn = document.getElementById('newReportBtn');
  3071. if (newBtn) newBtn.onclick = () => { alert('新建报告(示意)'); };
  3072. });
  3073. let currentParsingPopover = null;
  3074. function showParsingPopover(item) {
  3075. closeParsingPopover();
  3076. const rect = item.getBoundingClientRect();
  3077. const pop = document.createElement('div');
  3078. pop.className = 'parsing-popover';
  3079. pop.style.left = Math.min(rect.right + 12, window.innerWidth - 340) + 'px';
  3080. pop.style.top = Math.max(rect.top, 80) + 'px';
  3081. pop.innerHTML = `<div class="close-btn" onclick="closeParsingPopover()">×</div>
  3082. <div class="title"><div>解析过程 — ${item.querySelector('.file-name') ? item.querySelector('.file-name').textContent : ''}</div></div>
  3083. <div class="parsing-progress">
  3084. <span class="seg active"></span>
  3085. <span class="seg"></span>
  3086. <span class="seg"></span>
  3087. <span class="seg"></span>
  3088. <span class="seg"></span>
  3089. </div>
  3090. <div class="steps-list">
  3091. <div class="step active"><div class="dot"></div><div class="label">文档读取</div></div>
  3092. <div class="step pending"><div class="dot"></div><div class="label">结构解析</div></div>
  3093. <div class="step pending"><div class="dot"></div><div class="label">实体提取</div></div>
  3094. <div class="step pending"><div class="dot"></div><div class="label">关系分析</div></div>
  3095. <div class="step pending"><div class="dot"></div><div class="label">图谱构建</div></div>
  3096. </div>
  3097. <div style="margin-top:10px;font-size:12px;color:var(--text3);">注:此为演示流程,实际解析时间取决于文档大小与系统性能。</div>`;
  3098. document.body.appendChild(pop);
  3099. currentParsingPopover = pop;
  3100. // simulate progress steps (vertical, completed=green, active=blue, pending=gray)
  3101. let idx = 0;
  3102. const steps = pop.querySelectorAll('.step');
  3103. const segs = pop.querySelectorAll('.parsing-progress .seg');
  3104. // ensure initial classes: first active, others pending
  3105. steps.forEach((s, i) => {
  3106. if (i === 0) { s.classList.remove('pending'); s.classList.add('active'); }
  3107. else { s.classList.remove('active'); s.classList.add('pending'); }
  3108. });
  3109. const t = setInterval(() => {
  3110. // mark previous as completed
  3111. if (idx > 0 && steps[idx-1]) {
  3112. steps[idx-1].classList.remove('active');
  3113. steps[idx-1].classList.remove('pending');
  3114. steps[idx-1].classList.add('completed');
  3115. }
  3116. // set current as active
  3117. if (idx < steps.length && steps[idx]) {
  3118. steps[idx].classList.remove('pending');
  3119. steps[idx].classList.remove('completed');
  3120. steps[idx].classList.add('active');
  3121. }
  3122. // update segs
  3123. segs.forEach((s, i) => {
  3124. if (i < idx) s.classList.add('active');
  3125. else s.classList.remove('active');
  3126. });
  3127. idx++;
  3128. if (idx > steps.length) {
  3129. clearInterval(t);
  3130. // mark last as completed
  3131. if (steps[steps.length-1]) {
  3132. steps[steps.length-1].classList.remove('active');
  3133. steps[steps.length-1].classList.remove('pending');
  3134. steps[steps.length-1].classList.add('completed');
  3135. }
  3136. showToast('解析完成:已生成要素与图谱', 'success');
  3137. // update status to done for demo
  3138. const statusEl = item.querySelector('.file-status');
  3139. if (statusEl) { statusEl.textContent = '✓ 已完成'; statusEl.className = 'file-status done'; }
  3140. initializeFileActions();
  3141. closeParsingPopover();
  3142. }
  3143. }, 900);
  3144. }
  3145. function closeParsingPopover() {
  3146. if (currentParsingPopover) { currentParsingPopover.remove(); currentParsingPopover = null; }
  3147. }
  3148. // === 本地文件预览映射与本地上下文存储(演示 / 可替换为后端接口) ===
  3149. const fileContentsMap = {
  3150. '市场调研数据.pdf': '【市场调研数据.pdf】\\n摘要:本报告基于2024年全国范围抽样调研,市场规模、增长率、细分行业表现等关键指标已整理。',
  3151. '技术方案说明.pdf': '【技术方案说明.pdf】\\n摘要:本文档描述了平台架构、数据接入与解析流程、实体抽取模型与接口。',
  3152. '项目可行性研究报告.docx': '【项目可行性研究报告.docx】\\n原文节选:本项目位于华南地区,规划总面积约50万平方米,预计总投资12.5亿元。',
  3153. '财务预测表.xlsx': '【财务预测表.xlsx】\\n表格摘要:收入、利润、投资回收期与分年度预测数据(见表格)。'
  3154. };
  3155. const localKnowledgeContext = {
  3156. files: {},
  3157. addFile(fileName, content) { this.files[fileName] = content; },
  3158. removeFile(fileName) { delete this.files[fileName]; },
  3159. listFiles() { return Object.keys(this.files); },
  3160. getCombinedContext() { return Object.values(this.files).join('\\n\\n'); }
  3161. };
  3162. function showFilePreview(item) {
  3163. // 使用大弹窗模态展示文件预览(不再在右侧显示)
  3164. closeFilePreview();
  3165. const nameEl = item.querySelector('.file-name');
  3166. const fileName = nameEl ? nameEl.textContent.trim() : null;
  3167. const content = (fileName && fileContentsMap[fileName]) ? fileContentsMap[fileName] : (item.dataset.content || (fileName ? `原文:${fileName}` : '无预览内容'));
  3168. const overlay = document.createElement('div');
  3169. overlay.className = 'file-preview-modal-overlay';
  3170. const modal = document.createElement('div');
  3171. modal.className = 'file-preview-modal';
  3172. modal.innerHTML = `
  3173. <div class="modal-header">
  3174. <div class="modal-title">${fileName || '文件预览'}</div>
  3175. <div style="font-size:12px;color:var(--text3)">${new Date().toLocaleString()}</div>
  3176. </div>
  3177. <button class="file-preview-close" onclick="closeFilePreview()">×</button>
  3178. <div class="modal-body" id="filePreviewContent">${content}</div>
  3179. `;
  3180. overlay.appendChild(modal);
  3181. document.body.appendChild(overlay);
  3182. // prevent scrolling behind modal
  3183. document.body.style.overflow = 'hidden';
  3184. showToast('已打开文件预览弹窗', 'info');
  3185. }
  3186. function closeFilePreview() {
  3187. const overlay = document.querySelector('.file-preview-modal-overlay');
  3188. if (overlay) overlay.remove();
  3189. document.body.style.overflow = '';
  3190. }
  3191. // 将文件内容加入本地上下文(用于模拟后端检索/聊天上下文)
  3192. function quoteFileToAI(item) {
  3193. const nameEl = item.querySelector('.file-name');
  3194. const fileName = nameEl ? nameEl.textContent.trim() : ('文件_' + Date.now());
  3195. const content = (fileName && fileContentsMap[fileName]) ? fileContentsMap[fileName] : (item.dataset.content || fileName);
  3196. localKnowledgeContext.addFile(fileName, content);
  3197. const aiMessages = document.getElementById('aiMessages');
  3198. const msg = `[知识背景引用] 已将 ${fileName} 的内容加入本地上下文。当前上下文文件:${localKnowledgeContext.listFiles().join(', ')}`;
  3199. if (aiMessages) {
  3200. aiMessages.innerHTML += `
  3201. <div class="msg user">
  3202. <div class="msg-avatar">张</div>
  3203. <div class="msg-bubble">${msg}</div>
  3204. </div>
  3205. `;
  3206. aiMessages.scrollTop = aiMessages.scrollHeight;
  3207. }
  3208. showToast('已把文件内容加入本地上下文,用于AI检索/问答(模拟)', 'success');
  3209. }
  3210. function deleteFile(item) {
  3211. const name = item.querySelector('.file-name') ? item.querySelector('.file-name').textContent : '文件';
  3212. if (!confirm(`确认删除 "${name}" 吗?`)) return;
  3213. item.remove();
  3214. showToast(`已删除 ${name}`, 'info');
  3215. }
  3216. // === 我的附件(左侧)渲染与操作 ===
  3217. // 附件示例数据(显示为样例截图中的文件名与大小,parsedAt 用于排序)
  3218. let attachmentsData = [
  3219. { name: '市场调研数据.pdf', size: '5.8 MB', parsedAt: '2026-01-27T10:00:00' },
  3220. { name: '技术方案说明.pdf', size: '3.6 MB', parsedAt: '2026-01-26T16:00:00' },
  3221. { name: '项目可行性研究报告.docx', size: '2.4 MB', parsedAt: '2026-01-25T09:00:00' },
  3222. { name: '财务预测表.xlsx', size: '1.2 MB', parsedAt: '2026-01-24T11:30:00' },
  3223. { name: '会议纪要.md', size: '48 KB', parsedAt: '2026-01-23T08:00:00' }
  3224. ];
  3225. function formatParsedAt(iso) {
  3226. const d = new Date(iso);
  3227. if (isNaN(d)) return iso;
  3228. const pad = n => n.toString().padStart(2, '0');
  3229. return `${d.getFullYear()}/${d.getMonth()+1}/${d.getDate()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
  3230. }
  3231. function getIconForName(name) {
  3232. const ext = (name.split('.').pop() || '').toLowerCase();
  3233. if (ext === 'xlsx') return '<span class="file-badge xls">XLS</span>';
  3234. if (ext === 'docx' || ext === 'doc') return '<span class="file-badge doc">DOC</span>';
  3235. if (ext === 'pdf') return '<span class="file-badge pdf">PDF</span>';
  3236. if (ext === 'md' || ext === 'txt') return '<span class="file-badge md">MD</span>';
  3237. return '<span class="file-badge">FILE</span>';
  3238. }
  3239. function renderAttachments() {
  3240. // Render into the left files top area by default (original behavior).
  3241. // Fallback to leftDocsSection top area only if files area is missing.
  3242. let container = document.querySelector('#leftFilesSection .doc-list');
  3243. if (!container) container = document.querySelector('#leftDocsSection .docs-area .doc-list');
  3244. if (!container) return;
  3245. // sort by parsedAt desc
  3246. attachmentsData.sort((a, b) => new Date(b.parsedAt) - new Date(a.parsedAt));
  3247. container.innerHTML = attachmentsData.map(att => {
  3248. return `
  3249. <div class="doc-card file-item" data-name="${att.name}" data-parsedat="${att.parsedAt}">
  3250. <div class="doc-thumb">${getIconForName(att.name)}</div>
  3251. <div class="file-actions" style="display:flex;gap:6px;">
  3252. <button class="action-btn" title="查看" onclick="showFilePreview(this.closest('.file-item'))">👁️</button>
  3253. <button class="action-btn" title="引用到AI助手" onclick="quoteFileToAI(this.closest('.file-item'))">🔗</button>
  3254. <button class="action-btn" title="删除" onclick="deleteAttachment(this.closest('.file-item'))">🗑️</button>
  3255. </div>
  3256. <div class="doc-meta">
  3257. <div class="file-name">${att.name}</div>
  3258. <div class="file-time">${att.size} · <span style="color:var(--success);font-weight:600;">✓ 已完成</span></div>
  3259. </div>
  3260. </div>
  3261. `;
  3262. }).join('');
  3263. // update header title/count to "全部附件"
  3264. try {
  3265. const reportsTitleEl = document.getElementById('reportsTitle');
  3266. const reportsCountEl = document.getElementById('reportsCount');
  3267. if (reportsTitleEl && reportsTitleEl.firstChild) reportsTitleEl.firstChild.nodeValue = '全部附件 ';
  3268. if (reportsCountEl) reportsCountEl.textContent = '· ' + attachmentsData.length;
  3269. } catch (e) { console.warn('update reports title err', e); }
  3270. // ensure hover/action wiring consistent
  3271. initializeFileActions();
  3272. }
  3273. function deleteAttachment(item) {
  3274. const name = item.querySelector('.file-name') ? item.querySelector('.file-name').textContent : '文件';
  3275. if (!confirm(`确认删除 "${name}" 吗?`)) return;
  3276. // remove from DOM
  3277. item.remove();
  3278. // remove from data model
  3279. attachmentsData = attachmentsData.filter(a => a.name !== name);
  3280. showToast(`已删除 ${name}`, 'info');
  3281. }
  3282. // 自动在 DOM 加载后渲染附件列表
  3283. document.addEventListener('DOMContentLoaded', function() {
  3284. try {
  3285. console.debug('[debug] DOMContentLoaded: calling renderAttachments()');
  3286. renderAttachments();
  3287. } catch (e) { console.warn('renderAttachments err', e); }
  3288. });
  3289. // 安全绑定:确保顶部左右 tab 按钮可用并在页面刷新后渲染附件(防止浏览器缓存/执行顺序问题)
  3290. document.addEventListener('DOMContentLoaded', function() {
  3291. try {
  3292. const tabDocsTop = document.getElementById('tabDocsTop');
  3293. const tabFilesTop = document.getElementById('tabFilesTop');
  3294. if (tabDocsTop) {
  3295. try { tabDocsTop.removeAttribute('onclick'); } catch(e){}
  3296. tabDocsTop.addEventListener('click', () => { console.debug('[debug] tabDocsTop clicked'); switchLeftTab('docs'); });
  3297. }
  3298. if (tabFilesTop) {
  3299. try { tabFilesTop.removeAttribute('onclick'); } catch(e){}
  3300. tabFilesTop.addEventListener('click', () => { console.debug('[debug] tabFilesTop clicked'); switchLeftTab('files'); });
  3301. }
  3302. // Force a re-render after a short delay to work around stale caches or race conditions
  3303. setTimeout(() => {
  3304. try { console.debug('[debug] delayed renderAttachments() executing'); if (typeof renderAttachments === 'function') renderAttachments(); } catch (e) { console.warn('delayed renderAttachments err', e); }
  3305. }, 200);
  3306. } catch (err) { console.warn('init tabs binding err', err); }
  3307. });
  3308. // call initializer after DOM ready
  3309. document.addEventListener('DOMContentLoaded', function() {
  3310. try { initializeFileActions(); } catch (e) { console.warn('init file actions err', e); }
  3311. try { openEditor(); } catch (e) { console.warn('openEditor init err', e); }
  3312. });
  3313. // 确保左侧资源默认不选中任何文件
  3314. document.addEventListener('DOMContentLoaded', function() {
  3315. try {
  3316. document.querySelectorAll('.left-panel .file-item.active').forEach(el => el.classList.remove('active'));
  3317. } catch (e) { console.warn('clear active', e); }
  3318. });
  3319. // === 左侧资源面板 折叠/展开 控制 ===
  3320. function toggleResourcePanel(btn) {
  3321. const panel = document.querySelector('.left-panel');
  3322. if (!panel) return;
  3323. const isHidden = panel.dataset && panel.dataset.collapsed === 'true';
  3324. if (!isHidden) {
  3325. // collapse
  3326. panel.dataset.collapsed = 'true';
  3327. panel.style.display = 'none';
  3328. if (btn) btn.textContent = '❯';
  3329. // create small floating expand button
  3330. let expand = document.getElementById('resourceExpandBtn');
  3331. if (!expand) {
  3332. expand = document.createElement('button');
  3333. expand.id = 'resourceExpandBtn';
  3334. expand.className = 'resource-expand-btn';
  3335. expand.title = '展开资源面板';
  3336. expand.innerHTML = '≡';
  3337. expand.onclick = () => {
  3338. expand.remove();
  3339. panel.dataset.collapsed = 'false';
  3340. panel.style.display = '';
  3341. const pbtn = document.querySelector('.panel-toggle-btn');
  3342. if (pbtn) pbtn.textContent = '❮';
  3343. };
  3344. document.body.appendChild(expand);
  3345. }
  3346. } else {
  3347. // expand
  3348. panel.dataset.collapsed = 'false';
  3349. panel.style.display = '';
  3350. if (btn) btn.textContent = '❮';
  3351. const expand = document.getElementById('resourceExpandBtn');
  3352. if (expand) expand.remove();
  3353. }
  3354. }
  3355. </script>
  3356. <script>
  3357. // 页面级别强制:所有导航重定向到编辑器,并确保编辑器在初次打开时是可见的
  3358. try {
  3359. // 覆盖 navTo,以防止任何后续脚本或事件跳回首页/模板等
  3360. window.navTo = function(page) {
  3361. try { if (typeof openEditor === 'function') openEditor(); } catch (e) { console.warn('navTo override err', e); }
  3362. return;
  3363. };
  3364. // 隐藏主内容并显示编辑器(双保险,防止样式闪烁)
  3365. try {
  3366. const mc = document.getElementById('mainContent');
  3367. if (mc) mc.style.display = 'none';
  3368. const pe = document.getElementById('page-editor');
  3369. if (pe) pe.classList.add('active');
  3370. // remove active state from other pages
  3371. document.querySelectorAll('.page.active').forEach(p => { if (p.id !== 'page-editor') p.classList.remove('active'); });
  3372. } catch (e) { console.warn('editor show err', e); }
  3373. } catch (e) {
  3374. console.warn('page-level override init err', e);
  3375. }
  3376. </script>
  3377. <script>
  3378. // 扫描并禁用所有内联 onclick 中的 openEditor/navTo(最终保障)
  3379. try {
  3380. document.querySelectorAll('[onclick]').forEach(el => {
  3381. try {
  3382. const v = el.getAttribute('onclick') || '';
  3383. if (/\bopenEditor\s*\(|\bnavTo\s*\(/.test(v)) {
  3384. el.setAttribute('onclick', 'void(0)');
  3385. if (el.style) el.style.cursor = 'default';
  3386. }
  3387. } catch (e) {}
  3388. });
  3389. } catch (e) { console.warn('final disable onclicks err', e); }
  3390. </script>
  3391. <script>
  3392. // 切换原文/标记(函数)
  3393. function toggleView() {
  3394. try {
  3395. const ori = document.getElementById('contentOriginal');
  3396. const mk = document.getElementById('contentMarked');
  3397. const btn = document.getElementById('toggleViewBtn');
  3398. if (!ori || !mk || !btn) return;
  3399. // 切换视图显示
  3400. if (ori.style.display === 'none') {
  3401. ori.style.display = '';
  3402. mk.style.display = 'none';
  3403. } else {
  3404. ori.style.display = 'none';
  3405. mk.style.display = '';
  3406. }
  3407. // 切换按钮内的 icon(在两种 icon 之间切换)
  3408. try {
  3409. const hasBig = btn.querySelector && btn.querySelector('.icon-BIG_PROMOTION');
  3410. if (hasBig) {
  3411. btn.innerHTML = '<i class="iconfont icon-BASE_INFO"></i>';
  3412. } else {
  3413. btn.innerHTML = '<i class="iconfont icon-BIG_PROMOTION"></i>';
  3414. }
  3415. } catch (iconErr) {
  3416. console.warn('toggleView icon swap err', iconErr);
  3417. }
  3418. } catch (e) { console.warn('toggleView err', e); }
  3419. }
  3420. // 绑定(以防内联 onclick 被覆写)
  3421. document.addEventListener('DOMContentLoaded', function () {
  3422. const tv = document.getElementById('toggleViewBtn');
  3423. if (tv) {
  3424. // remove any broken inline onclick and attach a robust listener
  3425. try { tv.removeAttribute('onclick'); } catch(e) {}
  3426. tv.addEventListener('click', toggleView);
  3427. }
  3428. // 搜索按钮 -> 聚焦顶部搜索框
  3429. const searchBtn = document.getElementById('leftSearchBtn');
  3430. if (searchBtn) {
  3431. searchBtn.removeAttribute('onclick');
  3432. searchBtn.addEventListener('click', () => {
  3433. const topSearch = document.querySelector('.top-search') || document.querySelector('.search-input') || document.querySelector('.top-search-input');
  3434. if (topSearch) { topSearch.focus(); if (typeof topSearch.select === 'function') topSearch.select(); }
  3435. });
  3436. }
  3437. // 新建报告按钮 -> 在左侧插入示例新报告并进入编辑
  3438. const newBtn = document.getElementById('newReportBtn');
  3439. if (newBtn) {
  3440. newBtn.removeAttribute('onclick');
  3441. newBtn.addEventListener('click', () => {
  3442. try {
  3443. const list = document.querySelector('#leftDocsSection .doc-list');
  3444. if (!list) return;
  3445. const now = new Date();
  3446. const pad = n => n.toString().padStart(2,'0');
  3447. const fmt = `${now.getFullYear()}/${now.getMonth()+1}/${now.getDate()} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
  3448. const card = document.createElement('div');
  3449. card.className = 'doc-card file-item';
  3450. card.setAttribute('data-name', '新建报告 - 未命名');
  3451. card.innerHTML = `<div class="doc-thumb">📄</div>
  3452. <div class="doc-meta" style="width:100%;">
  3453. <div class="doc-title">新建报告 - 未命名</div>
  3454. <div style="display:flex;gap:10px;margin-top:8px;font-size:13px;color:var(--text2);">
  3455. <span>📅 ${fmt}</span><span>👤 我</span>
  3456. </div>
  3457. </div>`;
  3458. card.addEventListener('click', function(e){ e.stopPropagation(); showFilePreview(this); });
  3459. list.insertBefore(card, list.firstChild);
  3460. // update count/sort if function exists
  3461. if (typeof updateReportsCountAndSort === 'function') updateReportsCountAndSort();
  3462. // open editor if available
  3463. try { if (typeof openEditor === 'function') openEditor(); } catch(e){}
  3464. } catch (e) { console.warn('new report err', e); }
  3465. });
  3466. }
  3467. // 安全绑定:给每个悬浮操作按钮(若存在)绑定实现(防止 inline 被清空)
  3468. document.querySelectorAll('.action-btn').forEach(btn => {
  3469. // if already has onclick via attribute, keep; else attach noop handlers for clarity
  3470. if (!btn.onclick) {
  3471. const t = btn.title || btn.getAttribute('title') || '';
  3472. if (t.includes('下载') || t.includes('下载')) btn.addEventListener('click', () => { downloadReport(btn); });
  3473. else if (t.includes('归档')) btn.addEventListener('click', () => { archiveReport(btn); });
  3474. else if (t.includes('删除')) btn.addEventListener('click', () => { deleteReport(btn); });
  3475. }
  3476. });
  3477. });
  3478. // 占位:下载/归档/删除(若页面已定义则不覆盖)
  3479. window.downloadReport = window.downloadReport || function(btn){
  3480. try{ const card = btn.closest('.doc-card'); const name = card?.getAttribute('data-name')||'报告'; alert('下载:'+name); }catch(e){console.warn(e);}
  3481. };
  3482. window.archiveReport = window.archiveReport || function(btn){
  3483. try{ const card = btn.closest('.doc-card'); if(card) card.classList.toggle('archived'); alert('已切换归档状态'); }catch(e){console.warn(e);}
  3484. };
  3485. window.deleteReport = window.deleteReport || function(btn){
  3486. try{ const card = btn.closest('.doc-card'); const name = card?.getAttribute('data-name')||'报告'; if(!confirm('确认删除 "'+name+'"?')) return; card.remove(); if(typeof updateReportsCountAndSort==='function') updateReportsCountAndSort(); alert('已删除:'+name); }catch(e){console.warn(e);}
  3487. };
  3488. </script>
  3489. <script>
  3490. (function(){
  3491. let pageSize = 10;
  3492. let elements = [], filtered = [], currentPage = 1;
  3493. function extractRawText(node) {
  3494. let txt = '';
  3495. node.childNodes.forEach(n => {
  3496. if (n.nodeType === Node.TEXT_NODE) txt += n.textContent;
  3497. else if (n.nodeType === Node.ELEMENT_NODE && !n.classList.contains('source-badge')) txt += n.textContent;
  3498. });
  3499. return (txt || node.textContent || '').trim();
  3500. }
  3501. function detectValueType(val) {
  3502. if (!val) return '文本';
  3503. if (/[¥¥元]|亿元|万/.test(val)) return '金额';
  3504. const v = val.replace(/[,,\s]/g,'');
  3505. if (/^\d+(\.\d+)?$/.test(v)) return '数字';
  3506. return '文本';
  3507. }
  3508. function buildElementsFromContent() {
  3509. elements = [];
  3510. const nodes = document.querySelectorAll('.editor-content .entity-highlight');
  3511. let idx = 0;
  3512. nodes.forEach(node => {
  3513. try {
  3514. const name = extractRawText(node);
  3515. if (!name) return;
  3516. const desc = node.getAttribute('title') || '';
  3517. const srcBadge = node.querySelector && node.querySelector('.source-badge');
  3518. const src = (node.dataset && node.dataset.source) ? node.dataset.source : (srcBadge ? srcBadge.textContent : '未知');
  3519. let elemType = '变量';
  3520. try {
  3521. const ds = (node.dataset && node.dataset.source) ? String(node.dataset.source) : '';
  3522. if (ds && /ai|生成|AI/i.test(ds)) elemType = 'AI生成';
  3523. else if (node.querySelector && node.querySelector('.source-badge.ai')) elemType = 'AI生成';
  3524. } catch(e){}
  3525. const valType = detectValueType(name);
  3526. const id = 'el_' + (++idx) + '_' + sanitizeId(name);
  3527. elements.push({ id, name, description: desc, type: valType, elementType: elemType, originalValue: name, newValue: name, source: src, node });
  3528. } catch (e) { console.warn('buildElementsFromContent err', e); }
  3529. });
  3530. filtered = elements.slice();
  3531. currentPage = 1;
  3532. }
  3533. function renderElementsList() {
  3534. const tbody = document.getElementById('elementsTbody');
  3535. const info = document.getElementById('elementsPaginationInfo');
  3536. if (!tbody) return;
  3537. const start = (currentPage-1)*pageSize;
  3538. const pageItems = filtered.slice(start, start+pageSize);
  3539. tbody.innerHTML = pageItems.map(it => `
  3540. <tr data-id="${it.id}">
  3541. <td>${it.name}</td>
  3542. <td>${it.description || '-'}</td>
  3543. <td>${it.type}</td>
  3544. <td>${it.elementType}</td>
  3545. <td class="orig">${it.originalValue}</td>
  3546. <td class="new"><input type="text" value="${it.newValue}" style="width:100%;padding:6px;border:1px solid var(--border);border-radius:6px;" data-id="${it.id}" /></td>
  3547. <td>${it.source}</td>
  3548. <td><button class="icon-btn edit-btn" title="编辑" data-id="${it.id}"><i class="iconfont icon-EDIT"></i></button></td>
  3549. </tr>
  3550. `).join('');
  3551. const total = filtered.length;
  3552. const totalPages = Math.max(1, Math.ceil(total/pageSize));
  3553. info.textContent = `第 ${currentPage} / ${totalPages} 页 • 共 ${total} 条`;
  3554. renderPager();
  3555. tbody.querySelectorAll('.edit-btn').forEach(btn=>{
  3556. btn.addEventListener('click', ()=> {
  3557. const id = btn.dataset.id;
  3558. const input = tbody.querySelector('input[data-id="'+id+'"]');
  3559. if (input) { input.focus(); input.select(); }
  3560. });
  3561. });
  3562. }
  3563. function renderPager() {
  3564. const pagerContainer = document.getElementById('elementsPaginationInfo');
  3565. if (!pagerContainer) return;
  3566. const total = filtered.length;
  3567. const totalPages = Math.max(1, Math.ceil(total/pageSize));
  3568. // build numeric pager with up to 5 buttons (显示中间区域最多 5 个页码)
  3569. const maxButtons = 5;
  3570. let start = Math.max(1, currentPage - Math.floor(maxButtons/2));
  3571. let end = start + maxButtons - 1;
  3572. if (end > totalPages) { end = totalPages; start = Math.max(1, end - maxButtons + 1); }
  3573. let html = `<div class="pager">`;
  3574. // 恢复小型上一页 / 下一页 按钮(位于数字页码两侧)
  3575. html += `<button class="pager-btn" data-action="prev" title="上一页">‹</button>`;
  3576. for (let p = start; p <= end; p++) {
  3577. html += `<button class="pager-btn ${p===currentPage?'active':''}" data-page="${p}">${p}</button>`;
  3578. }
  3579. html += `<button class="pager-btn" data-action="next" title="下一页">›</button>`;
  3580. html += `</div>`;
  3581. pagerContainer.innerHTML = html;
  3582. // wire events
  3583. pagerContainer.querySelectorAll('.pager-btn').forEach(btn=>{
  3584. btn.addEventListener('click', (e)=>{
  3585. const action = btn.dataset.action;
  3586. if (action === 'first') { currentPage = 1; }
  3587. else if (action === 'prev') { currentPage = Math.max(1, currentPage - 1); }
  3588. else if (action === 'next') { currentPage = Math.min(totalPages, currentPage + 1); }
  3589. else if (action === 'last') { currentPage = totalPages; }
  3590. else if (btn.dataset.page) { currentPage = parseInt(btn.dataset.page, 10); }
  3591. renderElementsList();
  3592. });
  3593. });
  3594. // update page size select to current value
  3595. const sel = document.getElementById('elementsPageSize');
  3596. if (sel) sel.value = String(pageSize);
  3597. }
  3598. function openReportElementsModal() {
  3599. try {
  3600. buildElementsFromContent();
  3601. renderElementsList();
  3602. document.getElementById('reportElementsModal').classList.add('show');
  3603. document.getElementById('reportElementsModal').setAttribute('aria-hidden','false');
  3604. } catch (e) { console.warn('openReportElementsModal err', e); }
  3605. }
  3606. function closeReportElementsModal() {
  3607. const el = document.getElementById('reportElementsModal');
  3608. if (el) { el.classList.remove('show'); el.setAttribute('aria-hidden','true'); }
  3609. }
  3610. function applySearch() {
  3611. const q = (document.getElementById('elementsSearchInput').value || '').toLowerCase().trim();
  3612. if (!q) filtered = elements.slice();
  3613. else filtered = elements.filter(it => (it.name||'').toLowerCase().includes(q) || (it.type||'').toLowerCase().includes(q) || (it.elementType||'').toLowerCase().includes(q));
  3614. currentPage = 1;
  3615. renderElementsList();
  3616. }
  3617. function changePage(delta) {
  3618. const total = filtered.length;
  3619. const totalPages = Math.max(1, Math.ceil(total/pageSize));
  3620. currentPage = Math.min(Math.max(1, currentPage + delta), totalPages);
  3621. renderElementsList();
  3622. }
  3623. function saveAllElements() {
  3624. const tbody = document.getElementById('elementsTbody');
  3625. if (!tbody) return;
  3626. tbody.querySelectorAll('input[data-id]').forEach(inp => {
  3627. const id = inp.dataset.id;
  3628. const item = elements.find(e=>e.id===id);
  3629. if (!item) return;
  3630. item.newValue = inp.value;
  3631. });
  3632. elements.forEach(item => {
  3633. try {
  3634. if (item.newValue !== item.originalValue && item.node) {
  3635. const badge = item.node.querySelector && item.node.querySelector('.source-badge');
  3636. while (item.node.firstChild) item.node.removeChild(item.node.firstChild);
  3637. item.node.appendChild(document.createTextNode(item.newValue));
  3638. if (badge) item.node.appendChild(badge);
  3639. item.node.dataset.currentValue = item.newValue;
  3640. }
  3641. } catch (e) { console.warn('apply element change err', e); }
  3642. });
  3643. showToast('✓ 要素已保存', 'success');
  3644. closeReportElementsModal();
  3645. }
  3646. document.addEventListener('DOMContentLoaded', function(){
  3647. const search = document.getElementById('elementsSearchInput');
  3648. if (search) search.addEventListener('input', applySearch);
  3649. const prev = document.getElementById('elementsPrevBtn');
  3650. const next = document.getElementById('elementsNextBtn');
  3651. if (prev) prev.addEventListener('click', ()=> changePage(-1));
  3652. if (next) next.addEventListener('click', ()=> changePage(1));
  3653. const saveBtn = document.getElementById('saveElementsBtn');
  3654. if (saveBtn) saveBtn.addEventListener('click', saveAllElements);
  3655. const openTopBtn = document.getElementById('reportElementsTopBtn');
  3656. if (openTopBtn) openTopBtn.addEventListener('click', openReportElementsModal);
  3657. const pageSizeSel = document.getElementById('elementsPageSize');
  3658. if (pageSizeSel) {
  3659. pageSizeSel.addEventListener('change', function(){
  3660. const v = parseInt(this.value, 10) || 10;
  3661. pageSize = v;
  3662. currentPage = 1;
  3663. renderElementsList();
  3664. });
  3665. }
  3666. });
  3667. window.openReportElementsModal = openReportElementsModal;
  3668. window.closeReportElementsModal = closeReportElementsModal;
  3669. })();
  3670. </script>
  3671. <script>
  3672. // report status quick toggle
  3673. (function(){
  3674. const tag = document.getElementById('reportStatusTag');
  3675. if (!tag) return;
  3676. const statuses = ['草稿','审核中','已定稿'];
  3677. let idx = 0;
  3678. tag.addEventListener('click', function(){
  3679. idx = (idx + 1) % statuses.length;
  3680. tag.textContent = statuses[idx];
  3681. showToast && showToast('状态已切换:' + statuses[idx], 'info');
  3682. });
  3683. })();
  3684. </script>
  3685. <script>
  3686. (function(){
  3687. let tooltip = null;
  3688. function createTooltip() {
  3689. tooltip = document.createElement('div');
  3690. tooltip.className = 'icon-tooltip';
  3691. document.body.appendChild(tooltip);
  3692. return tooltip;
  3693. }
  3694. function showTooltip(el, text) {
  3695. if (!tooltip) createTooltip();
  3696. tooltip.textContent = text;
  3697. tooltip.style.opacity = '1';
  3698. const rect = el.getBoundingClientRect();
  3699. const top = Math.max(8, rect.top - 36);
  3700. const left = rect.left + (rect.width/2) - (tooltip.offsetWidth/2);
  3701. tooltip.style.top = top + 'px';
  3702. tooltip.style.left = Math.max(8, left) + 'px';
  3703. }
  3704. function hideTooltip() {
  3705. if (!tooltip) return;
  3706. tooltip.style.opacity = '0';
  3707. }
  3708. document.addEventListener('DOMContentLoaded', function(){
  3709. document.querySelectorAll('.icon-btn, .ai-icon-btn').forEach(btn => {
  3710. btn.addEventListener('mouseenter', function(){
  3711. const txt = btn.dataset.tooltip || btn.getAttribute('title') || btn.getAttribute('aria-label') || '';
  3712. if (txt) {
  3713. showTooltip(btn, txt);
  3714. }
  3715. });
  3716. btn.addEventListener('mouseleave', function(){ hideTooltip(); });
  3717. });
  3718. });
  3719. })();
  3720. </script>
  3721. </body>
  3722. <script>
  3723. // 切换原文 / 标记(同时切换按钮内 icon)
  3724. function toggleView() {
  3725. try {
  3726. const ori = document.getElementById('contentOriginal');
  3727. const mk = document.getElementById('contentMarked');
  3728. const btn = document.getElementById('toggleViewBtn');
  3729. if (!ori || !mk || !btn) return;
  3730. if (ori.style.display === 'none') {
  3731. ori.style.display = '';
  3732. mk.style.display = 'none';
  3733. } else {
  3734. ori.style.display = 'none';
  3735. mk.style.display = '';
  3736. }
  3737. try {
  3738. const hasBig = btn.querySelector && btn.querySelector('.icon-BIG_PROMOTION');
  3739. if (hasBig) {
  3740. btn.innerHTML = '<i class="iconfont icon-BASE_INFO"></i>';
  3741. } else {
  3742. btn.innerHTML = '<i class="iconfont icon-BIG_PROMOTION"></i>';
  3743. }
  3744. } catch (iconErr) {
  3745. console.warn('toggleView icon swap err', iconErr);
  3746. }
  3747. } catch (e) { console.warn('toggleView err', e); }
  3748. }
  3749. // 更多菜单(示意)
  3750. function showMoreMenu(btn) { try { showToast && showToast('更多菜单(示意)', 'info'); } catch(e){} }
  3751. // 报告操作(占位实现)
  3752. window.downloadReport = window.downloadReport || function(btn) {
  3753. try {
  3754. const card = btn.closest('.doc-card');
  3755. const name = card?.getAttribute('data-name') || '报告';
  3756. showToast && showToast('开始下载:' + name, 'info');
  3757. } catch (e) { console.warn(e); }
  3758. };
  3759. window.archiveReport = window.archiveReport || function(btn) {
  3760. try {
  3761. const card = btn.closest('.doc-card');
  3762. if (card) card.classList.toggle('archived');
  3763. showToast && showToast('已切换归档状态', 'success');
  3764. } catch (e) { console.warn(e); }
  3765. };
  3766. window.deleteReport = window.deleteReport || function(btn) {
  3767. try {
  3768. const card = btn.closest('.doc-card');
  3769. const name = card?.getAttribute('data-name') || '报告';
  3770. if (!confirm('确认删除 "' + name + '" 吗?')) return;
  3771. card.remove();
  3772. if (typeof updateReportsCountAndSort === 'function') updateReportsCountAndSort();
  3773. showToast && showToast('已删除:' + name, 'success');
  3774. } catch (e) { console.warn(e); }
  3775. };
  3776. </script>
  3777. </html>