index.vue 125 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074
  1. <template>
  2. <div class="canvas-editor-wrapper">
  3. <div class="canvas-container">
  4. <el-container style="height: 100%">
  5. <el-header>
  6. <div class="menu" editor-component="menu">
  7. <div class="menu-item">
  8. <div class="menu-item__save" title="保存">
  9. <i class="el-icon-s-claim" style="color: #646464"></i>
  10. </div>
  11. <div class="menu-item__undo">
  12. <i></i>
  13. </div>
  14. <div class="menu-item__redo">
  15. <i></i>
  16. </div>
  17. <div class="menu-item__painter" title="格式刷(双击可连续使用)">
  18. <i></i>
  19. </div>
  20. <div class="menu-item__format" title="清除格式">
  21. <i></i>
  22. </div>
  23. </div>
  24. <div class="menu-divider"></div>
  25. <div class="menu-item">
  26. <div class="menu-item__font">
  27. <span class="select" title="字体">Arial</span>
  28. <div class="options">
  29. <ul>
  30. <li data-family="Arial" style="font-family: 'Arial'">
  31. Arial
  32. </li>
  33. <li
  34. data-family="Microsoft YaHei"
  35. style="font-family: 'Microsoft YaHei'"
  36. >
  37. 微软雅黑
  38. </li>
  39. <li data-family="宋体" style="font-family: '宋体'">宋体</li>
  40. <li data-family="黑体" style="font-family: '黑体'">黑体</li>
  41. <li data-family="仿宋" style="font-family: '仿宋'">仿宋</li>
  42. <li data-family="楷体" style="font-family: '楷体'">楷体</li>
  43. <li data-family="等线" style="font-family: '等线'">等线</li>
  44. <!-- <li data-family="华文琥珀" style="font-family:'华文琥珀';">华文琥珀</li>
  45. <li data-family="华文楷体" style="font-family:'华文楷体';">华文楷体</li>
  46. <li data-family="华文隶书" style="font-family:'华文隶书';">华文隶书</li>
  47. <li data-family="华文新魏" style="font-family:'华文新魏';">华文新魏</li>
  48. <li data-family="华文行楷" style="font-family:'华文行楷';">华文行楷</li>
  49. <li data-family="华文中宋" style="font-family:'华文中宋';">华文中宋</li>
  50. <li data-family="华文彩云" style="font-family:'华文彩云';">华文彩云</li> -->
  51. <li data-family="Segoe UI" style="font-family: 'Segoe UI'">
  52. Segoe UI
  53. </li>
  54. <li data-family="Ink Free" style="font-family: 'Ink Free'">
  55. Ink Free
  56. </li>
  57. <li data-family="Fantasy" style="font-family: 'Fantasy'">
  58. Fantasy
  59. </li>
  60. </ul>
  61. </div>
  62. </div>
  63. <div class="menu-item__size">
  64. <span class="select" title="字体">小四</span>
  65. <div class="options">
  66. <ul>
  67. <li data-size="56">初号</li>
  68. <li data-size="48">小初</li>
  69. <li data-size="34">一号</li>
  70. <li data-size="32">小一</li>
  71. <li data-size="29">二号</li>
  72. <li data-size="24">小二</li>
  73. <li data-size="21">三号</li>
  74. <li data-size="20">小三</li>
  75. <li data-size="18">四号</li>
  76. <li data-size="16">小四</li>
  77. <li data-size="14">五号</li>
  78. <li data-size="12">小五</li>
  79. <li data-size="10">六号</li>
  80. <li data-size="8">小六</li>
  81. <li data-size="7">七号</li>
  82. <li data-size="6">八号</li>
  83. </ul>
  84. </div>
  85. </div>
  86. <div class="menu-item__size-add">
  87. <i></i>
  88. </div>
  89. <div class="menu-item__size-minus">
  90. <i></i>
  91. </div>
  92. <div class="menu-item__bold">
  93. <i></i>
  94. </div>
  95. <div class="menu-item__italic">
  96. <i></i>
  97. </div>
  98. <div class="menu-item__underline">
  99. <i></i>
  100. </div>
  101. <div class="menu-item__strikeout" title="删除线(Ctrl+Shift+X)">
  102. <i></i>
  103. </div>
  104. <div class="menu-item__superscript">
  105. <i></i>
  106. </div>
  107. <div class="menu-item__subscript">
  108. <i></i>
  109. </div>
  110. <div class="menu-item__color" title="字体颜色">
  111. <i></i>
  112. <div class="options">
  113. <div class="color-section">
  114. <div class="section-title">主题颜色</div>
  115. <ul>
  116. <!-- 第一行 -->
  117. <li
  118. data-color="#000000"
  119. title="黑色"
  120. style="background-color: #000000"
  121. ></li>
  122. <li
  123. data-color="#2B579A"
  124. title="深蓝色"
  125. style="background-color: #2b579a"
  126. ></li>
  127. <li
  128. data-color="#4472C4"
  129. title="蓝色"
  130. style="background-color: #4472c4"
  131. ></li>
  132. <li
  133. data-color="#70AD47"
  134. title="绿色"
  135. style="background-color: #70ad47"
  136. ></li>
  137. <li
  138. data-color="#FFC000"
  139. title="黄色"
  140. style="background-color: #ffc000"
  141. ></li>
  142. <li
  143. data-color="#ED7D31"
  144. title="橙色"
  145. style="background-color: #ed7d31"
  146. ></li>
  147. <li
  148. data-color="#C00000"
  149. title="红色"
  150. style="background-color: #c00000"
  151. ></li>
  152. <li
  153. data-color="#7030A0"
  154. title="紫色"
  155. style="background-color: #7030a0"
  156. ></li>
  157. <li
  158. data-color="#FFFFFF"
  159. title="白色"
  160. style="background-color: #ffffff"
  161. ></li>
  162. <!-- 灰度行 -->
  163. <li
  164. data-color="#7F7F7F"
  165. title="深灰色"
  166. style="background-color: #7f7f7f"
  167. ></li>
  168. <li
  169. data-color="#A5A5A5"
  170. title="灰色"
  171. style="background-color: #a5a5a5"
  172. ></li>
  173. <li
  174. data-color="#D0D0D0"
  175. title="浅灰色"
  176. style="background-color: #d0d0d0"
  177. ></li>
  178. <!-- 蓝色系列 -->
  179. <li
  180. data-color="#D6E3F3"
  181. title="浅蓝色-1"
  182. style="background-color: #d6e3f3"
  183. ></li>
  184. <li
  185. data-color="#B4C7E7"
  186. title="浅蓝色-2"
  187. style="background-color: #b4c7e7"
  188. ></li>
  189. <li
  190. data-color="#8EAADB"
  191. title="浅蓝色-3"
  192. style="background-color: #8eaadb"
  193. ></li>
  194. <li
  195. data-color="#2E75B6"
  196. title="深蓝色-1"
  197. style="background-color: #2e75b6"
  198. ></li>
  199. <li
  200. data-color="#1F4E79"
  201. title="深蓝色-2"
  202. style="background-color: #1f4e79"
  203. ></li>
  204. </ul>
  205. </div>
  206. <div class="color-section">
  207. <div class="section-title">标准色</div>
  208. <ul>
  209. <li
  210. data-color="#FF0000"
  211. title="红色"
  212. style="background-color: #ff0000"
  213. ></li>
  214. <li
  215. data-color="#FF6600"
  216. title="橙色"
  217. style="background-color: #ff6600"
  218. ></li>
  219. <li
  220. data-color="#FFFF00"
  221. title="黄色"
  222. style="background-color: #ffff00"
  223. ></li>
  224. <li
  225. data-color="#00FF00"
  226. title="绿色"
  227. style="background-color: #00ff00"
  228. ></li>
  229. <li
  230. data-color="#00FFFF"
  231. title="青色"
  232. style="background-color: #00ffff"
  233. ></li>
  234. <li
  235. data-color="#0000FF"
  236. title="蓝色"
  237. style="background-color: #0000ff"
  238. ></li>
  239. <li
  240. data-color="#FF00FF"
  241. title="紫色"
  242. style="background-color: #ff00ff"
  243. ></li>
  244. </ul>
  245. </div>
  246. </div>
  247. </div>
  248. <div class="menu-item__highlight" title="高亮">
  249. <i></i>
  250. <div class="options">
  251. <ul>
  252. <!-- 第一行 -->
  253. <li
  254. data-color="transparent"
  255. title="无"
  256. style="background-color: transparent"
  257. >
  258. </li>
  259. <li
  260. data-color="#ffff00"
  261. title="黄色"
  262. style="background-color: #ffff00"
  263. ></li>
  264. <li
  265. data-color="#00ff00"
  266. title="绿色"
  267. style="background-color: #00ff00"
  268. ></li>
  269. <li
  270. data-color="#00ffff"
  271. title="青色"
  272. style="background-color: #00ffff"
  273. ></li>
  274. <li
  275. data-color="#ff00ff"
  276. title="粉色"
  277. style="background-color: #ff00ff"
  278. ></li>
  279. <li
  280. data-color="#0000ff"
  281. title="蓝色"
  282. style="background-color: #0000ff"
  283. ></li>
  284. <!-- 第二行 -->
  285. <li
  286. data-color="#800000"
  287. title="深红色"
  288. style="background-color: #800000"
  289. ></li>
  290. <li
  291. data-color="#ff0000"
  292. title="红色"
  293. style="background-color: #ff0000"
  294. ></li>
  295. <li
  296. data-color="#808000"
  297. title="橄榄色"
  298. style="background-color: #808000"
  299. ></li>
  300. <li
  301. data-color="#008000"
  302. title="深绿色"
  303. style="background-color: #008000"
  304. ></li>
  305. <li
  306. data-color="#008080"
  307. title="青灰色"
  308. style="background-color: #008080"
  309. ></li>
  310. <li
  311. data-color="#000080"
  312. title="深蓝色"
  313. style="background-color: #000080"
  314. ></li>
  315. <!-- 第三行 -->
  316. <li
  317. data-color="#000000"
  318. title="黑色"
  319. style="background-color: #000000"
  320. ></li>
  321. <li
  322. data-color="#808080"
  323. title="深灰色"
  324. style="background-color: #808080"
  325. ></li>
  326. <li
  327. data-color="#c0c0c0"
  328. title="灰色"
  329. style="background-color: #c0c0c0"
  330. ></li>
  331. </ul>
  332. </div>
  333. </div>
  334. </div>
  335. <div class="menu-divider"></div>
  336. <div class="menu-item">
  337. <div class="menu-item__title">
  338. <i></i>
  339. <span class="select" title="切换标题">正文</span>
  340. <div class="options">
  341. <ul>
  342. <li style="font-size: 16px">正文</li>
  343. <li data-level="first" style="font-size: 26px">标题1</li>
  344. <li data-level="second" style="font-size: 24px">标题2</li>
  345. <li data-level="third" style="font-size: 22px">标题3</li>
  346. <li data-level="fourth" style="font-size: 20px">标题4</li>
  347. <li data-level="fifth" style="font-size: 18px">标题5</li>
  348. <li data-level="sixth" style="font-size: 16px">标题6</li>
  349. </ul>
  350. </div>
  351. </div>
  352. <div class="menu-item__left">
  353. <i></i>
  354. </div>
  355. <div class="menu-item__center">
  356. <i></i>
  357. </div>
  358. <div class="menu-item__right">
  359. <i></i>
  360. </div>
  361. <div class="menu-item__alignment">
  362. <i></i>
  363. </div>
  364. <div class="menu-item__row-margin">
  365. <i title="行间距"></i>
  366. <div class="options">
  367. <ul>
  368. <li data-rowmargin="1">1</li>
  369. <li data-rowmargin="1.25">1.25</li>
  370. <li data-rowmargin="1.5">1.5</li>
  371. <li data-rowmargin="1.75">1.75</li>
  372. <li data-rowmargin="2">2</li>
  373. <li data-rowmargin="2.5">2.5</li>
  374. <li data-rowmargin="3">3</li>
  375. </ul>
  376. </div>
  377. </div>
  378. <div class="menu-item__list">
  379. <i></i>
  380. <div class="options">
  381. <ul>
  382. <li>
  383. <label>取消列表</label>
  384. </li>
  385. <li data-list-type="ol" data-list-style="decimal">
  386. <label>有序列表:</label>
  387. <ol>
  388. <li>________</li>
  389. </ol>
  390. </li>
  391. <li data-list-type="ul" data-list-style="disc">
  392. <label>实心圆点列表:</label>
  393. <ul style="list-style-type: disc">
  394. <li>________</li>
  395. </ul>
  396. </li>
  397. <li data-list-type="ul" data-list-style="circle">
  398. <label>空心圆点列表:</label>
  399. <ul style="list-style-type: circle">
  400. <li>________</li>
  401. </ul>
  402. </li>
  403. <li data-list-type="ul" data-list-style="square">
  404. <label>空心方块列表:</label>
  405. <ul style="list-style-type: square">
  406. <li>________</li>
  407. </ul>
  408. </li>
  409. </ul>
  410. </div>
  411. </div>
  412. </div>
  413. <div class="menu-divider"></div>
  414. <div class="menu-item">
  415. <div class="menu-item__table">
  416. <i title="表格"></i>
  417. </div>
  418. <div class="menu-item__table__collapse">
  419. <div class="table-close">×</div>
  420. <div class="table-title">
  421. <span class="table-select">插入</span>
  422. <span>表格</span>
  423. </div>
  424. <div class="table-panel"></div>
  425. </div>
  426. <div class="menu-item__image">
  427. <i title="图片"></i>
  428. <input
  429. type="file"
  430. id="image"
  431. accept=".png, .jpg, .jpeg, .svg, .gif"
  432. />
  433. </div>
  434. <div class="menu-item__hyperlink">
  435. <i title="超链接"></i>
  436. </div>
  437. <div class="menu-item__separator">
  438. <i title="分割线"></i>
  439. <div class="options">
  440. <ul>
  441. <li data-separator="0,0">
  442. <i></i>
  443. </li>
  444. <li data-separator="1,1">
  445. <i></i>
  446. </li>
  447. <li data-separator="3,1">
  448. <i></i>
  449. </li>
  450. <li data-separator="4,4">
  451. <i></i>
  452. </li>
  453. <li data-separator="7,3,3,3">
  454. <i></i>
  455. </li>
  456. <li data-separator="6,2,2,2,2,2">
  457. <i></i>
  458. </li>
  459. </ul>
  460. </div>
  461. </div>
  462. <!-- <div class="menu-item__watermark">
  463. <i title="水印(添加、删除)"></i>
  464. <div class="options">
  465. <ul>
  466. <li data-menu="add">添加水印</li>
  467. <li data-menu="delete">删除水印</li>
  468. </ul>
  469. </div>
  470. </div>
  471. <div class="menu-item__codeblock" title="代码块">
  472. <i></i>
  473. </div>
  474. <div class="menu-item__page-break" title="分页符">
  475. <i></i>
  476. </div>
  477. <div class="menu-item__control">
  478. <i title="控件"></i>
  479. <div class="options">
  480. <ul>
  481. <li data-control='text'>文本</li>
  482. <li data-control="select">列举</li>
  483. <li data-control="checkbox">复选框</li>
  484. </ul>
  485. </div>
  486. </div>
  487. <div class="menu-item__checkbox" title="复选框">
  488. <i></i>
  489. </div>
  490. <div class="menu-item__latex" title="LateX">
  491. <i></i>
  492. </div>
  493. <div class="menu-item__date">
  494. <i title="日期"></i>
  495. <div class="options">
  496. <ul>
  497. <li data-format="yyyy-MM-dd"></li>
  498. <li data-format="yyyy-MM-dd hh:mm:ss"></li>
  499. </ul>
  500. </div>
  501. </div>
  502. <div class="menu-item__block" title="内容块">
  503. <i></i>
  504. </div> -->
  505. </div>
  506. <div class="menu-divider"></div>
  507. <div class="menu-item">
  508. <div class="menu-item__search" data-menu="search">
  509. <i></i>
  510. </div>
  511. <div class="menu-item__search__collapse" data-menu="search">
  512. <div class="menu-item__search__collapse__search">
  513. <input type="text" />
  514. <label class="search-result"></label>
  515. <div class="arrow-left">
  516. <i></i>
  517. </div>
  518. <div class="arrow-right">
  519. <i></i>
  520. </div>
  521. <span>×</span>
  522. </div>
  523. <div class="menu-item__search__collapse__replace">
  524. <input type="text" />
  525. <button>替换</button>
  526. </div>
  527. </div>
  528. <!-- <div class="menu-item__print" data-menu="print">
  529. <i></i>
  530. </div> -->
  531. </div>
  532. </div>
  533. </el-header>
  534. <el-main class="content">
  535. <!-- <div class="catalog" editor-component="catalog">
  536. <div class="catalog__header">
  537. <span>目录</span>
  538. <div class="catalog__header__close">
  539. <i></i>
  540. </div>
  541. </div>
  542. <div class="catalog__main"></div>
  543. </div> -->
  544. <div
  545. id="canvasEditor"
  546. class="canvas-editor"
  547. editor-component="main"
  548. ></div>
  549. <!-- <div class="comment" editor-component="comment"></div> -->
  550. </el-main>
  551. <el-footer style="height: 30px">
  552. <div class="footer" editor-component="footer">
  553. <div>
  554. <!-- <div class="catalog-mode" title="目录">
  555. <i></i>
  556. </div> -->
  557. <div class="page-mode">
  558. <i title="页面模式(分页、连页)"></i>
  559. <div class="options">
  560. <ul>
  561. <li data-page-mode="paging">分页</li>
  562. <li data-page-mode="continuity" class="active">连页</li>
  563. </ul>
  564. </div>
  565. </div>
  566. <span>可见页码:<span class="page-no-list">1</span></span>
  567. <span
  568. >页面:<span class="page-no">1</span>/<span class="page-size"
  569. >1</span
  570. ></span
  571. >
  572. <span>字数:<span class="word-count">0</span></span>
  573. </div>
  574. <div class="editor-mode" title="编辑模式(编辑、清洁、只读、表单)">
  575. 编辑模式
  576. </div>
  577. <div>
  578. <div class="page-scale-minus" title="缩小(Ctrl+-)">
  579. <i></i>
  580. </div>
  581. <span
  582. class="page-scale-percentage"
  583. title="显示比例(点击可复原Ctrl+0)"
  584. >100%</span
  585. >
  586. <div class="page-scale-add" title="放大(Ctrl+=)">
  587. <i></i>
  588. </div>
  589. <div class="paper-size">
  590. <i title="纸张类型"></i>
  591. <div class="options">
  592. <ul>
  593. <li data-paper-size="813*1123" class="active">A4</li>
  594. <li data-paper-size="1593*2251">A2</li>
  595. <li data-paper-size="1125*1593">A3</li>
  596. <li data-paper-size="565*796">A5</li>
  597. <li data-paper-size="412*488">5号信封</li>
  598. <li data-paper-size="450*866">6号信封</li>
  599. <li data-paper-size="609*862">7号信封</li>
  600. <li data-paper-size="862*1221">9号信封</li>
  601. <li data-paper-size="813*1266">法律用纸</li>
  602. <li data-paper-size="813*1054">信纸</li>
  603. </ul>
  604. </div>
  605. </div>
  606. <div class="paper-direction">
  607. <i title="纸张方向"></i>
  608. <div class="options">
  609. <ul>
  610. <li data-paper-direction="vertical" class="active">纵向</li>
  611. <li data-paper-direction="horizontal">横向</li>
  612. </ul>
  613. </div>
  614. </div>
  615. <div class="paper-margin" title="页边距">
  616. <i></i>
  617. </div>
  618. <div class="fullscreen" title="全屏显示">
  619. <i></i>
  620. </div>
  621. </div>
  622. </div>
  623. </el-footer>
  624. </el-container>
  625. </div>
  626. </div>
  627. </template>
  628. <script>
  629. import Editor from "@hufe921/canvas-editor";
  630. import { Dialog } from "./components/dialog/Dialog";
  631. import { Signature } from "./components/signature/Signature";
  632. import { IEditorOption, ITableOption, IHeader, IFooter } from "./option";
  633. import {
  634. BlockType,
  635. Command,
  636. ControlType,
  637. EditorMode,
  638. EditorZone,
  639. ElementType,
  640. IBlock,
  641. ICatalogItem,
  642. IElement,
  643. KeyMap,
  644. ListStyle,
  645. ListType,
  646. PageMode,
  647. PaperDirection,
  648. RowFlex,
  649. TitleLevel,
  650. splitText,
  651. } from "@hufe921/canvas-editor";
  652. export default {
  653. name: "CanvasEditor", // 确保设置了组件名称
  654. data() {
  655. return {
  656. editorRef: null, // 将editor实例存储在data中
  657. isApple:
  658. typeof navigator !== "undefined" &&
  659. /Mac OS X/.test(navigator.userAgent),
  660. // 编辑模式
  661. modeList: [
  662. {
  663. mode: EditorMode.READONLY,
  664. name: "只读模式",
  665. },
  666. {
  667. mode: EditorMode.EDIT,
  668. name: "编辑模式",
  669. },
  670. ],
  671. header: [], // 页眉配置
  672. main: [], // 主要编辑内容
  673. footer: [], // 页脚信息
  674. options: {
  675. width: 813, // A4 纸张宽度
  676. height: 1123, // A4 纸张高度
  677. defaultSize: 14,//Number(this.editorOptions.size),
  678. defaultFont: this.editorOptions.font,
  679. // defaultSize: 14,//Number(this.editorOptions.size),
  680. defaultBold: this.editorOptions.bold,
  681. defaultAlign: this.editorOptions.align,
  682. pageMode: 'continuity',
  683. }, // IEditorOption,
  684. // 批注 TODO
  685. commentList: [],
  686. };
  687. },
  688. props: {
  689. // 编辑模式
  690. editMode: {
  691. type: String,
  692. },
  693. // html数据
  694. htmlData: {
  695. type: String,
  696. },
  697. // 后端传过来的保存html的JSON数据(用于回显)
  698. docJson: {
  699. type: String,
  700. },
  701. editorOptions: {
  702. type: Object,
  703. default: () => ({
  704. defaultFont: "Arial",
  705. //defaultSize: 14,
  706. defaultBold: false,
  707. defaultAlign: "left",
  708. }),
  709. },
  710. },
  711. watch: {
  712. // 监听父组件传过来的编辑模式,设置模式
  713. editMode: {
  714. handler(val) {
  715. if (this.editorRef) {
  716. this.editorRef.command.executeMode(val);
  717. // 设置模式
  718. const modeElement = document.querySelector(".editor-mode");
  719. modeElement.innerText =
  720. this.modeList
  721. .filter((item) => item.mode == val)
  722. .map((data) => data.name) || "";
  723. // 设置菜单栏权限视觉反馈
  724. const isReadonly = val === EditorMode.READONLY;
  725. const enableMenuList = ["search", "print"];
  726. document.querySelectorAll(".menu-item>div").forEach((dom) => {
  727. const menu = dom.dataset.menu;
  728. isReadonly && (!menu || !enableMenuList.includes(menu))
  729. ? dom.classList.add("disable")
  730. : dom.classList.remove("disable");
  731. });
  732. }
  733. },
  734. deep: true,
  735. },
  736. },
  737. methods: {
  738. debounce(func, delay) {
  739. let timer;
  740. return function (...args) {
  741. if (timer) {
  742. window.clearTimeout(timer);
  743. }
  744. timer = window.setTimeout(() => {
  745. func.apply(this, args);
  746. }, delay);
  747. };
  748. },
  749. applyBoldToAll() {
  750. if (!this.editorRef) return;
  751. try {
  752. this.editorRef.command.executeSelectAll();
  753. this.editorRef.command.executeBold();
  754. this.editorRef.command.executeSelectAll();
  755. this.editorRef.command.executeSelect(0, 0);
  756. } catch (error) {
  757. console.error('Apply bold failed:', error);
  758. }
  759. },
  760. // 修改为异步的粘贴处理方法
  761. async handlePaste(e) {
  762. try {
  763. if (!this.editorRef) return;
  764. // 阻止默认粘贴行为
  765. e.preventDefault();
  766. let text = '';
  767. let htmlContent = '';
  768. // 优先尝试使用现代的 Async Clipboard API
  769. if (navigator.clipboard && navigator.clipboard.readText) {
  770. text = await navigator.clipboard.readText();
  771. // 尝试获取HTML内容
  772. if (navigator.clipboard.read) {
  773. try {
  774. const clipboardItems = await navigator.clipboard.read();
  775. for (const clipboardItem of clipboardItems) {
  776. for (const type of clipboardItem.types) {
  777. if (type === 'text/html') {
  778. const htmlBlob = await clipboardItem.getType(type);
  779. htmlContent = await htmlBlob.text();
  780. break;
  781. }
  782. }
  783. if (htmlContent) break;
  784. }
  785. } catch (htmlError) {
  786. console.log('HTML clipboard access failed, using text only:', htmlError);
  787. }
  788. }
  789. }
  790. // 作为备选,尝试从粘贴事件对象中获取
  791. else if (e.clipboardData && e.clipboardData.getData) {
  792. text = e.clipboardData.getData('text/plain');
  793. htmlContent = e.clipboardData.getData('text/html');
  794. }
  795. // 最后降级到已不推荐的 window.clipboardData (主要针对旧版IE)
  796. else if (window.clipboardData && window.clipboardData.getData) {
  797. text = window.clipboardData.getData('Text');
  798. htmlContent = window.clipboardData.getData('Text');
  799. }
  800. // 如果成功获取到内容,则执行插入操作
  801. if (text || htmlContent) {
  802. // 如果有HTML内容,先处理HTML以清理超链接样式
  803. if (htmlContent && htmlContent.trim()) {
  804. console.log('Processing HTML content from clipboard');
  805. const processedHtml = this.processPastedHtml(htmlContent);
  806. // 使用处理后的HTML内容
  807. this.editorRef.command.executeSetHTML({ main: processedHtml });
  808. } else if (text && text.trim()) {
  809. console.log('Processing plain text from clipboard');
  810. // 使用纯文本
  811. this.editorRef.command.executeInsertText(text);
  812. } else {
  813. console.warn('Both HTML and text content are empty or invalid.');
  814. }
  815. } else {
  816. console.warn('Failed to retrieve any content from clipboard.');
  817. }
  818. } catch (error) {
  819. console.error('Custom paste handler failed:', error);
  820. // 错误处理见下一步
  821. }
  822. },
  823. // 处理粘贴的HTML内容,清理超链接样式
  824. processPastedHtml(htmlContent) {
  825. try {
  826. console.log('Processing pasted HTML:', htmlContent);
  827. // 创建临时DOM元素来解析HTML,保持结构完整
  828. const tempDiv = document.createElement('div');
  829. tempDiv.innerHTML = htmlContent;
  830. // 查找并处理所有超链接,但保持其他样式
  831. const links = tempDiv.querySelectorAll('a');
  832. links.forEach(link => {
  833. // 创建span元素替换超链接
  834. const span = document.createElement('span');
  835. // 复制超链接的文本内容
  836. span.textContent = link.textContent;
  837. // 复制父元素的样式(保持表头底色等)
  838. const parentElement = link.parentElement;
  839. if (parentElement && parentElement.style) {
  840. const parentStyles = window.getComputedStyle(parentElement);
  841. // 复制重要的样式属性,但排除链接相关样式
  842. const stylesToCopy = [
  843. 'background-color', 'background', 'font-family', 'font-size',
  844. 'font-weight', 'font-style', 'text-align', 'padding', 'margin',
  845. 'border', 'border-color', 'border-width', 'border-style'
  846. ];
  847. stylesToCopy.forEach(styleProp => {
  848. const value = parentStyles.getPropertyValue(styleProp);
  849. if (value && value !== 'initial' && value !== 'inherit' && value !== 'transparent') {
  850. span.style.setProperty(styleProp, value);
  851. }
  852. });
  853. }
  854. // 设置文本样式为普通文本
  855. span.style.color = 'inherit';
  856. span.style.textDecoration = 'none';
  857. span.style.cursor = 'default';
  858. // 替换超链接元素
  859. link.parentNode.replaceChild(span, link);
  860. });
  861. // 特别处理表格,确保表头样式正确
  862. const tables = tempDiv.querySelectorAll('table');
  863. tables.forEach(table => {
  864. // 处理表头行
  865. const headerRows = table.querySelectorAll('thead tr, tr:first-child');
  866. headerRows.forEach(headerRow => {
  867. // 确保表头有背景色
  868. if (!headerRow.style.backgroundColor) {
  869. headerRow.style.backgroundColor = '#f5f5f5'; // 默认表头背景色
  870. }
  871. // 处理表头单元格
  872. const headerCells = headerRow.querySelectorAll('th, td');
  873. headerCells.forEach(cell => {
  874. // 确保表头单元格有背景色
  875. if (!cell.style.backgroundColor) {
  876. cell.style.backgroundColor = '#f5f5f5';
  877. }
  878. // 处理单元格内的超链接
  879. const cellLinks = cell.querySelectorAll('a');
  880. cellLinks.forEach(cellLink => {
  881. const span = document.createElement('span');
  882. span.textContent = cellLink.textContent;
  883. // 保持表头样式
  884. span.style.color = 'inherit';
  885. span.style.textDecoration = 'none';
  886. span.style.cursor = 'default';
  887. span.style.fontWeight = 'bold'; // 表头通常加粗
  888. cellLink.parentNode.replaceChild(span, cellLink);
  889. });
  890. });
  891. });
  892. // 处理表格单元格中的超链接
  893. const cellLinks = table.querySelectorAll('td a, tbody a');
  894. cellLinks.forEach(cellLink => {
  895. const span = document.createElement('span');
  896. span.textContent = cellLink.textContent;
  897. // 保持单元格的默认样式
  898. span.style.color = 'inherit';
  899. span.style.textDecoration = 'none';
  900. span.style.cursor = 'default';
  901. cellLink.parentNode.replaceChild(span, cellLink);
  902. });
  903. });
  904. const result = tempDiv.innerHTML;
  905. console.log('Processed HTML result:', result);
  906. return result;
  907. } catch (error) {
  908. console.error('Error processing pasted HTML:', error);
  909. // 如果处理失败,尝试简单的文本提取
  910. try {
  911. const tempDiv = document.createElement('div');
  912. tempDiv.innerHTML = htmlContent;
  913. return tempDiv.textContent || tempDiv.innerText || htmlContent;
  914. } catch (fallbackError) {
  915. console.error('Fallback processing also failed:', fallbackError);
  916. return htmlContent;
  917. }
  918. }
  919. }
  920. },
  921. beforeDestroy() {
  922. // 清理事件监听器
  923. const editorElement = document.querySelector(".canvas-editor");
  924. if (editorElement && this.eventHandlers) {
  925. editorElement.removeEventListener('paste', this.eventHandlers.paste, true);
  926. editorElement.removeEventListener('keydown', this.eventHandlers.keydown, true);
  927. editorElement.removeEventListener('contextmenu', this.eventHandlers.contextmenu, true);
  928. }
  929. if (this.eventHandlers && this.eventHandlers.documentPaste) {
  930. document.removeEventListener('paste', this.eventHandlers.documentPaste, true);
  931. }
  932. if (this.globalPasteHandler) {
  933. document.removeEventListener('paste', this.globalPasteHandler, true);
  934. window.removeEventListener('paste', this.globalPasteHandler, true);
  935. }
  936. if (this.globalClickHandler) {
  937. document.removeEventListener('click', this.globalClickHandler, true);
  938. }
  939. },
  940. mounted() {
  941. console.log(this.editorOptions);
  942. const isApple =
  943. typeof navigator !== "undefined" && /Mac OS X/.test(navigator.userAgent);
  944. const instance = new Editor(
  945. document.querySelector(".canvas-editor"),
  946. {
  947. header: this.header,
  948. main: this.main,
  949. footer: this.footer,
  950. defaultFont: this.editorOptions.font,
  951. // defaultSize: 14,//Number(this.editorOptions.size),
  952. defaultBold: this.editorOptions.bold,
  953. defaultAlign: this.editorOptions.align,
  954. },
  955. this.options
  956. );
  957. this.editorRef = instance;
  958. // cypress使用
  959. Reflect.set(window, "editor", instance);
  960. // 重写编辑器的粘贴命令,确保所有粘贴操作都经过我们的处理
  961. const originalExecuteInsertText = instance.command.executeInsertText;
  962. const originalExecuteSetHTML = instance.command.executeSetHTML;
  963. // 重写 executeInsertText 方法
  964. instance.command.executeInsertText = (text) => {
  965. console.log('Intercepted executeInsertText:', text);
  966. // 直接调用原始方法,因为纯文本不需要特殊处理
  967. return originalExecuteInsertText.call(instance.command, text);
  968. };
  969. // 重写 executeSetHTML 方法
  970. instance.command.executeSetHTML = (htmlData) => {
  971. console.log('Intercepted executeSetHTML:', htmlData);
  972. if (htmlData && htmlData.main) {
  973. // 处理HTML内容
  974. const processedHtml = this.processPastedHtml(htmlData.main);
  975. htmlData.main = processedHtml;
  976. }
  977. return originalExecuteSetHTML.call(instance.command, htmlData);
  978. };
  979. // 添加全局粘贴事件监听器,确保所有粘贴操作都经过处理
  980. const editorElement = document.querySelector(".canvas-editor");
  981. if (editorElement) {
  982. // 创建事件处理器引用
  983. this.eventHandlers = {
  984. paste: this.handlePaste.bind(this),
  985. keydown: (e) => {
  986. if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
  987. e.preventDefault();
  988. this.handlePaste(e);
  989. }
  990. },
  991. contextmenu: (e) => {
  992. // 使用MutationObserver监听动态创建的右键菜单
  993. const observer = new MutationObserver((mutations) => {
  994. mutations.forEach((mutation) => {
  995. mutation.addedNodes.forEach((node) => {
  996. if (node.nodeType === Node.ELEMENT_NODE) {
  997. // 查找所有菜单项
  998. const allMenuItems = node.querySelectorAll ?
  999. node.querySelectorAll('*') : [];
  1000. // 也检查节点本身
  1001. const allItems = [...allMenuItems, node];
  1002. allItems.forEach(item => {
  1003. // 为所有菜单项添加点击事件监听器
  1004. if (item.textContent && (
  1005. item.textContent.includes('粘贴') ||
  1006. item.textContent.includes('剪切') ||
  1007. item.textContent.includes('全选') ||
  1008. item.textContent.includes('打印') ||
  1009. item.textContent.includes('签名') ||
  1010. item.textContent.includes('格式整理')
  1011. )) {
  1012. console.log('Found menu item:', item.textContent);
  1013. item.addEventListener('click', (e) => {
  1014. e.preventDefault();
  1015. e.stopPropagation();
  1016. console.log('Menu item clicked:', item.textContent);
  1017. // 处理特定功能
  1018. if (item.textContent.includes('粘贴')) {
  1019. this.handlePaste(e);
  1020. }
  1021. // 点击菜单项后关闭菜单
  1022. setTimeout(() => {
  1023. this.closeAllContextMenus();
  1024. }, 100);
  1025. }, true);
  1026. }
  1027. });
  1028. }
  1029. });
  1030. });
  1031. });
  1032. // 开始观察文档变化
  1033. observer.observe(document.body, {
  1034. childList: true,
  1035. subtree: true
  1036. });
  1037. // 5秒后停止观察(避免内存泄漏)
  1038. setTimeout(() => {
  1039. observer.disconnect();
  1040. }, 5000);
  1041. },
  1042. documentPaste: (e) => {
  1043. // 检查是否在编辑器区域内
  1044. const editorElement = document.querySelector(".canvas-editor");
  1045. if (editorElement && editorElement.contains(e.target)) {
  1046. e.preventDefault();
  1047. this.handlePaste(e);
  1048. }
  1049. }
  1050. };
  1051. // 监听所有可能的粘贴事件
  1052. editorElement.addEventListener('paste', this.eventHandlers.paste, true);
  1053. // 监听键盘粘贴事件
  1054. editorElement.addEventListener('keydown', this.eventHandlers.keydown, true);
  1055. // 监听右键菜单粘贴(如果存在)
  1056. editorElement.addEventListener('contextmenu', this.eventHandlers.contextmenu, true);
  1057. }
  1058. // 监听整个文档的粘贴事件作为备用
  1059. if (this.eventHandlers) {
  1060. document.addEventListener('paste', this.eventHandlers.documentPaste, true);
  1061. }
  1062. // 添加全局的粘贴拦截,确保所有粘贴操作都被拦截
  1063. const globalPasteHandler = (e) => {
  1064. const editorElement = document.querySelector(".canvas-editor");
  1065. if (editorElement && editorElement.contains(e.target)) {
  1066. console.log('Global paste intercepted');
  1067. e.preventDefault();
  1068. e.stopPropagation();
  1069. this.handlePaste(e);
  1070. }
  1071. };
  1072. // 监听所有可能的粘贴事件
  1073. document.addEventListener('paste', globalPasteHandler, true);
  1074. window.addEventListener('paste', globalPasteHandler, true);
  1075. // 保存处理器引用以便清理
  1076. this.globalPasteHandler = globalPasteHandler;
  1077. // 添加全局点击事件监听器,用于关闭右键菜单
  1078. this.globalClickHandler = (e) => {
  1079. // 检查点击的元素是否是右键菜单
  1080. const contextMenus = document.querySelectorAll('.ce-contextmenu, [class*="contextmenu"], [class*="context-menu"]');
  1081. let isClickOnMenu = false;
  1082. let clickedMenuItem = false;
  1083. contextMenus.forEach(menu => {
  1084. if (menu.contains(e.target)) {
  1085. isClickOnMenu = true;
  1086. // 检查是否点击的是菜单项(按钮)
  1087. const menuItems = menu.querySelectorAll('li, div[role="menuitem"], button, a, span');
  1088. menuItems.forEach(item => {
  1089. if (item.contains(e.target) && item !== menu) {
  1090. clickedMenuItem = true;
  1091. }
  1092. });
  1093. }
  1094. });
  1095. // 如果点击的是菜单项或者不是菜单,则关闭所有右键菜单
  1096. if (clickedMenuItem || !isClickOnMenu) {
  1097. this.closeAllContextMenus();
  1098. }
  1099. };
  1100. // 关闭所有右键菜单的方法
  1101. this.closeAllContextMenus = () => {
  1102. const contextMenus = document.querySelectorAll('.ce-contextmenu, [class*="contextmenu"], [class*="context-menu"]');
  1103. contextMenus.forEach(menu => {
  1104. if (menu.style.display !== 'none') {
  1105. menu.style.display = 'none';
  1106. menu.style.visibility = 'hidden';
  1107. }
  1108. });
  1109. // 也尝试通过移除菜单元素来关闭
  1110. const menuElements = document.querySelectorAll('.ce-contextmenu');
  1111. menuElements.forEach(element => {
  1112. if (element.parentNode) {
  1113. element.parentNode.removeChild(element);
  1114. }
  1115. });
  1116. };
  1117. // 监听全局点击事件
  1118. document.addEventListener('click', this.globalClickHandler, true);
  1119. // 回显编辑器数据
  1120. if (this.docJson !== null) {
  1121. // 通过getValue来的数据回显页面(因为用html回显页面会丢掉font-family,官网git issue有解释)
  1122. instance.command.executeSetValue({ main: JSON.parse(this.docJson) });
  1123. } else {
  1124. // 处理后端返回的html字符串
  1125. // 先替换 \r\n 为 空格,以统一处理空格问题
  1126. let step1 = this.htmlData.replace(/\r\n/g, " ");
  1127. // 然后替换 \\\" 为 \" ,确保样式字符串内的引号正确
  1128. let step2 = step1.replace(/\\\"/g, '"');
  1129. // 接着替换 \\ 为 空字符,去掉其他不必要的转义
  1130. let cleanedHtml = step2.replace(/\\+/g, "");
  1131. // 设置Word模板数据
  1132. instance.command.executeSetHTML({ main: cleanedHtml });
  1133. }
  1134. // 菜单弹窗销毁
  1135. window.addEventListener(
  1136. "click",
  1137. (evt) => {
  1138. const visibleDom = document.querySelector(".visible");
  1139. if (!visibleDom || visibleDom.contains(evt.target)) return;
  1140. visibleDom.classList.remove("visible");
  1141. },
  1142. {
  1143. capture: true,
  1144. }
  1145. );
  1146. /* if (this.editorOptions.align == "") {
  1147. instance.command.executeRowFlex(RowFlex.CENTER);
  1148. }
  1149. if (this.editorOptions.bold == "不加粗") {
  1150. console.log(instance);
  1151. // 等待editor实例初始化完成
  1152. this.$nextTick(() => {
  1153. instance.command.executeBold(); // 切换加粗状态
  1154. });
  1155. } */
  1156. /*
  1157. 工具栏方法
  1158. */
  1159. // 1.保存(自定义)
  1160. const saveDom = document.querySelector(".menu-item__save");
  1161. saveDom.title = `保存(${this.isApple ? "⌘" : "Ctrl"}+S)`;
  1162. saveDom.onclick = () => {
  1163. const value = instance.command.getValue(this.options);
  1164. const htmlVal = instance.command.getHTML();
  1165. this.$emit("save", htmlVal); // 保存数据传给父组件
  1166. };
  1167. // 快捷键保存
  1168. instance.listener.saved = (payload) => {
  1169. console.log("elementList: ", payload);
  1170. this.$emit("save", htmlVal); // 保存数据传给父组件
  1171. };
  1172. // 2. | 撤销 | 重做 | 格式刷 | 清除格式 |
  1173. const undoDom = document.querySelector(".menu-item__undo");
  1174. undoDom.title = `撤销(${isApple ? "⌘" : "Ctrl"}+Z)`;
  1175. undoDom.onclick = function () {
  1176. console.log("undo");
  1177. instance.command.executeUndo();
  1178. };
  1179. const redoDom = document.querySelector(".menu-item__redo");
  1180. redoDom.title = `重做(${isApple ? "⌘" : "Ctrl"}+Y)`;
  1181. redoDom.onclick = function () {
  1182. console.log("redo");
  1183. instance.command.executeRedo();
  1184. };
  1185. const painterDom = document.querySelector(".menu-item__painter");
  1186. painterDom.onclick = function () {
  1187. console.log("painter");
  1188. instance.command.executePainter({
  1189. isDblclick: false,
  1190. });
  1191. };
  1192. painterDom.ondblclick = function () {
  1193. console.log("painter");
  1194. instance.command.executePainter({
  1195. isDblclick: true,
  1196. });
  1197. };
  1198. document.querySelector(".menu-item__format").onclick = function () {
  1199. console.log("format");
  1200. instance.command.executeFormat();
  1201. };
  1202. // 3. | 字体 | 字体变大 | 字体变小 | 加粗 | 斜体 | 下划线 | 删除线 | 上标 | 下标 | 字体颜色 | 背景色 |
  1203. const fontDom = document.querySelector(".menu-item__font");
  1204. const fontSelectDom = fontDom.querySelector(".select");
  1205. const fontOptionDom = fontDom.querySelector(".options");
  1206. fontDom.onclick = function () {
  1207. console.log("font");
  1208. fontOptionDom.classList.toggle("visible");
  1209. };
  1210. fontOptionDom.onclick = function (evt) {
  1211. const li = evt.target;
  1212. instance.command.executeFont(li.dataset.family);
  1213. };
  1214. const sizeSetDom = document.querySelector(".menu-item__size");
  1215. const sizeSelectDom = sizeSetDom.querySelector(".select");
  1216. const sizeOptionDom = sizeSetDom.querySelector(".options");
  1217. sizeSetDom.title = `设置字号`;
  1218. sizeSetDom.onclick = function () {
  1219. console.log("size");
  1220. sizeOptionDom.classList.toggle("visible");
  1221. };
  1222. sizeOptionDom.onclick = function (evt) {
  1223. const li = evt.target;
  1224. instance.command.executeSize(Number(li.dataset.size));
  1225. };
  1226. const sizeAddDom = document.querySelector(".menu-item__size-add");
  1227. sizeAddDom.title = `增大字号(${this.isApple ? "⌘" : "Ctrl"}+[)`;
  1228. sizeAddDom.onclick = function () {
  1229. console.log("size-add");
  1230. instance.command.executeSizeAdd();
  1231. };
  1232. const sizeMinusDom = document.querySelector(".menu-item__size-minus");
  1233. sizeMinusDom.title = `减小字号(${this.isApple ? "⌘" : "Ctrl"}+])`;
  1234. sizeMinusDom.onclick = function () {
  1235. console.log("size-minus");
  1236. instance.command.executeSizeMinus();
  1237. };
  1238. const boldDom = document.querySelector(".menu-item__bold");
  1239. boldDom.title = `加粗(${this.isApple ? "⌘" : "Ctrl"}+B)`;
  1240. boldDom.onclick = function () {
  1241. console.log("bold");
  1242. instance.command.executeBold();
  1243. };
  1244. const italicDom = document.querySelector(".menu-item__italic");
  1245. italicDom.title = `斜体(${this.isApple ? "⌘" : "Ctrl"}+I)`;
  1246. italicDom.onclick = function () {
  1247. console.log("italic");
  1248. instance.command.executeItalic();
  1249. };
  1250. const underlineDom = document.querySelector(".menu-item__underline");
  1251. underlineDom.title = `下划线(${this.isApple ? "⌘" : "Ctrl"}+U)`;
  1252. underlineDom.onclick = function () {
  1253. console.log("underline");
  1254. instance.command.executeUnderline();
  1255. };
  1256. const strikeoutDom = document.querySelector(".menu-item__strikeout");
  1257. strikeoutDom.onclick = function () {
  1258. console.log("strikeout");
  1259. instance.command.executeStrikeout();
  1260. };
  1261. const superscriptDom = document.querySelector(".menu-item__superscript");
  1262. superscriptDom.title = `上标(${this.isApple ? "⌘" : "Ctrl"}+Shift+,)`;
  1263. superscriptDom.onclick = function () {
  1264. console.log("superscript");
  1265. instance.command.executeSuperscript();
  1266. };
  1267. const subscriptDom = document.querySelector(".menu-item__subscript");
  1268. subscriptDom.title = `下标(${this.isApple ? "⌘" : "Ctrl"}+Shift+.)`;
  1269. subscriptDom.onclick = function () {
  1270. console.log("subscript");
  1271. instance.command.executeSubscript();
  1272. };
  1273. const colorDom = document.querySelector(".menu-item__color");
  1274. const colorOptionDom = colorDom.querySelector(".options");
  1275. colorDom.onclick = function () {
  1276. console.log("color");
  1277. colorOptionDom.classList.toggle("visible");
  1278. };
  1279. colorOptionDom.onclick = function (evt) {
  1280. const li = evt.target;
  1281. const color = li.dataset.color;
  1282. if (color) {
  1283. instance.command.executeColor(color);
  1284. }
  1285. };
  1286. // 高亮功能代码
  1287. const highlightDom = document.querySelector(".menu-item__highlight");
  1288. const highlightOptionDom = highlightDom.querySelector(".options");
  1289. highlightDom.onclick = function () {
  1290. console.log("highlight");
  1291. highlightOptionDom.classList.toggle("visible");
  1292. };
  1293. highlightOptionDom.onclick = function (evt) {
  1294. const li = evt.target;
  1295. const color = li.dataset.color;
  1296. if (color) {
  1297. // 如果选择"无",则清除高亮
  1298. if (color === "transparent") {
  1299. instance.command.executeHighlight(null);
  1300. } else {
  1301. instance.command.executeHighlight(color);
  1302. }
  1303. }
  1304. };
  1305. const titleDom = document.querySelector(".menu-item__title");
  1306. const titleSelectDom = titleDom.querySelector(".select");
  1307. const titleOptionDom = titleDom.querySelector(".options");
  1308. titleOptionDom.querySelectorAll("li").forEach((li, index) => {
  1309. li.title = `Ctrl+${this.isApple ? "Option" : "Alt"}+${index}`;
  1310. });
  1311. titleDom.onclick = function () {
  1312. console.log("title");
  1313. titleOptionDom.classList.toggle("visible");
  1314. };
  1315. titleOptionDom.onclick = function (evt) {
  1316. const li = evt.target;
  1317. const level = li.dataset.level;
  1318. instance.command.executeTitle(level || null);
  1319. };
  1320. const leftDom = document.querySelector(".menu-item__left");
  1321. leftDom.title = `左对齐(${this.isApple ? "⌘" : "Ctrl"}+L)`;
  1322. leftDom.onclick = function () {
  1323. console.log("left");
  1324. instance.command.executeRowFlex(RowFlex.LEFT);
  1325. };
  1326. const centerDom = document.querySelector(".menu-item__center");
  1327. centerDom.title = `居中对齐(${this.isApple ? "⌘" : "Ctrl"}+E)`;
  1328. centerDom.onclick = function () {
  1329. console.log("center");
  1330. instance.command.executeRowFlex(RowFlex.CENTER);
  1331. };
  1332. const rightDom = document.querySelector(".menu-item__right");
  1333. rightDom.title = `右对齐(${this.isApple ? "⌘" : "Ctrl"}+R)`;
  1334. rightDom.onclick = function () {
  1335. console.log("right");
  1336. instance.command.executeRowFlex(RowFlex.RIGHT);
  1337. };
  1338. const alignmentDom = document.querySelector(".menu-item__alignment");
  1339. alignmentDom.title = `两端对齐(${this.isApple ? "⌘" : "Ctrl"}+J)`;
  1340. alignmentDom.onclick = function () {
  1341. console.log("alignment");
  1342. instance.command.executeRowFlex(RowFlex.ALIGNMENT);
  1343. };
  1344. const rowMarginDom = document.querySelector(".menu-item__row-margin");
  1345. const rowOptionDom = rowMarginDom.querySelector(".options");
  1346. rowMarginDom.onclick = function () {
  1347. console.log("row-margin");
  1348. rowOptionDom.classList.toggle("visible");
  1349. };
  1350. rowOptionDom.onclick = function (evt) {
  1351. const li = evt.target;
  1352. instance.command.executeRowMargin(Number(li.dataset.rowmargin));
  1353. };
  1354. const listDom = document.querySelector(".menu-item__list");
  1355. listDom.title = `列表(${this.isApple ? "⌘" : "Ctrl"}+Shift+U)`;
  1356. const listOptionDom = listDom.querySelector(".options");
  1357. listDom.onclick = function () {
  1358. console.log("list");
  1359. listOptionDom.classList.toggle("visible");
  1360. };
  1361. listOptionDom.onclick = function (evt) {
  1362. const li = evt.target;
  1363. const listType = li.dataset.listType || null;
  1364. const listStyle = li.dataset.listStyle;
  1365. instance.command.executeList(listType, listStyle);
  1366. };
  1367. // 4. | 表格 | 图片 | 超链接 | 分割线 | 水印 | 代码块 | 分隔符 | 控件 | 复选框 | LaTeX | 日期选择器
  1368. const tableDom = document.querySelector(".menu-item__table");
  1369. const tablePanelContainer = document.querySelector(
  1370. ".menu-item__table__collapse"
  1371. );
  1372. const tableClose = document.querySelector(".table-close");
  1373. const tableTitle = document.querySelector(".table-select");
  1374. const tablePanel = document.querySelector(".table-panel");
  1375. // 绘制行列
  1376. const tableCellList = [];
  1377. for (let i = 0; i < 10; i++) {
  1378. const tr = document.createElement("tr");
  1379. tr.classList.add("table-row");
  1380. const trCellList = [];
  1381. for (let j = 0; j < 10; j++) {
  1382. const td = document.createElement("td");
  1383. td.classList.add("table-cel");
  1384. tr.append(td);
  1385. trCellList.push(td);
  1386. }
  1387. tablePanel.append(tr);
  1388. tableCellList.push(trCellList);
  1389. }
  1390. let colIndex = 0;
  1391. let rowIndex = 0;
  1392. // 移除所有格选择
  1393. function removeAllTableCellSelect() {
  1394. tableCellList.forEach((tr) => {
  1395. tr.forEach((td) => td.classList.remove("active"));
  1396. });
  1397. }
  1398. // 设置标题内容
  1399. function setTableTitle(payload) {
  1400. tableTitle.innerText = payload;
  1401. }
  1402. // 恢复初始状态
  1403. function recoveryTable() {
  1404. // 还原选择样式、标题、选择行列
  1405. removeAllTableCellSelect();
  1406. setTableTitle("插入");
  1407. colIndex = 0;
  1408. rowIndex = 0;
  1409. // 隐藏panel
  1410. tablePanelContainer.style.display = "none";
  1411. }
  1412. tableDom.onclick = function () {
  1413. console.log("table");
  1414. tablePanelContainer.style.display = "block";
  1415. };
  1416. tablePanel.onmousemove = function (evt) {
  1417. const celSize = 16;
  1418. const rowMarginTop = 10;
  1419. const celMarginRight = 6;
  1420. const { offsetX, offsetY } = evt;
  1421. // 移除所有选择
  1422. removeAllTableCellSelect();
  1423. colIndex = Math.ceil(offsetX / (celSize + celMarginRight)) || 1;
  1424. rowIndex = Math.ceil(offsetY / (celSize + rowMarginTop)) || 1;
  1425. // 改变选择样式
  1426. tableCellList.forEach((tr, trIndex) => {
  1427. tr.forEach((td, tdIndex) => {
  1428. if (tdIndex < colIndex && trIndex < rowIndex) {
  1429. td.classList.add("active");
  1430. }
  1431. });
  1432. });
  1433. // 改变表格标题
  1434. setTableTitle(`${rowIndex}×${colIndex}`);
  1435. };
  1436. tableClose.onclick = function () {
  1437. recoveryTable();
  1438. };
  1439. tablePanel.onclick = function () {
  1440. // 应用选择
  1441. instance.command.executeInsertTable(rowIndex, colIndex);
  1442. recoveryTable();
  1443. };
  1444. const imageDom = document.querySelector(".menu-item__image");
  1445. const imageFileDom = document.querySelector("#image");
  1446. imageDom.onclick = function () {
  1447. imageFileDom.click();
  1448. };
  1449. imageFileDom.onchange = function () {
  1450. const file = imageFileDom.files[0];
  1451. const fileReader = new FileReader();
  1452. fileReader.readAsDataURL(file);
  1453. fileReader.onload = function () {
  1454. // 计算宽高
  1455. const image = new Image();
  1456. const value = String(fileReader.result);
  1457. image.src = value;
  1458. image.onload = function () {
  1459. instance.command.executeImage({
  1460. value,
  1461. width: image.width,
  1462. height: image.height,
  1463. });
  1464. imageFileDom.value = "";
  1465. };
  1466. };
  1467. };
  1468. const hyperlinkDom = document.querySelector(".menu-item__hyperlink");
  1469. hyperlinkDom.onclick = function () {
  1470. console.log("hyperlink");
  1471. new Dialog({
  1472. title: "超链接",
  1473. data: [
  1474. {
  1475. type: "text",
  1476. label: "文本",
  1477. name: "name",
  1478. required: true,
  1479. placeholder: "请输入文本",
  1480. value: instance.command.getRangeText(),
  1481. },
  1482. {
  1483. type: "text",
  1484. label: "链接",
  1485. name: "url",
  1486. required: true,
  1487. placeholder: "请输入链接",
  1488. },
  1489. ],
  1490. onConfirm: (payload) => {
  1491. const name = payload.find((p) => p.name === "name")?.value;
  1492. if (!name) return;
  1493. const url = payload.find((p) => p.name === "url")?.value;
  1494. if (!url) return;
  1495. instance.command.executeHyperlink({
  1496. type: ElementType.HYPERLINK,
  1497. value: "",
  1498. url,
  1499. valueList: splitText(name).map((n) => ({
  1500. value: n,
  1501. size: 16,
  1502. })),
  1503. });
  1504. },
  1505. });
  1506. };
  1507. const separatorDom = document.querySelector(".menu-item__separator");
  1508. const separatorOptionDom = separatorDom.querySelector(".options");
  1509. separatorDom.onclick = function () {
  1510. console.log("separator");
  1511. separatorOptionDom.classList.toggle("visible");
  1512. };
  1513. separatorOptionDom.onmousedown = function (evt) {
  1514. let payload = [];
  1515. const li = evt.target;
  1516. const separatorDash = li.dataset.separator?.split(",").map(Number);
  1517. if (separatorDash) {
  1518. const isSingleLine = separatorDash.every((d) => d === 0);
  1519. if (!isSingleLine) {
  1520. payload = separatorDash;
  1521. }
  1522. }
  1523. instance.command.executeSeparator(payload);
  1524. };
  1525. // const pageBreakDom = document.querySelector(
  1526. // '.menu-item__page-break'
  1527. // )
  1528. // pageBreakDom.onclick = function () {
  1529. // console.log('pageBreak')
  1530. // instance.command.executePageBreak()
  1531. // }
  1532. // const watermarkDom = document.querySelector(
  1533. // '.menu-item__watermark'
  1534. // )
  1535. // const watermarkOptionDom =
  1536. // watermarkDom.querySelector('.options')
  1537. // watermarkDom.onclick = function () {
  1538. // console.log('watermark')
  1539. // watermarkOptionDom.classList.toggle('visible')
  1540. // }
  1541. // watermarkOptionDom.onmousedown = function (evt) {
  1542. // const li = evt.target
  1543. // const menu = li.dataset.menu
  1544. // watermarkOptionDom.classList.toggle('visible')
  1545. // if (menu === 'add') {
  1546. // new Dialog({
  1547. // title: '水印',
  1548. // data: [
  1549. // {
  1550. // type: 'text',
  1551. // label: '内容',
  1552. // name: 'data',
  1553. // required: true,
  1554. // placeholder: '请输入内容'
  1555. // },
  1556. // {
  1557. // type: 'color',
  1558. // label: '颜色',
  1559. // name: 'color',
  1560. // required: true,
  1561. // value: '#AEB5C0'
  1562. // },
  1563. // {
  1564. // type: 'number',
  1565. // label: '字体大小',
  1566. // name: 'size',
  1567. // required: true,
  1568. // value: '120'
  1569. // }
  1570. // ],
  1571. // onConfirm: payload => {
  1572. // const nullableIndex = payload.findIndex(p => !p.value)
  1573. // if (~nullableIndex) return
  1574. // const watermark = payload.reduce((pre, cur) => {
  1575. // pre[cur.name] = cur.value
  1576. // return pre
  1577. // }, {})
  1578. // instance.command.executeAddWatermark({
  1579. // data: watermark.data,
  1580. // color: watermark.color,
  1581. // size: Number(watermark.size)
  1582. // })
  1583. // }
  1584. // })
  1585. // } else {
  1586. // instance.command.executeDeleteWatermark()
  1587. // }
  1588. // }
  1589. // const codeblockDom = document.querySelector(
  1590. // '.menu-item__codeblock'
  1591. // )
  1592. // codeblockDom.onclick = function () {
  1593. // console.log('codeblock')
  1594. // new Dialog({
  1595. // title: '代码块',
  1596. // data: [
  1597. // {
  1598. // type: 'textarea',
  1599. // name: 'codeblock',
  1600. // placeholder: '请输入代码',
  1601. // width: 500,
  1602. // height: 300
  1603. // }
  1604. // ],
  1605. // onConfirm: payload => {
  1606. // const codeblock = payload.find(p => p.name === 'codeblock')?.value
  1607. // if (!codeblock) return
  1608. // const tokenList = prism.tokenize(codeblock, prism.languages.javascript)
  1609. // const formatTokenList = formatPrismToken(tokenList)
  1610. // const elementList = []
  1611. // for (let i = 0; i < formatTokenList.length; i++) {
  1612. // const formatToken = formatTokenList[i]
  1613. // const tokenStringList = splitText(formatToken.content)
  1614. // for (let j = 0; j < tokenStringList.length; j++) {
  1615. // const value = tokenStringList[j]
  1616. // const element = {
  1617. // value
  1618. // }
  1619. // if (formatToken.color) {
  1620. // element.color = formatToken.color
  1621. // }
  1622. // if (formatToken.bold) {
  1623. // element.bold = true
  1624. // }
  1625. // if (formatToken.italic) {
  1626. // element.italic = true
  1627. // }
  1628. // elementList.push(element)
  1629. // }
  1630. // }
  1631. // elementList.unshift({
  1632. // value: '\n'
  1633. // })
  1634. // instance.command.executeInsertElementList(elementList)
  1635. // }
  1636. // })
  1637. // }
  1638. // const controlDom = document.querySelector(
  1639. // '.menu-item__control'
  1640. // )
  1641. // const controlOptionDom = controlDom.querySelector('.options')
  1642. // controlDom.onclick = function () {
  1643. // console.log('control')
  1644. // controlOptionDom.classList.toggle('visible')
  1645. // }
  1646. // controlOptionDom.onmousedown = function (evt) {
  1647. // controlOptionDom.classList.toggle('visible')
  1648. // const li = evt.target
  1649. // const type = li.dataset.control
  1650. // switch (type) {
  1651. // case ControlType.TEXT:
  1652. // new Dialog({
  1653. // title: '文本控件',
  1654. // data: [
  1655. // {
  1656. // type: 'text',
  1657. // label: '占位符',
  1658. // name: 'placeholder',
  1659. // required: true,
  1660. // placeholder: '请输入占位符'
  1661. // },
  1662. // {
  1663. // type: 'text',
  1664. // label: '默认值',
  1665. // name: 'value',
  1666. // placeholder: '请输入默认值'
  1667. // }
  1668. // ],
  1669. // onConfirm: payload => {
  1670. // const placeholder = payload.find(
  1671. // p => p.name === 'placeholder'
  1672. // )?.value
  1673. // if (!placeholder) return
  1674. // const value = payload.find(p => p.name === 'value')?.value || ''
  1675. // instance.command.executeInsertElementList([
  1676. // {
  1677. // type: ElementType.CONTROL,
  1678. // value: '',
  1679. // control: {
  1680. // type,
  1681. // value: value
  1682. // ? [
  1683. // {
  1684. // value
  1685. // }
  1686. // ]
  1687. // : null,
  1688. // placeholder
  1689. // }
  1690. // }
  1691. // ])
  1692. // }
  1693. // })
  1694. // break
  1695. // case ControlType.SELECT:
  1696. // new Dialog({
  1697. // title: '列举控件',
  1698. // data: [
  1699. // {
  1700. // type: 'text',
  1701. // label: '占位符',
  1702. // name: 'placeholder',
  1703. // required: true,
  1704. // placeholder: '请输入占位符'
  1705. // },
  1706. // {
  1707. // type: 'text',
  1708. // label: '默认值',
  1709. // name: 'code',
  1710. // placeholder: '请输入默认值'
  1711. // },
  1712. // {
  1713. // type: 'textarea',
  1714. // label: '值集',
  1715. // name: 'valueSets',
  1716. // required: true,
  1717. // height: 100,
  1718. // placeholder: `请输入值集JSON,例:\n[{\n"value":"有",\n"code":"98175"\n}]`
  1719. // }
  1720. // ],
  1721. // onConfirm: payload => {
  1722. // const placeholder = payload.find(
  1723. // p => p.name === 'placeholder'
  1724. // )?.value
  1725. // if (!placeholder) return
  1726. // const valueSets = payload.find(p => p.name === 'valueSets')?.value
  1727. // if (!valueSets) return
  1728. // const code = payload.find(p => p.name === 'code')?.value
  1729. // instance.command.executeInsertElementList([
  1730. // {
  1731. // type: ElementType.CONTROL,
  1732. // value: '',
  1733. // control: {
  1734. // type,
  1735. // code,
  1736. // value: null,
  1737. // placeholder,
  1738. // valueSets: JSON.parse(valueSets)
  1739. // }
  1740. // }
  1741. // ])
  1742. // }
  1743. // })
  1744. // break
  1745. // case ControlType.CHECKBOX:
  1746. // new Dialog({
  1747. // title: '复选框控件',
  1748. // data: [
  1749. // {
  1750. // type: 'text',
  1751. // label: '默认值',
  1752. // name: 'code',
  1753. // placeholder: '请输入默认值,多个值以英文逗号分割'
  1754. // },
  1755. // {
  1756. // type: 'textarea',
  1757. // label: '值集',
  1758. // name: 'valueSets',
  1759. // required: true,
  1760. // height: 100,
  1761. // placeholder: `请输入值集JSON,例:\n[{\n"value":"有",\n"code":"98175"\n}]`
  1762. // }
  1763. // ],
  1764. // onConfirm: payload => {
  1765. // const valueSets = payload.find(p => p.name === 'valueSets')?.value
  1766. // if (!valueSets) return
  1767. // const code = payload.find(p => p.name === 'code')?.value
  1768. // instance.command.executeInsertElementList([
  1769. // {
  1770. // type: ElementType.CONTROL,
  1771. // value: '',
  1772. // control: {
  1773. // type,
  1774. // code,
  1775. // value: null,
  1776. // valueSets: JSON.parse(valueSets)
  1777. // }
  1778. // }
  1779. // ])
  1780. // }
  1781. // })
  1782. // break
  1783. // default:
  1784. // break
  1785. // }
  1786. // }
  1787. // const checkboxDom = document.querySelector(
  1788. // '.menu-item__checkbox'
  1789. // )
  1790. // checkboxDom.onclick = function () {
  1791. // console.log('checkbox')
  1792. // instance.command.executeInsertElementList([
  1793. // {
  1794. // type: ElementType.CHECKBOX,
  1795. // checkbox: {
  1796. // value: false
  1797. // },
  1798. // value: ''
  1799. // }
  1800. // ])
  1801. // }
  1802. // const latexDom = document.querySelector('.menu-item__latex')
  1803. // latexDom.onclick = function () {
  1804. // console.log('LaTeX')
  1805. // new Dialog({
  1806. // title: 'LaTeX',
  1807. // data: [
  1808. // {
  1809. // type: 'textarea',
  1810. // height: 100,
  1811. // name: 'value',
  1812. // placeholder: '请输入LaTeX文本'
  1813. // }
  1814. // ],
  1815. // onConfirm: payload => {
  1816. // const value = payload.find(p => p.name === 'value')?.value
  1817. // if (!value) return
  1818. // instance.command.executeInsertElementList([
  1819. // {
  1820. // type: ElementType.LATEX,
  1821. // value
  1822. // }
  1823. // ])
  1824. // }
  1825. // })
  1826. // }
  1827. // const dateDom = document.querySelector('.menu-item__date')
  1828. // const dateDomOptionDom = dateDom.querySelector('.options')
  1829. // dateDom.onclick = function () {
  1830. // console.log('date')
  1831. // dateDomOptionDom.classList.toggle('visible')
  1832. // // 定位调整
  1833. // const bodyRect = document.body.getBoundingClientRect()
  1834. // const dateDomOptionRect = dateDomOptionDom.getBoundingClientRect()
  1835. // if (dateDomOptionRect.left + dateDomOptionRect.width > bodyRect.width) {
  1836. // dateDomOptionDom.style.right = '0px'
  1837. // dateDomOptionDom.style.left = 'unset'
  1838. // } else {
  1839. // dateDomOptionDom.style.right = 'unset'
  1840. // dateDomOptionDom.style.left = '0px'
  1841. // }
  1842. // // 当前日期
  1843. // const date = new Date()
  1844. // const year = date.getFullYear().toString()
  1845. // const month = (date.getMonth() + 1).toString().padStart(2, '0')
  1846. // const day = date.getDate().toString().padStart(2, '0')
  1847. // const hour = date.getHours().toString().padStart(2, '0')
  1848. // const minute = date.getMinutes().toString().padStart(2, '0')
  1849. // const second = date.getSeconds().toString().padStart(2, '0')
  1850. // const dateString = `${year}-${month}-${day}`
  1851. // const dateTimeString = `${dateString} ${hour}:${minute}:${second}`
  1852. // dateDomOptionDom.querySelector('li:first-child').innerText = dateString
  1853. // dateDomOptionDom.querySelector('li:last-child').innerText = dateTimeString
  1854. // }
  1855. // dateDomOptionDom.onmousedown = function (evt) {
  1856. // const li = evt.target
  1857. // const dateFormat = li.dataset.format
  1858. // dateDomOptionDom.classList.toggle('visible')
  1859. // instance.command.executeInsertElementList([
  1860. // {
  1861. // type: ElementType.DATE,
  1862. // value: '',
  1863. // dateFormat,
  1864. // valueList: [
  1865. // {
  1866. // value: li.innerText.trim()
  1867. // }
  1868. // ]
  1869. // }
  1870. // ])
  1871. // }
  1872. // const blockDom = document.querySelector('.menu-item__block')
  1873. // blockDom.onclick = function () {
  1874. // console.log('block')
  1875. // new Dialog({
  1876. // title: '内容块',
  1877. // data: [
  1878. // {
  1879. // type: 'select',
  1880. // label: '类型',
  1881. // name: 'type',
  1882. // value: 'iframe',
  1883. // required: true,
  1884. // options: [
  1885. // {
  1886. // label: '网址',
  1887. // value: 'iframe'
  1888. // },
  1889. // {
  1890. // label: '视频',
  1891. // value: 'video'
  1892. // }
  1893. // ]
  1894. // },
  1895. // {
  1896. // type: 'number',
  1897. // label: '宽度',
  1898. // name: 'width',
  1899. // placeholder: '请输入宽度(默认页面内宽度)'
  1900. // },
  1901. // {
  1902. // type: 'number',
  1903. // label: '高度',
  1904. // name: 'height',
  1905. // required: true,
  1906. // placeholder: '请输入高度'
  1907. // },
  1908. // {
  1909. // type: 'textarea',
  1910. // label: '地址',
  1911. // height: 100,
  1912. // name: 'value',
  1913. // required: true,
  1914. // placeholder: '请输入地址'
  1915. // }
  1916. // ],
  1917. // onConfirm: payload => {
  1918. // const type = payload.find(p => p.name === 'type')?.value
  1919. // if (!type) return
  1920. // const value = payload.find(p => p.name === 'value')?.value
  1921. // if (!value) return
  1922. // const width = payload.find(p => p.name === 'width')?.value
  1923. // const height = payload.find(p => p.name === 'height')?.value
  1924. // if (!height) return
  1925. // const block = {
  1926. // type: null
  1927. // }
  1928. // if (block.type === BlockType.IFRAME) {
  1929. // block.iframeBlock = {
  1930. // src: value
  1931. // }
  1932. // } else if (block.type === BlockType.VIDEO) {
  1933. // block.videoBlock = {
  1934. // src: value
  1935. // }
  1936. // }
  1937. // const blockElemen = {
  1938. // type: ElementType.BLOCK,
  1939. // value: '',
  1940. // height: Number(height),
  1941. // block
  1942. // }
  1943. // if (width) {
  1944. // blockElement.width = Number(width)
  1945. // }
  1946. // instance.command.executeInsertElementList([blockElement])
  1947. // }
  1948. // })
  1949. // }
  1950. // 5. | 搜索&替换 | 打印 |
  1951. const searchCollapseDom = document.querySelector(
  1952. ".menu-item__search__collapse"
  1953. );
  1954. const searchInputDom = document.querySelector(
  1955. ".menu-item__search__collapse__search input"
  1956. );
  1957. const replaceInputDom = document.querySelector(
  1958. ".menu-item__search__collapse__replace input"
  1959. );
  1960. const searchDom = document.querySelector(".menu-item__search");
  1961. searchDom.title = `搜索与替换(${isApple ? "⌘" : "Ctrl"}+F)`;
  1962. const searchResultDom = searchCollapseDom.querySelector(".search-result");
  1963. function setSearchResult() {
  1964. const result = instance.command.getSearchNavigateInfo();
  1965. if (result) {
  1966. const { index, count } = result;
  1967. searchResultDom.innerText = `${index}/${count}`;
  1968. } else {
  1969. searchResultDom.innerText = "";
  1970. }
  1971. }
  1972. searchDom.onclick = function () {
  1973. console.log("search");
  1974. searchCollapseDom.style.display = "block";
  1975. const bodyRect = document.body.getBoundingClientRect();
  1976. const searchRect = searchDom.getBoundingClientRect();
  1977. const searchCollapseRect = searchCollapseDom.getBoundingClientRect();
  1978. if (searchRect.left + searchCollapseRect.width > bodyRect.width) {
  1979. searchCollapseDom.style.right = "0px";
  1980. searchCollapseDom.style.left = "unset";
  1981. } else {
  1982. searchCollapseDom.style.right = "unset";
  1983. }
  1984. searchInputDom.focus();
  1985. };
  1986. searchCollapseDom.querySelector("span").onclick = function () {
  1987. searchCollapseDom.style.display = "none";
  1988. searchInputDom.value = "";
  1989. replaceInputDom.value = "";
  1990. instance.command.executeSearch(null);
  1991. setSearchResult();
  1992. };
  1993. searchInputDom.oninput = function () {
  1994. instance.command.executeSearch(searchInputDom.value || null);
  1995. setSearchResult();
  1996. };
  1997. searchInputDom.onkeydown = function (evt) {
  1998. if (evt.key === "Enter") {
  1999. instance.command.executeSearch(searchInputDom.value || null);
  2000. setSearchResult();
  2001. }
  2002. };
  2003. searchCollapseDom.querySelector("button").onclick = function () {
  2004. const searchValue = searchInputDom.value;
  2005. const replaceValue = replaceInputDom.value;
  2006. if (searchValue && replaceValue && searchValue !== replaceValue) {
  2007. instance.command.executeReplace(replaceValue);
  2008. }
  2009. };
  2010. searchCollapseDom.querySelector(".arrow-left").onclick = function () {
  2011. instance.command.executeSearchNavigatePre();
  2012. setSearchResult();
  2013. };
  2014. searchCollapseDom.querySelector(".arrow-right").onclick = function () {
  2015. instance.command.executeSearchNavigateNext();
  2016. setSearchResult();
  2017. };
  2018. // const printDom = document.querySelector('.menu-item__print')
  2019. // printDom.title = `打印(${isApple ? '⌘' : 'Ctrl'}+P)`
  2020. // printDom.onclick = function () {
  2021. // console.log('print')
  2022. // instance.command.executePrint()
  2023. // }
  2024. // 6. 目录显隐 | 页面模式 | 纸张缩放 | 纸张大小 | 纸张方向 | 页边距 | 全屏
  2025. // async function updateCatalog() {
  2026. // const catalog = await instance.command.getCatalog()
  2027. // const catalogMainDom =
  2028. // document.querySelector('.catalog__main')
  2029. // catalogMainDom.innerHTML = ''
  2030. // if (catalog) {
  2031. // const appendCatalog = (
  2032. // parent,
  2033. // catalogItems
  2034. // ) => {
  2035. // for (let c = 0; c < catalogItems.length; c++) {
  2036. // const catalogItem = catalogItems[c]
  2037. // const catalogItemDom = document.createElement('div')
  2038. // catalogItemDom.classList.add('catalog-item')
  2039. // // 渲染
  2040. // const catalogItemContentDom = document.createElement('div')
  2041. // catalogItemContentDom.classList.add('catalog-item__content')
  2042. // const catalogItemContentSpanDom = document.createElement('span')
  2043. // catalogItemContentSpanDom.innerText = catalogItem.name
  2044. // catalogItemContentDom.append(catalogItemContentSpanDom)
  2045. // // 定位
  2046. // catalogItemContentDom.onclick = () => {
  2047. // instance.command.executeLocationCatalog(catalogItem.id)
  2048. // }
  2049. // catalogItemDom.append(catalogItemContentDom)
  2050. // if (catalogItem.subCatalog && catalogItem.subCatalog.length) {
  2051. // appendCatalog(catalogItemDom, catalogItem.subCatalog)
  2052. // }
  2053. // // 追加
  2054. // parent.append(catalogItemDom)
  2055. // }
  2056. // }
  2057. // appendCatalog(catalogMainDom, catalog)
  2058. // }
  2059. // }
  2060. // let isCatalogShow = true
  2061. // const catalogDom = document.querySelector('.catalog')
  2062. // const catalogModeDom =
  2063. // document.querySelector('.catalog-mode')
  2064. // const catalogHeaderCloseDom = document.querySelector(
  2065. // '.catalog__header__close'
  2066. // )
  2067. // const switchCatalog = () => {
  2068. // console.log('目录', isCatalogShow)
  2069. // isCatalogShow = !isCatalogShow
  2070. // if (isCatalogShow) {
  2071. // console.log('目录', isCatalogShow)
  2072. // catalogDom.style.display = 'block'
  2073. // updateCatalog()
  2074. // } else {
  2075. // catalogDom.style.display = 'none'
  2076. // }
  2077. // }
  2078. // catalogModeDom.onclick = switchCatalog
  2079. // catalogHeaderCloseDom.onclick = switchCatalog
  2080. const pageModeDom = document.querySelector(".page-mode");
  2081. const pageModeOptionsDom = pageModeDom.querySelector(".options");
  2082. pageModeDom.onclick = function () {
  2083. pageModeOptionsDom.classList.toggle("visible");
  2084. };
  2085. pageModeOptionsDom.onclick = function (evt) {
  2086. const li = evt.target;
  2087. instance.command.executePageMode(li.dataset.pageMode);
  2088. };
  2089. document.querySelector(".page-scale-percentage").onclick = function () {
  2090. console.log("page-scale-recovery");
  2091. instance.command.executePageScaleRecovery();
  2092. };
  2093. document.querySelector(".page-scale-minus").onclick = function () {
  2094. console.log("page-scale-minus");
  2095. instance.command.executePageScaleMinus();
  2096. };
  2097. document.querySelector(".page-scale-add").onclick = function () {
  2098. console.log("page-scale-add");
  2099. instance.command.executePageScaleAdd();
  2100. };
  2101. // 纸张大小
  2102. const paperSizeDom = document.querySelector(".paper-size");
  2103. const paperSizeDomOptionsDom = paperSizeDom.querySelector(".options");
  2104. paperSizeDom.onclick = function () {
  2105. paperSizeDomOptionsDom.classList.toggle("visible");
  2106. };
  2107. paperSizeDomOptionsDom.onclick = function (evt) {
  2108. const li = evt.target;
  2109. const paperType = li.dataset.paperSize;
  2110. const [width, height] = paperType.split("*").map(Number);
  2111. instance.command.executePaperSize(width, height);
  2112. // 纸张状态回显
  2113. paperSizeDomOptionsDom
  2114. .querySelectorAll("li")
  2115. .forEach((child) => child.classList.remove("active"));
  2116. li.classList.add("active");
  2117. };
  2118. // 纸张方向
  2119. const paperDirectionDom = document.querySelector(".paper-direction");
  2120. const paperDirectionDomOptionsDom =
  2121. paperDirectionDom.querySelector(".options");
  2122. paperDirectionDom.onclick = function () {
  2123. paperDirectionDomOptionsDom.classList.toggle("visible");
  2124. };
  2125. paperDirectionDomOptionsDom.onclick = function (evt) {
  2126. const li = evt.target;
  2127. const paperDirection = li.dataset.paperDirection;
  2128. instance.command.executePaperDirection(paperDirection);
  2129. // 纸张方向状态回显
  2130. paperDirectionDomOptionsDom
  2131. .querySelectorAll("li")
  2132. .forEach((child) => child.classList.remove("active"));
  2133. li.classList.add("active");
  2134. };
  2135. // 页面边距
  2136. const paperMarginDom = document.querySelector(".paper-margin");
  2137. paperMarginDom.onclick = function () {
  2138. const [topMargin, rightMargin, bottomMargin, leftMargin] =
  2139. instance.command.getPaperMargin();
  2140. new Dialog({
  2141. title: "页边距",
  2142. data: [
  2143. {
  2144. type: "text",
  2145. label: "上边距",
  2146. name: "top",
  2147. required: true,
  2148. value: `${topMargin}`,
  2149. placeholder: "请输入上边距",
  2150. },
  2151. {
  2152. type: "text",
  2153. label: "下边距",
  2154. name: "bottom",
  2155. required: true,
  2156. value: `${bottomMargin}`,
  2157. placeholder: "请输入下边距",
  2158. },
  2159. {
  2160. type: "text",
  2161. label: "左边距",
  2162. name: "left",
  2163. required: true,
  2164. value: `${leftMargin}`,
  2165. placeholder: "请输入左边距",
  2166. },
  2167. {
  2168. type: "text",
  2169. label: "右边距",
  2170. name: "right",
  2171. required: true,
  2172. value: `${rightMargin}`,
  2173. placeholder: "请输入右边距",
  2174. },
  2175. ],
  2176. onConfirm: (payload) => {
  2177. const top = payload.find((p) => p.name === "top")?.value;
  2178. if (!top) return;
  2179. const bottom = payload.find((p) => p.name === "bottom")?.value;
  2180. if (!bottom) return;
  2181. const left = payload.find((p) => p.name === "left")?.value;
  2182. if (!left) return;
  2183. const right = payload.find((p) => p.name === "right")?.value;
  2184. if (!right) return;
  2185. instance.command.executeSetPaperMargin([
  2186. Number(top),
  2187. Number(right),
  2188. Number(bottom),
  2189. Number(left),
  2190. ]);
  2191. },
  2192. });
  2193. };
  2194. // 全屏
  2195. const fullscreenDom = document.querySelector(".fullscreen");
  2196. fullscreenDom.onclick = toggleFullscreen;
  2197. window.addEventListener("keydown", (evt) => {
  2198. if (evt.key === "F11") {
  2199. toggleFullscreen();
  2200. evt.preventDefault();
  2201. }
  2202. });
  2203. document.addEventListener("fullscreenchange", () => {
  2204. fullscreenDom.classList.toggle("exist");
  2205. });
  2206. function toggleFullscreen() {
  2207. console.log("fullscreen");
  2208. if (!document.fullscreenElement) {
  2209. document.documentElement.requestFullscreen();
  2210. } else {
  2211. document.exitFullscreen();
  2212. }
  2213. }
  2214. // 7. 编辑器使用模式
  2215. let modeIndex = 1;
  2216. const modeList = [
  2217. {
  2218. mode: EditorMode.READONLY,
  2219. name: "只读模式",
  2220. },
  2221. {
  2222. mode: EditorMode.EDIT,
  2223. name: "编辑模式",
  2224. },
  2225. {
  2226. mode: EditorMode.CLEAN,
  2227. name: "清洁模式",
  2228. },
  2229. {
  2230. mode: EditorMode.FORM,
  2231. name: "表单模式",
  2232. },
  2233. {
  2234. mode: EditorMode.PRINT,
  2235. name: "打印模式",
  2236. },
  2237. ];
  2238. const modeElement = document.querySelector(".editor-mode");
  2239. // 初始设置只读模式
  2240. const { name, mode } = modeList[modeIndex];
  2241. modeElement.innerText = name;
  2242. instance.command.executeMode(mode);
  2243. // 设置菜单栏权限视觉反馈
  2244. const isReadonly = mode === EditorMode.READONLY;
  2245. const enableMenuList = ["search", "print"];
  2246. document.querySelectorAll(".menu-item>div").forEach((dom) => {
  2247. const menu = dom.dataset.menu;
  2248. isReadonly && (!menu || !enableMenuList.includes(menu))
  2249. ? dom.classList.add("disable")
  2250. : dom.classList.remove("disable");
  2251. });
  2252. modeElement.onclick = function () {
  2253. // 模式选择循环
  2254. modeIndex === modeList.length - 1 ? (modeIndex = 0) : modeIndex++;
  2255. // 设置模式
  2256. const { name, mode } = modeList[modeIndex];
  2257. modeElement.innerText = name;
  2258. console.log(1212, name);
  2259. instance.command.executeMode(mode);
  2260. // 设置菜单栏权限视觉反馈
  2261. const isReadonly = mode === EditorMode.READONLY;
  2262. const enableMenuList = ["search", "print"];
  2263. document.querySelectorAll(".menu-item>div").forEach((dom) => {
  2264. const menu = dom.dataset.menu;
  2265. isReadonly && (!menu || !enableMenuList.includes(menu))
  2266. ? dom.classList.add("disable")
  2267. : dom.classList.remove("disable");
  2268. });
  2269. };
  2270. // 模拟批注
  2271. // const commentDom = document.querySelector('.comment')
  2272. // const updateComment = async() => {
  2273. // const groupIds = await instance.command.getGroupIds()
  2274. // for (const comment of this.commentList) {
  2275. // const activeCommentDom = commentDom.querySelector(
  2276. // `.comment-item[data-id='${comment.id}']`
  2277. // )
  2278. // // 编辑器是否存在对应成组id
  2279. // if (!groupIds.includes(comment.id)) {
  2280. // // 当前dom是否存在-不存在则追加
  2281. // if (!activeCommentDom) {
  2282. // const commentItem = document.createElement('div')
  2283. // commentItem.classList.add('comment-item')
  2284. // commentItem.setAttribute('data-id', comment.id)
  2285. // commentItem.onclick = () => {
  2286. // instance.command.executeLocationGroup(comment.id)
  2287. // }
  2288. // commentDom.append(commentItem)
  2289. // // 选区信息
  2290. // const commentItemTitle = document.createElement('div')
  2291. // commentItemTitle.classList.add('comment-item__title')
  2292. // commentItemTitle.append(document.createElement('span'))
  2293. // const commentItemTitleContent = document.createElement('span')
  2294. // commentItemTitleContent.innerText = comment.rangeText
  2295. // commentItemTitle.append(commentItemTitleContent)
  2296. // const closeDom = document.createElement('i')
  2297. // closeDom.onclick = () => {
  2298. // instance.command.executeDeleteGroup(comment.id)
  2299. // }
  2300. // commentItemTitle.append(closeDom)
  2301. // commentItem.append(commentItemTitle)
  2302. // // 基础信息
  2303. // const commentItemInfo = document.createElement('div')
  2304. // commentItemInfo.classList.add('comment-item__info')
  2305. // const commentItemInfoName = document.createElement('span')
  2306. // commentItemInfoName.innerText = comment.userName
  2307. // const commentItemInfoDate = document.createElement('span')
  2308. // commentItemInfoDate.innerText = comment.createdDate
  2309. // commentItemInfo.append(commentItemInfoName)
  2310. // commentItemInfo.append(commentItemInfoDate)
  2311. // commentItem.append(commentItemInfo)
  2312. // // 详细评论
  2313. // const commentItemContent = document.createElement('div')
  2314. // commentItemContent.classList.add('comment-item__content')
  2315. // commentItemContent.innerText = comment.content
  2316. // commentItem.append(commentItemContent)
  2317. // commentDom.append(commentItem)
  2318. // }
  2319. // } else {
  2320. // // 编辑器内不存在对应成组id则dom则移除
  2321. // activeCommentDom?.remove()
  2322. // }
  2323. // }
  2324. // }
  2325. // 8. 内部事件监听
  2326. instance.listener.rangeStyleChange = function (payload) {
  2327. // 控件类型
  2328. payload.type === ElementType.SUBSCRIPT
  2329. ? subscriptDom.classList.add("active")
  2330. : subscriptDom.classList.remove("active");
  2331. payload.type === ElementType.SUPERSCRIPT
  2332. ? superscriptDom.classList.add("active")
  2333. : superscriptDom.classList.remove("active");
  2334. payload.type === ElementType.SEPARATOR
  2335. ? separatorDom.classList.add("active")
  2336. : separatorDom.classList.remove("active");
  2337. separatorOptionDom
  2338. .querySelectorAll("li")
  2339. .forEach((li) => li.classList.remove("active"));
  2340. if (payload.type === ElementType.SEPARATOR) {
  2341. const separator = payload.dashArray.join(",") || "0,0";
  2342. const curSeparatorDom = separatorOptionDom.querySelector(
  2343. `[data-separator='${separator}']`
  2344. );
  2345. if (curSeparatorDom) {
  2346. curSeparatorDom.classList.add("active");
  2347. }
  2348. }
  2349. // 富文本
  2350. fontOptionDom
  2351. .querySelectorAll("li")
  2352. .forEach((li) => li.classList.remove("active"));
  2353. const curFontDom = fontOptionDom.querySelector(
  2354. `[data-family='${payload.font}']`
  2355. );
  2356. if (curFontDom) {
  2357. fontSelectDom.innerText = curFontDom.innerText;
  2358. fontSelectDom.style.fontFamily = payload.font;
  2359. curFontDom.classList.add("active");
  2360. }
  2361. sizeOptionDom
  2362. .querySelectorAll("li")
  2363. .forEach((li) => li.classList.remove("active"));
  2364. const curSizeDom = sizeOptionDom.querySelector(
  2365. `[data-size='${payload.size}']`
  2366. );
  2367. if (curSizeDom) {
  2368. sizeSelectDom.innerText = curSizeDom.innerText;
  2369. curSizeDom.classList.add("active");
  2370. } else {
  2371. sizeSelectDom.innerText = `${payload.size}`;
  2372. }
  2373. payload.bold
  2374. ? boldDom.classList.add("active")
  2375. : boldDom.classList.remove("active");
  2376. payload.italic
  2377. ? italicDom.classList.add("active")
  2378. : italicDom.classList.remove("active");
  2379. payload.underline
  2380. ? underlineDom.classList.add("active")
  2381. : underlineDom.classList.remove("active");
  2382. payload.strikeout
  2383. ? strikeoutDom.classList.add("active")
  2384. : strikeoutDom.classList.remove("active");
  2385. if (payload.color) {
  2386. colorDom.classList.add("active");
  2387. const colorSpan = colorDom.querySelector("span");
  2388. if (colorSpan) {
  2389. colorSpan.style.backgroundColor = payload.color;
  2390. }
  2391. } else {
  2392. colorDom.classList.remove("active");
  2393. const colorSpan = colorDom.querySelector("span");
  2394. if (colorSpan) {
  2395. colorSpan.style.backgroundColor = "#000000";
  2396. }
  2397. }
  2398. if (payload.highlight) {
  2399. highlightDom.classList.add("active");
  2400. } else {
  2401. highlightDom.classList.remove("active");
  2402. }
  2403. // 行布局
  2404. leftDom.classList.remove("active");
  2405. centerDom.classList.remove("active");
  2406. rightDom.classList.remove("active");
  2407. alignmentDom.classList.remove("active");
  2408. if (payload.rowFlex && payload.rowFlex === "right") {
  2409. rightDom.classList.add("active");
  2410. } else if (payload.rowFlex && payload.rowFlex === "center") {
  2411. centerDom.classList.add("active");
  2412. } else if (payload.rowFlex && payload.rowFlex === "alignment") {
  2413. alignmentDom.classList.add("active");
  2414. } else {
  2415. leftDom.classList.add("active");
  2416. }
  2417. // 行间距
  2418. rowOptionDom
  2419. .querySelectorAll("li")
  2420. .forEach((li) => li.classList.remove("active"));
  2421. const curRowMarginDom = rowOptionDom.querySelector(
  2422. `[data-rowmargin='${payload.rowMargin}']`
  2423. );
  2424. curRowMarginDom.classList.add("active");
  2425. // 功能
  2426. payload.undo
  2427. ? undoDom.classList.remove("no-allow")
  2428. : undoDom.classList.add("no-allow");
  2429. payload.redo
  2430. ? redoDom.classList.remove("no-allow")
  2431. : redoDom.classList.add("no-allow");
  2432. payload.painter
  2433. ? painterDom.classList.add("active")
  2434. : painterDom.classList.remove("active");
  2435. // 标题
  2436. titleOptionDom
  2437. .querySelectorAll("li")
  2438. .forEach((li) => li.classList.remove("active"));
  2439. if (payload.level) {
  2440. const curTitleDom = titleOptionDom.querySelector(
  2441. `[data-level='${payload.level}']`
  2442. );
  2443. titleSelectDom.innerText = curTitleDom.innerText;
  2444. curTitleDom.classList.add("active");
  2445. } else {
  2446. titleSelectDom.innerText = "正文";
  2447. titleOptionDom.querySelector("li:first-child").classList.add("active");
  2448. }
  2449. // 列表
  2450. listOptionDom
  2451. .querySelectorAll("li")
  2452. .forEach((li) => li.classList.remove("active"));
  2453. if (payload.listType) {
  2454. listDom.classList.add("active");
  2455. const listType = payload.listType;
  2456. const listStyle =
  2457. payload.listType === ListType.OL
  2458. ? ListStyle.DECIMAL
  2459. : payload.listType;
  2460. const curListDom = listOptionDom.querySelector(
  2461. `[data-list-type='${listType}'][data-list-style='${listStyle}']`
  2462. );
  2463. if (curListDom) {
  2464. curListDom.classList.add("active");
  2465. }
  2466. } else {
  2467. listDom.classList.remove("active");
  2468. }
  2469. // 批注
  2470. // commentDom
  2471. // .querySelectorAll('.comment-item')
  2472. // .forEach(commentItemDom => {
  2473. // commentItemDom.classList.remove('active')
  2474. // })
  2475. // if (payload.groupIds) {
  2476. // const [id] = payload.groupIds
  2477. // const activeCommentDom = commentDom.querySelector(
  2478. // `.comment-item[data-id='${id}']`
  2479. // )
  2480. // if (activeCommentDom) {
  2481. // activeCommentDom.classList.add('active')
  2482. // scrollIntoView(commentDom, activeCommentDom)
  2483. // }
  2484. // }
  2485. };
  2486. instance.listener.visiblePageNoListChange = function (payload) {
  2487. const text = payload.map((i) => i + 1).join("、");
  2488. document.querySelector(".page-no-list").innerText = text;
  2489. };
  2490. instance.listener.pageSizeChange = function (payload) {
  2491. if (document.querySelector(".page-size")) {
  2492. document.querySelector(".page-size").innerText = payload.toString();
  2493. }
  2494. };
  2495. instance.listener.intersectionPageNoChange = function (payload) {
  2496. document.querySelector(".page-no").innerText = `${payload + 1}`;
  2497. };
  2498. instance.listener.pageScaleChange = function (payload) {
  2499. document.querySelector(
  2500. ".page-scale-percentage"
  2501. ).innerText = `${Math.floor(payload * 10 * 10)}%`;
  2502. };
  2503. instance.listener.controlChange = function (payload) {
  2504. const disableMenusInControlContext = [
  2505. "table",
  2506. "hyperlink",
  2507. "separator",
  2508. "page-break",
  2509. ];
  2510. // 菜单操作权限
  2511. disableMenusInControlContext.forEach((menu) => {
  2512. const menuDom = document.querySelector(`.menu-item__${menu}`);
  2513. payload
  2514. ? menuDom.classList.add("disable")
  2515. : menuDom.classList.remove("disable");
  2516. });
  2517. };
  2518. instance.listener.pageModeChange = function (payload) {
  2519. const activeMode = pageModeOptionsDom.querySelector(
  2520. `[data-page-mode='${payload}']`
  2521. );
  2522. pageModeOptionsDom
  2523. .querySelectorAll("li")
  2524. .forEach((li) => li.classList.remove("active"));
  2525. activeMode.classList.add("active");
  2526. };
  2527. const handleContentChange = async () => {
  2528. this.$emit("isSave", true);
  2529. // 字数
  2530. const wordCount = await instance.command.getWordCount();
  2531. document.querySelector(".word-count").innerText = `${wordCount || 0}`;
  2532. // 目录
  2533. // if (isCatalogShow) {
  2534. // this.$nextTick(() => {
  2535. // updateCatalog()
  2536. // })
  2537. // }
  2538. // // 批注
  2539. // this.$nextTick(() => {
  2540. // updateComment()
  2541. // })
  2542. };
  2543. instance.listener.contentChange = this.debounce(handleContentChange, 200);
  2544. handleContentChange();
  2545. // 9. 右键菜单注册
  2546. instance.register.contextMenuList([
  2547. {
  2548. name: "批注",
  2549. when: (payload) => {
  2550. return (
  2551. !payload.isReadonly &&
  2552. payload.editorHasSelection &&
  2553. payload.zone === EditorZone.MAIN
  2554. );
  2555. },
  2556. callback: (command) => {
  2557. new Dialog({
  2558. title: "批注",
  2559. data: [
  2560. {
  2561. type: "textarea",
  2562. label: "批注",
  2563. height: 100,
  2564. name: "value",
  2565. required: true,
  2566. placeholder: "请输入批注",
  2567. },
  2568. ],
  2569. onConfirm: (payload) => {
  2570. const value = payload.find((p) => p.name === "value")?.value;
  2571. if (!value) return;
  2572. const groupId = command.executeSetGroup();
  2573. if (!groupId) return;
  2574. commentList.push({
  2575. id: groupId,
  2576. content: value,
  2577. userName: "Hufe",
  2578. rangeText: command.getRangeText(),
  2579. createdDate: new Date().toLocaleString(),
  2580. });
  2581. },
  2582. });
  2583. },
  2584. },
  2585. {
  2586. name: "签名",
  2587. icon: "signature",
  2588. when: (payload) => {
  2589. return !payload.isReadonly && payload.editorTextFocus;
  2590. },
  2591. callback: (command) => {
  2592. new Signature({
  2593. onConfirm(payload) {
  2594. if (!payload) return;
  2595. const { value, width, height } = payload;
  2596. if (!value || !width || !height) return;
  2597. command.executeInsertElementList([
  2598. {
  2599. value,
  2600. width,
  2601. height,
  2602. type: ElementType.IMAGE,
  2603. },
  2604. ]);
  2605. },
  2606. });
  2607. },
  2608. },
  2609. {
  2610. name: "格式整理",
  2611. icon: "word-tool",
  2612. when: (payload) => {
  2613. return !payload.isReadonly;
  2614. },
  2615. callback: (command) => {
  2616. command.executeWordTool();
  2617. },
  2618. },
  2619. ]);
  2620. // 10. 快捷键注册
  2621. instance.register.shortcutList([
  2622. {
  2623. key: KeyMap.P,
  2624. mod: true,
  2625. isGlobal: true,
  2626. callback: (command) => {
  2627. command.executePrint();
  2628. },
  2629. },
  2630. {
  2631. key: KeyMap.F,
  2632. mod: true,
  2633. isGlobal: true,
  2634. callback: (command) => {
  2635. const text = command.getRangeText();
  2636. searchDom.click();
  2637. if (text) {
  2638. searchInputDom.value = text;
  2639. instance.command.executeSearch(text);
  2640. setSearchResult();
  2641. }
  2642. },
  2643. },
  2644. {
  2645. key: KeyMap.MINUS,
  2646. ctrl: true,
  2647. isGlobal: true,
  2648. callback: (command) => {
  2649. command.executePageScaleMinus();
  2650. },
  2651. },
  2652. {
  2653. key: KeyMap.EQUAL,
  2654. ctrl: true,
  2655. isGlobal: true,
  2656. callback: (command) => {
  2657. command.executePageScaleAdd();
  2658. },
  2659. },
  2660. {
  2661. key: KeyMap.ZERO,
  2662. ctrl: true,
  2663. isGlobal: true,
  2664. callback: (command) => {
  2665. command.executePageScaleRecovery();
  2666. },
  2667. },
  2668. ]);
  2669. /* // 设置初始样式
  2670. this.$nextTick(() => {
  2671. // 设置字体
  2672. instance.command.executeFont(this.editorOptions.defaultFont)
  2673. // 设置字号
  2674. instance.command.executeSize(this.editorOptions.defaultSize)
  2675. // 设置加粗
  2676. if (this.editorOptions.defaultBold) {
  2677. instance.command.executeBold()
  2678. }
  2679. // 设置对齐方式
  2680. instance.command.executeAlign(this.editorOptions.defaultAlign)
  2681. }) */
  2682. // 在编辑器实例化后立即应用默认样式
  2683. this.$nextTick(() => {
  2684. if (this.editorOptions.bold !== undefined) {
  2685. const shouldBeBold = this.editorOptions.bold !== "不加粗";
  2686. if (shouldBeBold) {
  2687. this.editorRef.command.executeBold();
  2688. }
  2689. }
  2690. });
  2691. // 使用setTimeout确保编辑器完全准备就绪
  2692. setTimeout(() => {
  2693. if (this.editorOptions.bold === "加粗") {
  2694. try {
  2695. // 1. 先全选所有内容
  2696. instance.command.executeSelectAll();
  2697. // 2. 执行加粗命令
  2698. instance.command.executeBold();
  2699. // 3. 取消选择
  2700. instance.command.executeSelectAll();
  2701. // 4. 将光标移到开始位置
  2702. instance.command.executeSelect(0, 0);
  2703. } catch (error) {
  2704. console.error('Apply bold failed:', error);
  2705. }
  2706. }
  2707. // 处理居中对齐
  2708. if (this.editorOptions.align === "居中") {
  2709. try {
  2710. // 1. 先全选所有内容
  2711. instance.command.executeSelectAll();
  2712. // 2. 执行居中命令
  2713. instance.command.executeRowFlex(RowFlex.CENTER);;
  2714. // 3. 取消选择
  2715. instance.command.executeSelectAll();
  2716. // 4. 将光标移到开始位置
  2717. instance.command.executeSelect(0, 0);
  2718. } catch (error) {
  2719. console.error('Apply center alignment failed:', error);
  2720. }
  2721. }
  2722. }, 500); // 延迟500ms执行
  2723. },
  2724. };
  2725. </script>
  2726. <style lang="scss" >
  2727. @import url("./components/dialog/dialog.css");
  2728. @import url("./components/signature/signature.css");
  2729. /* @import url("./styles.scss"); */
  2730. .canvas-editor-wrapper {
  2731. position: relative;
  2732. height: 100%; // 添加固定高度
  2733. overflow: hidden; // 防止内容溢出
  2734. .canvas-container {
  2735. .footer {
  2736. position: static;
  2737. }
  2738. .menu {
  2739. position: absolute;
  2740. top: 0;
  2741. left: 0;
  2742. }
  2743. .el-footer {
  2744. background-color: #f2f4f7;
  2745. text-align: center;
  2746. line-height: 30px !important; /* 调整footer高度 */
  2747. }
  2748. .content {
  2749. position: static;
  2750. display: flex;
  2751. flex-direction: column;
  2752. max-height: 600px; //calc(100% - 90px); /* 减去header和footer的高度 */
  2753. background-color: #f2f4f7;
  2754. overflow: auto; /* 仅让Main区域可滚动 */
  2755. flex-grow: 1; /* 让Main区域占据剩余空间 */
  2756. }
  2757. @media (max-width: 1220px) {
  2758. .menu {
  2759. display: flex;
  2760. align-items: center;
  2761. flex-wrap: wrap;
  2762. justify-content: flex-start;
  2763. }
  2764. }
  2765. #canvasEditor {
  2766. display: flex;
  2767. justify-content: center;
  2768. background: #f2f4f7;
  2769. }
  2770. .el-header {
  2771. position: relative;
  2772. }
  2773. .canvas-container {
  2774. height: 100%;
  2775. margin-top: 10px;
  2776. }
  2777. ::-webkit-scrollbar {
  2778. height: 16px;
  2779. width: 16px;
  2780. overflow: visible;
  2781. }
  2782. ::-webkit-scrollbar-button {
  2783. width: 0;
  2784. height: 0;
  2785. }
  2786. ::-webkit-scrollbar-corner {
  2787. background: transparent;
  2788. }
  2789. ::-webkit-scrollbar-thumb {
  2790. background-color: #ddd;
  2791. background-clip: padding-box;
  2792. border: 4px solid #f2f4f7;
  2793. border-radius: 8px;
  2794. min-height: 24px;
  2795. }
  2796. ::-webkit-scrollbar-thumb:hover {
  2797. background-color: #c9c9c9;
  2798. }
  2799. ::-webkit-scrollbar-track {
  2800. background: #f2f4f7;
  2801. background-clip: padding-box;
  2802. }
  2803. * {
  2804. margin: 0;
  2805. padding: 0;
  2806. }
  2807. ul {
  2808. list-style: none;
  2809. }
  2810. .menu {
  2811. width: 100%;
  2812. height: 60px;
  2813. top: 0;
  2814. z-index: 9;
  2815. position: absolute;
  2816. display: flex;
  2817. align-items: center;
  2818. justify-content: center;
  2819. background: #f2f4f7;
  2820. box-shadow: 0 2px 4px 0 transparent;
  2821. }
  2822. .menu-divider {
  2823. width: 1px;
  2824. height: 16px;
  2825. margin: 0 8px;
  2826. display: inline-block;
  2827. background-color: #cfd2d8;
  2828. }
  2829. .menu-item {
  2830. height: 24px;
  2831. display: flex;
  2832. align-items: center;
  2833. position: relative;
  2834. }
  2835. .menu-item > div {
  2836. width: 24px;
  2837. height: 24px;
  2838. cursor: pointer;
  2839. display: flex;
  2840. align-items: center;
  2841. justify-content: center;
  2842. margin: 0 2px;
  2843. }
  2844. .menu-item > div:hover {
  2845. background: rgba(25, 55, 88, 0.04);
  2846. }
  2847. .menu-item > div.active {
  2848. background: rgba(25, 55, 88, 0.08);
  2849. }
  2850. .menu-item i {
  2851. width: 16px;
  2852. height: 16px;
  2853. display: inline-block;
  2854. background-repeat: no-repeat;
  2855. background-size: 100% 100%;
  2856. }
  2857. .menu-item > div > span {
  2858. width: 16px;
  2859. height: 3px;
  2860. display: inline-block;
  2861. border: 1px solid #e2e6ed;
  2862. }
  2863. .menu-item .select {
  2864. border: none;
  2865. font-size: 12px;
  2866. line-height: 24px;
  2867. user-select: none;
  2868. }
  2869. .menu-item .select::after {
  2870. position: absolute;
  2871. content: "";
  2872. top: 11px;
  2873. width: 0;
  2874. height: 0;
  2875. right: 2px;
  2876. border-color: #767c85 transparent transparent;
  2877. border-style: solid solid none;
  2878. border-width: 3px 3px 0;
  2879. }
  2880. .menu-item .options {
  2881. width: 70px;
  2882. position: absolute;
  2883. //left: 0;
  2884. top: 25px;
  2885. padding: 10px;
  2886. background: #fff;
  2887. font-size: 14px;
  2888. box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
  2889. border: 1px solid #e2e6ed;
  2890. border-radius: 2px;
  2891. display: none;
  2892. }
  2893. .menu-item .options.visible {
  2894. display: block;
  2895. }
  2896. .menu-item .options li {
  2897. padding: 5px;
  2898. margin: 5px 0;
  2899. user-select: none;
  2900. transition: all 0.3s;
  2901. }
  2902. .menu-item .options li:hover {
  2903. background-color: #ebecef;
  2904. }
  2905. .menu-item .options li.active {
  2906. background-color: #e2e6ed;
  2907. }
  2908. .menu-item .menu-item__font {
  2909. width: 65px;
  2910. position: relative;
  2911. }
  2912. .menu-item .menu-item__size {
  2913. width: 50px;
  2914. text-align: center;
  2915. position: relative;
  2916. }
  2917. .menu-item__font .select,
  2918. .menu-item__size .select {
  2919. width: 100%;
  2920. height: 100%;
  2921. }
  2922. .menu-item__undo.no-allow,
  2923. .menu-item__redo.no-allow,
  2924. .menu-item > div.disable {
  2925. color: #c0c4cc;
  2926. cursor: not-allowed;
  2927. opacity: 0.4;
  2928. pointer-events: none;
  2929. }
  2930. .menu-item__undo i {
  2931. background-image: url("./assets/images/undo.svg");
  2932. }
  2933. .menu-item__redo i {
  2934. background-image: url("./assets/images/redo.svg");
  2935. }
  2936. .menu-item__painter i {
  2937. background-image: url("./assets/images/painter.svg");
  2938. }
  2939. .menu-item__format i {
  2940. background-image: url("./assets/images/format.svg");
  2941. }
  2942. .menu-item__size-add i {
  2943. background-image: url("./assets/images/size-add.svg");
  2944. }
  2945. .menu-item__size-minus i {
  2946. background-image: url("./assets/images/size-minus.svg");
  2947. }
  2948. .menu-item__bold i {
  2949. background-image: url("./assets/images/bold.svg");
  2950. }
  2951. .menu-item__italic i {
  2952. background-image: url("./assets/images/italic.svg");
  2953. }
  2954. .menu-item .menu-item__underline {
  2955. width: 30px;
  2956. position: relative;
  2957. }
  2958. .menu-item__underline > i {
  2959. flex-shrink: 0;
  2960. background-image: url("./assets/images/underline.svg");
  2961. }
  2962. .menu-item__underline .select {
  2963. width: 100%;
  2964. height: 100%;
  2965. }
  2966. .menu-item .menu-item__underline .options {
  2967. width: 128px;
  2968. }
  2969. .menu-item .menu-item__underline li {
  2970. padding: 1px 5px;
  2971. }
  2972. .menu-item__underline li i {
  2973. pointer-events: none;
  2974. }
  2975. .menu-item__underline li[data-decoration-style="solid"] {
  2976. background-image: url("./assets/images/line-single.svg");
  2977. }
  2978. .menu-item__underline li[data-decoration-style="double"] {
  2979. background-image: url("./assets/images/line-double.svg");
  2980. }
  2981. .menu-item__underline li[data-decoration-style="dashed"] {
  2982. background-image: url("./assets/images/line-dash-small-gap.svg");
  2983. }
  2984. .menu-item__underline li[data-decoration-style="dotted"] {
  2985. background-image: url("./assets/images/line-dot.svg");
  2986. }
  2987. .menu-item__underline li[data-decoration-style="wavy"] {
  2988. background-image: url("./assets/images/line-wavy.svg");
  2989. }
  2990. .menu-item__strikeout i {
  2991. background-image: url("./assets/images/strikeout.svg");
  2992. }
  2993. .menu-item__superscript i {
  2994. background-image: url("./assets/images/superscript.svg");
  2995. }
  2996. .menu-item__subscript i {
  2997. background-image: url("./assets/images/subscript.svg");
  2998. }
  2999. .menu-item__color,
  3000. .menu-item__highlight {
  3001. display: flex;
  3002. flex-direction: column;
  3003. }
  3004. .menu-item__color #color,
  3005. .menu-item__highlight #highlight {
  3006. width: 1px;
  3007. height: 1px;
  3008. visibility: hidden;
  3009. outline: none;
  3010. appearance: none;
  3011. }
  3012. .menu-item__color i {
  3013. background-image: url("./assets/images/color.svg");
  3014. }
  3015. .menu-item__color span {
  3016. background-color: #000000;
  3017. }
  3018. .menu-item__highlight i {
  3019. background-image: url("./assets/images/highlight.svg");
  3020. }
  3021. .menu-item__highlight span {
  3022. background-color: #ffff00;
  3023. }
  3024. .menu-item .menu-item__title {
  3025. width: 60px;
  3026. position: relative;
  3027. }
  3028. .menu-item__title .select {
  3029. width: calc(100% - 20px);
  3030. height: 100%;
  3031. }
  3032. .menu-item__title i {
  3033. transform: translateX(-5px);
  3034. background-image: url("./assets/images/title.svg");
  3035. }
  3036. .menu-item__title .options {
  3037. width: 80px;
  3038. }
  3039. .menu-item__left i {
  3040. background-image: url("./assets/images/left.svg");
  3041. }
  3042. .menu-item__center i {
  3043. background-image: url("./assets/images/center.svg");
  3044. }
  3045. .menu-item__right i {
  3046. background-image: url("./assets/images/right.svg");
  3047. }
  3048. .menu-item__alignment i {
  3049. background-image: url("./assets/images/alignment.svg");
  3050. }
  3051. .menu-item__justify i {
  3052. background-image: url("./assets/images/justify.svg");
  3053. }
  3054. .menu-item__row-margin {
  3055. position: relative;
  3056. }
  3057. .menu-item__row-margin i {
  3058. background-image: url("./assets/images/row-margin.svg");
  3059. }
  3060. .menu-item__list {
  3061. position: relative;
  3062. }
  3063. .menu-item__list i {
  3064. background-image: url("./assets/images/list.svg");
  3065. }
  3066. .menu-item__list .options {
  3067. width: 110px;
  3068. }
  3069. .menu-item__list .options > ul > li * {
  3070. pointer-events: none;
  3071. }
  3072. .menu-item__list .options > ul > li li {
  3073. margin-left: 18px;
  3074. }
  3075. .menu-item__list .options > ul > li[data-list-style="checkbox"] li::marker {
  3076. font-size: 11px;
  3077. }
  3078. .menu-item__image i {
  3079. background-image: url("./assets/images/image.svg");
  3080. }
  3081. .menu-item__image input {
  3082. display: none;
  3083. }
  3084. .menu-item__table {
  3085. position: relative;
  3086. }
  3087. .menu-item__table i {
  3088. background-image: url("./assets/images/table.svg");
  3089. }
  3090. .menu-item .menu-item__table__collapse {
  3091. width: 270px;
  3092. height: 310px;
  3093. background: #fff;
  3094. box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
  3095. border: 1px solid #e2e6ed;
  3096. box-sizing: border-box;
  3097. border-radius: 2px;
  3098. position: absolute;
  3099. display: none;
  3100. z-index: 99;
  3101. top: 25px;
  3102. left: 0;
  3103. padding: 14px 27px;
  3104. cursor: auto;
  3105. }
  3106. .menu-item .menu-item__table__collapse .table-close {
  3107. position: absolute;
  3108. right: 10px;
  3109. top: 5px;
  3110. cursor: pointer;
  3111. }
  3112. .menu-item .menu-item__table__collapse .table-close:hover {
  3113. color: #7d7e80;
  3114. }
  3115. .menu-item .menu-item__table__collapse:hover {
  3116. background: #fff;
  3117. }
  3118. .menu-item .menu-item__table__collapse .table-title {
  3119. display: flex;
  3120. justify-content: flex-start;
  3121. padding-bottom: 5px;
  3122. border-bottom: 1px solid #e2e6ed;
  3123. }
  3124. .table-title span {
  3125. font-size: 12px;
  3126. color: #3d4757;
  3127. display: inline;
  3128. margin: 0;
  3129. }
  3130. .table-panel {
  3131. cursor: pointer;
  3132. }
  3133. .table-panel .table-row {
  3134. display: flex;
  3135. flex-wrap: nowrap;
  3136. margin-top: 10px;
  3137. pointer-events: none;
  3138. }
  3139. .table-panel .table-cel {
  3140. width: 16px;
  3141. height: 16px;
  3142. box-sizing: border-box;
  3143. border: 1px solid #e2e6ed;
  3144. background: #fff;
  3145. position: relative;
  3146. margin-right: 6px;
  3147. pointer-events: none;
  3148. }
  3149. .table-panel .table-cel.active {
  3150. border: 1px solid rgba(73, 145, 242, 0.2);
  3151. background: rgba(73, 145, 242, 0.15);
  3152. }
  3153. .table-panel .table-row .table-cel:last-child {
  3154. margin-right: 0;
  3155. }
  3156. .menu-item__hyperlink i {
  3157. background-image: url("./assets/images/hyperlink.svg");
  3158. }
  3159. .menu-item__separator {
  3160. position: relative;
  3161. }
  3162. .menu-item__separator > i {
  3163. background-image: url("./assets/images/separator.svg");
  3164. }
  3165. .menu-item .menu-item__separator .options {
  3166. width: 128px;
  3167. }
  3168. .menu-item .menu-item__separator li {
  3169. padding: 1px 5px;
  3170. }
  3171. .menu-item__separator li i {
  3172. pointer-events: none;
  3173. }
  3174. .menu-item__separator li[data-separator="0,0"] {
  3175. background-image: url("./assets/images/line-single.svg");
  3176. }
  3177. .menu-item__separator li[data-separator="1,1"] {
  3178. background-image: url("./assets/images/line-dot.svg");
  3179. }
  3180. .menu-item__separator li[data-separator="3,1"] {
  3181. background-image: url("./assets/images/line-dash-small-gap.svg");
  3182. }
  3183. .menu-item__separator li[data-separator="4,4"] {
  3184. background-image: url("./assets/images/line-dash-large-gap.svg");
  3185. }
  3186. .menu-item__separator li[data-separator="7,3,3,3"] {
  3187. background-image: url("./assets/images/line-dash-dot.svg");
  3188. }
  3189. .menu-item__separator li[data-separator="6,2,2,2,2,2"] {
  3190. background-image: url("./assets/images/line-dash-dot-dot.svg");
  3191. }
  3192. .menu-item__watermark > i {
  3193. background-image: url("./assets/images/watermark.svg");
  3194. }
  3195. .menu-item__watermark {
  3196. position: relative;
  3197. }
  3198. .menu-item__codeblock i {
  3199. background-image: url("./assets/images/codeblock.svg");
  3200. }
  3201. .menu-item__page-break i {
  3202. background-image: url("./assets/images/page-break.svg");
  3203. }
  3204. .menu-item__control {
  3205. position: relative;
  3206. }
  3207. .menu-item__control i {
  3208. background-image: url("./assets/images/control.svg");
  3209. }
  3210. .menu-item__checkbox i {
  3211. background-image: url("./assets/images/checkbox.svg");
  3212. }
  3213. .menu-item__radio i {
  3214. background-image: url("./assets/images/radio.svg");
  3215. }
  3216. .menu-item__latex i {
  3217. background-image: url("./assets/images/latex.svg");
  3218. }
  3219. .menu-item__date {
  3220. position: relative;
  3221. }
  3222. .menu-item__date i {
  3223. background-image: url("./assets/images/date.svg");
  3224. }
  3225. .menu-item__date .options {
  3226. width: 160px;
  3227. }
  3228. .menu-item__block i {
  3229. background-image: url("./assets/images/block.svg");
  3230. }
  3231. .menu-item .menu-item__control .options {
  3232. width: 55px;
  3233. }
  3234. .menu-item__search {
  3235. position: relative;
  3236. }
  3237. .menu-item__search i {
  3238. background-image: url("./assets/images/search.svg");
  3239. }
  3240. .menu-item .menu-item__search__collapse {
  3241. width: 260px;
  3242. height: 72px;
  3243. box-sizing: border-box;
  3244. position: absolute;
  3245. display: none;
  3246. z-index: 99;
  3247. top: 25px;
  3248. left: 0;
  3249. background: #ffffff;
  3250. box-shadow: 0px 5px 5px #e3dfdf;
  3251. }
  3252. .menu-item .menu-item__search__collapse:hover {
  3253. background: #ffffff;
  3254. }
  3255. .menu-item .menu-item__search__collapse > div {
  3256. width: 250px;
  3257. height: 36px;
  3258. padding: 0 5px;
  3259. line-height: 36px;
  3260. display: flex;
  3261. align-items: center;
  3262. justify-content: space-between;
  3263. border-radius: 4px;
  3264. }
  3265. .menu-item .menu-item__search__collapse > div input {
  3266. width: 205px;
  3267. height: 27px;
  3268. appearance: none;
  3269. background-color: #fff;
  3270. background-image: none;
  3271. border-radius: 4px;
  3272. border: 1px solid #ebebeb;
  3273. box-sizing: border-box;
  3274. color: #606266;
  3275. display: inline-block;
  3276. line-height: 27px;
  3277. outline: none;
  3278. padding: 0 5px;
  3279. }
  3280. .menu-item .menu-item__search__collapse > div span {
  3281. height: 100%;
  3282. color: #dcdfe6;
  3283. font-size: 25px;
  3284. display: inline-block;
  3285. border: 0;
  3286. padding: 0 10px;
  3287. }
  3288. .menu-item .menu-item__search__collapse__replace button {
  3289. display: inline-block;
  3290. border: 1px solid #e2e6ed;
  3291. border-radius: 2px;
  3292. background: #fff;
  3293. line-height: 22px;
  3294. padding: 0 6px;
  3295. white-space: nowrap;
  3296. margin-left: 4px;
  3297. cursor: pointer;
  3298. font-size: 12px;
  3299. }
  3300. .menu-item .menu-item__search__collapse__replace button:hover {
  3301. background: rgba(25, 55, 88, 0.04);
  3302. }
  3303. .menu-item .menu-item__search__collapse__search {
  3304. position: relative;
  3305. }
  3306. .menu-item .menu-item__search__collapse__search label {
  3307. right: 110px;
  3308. font-size: 12px;
  3309. color: #3d4757;
  3310. position: absolute;
  3311. }
  3312. .menu-item .menu-item__search__collapse__search > input {
  3313. padding: 5px 90px 5px 5px !important;
  3314. }
  3315. .menu-item .menu-item__search__collapse__search > div {
  3316. width: 28px;
  3317. height: 27px;
  3318. display: flex;
  3319. justify-content: center;
  3320. align-items: center;
  3321. position: absolute;
  3322. border-left: 1px solid #e2e6ed;
  3323. transition: all 0.5s;
  3324. }
  3325. .menu-item .menu-item__search__collapse__search > div:hover {
  3326. background-color: rgba(25, 55, 88, 0.04);
  3327. }
  3328. .menu-item .menu-item__search__collapse__search i {
  3329. width: 6px;
  3330. height: 8px;
  3331. transform: translateY(1px);
  3332. }
  3333. .menu-item .menu-item__search__collapse__search .arrow-left {
  3334. right: 76px;
  3335. }
  3336. .menu-item .menu-item__search__collapse__search .arrow-left i {
  3337. background: url(./assets/images/arrow-left.svg) no-repeat;
  3338. }
  3339. .menu-item .menu-item__search__collapse__search .arrow-right {
  3340. right: 48px;
  3341. }
  3342. .menu-item .menu-item__search__collapse__search .arrow-right i {
  3343. background: url(./assets/images/arrow-right.svg) no-repeat;
  3344. }
  3345. .menu-item__print i {
  3346. background-image: url("./assets/images/print.svg");
  3347. }
  3348. .catalog {
  3349. width: 250px;
  3350. position: absolute;
  3351. left: 0;
  3352. bottom: 0;
  3353. top: 100px;
  3354. padding: 0 20px 40px 20px;
  3355. }
  3356. .catalog .catalog__header {
  3357. height: 48px;
  3358. display: flex;
  3359. align-items: center;
  3360. justify-content: space-between;
  3361. border-bottom: 1px solid #e2e6ed;
  3362. }
  3363. .catalog .catalog__header span {
  3364. color: #3d4757;
  3365. font-size: 14px;
  3366. font-weight: bold;
  3367. }
  3368. .catalog .catalog__header i {
  3369. width: 16px;
  3370. height: 16px;
  3371. cursor: pointer;
  3372. display: inline-block;
  3373. background: url(./assets/images/close.svg) no-repeat;
  3374. transition: all 0.2s;
  3375. }
  3376. .catalog .catalog__header > div:hover {
  3377. background: rgba(235, 238, 241);
  3378. }
  3379. .catalog__main {
  3380. height: calc(100% - 60px);
  3381. padding: 10px 0;
  3382. overflow-y: auto;
  3383. overflow-x: hidden;
  3384. }
  3385. .catalog__main .catalog-item {
  3386. width: 100%;
  3387. padding-left: 10px;
  3388. box-sizing: border-box;
  3389. }
  3390. .catalog__main > .catalog-item {
  3391. padding-left: 0;
  3392. }
  3393. .catalog__main .catalog-item .catalog-item__content {
  3394. width: 100%;
  3395. overflow: hidden;
  3396. white-space: nowrap;
  3397. text-overflow: ellipsis;
  3398. }
  3399. .catalog__main .catalog-item .catalog-item__content:hover > span {
  3400. color: #4991f2;
  3401. }
  3402. .catalog__main .catalog-item .catalog-item__content span {
  3403. color: #3d4757;
  3404. line-height: 30px;
  3405. font-size: 12px;
  3406. white-space: nowrap;
  3407. cursor: pointer;
  3408. user-select: none;
  3409. }
  3410. .editor > div {
  3411. margin: 80px auto;
  3412. }
  3413. .ce-page-container canvas {
  3414. box-shadow: rgb(158 161 165 / 40%) 0px 2px 12px 0px;
  3415. }
  3416. .comment {
  3417. width: 250px;
  3418. height: 650px;
  3419. position: absolute;
  3420. transform: translateX(420px);
  3421. top: 200px;
  3422. left: 50%;
  3423. overflow-y: auto;
  3424. }
  3425. .comment-item {
  3426. background: #ffffff;
  3427. border: 1px solid #e2e6ed;
  3428. position: relative;
  3429. border-radius: 8px;
  3430. padding: 15px;
  3431. font-size: 14px;
  3432. margin-bottom: 20px;
  3433. cursor: pointer;
  3434. transition: all 0.5s;
  3435. }
  3436. .comment-item:hover {
  3437. border-color: #c0c6cf;
  3438. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  3439. }
  3440. .comment-item.active {
  3441. border-color: #e99d00;
  3442. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  3443. }
  3444. .comment-item__title {
  3445. height: 22px;
  3446. position: relative;
  3447. display: flex;
  3448. align-items: center;
  3449. color: #c1c6ce;
  3450. }
  3451. .comment-item__title span:first-child {
  3452. background-color: #dbdbdb;
  3453. width: 4px;
  3454. height: 16px;
  3455. margin-right: 5px;
  3456. display: inline-block;
  3457. border-radius: 999px;
  3458. }
  3459. .comment-item__title span:nth-child(2) {
  3460. width: 200px;
  3461. overflow: hidden;
  3462. white-space: nowrap;
  3463. text-overflow: ellipsis;
  3464. }
  3465. .comment-item__title i {
  3466. width: 16px;
  3467. height: 16px;
  3468. cursor: pointer;
  3469. position: absolute;
  3470. right: -8px;
  3471. top: -8px;
  3472. background: url(./assets/images/close.svg) no-repeat;
  3473. }
  3474. .comment-item__title i:hover {
  3475. opacity: 0.6;
  3476. }
  3477. .comment-item__info {
  3478. height: 28px;
  3479. display: flex;
  3480. align-items: center;
  3481. justify-content: space-between;
  3482. }
  3483. .comment-item__info > span:first-child {
  3484. font-weight: 600;
  3485. }
  3486. .comment-item__info > span:last-child {
  3487. color: #c1c6ce;
  3488. }
  3489. .comment-item__content {
  3490. line-height: 22px;
  3491. }
  3492. .footer {
  3493. width: 100%;
  3494. height: 30px;
  3495. display: flex;
  3496. align-items: center;
  3497. justify-content: space-between;
  3498. background: #f2f4f7;
  3499. z-index: 9;
  3500. position: absolute;
  3501. bottom: 0;
  3502. left: 0;
  3503. font-size: 12px;
  3504. padding: 0 4px 0 20px;
  3505. box-sizing: border-box;
  3506. }
  3507. .footer > div:first-child {
  3508. display: flex;
  3509. align-items: center;
  3510. }
  3511. .footer .catalog-mode {
  3512. padding: 1px;
  3513. position: relative;
  3514. }
  3515. .footer .catalog-mode i {
  3516. width: 16px;
  3517. height: 16px;
  3518. margin-right: 5px;
  3519. cursor: pointer;
  3520. display: inline-block;
  3521. background-image: url("./assets/images/catalog.svg");
  3522. }
  3523. .footer .page-mode {
  3524. padding: 1px;
  3525. position: relative;
  3526. }
  3527. .footer .page-mode i {
  3528. width: 16px;
  3529. height: 16px;
  3530. margin-right: 5px;
  3531. cursor: pointer;
  3532. display: inline-block;
  3533. background-image: url("./assets/images/page-mode.svg");
  3534. }
  3535. .footer .options {
  3536. width: 70px;
  3537. position: absolute;
  3538. left: 0;
  3539. bottom: 25px;
  3540. padding: 10px;
  3541. background: #fff;
  3542. font-size: 14px;
  3543. box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
  3544. border: 1px solid #e2e6ed;
  3545. border-radius: 2px;
  3546. display: none;
  3547. }
  3548. .footer .options.visible {
  3549. display: block;
  3550. }
  3551. .footer .options li {
  3552. padding: 5px;
  3553. margin: 5px 0;
  3554. user-select: none;
  3555. transition: all 0.3s;
  3556. text-align: center;
  3557. cursor: pointer;
  3558. }
  3559. .footer .options li:hover {
  3560. background-color: #ebecef;
  3561. }
  3562. .footer .options li.active {
  3563. background-color: #e2e6ed;
  3564. }
  3565. .footer > div:first-child > span {
  3566. display: inline-block;
  3567. margin-right: 5px;
  3568. letter-spacing: 1px;
  3569. }
  3570. .footer > div:last-child {
  3571. display: flex;
  3572. align-items: center;
  3573. justify-content: space-between;
  3574. }
  3575. .footer > div:last-child > div {
  3576. width: 24px;
  3577. height: 24px;
  3578. display: flex;
  3579. align-items: center;
  3580. justify-content: center;
  3581. }
  3582. .footer > div:last-child > div:hover {
  3583. background: rgba(25, 55, 88, 0.04);
  3584. }
  3585. .footer > div:last-child i {
  3586. width: 16px;
  3587. height: 16px;
  3588. display: inline-block;
  3589. cursor: pointer;
  3590. }
  3591. .footer .editor-option i {
  3592. background-image: url("./assets/images/option.svg");
  3593. }
  3594. .footer .page-scale-minus i {
  3595. background-image: url("./assets/images/page-scale-minus.svg");
  3596. }
  3597. .footer .page-scale-add i {
  3598. background-image: url("./assets/images/page-scale-add.svg");
  3599. }
  3600. .footer .page-scale-percentage {
  3601. cursor: pointer;
  3602. user-select: none;
  3603. }
  3604. .footer .fullscreen i {
  3605. background-image: url("./assets/images/request-fullscreen.svg");
  3606. }
  3607. .footer .fullscreen.exist i {
  3608. background-image: url("./assets/images/exit-fullscreen.svg");
  3609. }
  3610. .footer .paper-margin i {
  3611. background-image: url("./assets/images/paper-margin.svg");
  3612. }
  3613. .footer .editor-mode {
  3614. cursor: pointer;
  3615. user-select: none;
  3616. }
  3617. .footer .paper-size {
  3618. position: relative;
  3619. }
  3620. .footer .paper-size i {
  3621. background-image: url("./assets/images/paper-size.svg");
  3622. }
  3623. .footer .paper-size .options {
  3624. right: 0;
  3625. left: unset;
  3626. }
  3627. .footer .paper-direction {
  3628. position: relative;
  3629. }
  3630. .footer .paper-direction i {
  3631. background-image: url("./assets/images/paper-direction.svg");
  3632. }
  3633. .footer .paper-direction .options {
  3634. right: 0;
  3635. left: unset;
  3636. }
  3637. .ce-contextmenu-signature {
  3638. background-image: url("./assets/images/signature.svg");
  3639. }
  3640. .ce-contextmenu-word-tool {
  3641. background-image: url("./assets/images/word-tool.svg");
  3642. }
  3643. .menu-item__color .options {
  3644. width: 220px;
  3645. padding: 8px;
  3646. }
  3647. .menu-item__color .color-section {
  3648. margin-bottom: 10px;
  3649. }
  3650. .menu-item__color .section-title {
  3651. font-size: 12px;
  3652. color: #666;
  3653. margin-bottom: 5px;
  3654. padding-left: 2px;
  3655. }
  3656. .menu-item__color .options ul {
  3657. display: flex;
  3658. flex-wrap: wrap;
  3659. gap: 2px;
  3660. }
  3661. .menu-item__color .options li {
  3662. width: 18px;
  3663. height: 18px;
  3664. border: 1px solid #ddd;
  3665. cursor: pointer;
  3666. margin: 0;
  3667. position: relative;
  3668. }
  3669. .menu-item__color .options li:hover {
  3670. border-color: #000;
  3671. }
  3672. .menu-item__color .options li[data-color="#FFFFFF"] {
  3673. border: 1px solid #ddd;
  3674. }
  3675. // 添加悬浮提示样式
  3676. .menu-item__color .options li:hover::after {
  3677. content: attr(title);
  3678. position: absolute;
  3679. bottom: 100%;
  3680. left: 50%;
  3681. transform: translateX(-50%);
  3682. padding: 4px 8px;
  3683. background-color: #333;
  3684. color: white;
  3685. font-size: 12px;
  3686. border-radius: 4px;
  3687. white-space: nowrap;
  3688. z-index: 1000;
  3689. }
  3690. .menu-item__highlight .options {
  3691. width: 180px;
  3692. padding: 5px;
  3693. }
  3694. .menu-item__highlight .options ul {
  3695. display: grid;
  3696. grid-template-columns: repeat(6, 1fr);
  3697. gap: 2px;
  3698. }
  3699. .menu-item__highlight .options li {
  3700. width: 24px;
  3701. height: 24px;
  3702. border: 1px solid #ddd;
  3703. cursor: pointer;
  3704. margin: 0;
  3705. position: relative;
  3706. display: flex;
  3707. align-items: center;
  3708. justify-content: center;
  3709. color: #666;
  3710. }
  3711. /* 特殊处理"无"选项 */
  3712. .menu-item__highlight .options li[data-color="transparent"] {
  3713. background: white;
  3714. font-size: 16px;
  3715. }
  3716. .menu-item__highlight .options li:hover {
  3717. border-color: #000;
  3718. }
  3719. .menu-item__highlight .options li:hover::after {
  3720. content: attr(title);
  3721. position: absolute;
  3722. bottom: 100%;
  3723. left: 50%;
  3724. transform: translateX(-50%);
  3725. padding: 4px 8px;
  3726. background-color: #333;
  3727. color: white;
  3728. font-size: 12px;
  3729. border-radius: 4px;
  3730. white-space: nowrap;
  3731. z-index: 1000;
  3732. }
  3733. }
  3734. }
  3735. </style>