sheet.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. <script setup lang='ts'>
  2. import { PropType,computed,ref,nextTick } from 'vue';
  3. import { isMobile } from '@/utils/utils'
  4. import { useRoute } from 'vue-router';
  5. const route = useRoute();
  6. const props = defineProps({
  7. data: {
  8. type: Array as PropType<any[]>,
  9. required: true
  10. },
  11. config: {
  12. type: Object
  13. }
  14. })
  15. //手机端pc端又要不同样式
  16. const dynamicSty = computed(()=>{
  17. return isMobile() ? 'mobile-sty' : 'pc-sty';
  18. })
  19. const HtObj=ref({
  20. 0:"center",
  21. 1:'left',
  22. 2:'right'
  23. })
  24. // 默认取消鼠标右键操作
  25. const preventRight=()=>{
  26. document.addEventListener('contextmenu', function(event) {
  27. event.preventDefault();
  28. });
  29. }
  30. preventRight()
  31. const moveVal=ref(6)
  32. /* 右键 */
  33. const activeNames=ref('')
  34. const sizeOptions=ref([
  35. { label:'5',val:'5' },
  36. { label:'6',val:'6' },
  37. { label:'7',val:'7' },
  38. { label:'8',val:'8' },
  39. { label:'9',val:'9' },
  40. { label:'10',val:'10' },
  41. { label:'11',val:'11' },
  42. { label:'12',val:'12' },
  43. { label:'13',val:'13' },
  44. { label:'14',val:'14' },
  45. { label:'15',val:'15' },
  46. { label:'16',val:'16' },
  47. { label:'17',val:'17' },
  48. { label:'18',val:'18' },
  49. { label:'20',val:'20' },
  50. { label:'24',val:'24' },
  51. { label:'28',val:'28' },
  52. { label:'32',val:'32' },
  53. { label:'36',val:'36' },
  54. { label:'40',val:'40' },
  55. ])
  56. const contextMenuRef=ref(null)
  57. function rightClickHandle(e,cell,rowIndex,colIndex) {
  58. if(route.query.type==2){
  59. return
  60. }
  61. if(clickStatus.value || !selectionStart.value || !selectionEnd.value) return
  62. const start = {
  63. row: Math.min(selectionStart.value.row, selectionEnd.value.row),
  64. col: Math.min(selectionStart.value.col, selectionEnd.value.col)
  65. };
  66. const end = {
  67. row: Math.max(selectionStart.value.row, selectionEnd.value.row),
  68. col: Math.max(selectionStart.value.col, selectionEnd.value.col)
  69. };
  70. if(rowIndex >= start.row && rowIndex <= end.row && colIndex >= start.col && colIndex <= end.col){
  71. nextTick(() => {
  72. let dom = contextMenuRef.value;
  73. console.log(dom)
  74. console.log(e.clientX,e)
  75. if(e.clientY > window.innerHeight/2) {
  76. dom.style.left = e.clientX-3 + 'px';
  77. dom.style.top = e.clientY-dom.offsetHeight-3 + 'px';
  78. }else {
  79. dom.style.left = e.clientX-3 + 'px';
  80. dom.style.top = e.clientY-3 + 'px';
  81. }
  82. if(e.clientX > window.innerWidth/2){
  83. dom.style.left = e.clientX-dom.offsetWidth-3 + 'px';
  84. }
  85. })
  86. }
  87. }
  88. function hideContextMenu() {
  89. const dom = contextMenuRef.value;
  90. dom.style.left = '-9999px';
  91. dom.style.top = '-9999px';
  92. }
  93. function chooseCellSize(val){
  94. let indexArr = getCellIndices(selectionStart.value.row,selectionEnd.value.row,selectionStart.value.col,selectionEnd.value.col);
  95. console.log(indexArr)
  96. indexArr.forEach(el=>{
  97. props.data[el.row][el.column].fs=val
  98. })
  99. resetSelect()
  100. hideContextMenu()
  101. }
  102. // 获取区间所有坐标
  103. function getCellIndices(startRow, endRow, startCol, endCol) {
  104. const indices = [];
  105. for (let i = startRow; i <= endRow; i++) {
  106. for (let j = startCol; j <= endCol; j++) {
  107. indices.push({ row: i, column: j });
  108. }
  109. }
  110. return indices;
  111. }
  112. // 选区选择
  113. const selectionStart=ref(null)
  114. const selectionEnd=ref(null)
  115. const clickStatus=ref(false)
  116. function isCellSelected(row, col) {
  117. if (!selectionStart.value || !selectionEnd.value) {
  118. return false;
  119. }
  120. const start = {
  121. row: Math.min(selectionStart.value.row, selectionEnd.value.row),
  122. col: Math.min(selectionStart.value.col, selectionEnd.value.col)
  123. };
  124. const end = {
  125. row: Math.max(selectionStart.value.row, selectionEnd.value.row),
  126. col: Math.max(selectionStart.value.col, selectionEnd.value.col)
  127. };
  128. return (
  129. row >= start.row && row <= end.row && col >= start.col && col <= end.col
  130. );
  131. }
  132. function updateSelection(event,endRow, endCol) {
  133. if(event.button!=0){
  134. return
  135. }
  136. if (selectionStart.value&&clickStatus.value) {
  137. selectionEnd.value = { row: endRow, col: endCol };
  138. }
  139. }
  140. function endSelection(event,endRow, endCol) {
  141. if(event.button!=0){
  142. return
  143. }
  144. if (selectionStart.value&&clickStatus.value) {
  145. selectionEnd.value = { row: endRow, col: endCol };
  146. clickStatus.value=false
  147. }
  148. }
  149. //列宽行高
  150. const getSize=(cell,index,key)=>{
  151. if(key=='width'){
  152. return columnWidths.value[index]?columnWidths.value[index]+'px':''
  153. }else{
  154. return rowHeights.value[index]?rowHeights.value[index]+'px':'20px'
  155. }
  156. }
  157. const tableRef=ref(null)
  158. const dragging=ref(false)
  159. const dragState=ref({})
  160. const columnWidths=ref([])
  161. const draggingHeight=ref(false)
  162. const dragStateHeight=ref({})
  163. const rowHeights=ref([])
  164. measureColumnWidths();
  165. // 初始化获取高度
  166. function measureColumnWidths(type='init') {
  167. nextTick(()=>{
  168. const table = tableRef.value;
  169. if (table) {
  170. const cells = table.rows[0].cells;
  171. let widthArr= Array.from(cells).map(cell => cell.offsetWidth);
  172. console.log(widthArr)
  173. const rows=table.rows
  174. let heightArr=Array.from(rows).map(row => row.offsetHeight);
  175. console.log(heightArr)
  176. if(type=='init'){
  177. columnWidths.value=widthArr
  178. rowHeights.value=heightArr
  179. // console.log(columnWidths.value)
  180. // console.log(rowHeights.value)
  181. }
  182. }
  183. })
  184. }
  185. // 鼠标拖动列宽
  186. function handleMouseMove(event) {
  187. if(route.query.type==2){
  188. return
  189. }
  190. let target = event.target
  191. if (!dragging.value) {
  192. let rect = target.getBoundingClientRect()
  193. const bodyStyle = event.target.style
  194. if (rect.width > 12 && rect.right - event.pageX < moveVal.value) {
  195. // 拖拽的鼠标样式
  196. bodyStyle.cursor = 'col-resize'
  197. }else if (rect.height > 12 && rect.bottom - event.pageY < moveVal.value) {
  198. // 拖拽的鼠标样式
  199. bodyStyle.cursor = 'row-resize'
  200. } else if (!dragging.value) {
  201. bodyStyle.cursor = ''
  202. }
  203. }
  204. }
  205. function handleMouseOut(event) {
  206. event.target.style.cursor = ''
  207. }
  208. const startResize = (event,index,rowIndex) => {
  209. document.onselectstart = function() { return false; };//解决拖动会选中文字的问题
  210. if(route.query.type==2){
  211. return
  212. }
  213. if(event.button!=0){
  214. return
  215. }
  216. let target = event.target
  217. // let cellWidth=0
  218. // let startX, startWidth;
  219. let rect = target.getBoundingClientRect()
  220. if (rect.width > 12 && rect.right - event.pageX < moveVal.value) {
  221. // 开始拖拽
  222. // 当前拖拽的列所在的表格
  223. let tableEl = event.target
  224. // 当前所在列(单元格)
  225. let thEL = event.target
  226. dragging.value = true
  227. // 获取列宽拖拽的显示线(拖拽线)
  228. let resizeProxy = tableRef.value.querySelector(
  229. 'tbody'
  230. )
  231. // console.log(resizeProxy)
  232. const columnRect = thEL.getBoundingClientRect()
  233. thEL.classList.add('noclick')
  234. dragState.value = {
  235. startMouseLeft: event.clientX, // 鼠标开始的地方
  236. columnWidth: columnRect.width, // th开始拖拽的宽度
  237. }
  238. resizeProxy.classList.remove('dn')
  239. document.onselectstart = function () {
  240. return false
  241. }
  242. document.ondragstart = function () {
  243. return false
  244. }
  245. // startX = event.clientX;
  246. // startWidth = event.target.clientWidth;
  247. const doResize = (event) => {
  248. // 拖拽中,拖拽线与鼠标的位置同步
  249. resizeProxy.style.left = event.clientX + 'px'
  250. };
  251. const endResize = (event) => {
  252. if(dragging.value){
  253. // 拖拽完毕
  254. const { startMouseLeft, columnWidth } = dragState.value
  255. const finalLeft = parseInt(resizeProxy.style.left, 10)
  256. const columnWidthDiff = finalLeft - startMouseLeft
  257. const finalColumnWidth = columnWidthDiff + columnWidth
  258. // console.log(finalColumnWidth)
  259. columnWidths.value[index]=finalColumnWidth>=30?finalColumnWidth:30
  260. console.log(columnWidths.value,111)
  261. let widthTotal=0
  262. columnWidths.value.forEach((item,colIndex)=>{
  263. widthTotal+=item
  264. })
  265. // console.log(widthTotal)
  266. //多出来的宽度
  267. let otherWidth=widthTotal-tableRef.value.offsetWidth
  268. // console.log(otherWidth)
  269. //其余每个减去的宽度
  270. let oneWidth=parseFloat((+(parseFloat(otherWidth).toFixed(2)))/(columnWidths.value.length-1)).toFixed(2)
  271. // console.log(oneWidth)
  272. columnWidths.value.forEach((item,colIndex)=>{
  273. if(colIndex!=index){
  274. columnWidths.value[colIndex]=item-(+oneWidth)
  275. }
  276. })
  277. // console.log(columnWidths.value,222)
  278. setTimeout(()=>{
  279. measureColumnWidths('get')
  280. },500)
  281. resizeProxy.style.left = '0px'
  282. event.target.style.cursor = ''
  283. dragging.value = false
  284. dragState.value = {}
  285. resizeProxy.classList.add('dn')
  286. }
  287. document.removeEventListener('mousemove', doResize);
  288. document.removeEventListener('mouseup', endResize);
  289. document.onselectstart = null
  290. document.ondragstart = null
  291. setTimeout(function () {
  292. thEL.classList.remove('noclick')
  293. }, 0)
  294. }
  295. document.addEventListener('mousemove',doResize);
  296. document.addEventListener('mouseup', endResize);
  297. }
  298. //else if (rect.height > 12 && rect.bottom - event.pageY < moveVal.value){
  299. //}else{
  300. //hideContextMenu()
  301. //clickStatus.value=true
  302. //selectionStart.value = null;
  303. //selectionEnd.value = null;
  304. //document.onselectstart = function() { return false; };//解决拖动会选中文字的问题
  305. //selectionStart.value = { row: rowIndex, col: index };
  306. //}
  307. };
  308. function resetSelect(){
  309. clickStatus.value=false
  310. selectionStart.value = null;
  311. selectionEnd.value = null;
  312. }
  313. const startResizeHeight= (event,index) => {
  314. if(route.query.type==2){
  315. return
  316. }
  317. if(event.button!=0){
  318. return
  319. }
  320. let target = event.target
  321. let rect = target.getBoundingClientRect()
  322. if (rect.height > 12 && rect.bottom - event.pageY < moveVal.value) {
  323. // 开始拖拽
  324. // 当前拖拽的列所在的表格
  325. let tableEl = event.target
  326. // 当前所在列(单元格)
  327. let thEL = event.target
  328. draggingHeight.value = true
  329. // 获取列宽拖拽的显示线(拖拽线)
  330. let resizeProxy = tableRef.value.querySelector(
  331. 'tbody'
  332. )
  333. // console.log(resizeProxy)
  334. const columnRect = thEL.getBoundingClientRect()
  335. thEL.classList.add('noclick')
  336. dragStateHeight.value = {
  337. startMouseTop: event.clientY, // 鼠标开始的地方
  338. columnHeight: columnRect.height, // th开始拖拽的宽度
  339. }
  340. resizeProxy.classList.remove('dn')
  341. document.onselectstart = function () {
  342. return false
  343. }
  344. document.ondragstart = function () {
  345. return false
  346. }
  347. const doResize = (event) => {
  348. // 拖拽中,拖拽线与鼠标的位置同步
  349. resizeProxy.style.top = event.clientY + 'px'
  350. };
  351. const endResize = (event) => {
  352. if(draggingHeight.value){
  353. // 拖拽完毕
  354. const { startMouseTop, columnHeight } = dragStateHeight.value
  355. const finalTop = parseInt(resizeProxy.style.top, 10)
  356. const columnHeightDiff = finalTop - startMouseTop
  357. const finalColumnHeight = columnHeightDiff + columnHeight
  358. console.log(finalColumnHeight,index)
  359. rowHeights.value[index]=finalColumnHeight>=20?finalColumnHeight:20
  360. setTimeout(()=>{
  361. measureColumnWidths('get')
  362. },500)
  363. resizeProxy.style.top = '0px'
  364. event.target.style.cursor = ''
  365. draggingHeight.value = false
  366. dragStateHeight.value = {}
  367. resizeProxy.classList.add('dn')
  368. }
  369. document.removeEventListener('mousemove', doResize);
  370. document.removeEventListener('mouseup', endResize);
  371. document.onselectstart = null
  372. document.ondragstart = null
  373. setTimeout(function () {
  374. thEL.classList.remove('noclick')
  375. }, 0)
  376. }
  377. document.addEventListener('mousemove',doResize);
  378. document.addEventListener('mouseup', endResize);
  379. }
  380. };
  381. </script>
  382. <template>
  383. <div :class="['table-wrapper',dynamicSty ]">
  384. <table
  385. ref="tableRef"
  386. cellpadding="0"
  387. cellspacing="0"
  388. :class="config.Watermark?'background-watermark':'no-water'"
  389. :style="{'font-size':(config.FontSize||12)+'px',backgroundImage: 'url('+config.Watermark+')'}"
  390. >
  391. <tbody>
  392. <tr
  393. :style="`height:${getSize(item,index,'height')}`"
  394. @mousedown="event=>startResizeHeight(event,index)"
  395. @mouseout="handleMouseOut"
  396. v-for="(item,index) in props.data"
  397. :key="index"
  398. >
  399. <!-- @contextmenu.prevent="event=>rightClickHandle(event,cell,index,cell_index)" -->
  400. <td
  401. @mouseout="handleMouseOut"
  402. @mousemove="handleMouseMove"
  403. @mousedown="event=>startResize(event,cell_index,index)"
  404. @mouseover="event=>updateSelection(event,index, cell_index)"
  405. @mouseup="event=>endSelection(event,index, cell_index)"
  406. :class="['data-cell',{
  407. 'one-bg':(index+1)%2&&index>0&&!config.Watermark,
  408. 'tow-bg': (index+1)%2!==0&&index>0,
  409. 'head-column': index === 0
  410. },{ 'selected': isCellSelected(index, cell_index) }]"
  411. v-for="(cell,cell_index) in item"
  412. :key="cell_index"
  413. :colspan="cell.mc.cs||1"
  414. :rowspan="cell.mc.rs||1"
  415. :style="`
  416. color: ${cell.fc};
  417. font-weight: ${cell.bl ? 'bold' : 'normal'};
  418. font-style: ${cell.it ? 'italic' : 'normal'};
  419. font-size: ${cell.fs ? cell.fs: config.FontSize?config.FontSize:12}px;
  420. background: ${cell.bg};
  421. text-align: ${HtObj[cell.HorizontalType]};
  422. width:${index==0?getSize(cell,cell_index,'width'):'auto'}
  423. `"
  424. >
  425. <!-- 单元格拆分 -->
  426. <div class="split-word" v-if="cell.ct.s">
  427. <span
  428. v-for="(word,word_index) in cell.ct.s"
  429. :key="`${index}_${cell_index}_${word_index}`"
  430. :style="`
  431. color: ${word.fc};
  432. font-weight: ${word.bl ? 'bold' : 'normal'};
  433. font-style: ${word.it ? 'italic' : 'normal'};
  434. text-decoration:${word.un?'underline':word.cl?'line-through':'normal'};
  435. `"
  436. >{{word.v}}</span>
  437. </div>
  438. <div v-else
  439. :style="`text-decoration:${cell.un?'underline':cell.cl?'line-through':'normal'};`">{{cell.m}}</div>
  440. </td>
  441. </tr>
  442. </tbody>
  443. </table>
  444. <!-- 右键菜单 -->
  445. <div class="contextMenu-wrapper" ref="contextMenuRef" @mouseleave="()=>{activeNames=[];hideContextMenu()}">
  446. <el-collapse v-model="activeNames" accordion>
  447. <el-collapse-item name="1">
  448. <template #title>
  449. <div class="title-size">字号</div>
  450. </template>
  451. <div @click="chooseCellSize(item.val)" class="size-text" v-for="(item,index) in sizeOptions" :key="index">
  452. {{ item.label }}px
  453. </div>
  454. </el-collapse-item>
  455. </el-collapse>
  456. </div>
  457. </div>
  458. </template>
  459. <style lang='less' scoped>
  460. .selected {
  461. background-color: rgba(0, 82, 217, 0.1);
  462. }
  463. ::-webkit-scrollbar {
  464. width: 6px;
  465. height: 6px;
  466. }
  467. ::-webkit-scrollbar-track {
  468. background: rgb(239, 239, 239);
  469. border-radius: 2px;
  470. }
  471. ::-webkit-scrollbar-thumb {
  472. background: #ccc;
  473. border-radius: 10px;
  474. }
  475. ::-webkit-scrollbar-thumb:hover {
  476. background: #888;
  477. }
  478. ::-webkit-scrollbar-corner {
  479. background: #666;
  480. }
  481. .table-wrapper {
  482. max-width: calc(100vw - 20px);
  483. margin: 0 auto;
  484. // margin-right: -5px;
  485. overflow: auto;
  486. }
  487. table {
  488. width: 100%;
  489. font-size: 14px;
  490. color: #333;
  491. td,
  492. th {
  493. // min-width: 120px;
  494. word-break: break-all;
  495. word-wrap: break-word;
  496. line-height: 1.2em;
  497. border: 1px solid #dcdfe6;
  498. // height: 40px;
  499. text-align: center;
  500. border-left: none;
  501. border-top: none;
  502. &:first-child {
  503. border-left: 1px solid #dcdfe6;
  504. }
  505. }
  506. .data-cell{
  507. color: #333;
  508. min-width: 30px;
  509. min-height: 20px;
  510. padding: 0 !important;
  511. &>div{
  512. padding: 0.4em !important;
  513. }
  514. &.one-bg {
  515. background-color: #EFEEF1;
  516. }
  517. &.two-bg {
  518. background-color: #fff;
  519. }
  520. }
  521. .thead-sticky {
  522. position: sticky;
  523. top: 0;
  524. }
  525. .head-column {
  526. background-color: #505B78;
  527. color: #fff;
  528. }
  529. .split-word {
  530. span { display: inline; }
  531. }
  532. }
  533. .no-water{
  534. td,
  535. th {
  536. background-color: #fff;
  537. }
  538. .head-column {
  539. background-color: #505B78;
  540. color: #fff;
  541. }
  542. }
  543. .pc-sty table {
  544. table-layout: auto;
  545. td,th {
  546. width: auto;
  547. height: auto;
  548. padding: 0.4em 0;
  549. }
  550. }
  551. .mobile-sty table {
  552. table-layout: auto;
  553. td,th {
  554. min-width: 120px;
  555. height: 40px;
  556. }
  557. }
  558. .background-watermark{
  559. background-repeat: no-repeat;
  560. background-position: center center;
  561. background-size: 100%;
  562. }
  563. .contextMenu-wrapper {
  564. position: fixed;
  565. z-index: 99;
  566. top: -9999px;
  567. left: -9999px;
  568. background: #fff;
  569. min-width: 100px;
  570. max-height: 100px;
  571. overflow-y: auto;
  572. /* border: 1px solid #999; */
  573. box-shadow: 0 1px 4px #999;
  574. .item {
  575. padding: 10px 25px;
  576. cursor: pointer;
  577. &:hover {
  578. background-color: #f5f7fa;
  579. }
  580. &:hover .subMenu-wrapper {
  581. display: block;
  582. }
  583. }
  584. .title-size{
  585. text-align: center;
  586. width: 100%;
  587. position: absolute;
  588. }
  589. .size-text{
  590. text-align: center;
  591. padding: 5px 0;
  592. cursor: pointer;
  593. &:hover{
  594. background: #f5f7fa;
  595. }
  596. }
  597. }
  598. </style>