Table.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. <template>
  2. <div>
  3. <div ref="spreadsheet" class="spreadsheet-container" id="spreadsheet"></div>
  4. </div>
  5. </template>
  6. <script>
  7. import Spreadsheet from 'x-data-spreadsheet'
  8. import { indexedColors } from './color'
  9. import { zhCN } from 'handsontable/i18n'
  10. import _ from 'lodash'
  11. import tinycolor from 'tinycolor2'
  12. Spreadsheet.locale('zh-cn', zhCN)
  13. export default {
  14. name: 'ExcelViewer',
  15. props: {
  16. workbook: Object
  17. },
  18. data () {
  19. return {
  20. spreadsheet: null,
  21. themeColors: []
  22. }
  23. },
  24. mounted () {
  25. this.initSpreadsheet()
  26. },
  27. watch: {
  28. workbook () {
  29. this.parseTheme()
  30. this.updateTable()
  31. }
  32. },
  33. computed: {
  34. sheets () {
  35. if (this.workbook.worksheets) {
  36. return this.workbook.worksheets.filter((sheet) => sheet._rows.length)
  37. }
  38. return []
  39. }
  40. },
  41. methods: {
  42. initSpreadsheet () {
  43. // 优化性能配置
  44. this.spreadsheet = new Spreadsheet(this.$refs.spreadsheet, {
  45. view: {
  46. height: () => document.documentElement.clientHeight - 120,
  47. width: () => document.documentElement.clientWidth - 40
  48. },
  49. mode: 'read', // 只读模式
  50. showToolbar: false,
  51. showGrid: true,
  52. showContextmenu: false, // 禁用右键菜单提高性能
  53. multipleSheets: true,
  54. rpx: 1, // 减少渲染计算
  55. row: {
  56. len: 100, // 限制初始行数
  57. height: 25, // 固定行高
  58. autoHeight: true
  59. },
  60. col: {
  61. len: 26, // 限制初始列数
  62. width: 50, // 固定列宽
  63. indexWidth: 60, // 行索引宽度
  64. minWidth: 30, // 最小列宽
  65. autoWidth: true // 自动调整列宽
  66. },
  67. style: {
  68. // 确保默认样式正确
  69. bgcolor: '#ffffff',
  70. color: '#333333',
  71. align: 'left',
  72. valign: 'middle',
  73. textwrap: false,
  74. strike: false,
  75. underline: false,
  76. italic: false,
  77. bold: false,
  78. fontSize: 12
  79. }
  80. }).loadData({})
  81. // 延迟加载数据,避免初始化时卡死
  82. this.$nextTick(() => {
  83. this.parseTheme()
  84. this.updateSpreadsheet()
  85. })
  86. },
  87. updateSpreadsheet () {
  88. if (!this.spreadsheet) return
  89. // 显示加载状态
  90. this.$refs.spreadsheet.classList.add('loading')
  91. try {
  92. const data = this.convertWorksheetToData()
  93. if (this.spreadsheet) {
  94. this.$refs.spreadsheet.innerHTML = ''
  95. }
  96. // 重新创建实例
  97. this.spreadsheet = new Spreadsheet(this.$refs.spreadsheet, {
  98. view: {
  99. height: () => document.documentElement.clientHeight - 120,
  100. width: () => document.documentElement.clientWidth - 40
  101. },
  102. mode: 'read',
  103. showToolbar: false,
  104. showGrid: true,
  105. showContextmenu: false,
  106. multipleSheets: true,
  107. rpx: 1,
  108. row: {
  109. len: 100,
  110. height: 25,
  111. autoHeight: true
  112. },
  113. col: {
  114. len: 26,
  115. width: 50,
  116. indexWidth: 60,
  117. minWidth: 30,
  118. autoWidth: true
  119. }
  120. })
  121. .loadData(data)
  122. .change((data) => {
  123. console.log('数据已更改:', data)
  124. })
  125. } catch (error) {
  126. console.error('加载Excel数据失败:', error)
  127. } finally {
  128. this.$refs.spreadsheet.classList.remove('loading')
  129. }
  130. },
  131. convertWorksheetToData () {
  132. let data = []
  133. this.sheets.forEach((sheet) => {
  134. const sheetIndex = sheet.id
  135. const sheetData = {
  136. name: sheet._name,
  137. freeze: 'A1',
  138. styles: [
  139. {
  140. bgcolor: '#ffffff',
  141. color: '#000000',
  142. align: 'left',
  143. valign: 'middle',
  144. fontSize: 12
  145. }
  146. ],
  147. merges: [],
  148. rows: {},
  149. cols: {}
  150. }
  151. try {
  152. // 预处理合并单元格
  153. this.processMerges(sheet, sheetData)
  154. // 预处理样式缓存,避免重复创建相同样式
  155. const styleCache = new Map()
  156. // 处理单元格数据
  157. this.processRows(sheet, sheetData, styleCache)
  158. // 处理列宽
  159. this.processColumns(sheet, sheetData)
  160. data.push(sheetData)
  161. } catch (error) {
  162. console.error('转换工作表数据失败:', error)
  163. data[sheetIndex] = {
  164. name: sheet._name || 'Sheet1',
  165. rows: {},
  166. cols: {}
  167. }
  168. }
  169. })
  170. return data
  171. },
  172. // 处理合并单元格
  173. processMerges (sheet, sheetData) {
  174. if (!sheet._merges) return
  175. Object.values(sheet._merges).forEach((merge) => {
  176. try {
  177. const { top, left, bottom, right } = merge
  178. sheetData.merges.push(
  179. `${this.columnIndexToLetter(
  180. left - 1
  181. )}${top}:${this.columnIndexToLetter(right - 1)}${bottom}`
  182. )
  183. } catch (e) {
  184. console.warn('处理合并单元格出错:', e)
  185. }
  186. })
  187. },
  188. // 处理行和单元格数据
  189. processRows (sheet, sheetData, styleCache) {
  190. // 增加最大行数限制,确保所有行都能显示
  191. const maxRows = Math.min(sheet.rowCount || 50, 100)
  192. // 预处理所有行,确保即使空行也能被处理
  193. for (let rowIndex = 0; rowIndex < maxRows; rowIndex++) {
  194. const row = sheet.getRow(rowIndex + 1)
  195. // 初始化行数据和行高
  196. let maxHeight = 25 // 最小行高
  197. sheetData.rows[rowIndex] = {
  198. cells: {},
  199. height: maxHeight
  200. }
  201. // 如果行不存在,创建一个空行
  202. if (!row || !row.cellCount) {
  203. continue
  204. }
  205. // 处理行中的单元格
  206. // 使用列数而不是cellCount,确保处理所有可能的单元格
  207. const maxCols = Math.min(sheet.columnCount || 26, 100)
  208. for (let colIndex = 0; colIndex < maxCols; colIndex++) {
  209. try {
  210. const cell = row.getCell(colIndex + 1)
  211. if (!cell || (cell.type === 'null' && !cell.style)) {
  212. // 对于空单元格,添加一个默认样式的空单元格
  213. sheetData.rows[rowIndex].cells[colIndex] = {
  214. text: '',
  215. style: 0
  216. }
  217. continue
  218. }
  219. // 获取单元格文本和样式
  220. const { text: cellText, height: cellHeight } =
  221. this.getCellTextAndHeight(cell)
  222. const styleIndex = this.getCellStyle(
  223. cell,
  224. styleCache,
  225. sheetData.styles
  226. )
  227. // 更新行高
  228. maxHeight = Math.max(maxHeight, cellHeight)
  229. // 检查是否是合并单元格的起始单元格
  230. const mergeInfo = Object.values(sheet._merges || {}).find(
  231. (m) => m.top === row.number && m.left === colIndex + 1
  232. )
  233. // 设置单元格数据
  234. if (mergeInfo) {
  235. sheetData.rows[rowIndex].cells[colIndex] = {
  236. text: cellText,
  237. style: styleIndex,
  238. merge: [
  239. mergeInfo.bottom - mergeInfo.top,
  240. mergeInfo.right - mergeInfo.left
  241. ]
  242. }
  243. } else {
  244. sheetData.rows[rowIndex].cells[colIndex] = {
  245. text: cellText,
  246. style: styleIndex
  247. }
  248. }
  249. } catch (error) {
  250. console.warn(
  251. '处理单元格时出错:',
  252. error,
  253. '位置:',
  254. rowIndex + 1,
  255. colIndex + 1
  256. )
  257. sheetData.rows[rowIndex].cells[colIndex] = {
  258. text: '',
  259. style: 0
  260. }
  261. }
  262. }
  263. // 更新行高
  264. sheetData.rows[rowIndex].height = maxHeight
  265. }
  266. },
  267. // 获取单元格文本和计算高度
  268. getCellTextAndHeight (cell) {
  269. let cellText = ''
  270. try {
  271. // 处理不同类型的单元格内容
  272. if (!cell || cell.value === null || cell.value === undefined) {
  273. cellText = ''
  274. } else if (
  275. cell.type === 'date' ||
  276. (cell.numFmt &&
  277. (cell.numFmt.includes('yy') ||
  278. cell.numFmt.includes('mm') ||
  279. cell.numFmt.includes('dd') ||
  280. cell.numFmt.includes('h') ||
  281. cell.numFmt.includes('m:s')))
  282. ) {
  283. // 处理日期类型或带有日期格式的数字
  284. try {
  285. // 检查是否为日期值
  286. let date
  287. if (cell.value instanceof Date) {
  288. date = cell.value
  289. } else if (typeof cell.value === 'number') {
  290. // Excel日期是从1900年1月1日开始的天数
  291. // 需要转换为JavaScript日期
  292. const excelEpoch = new Date(1899, 11, 30) // Excel的起始日期
  293. date = new Date(
  294. excelEpoch.getTime() + cell.value * 24 * 60 * 60 * 1000
  295. )
  296. } else {
  297. date = new Date(cell.value)
  298. }
  299. if (!isNaN(date.getTime())) {
  300. // 格式化日期
  301. const year = date.getFullYear()
  302. const month = (date.getMonth() + 1).toString().padStart(2, '0')
  303. const day = date.getDate().toString().padStart(2, '0')
  304. // 检查是否需要显示时间
  305. if (
  306. cell.numFmt &&
  307. (cell.numFmt.includes('h') || cell.numFmt.includes('s'))
  308. ) {
  309. const hour = date.getHours().toString().padStart(2, '0')
  310. const minute = date.getMinutes().toString().padStart(2, '0')
  311. const second = date.getSeconds().toString().padStart(2, '0')
  312. cellText = `${year}/${month}/${day} ${hour}:${minute}:${second}`
  313. } else {
  314. cellText = `${year}/${month}/${day}`
  315. }
  316. } else {
  317. // 如果不是有效日期,使用原始文本
  318. cellText = String(cell.text || cell.value || '')
  319. }
  320. } catch (e) {
  321. console.warn('日期转换错误:', e)
  322. cellText = String(cell.text || cell.value || '')
  323. }
  324. } else if (typeof cell.value === 'object' && cell.value !== null) {
  325. if (cell.value.hyperlink) {
  326. cellText = '[链接]'
  327. } else if (cell.value.image) {
  328. cellText = '[图片]'
  329. } else if (cell.value.richText) {
  330. cellText = cell.value.richText
  331. .map((t) => String(t?.text || ''))
  332. .join('')
  333. } else {
  334. cellText = String(cell.text || '')
  335. }
  336. } else if (cell.formula) {
  337. // 检查公式结果是否可能是日期
  338. if (
  339. cell.numFmt &&
  340. (cell.numFmt.includes('yy') ||
  341. cell.numFmt.includes('mm') ||
  342. cell.numFmt.includes('dd'))
  343. ) {
  344. try {
  345. // 尝试将结果转换为日期
  346. const excelEpoch = new Date(1899, 11, 30)
  347. const date = new Date(
  348. excelEpoch.getTime() + cell.result * 24 * 60 * 60 * 1000
  349. )
  350. if (!isNaN(date.getTime())) {
  351. const year = date.getFullYear()
  352. const month = (date.getMonth() + 1).toString().padStart(2, '0')
  353. const day = date.getDate().toString().padStart(2, '0')
  354. cellText = `${year}-${month}-${day}`
  355. } else {
  356. cellText = String(cell.result || cell.value || '')
  357. }
  358. } catch (e) {
  359. cellText = String(cell.result || cell.value || '')
  360. }
  361. } else {
  362. cellText =
  363. cell.result !== undefined && cell.result !== null
  364. ? String(cell.result)
  365. : String(cell.value || '')
  366. }
  367. } else {
  368. cellText =
  369. cell.text !== undefined && cell.text !== null
  370. ? String(cell.text)
  371. : cell.value !== undefined && cell.value !== null
  372. ? String(cell.value)
  373. : ''
  374. }
  375. } catch (e) {
  376. console.warn('获取单元格文本出错:', e)
  377. cellText = '[格式错误]'
  378. }
  379. // 计算行高
  380. const lineBreaks = ((cellText || '').match(/\n/g) || []).length + 1
  381. const chars = [...(cellText || '')]
  382. const lines = Math.ceil(chars.length / 40)
  383. const neededHeight = Math.max(lineBreaks, lines) * 20
  384. return { text: cellText, height: neededHeight }
  385. },
  386. // 获取单元格样式
  387. getCellStyle (cell, styleCache, styles) {
  388. // 创建样式对象
  389. const cellStyle = {
  390. bgcolor: '#ffffff',
  391. color: '#000000',
  392. align: cell.alignment?.horizontal || 'left',
  393. valign: cell.alignment?.vertical || 'middle',
  394. fontSize: cell.font?.size || 12,
  395. textwrap: true,
  396. bold: cell.font?.bold,
  397. italic: cell.font?.italic,
  398. underline: false,
  399. strike: false
  400. }
  401. // 处理背景色
  402. if (cell.fill && cell.fill.type === 'pattern') {
  403. const fgColor = cell.fill.fgColor || {}
  404. // 处理索引颜色
  405. if (typeof fgColor.indexed === 'number') {
  406. const indexedColor = indexedColors[fgColor.indexed]
  407. if (indexedColor) {
  408. cellStyle.bgcolor = `#${indexedColor}`
  409. }
  410. }
  411. // 处理主题颜色
  412. else if (typeof fgColor.theme === 'number') {
  413. // Excel主题颜色索引映射
  414. const themeColorMap = {
  415. 0: 1, // 浅色1
  416. 1: 0, // 深色1
  417. 2: 3, // 浅色2
  418. 3: 2, // 深色2
  419. 4: 4, // 强调色1
  420. 5: 5, // 强调色2
  421. 6: 6, // 强调色3
  422. 7: 7, // 强调色4
  423. 8: 8, // 强调色5
  424. 9: 9 // 强调色6
  425. }
  426. const mappedIndex =
  427. themeColorMap[fgColor.theme] !== undefined
  428. ? themeColorMap[fgColor.theme]
  429. : fgColor.theme
  430. if (this.themeColors[mappedIndex]) {
  431. let color = `#${this.themeColors[mappedIndex]}`
  432. // 应用色调调整
  433. if (typeof fgColor.tint === 'number' && fgColor.tint !== 0) {
  434. color = this.applyTint(color, fgColor.tint)
  435. }
  436. cellStyle.bgcolor = color
  437. }
  438. }
  439. // 处理RGB颜色
  440. else if (fgColor.rgb) {
  441. cellStyle.bgcolor = `#${fgColor.rgb.substring(
  442. fgColor.rgb.length - 6
  443. )}`
  444. }
  445. // 处理ARGB颜色
  446. else if (fgColor.argb) {
  447. const color = this.convertArgbToHex(fgColor.argb)
  448. if (color) cellStyle.bgcolor = color
  449. }
  450. }
  451. // 处理字体颜色
  452. if (cell.font && cell.font.color) {
  453. const fontColor = cell.font.color
  454. // 处理索引颜色
  455. if (typeof fontColor.indexed === 'number') {
  456. const indexedColor = indexedColors[fontColor.indexed]
  457. if (indexedColor) {
  458. cellStyle.color = `#${indexedColor}`
  459. }
  460. }
  461. // 处理主题颜色
  462. else if (typeof fontColor.theme === 'number') {
  463. // Excel主题颜色索引映射
  464. const themeColorMap = {
  465. 0: 1, // 浅色1
  466. 1: 0, // 深色1
  467. 2: 3, // 浅色2
  468. 3: 2, // 深色2
  469. 4: 4, // 强调色1
  470. 5: 5, // 强调色2
  471. 6: 6, // 强调色3
  472. 7: 7, // 强调色4
  473. 8: 8, // 强调色5
  474. 9: 9 // 强调色6
  475. }
  476. const mappedIndex =
  477. themeColorMap[fontColor.theme] !== undefined
  478. ? themeColorMap[fontColor.theme]
  479. : fontColor.theme
  480. if (this.themeColors[mappedIndex]) {
  481. let color = `#${this.themeColors[mappedIndex]}`
  482. // 应用色调调整
  483. if (typeof fontColor.tint === 'number' && fontColor.tint !== 0) {
  484. color = this.applyTint(color, fontColor.tint)
  485. }
  486. cellStyle.color = color
  487. }
  488. }
  489. // 处理RGB颜色
  490. else if (fontColor.rgb) {
  491. cellStyle.color = `#${fontColor.rgb.substring(
  492. fontColor.rgb.length - 6
  493. )}`
  494. }
  495. // 处理ARGB颜色
  496. else if (fontColor.argb) {
  497. const color = this.convertArgbToHex(fontColor.argb)
  498. if (color) cellStyle.color = color
  499. }
  500. }
  501. // 使用样式缓存避免重复
  502. const styleKey = JSON.stringify(cellStyle)
  503. if (styleCache.has(styleKey)) {
  504. return styleCache.get(styleKey)
  505. }
  506. // 添加新样式
  507. styles.push(cellStyle)
  508. const styleIndex = styles.length - 1
  509. styleCache.set(styleKey, styleIndex)
  510. return styleIndex
  511. },
  512. applyTint (hexColor, tint) {
  513. try {
  514. // 将十六进制颜色转换为RGB
  515. const color = tinycolor(hexColor)
  516. const rgb = color.toRgb()
  517. // 根据Excel的色调算法调整颜色
  518. const adjustColor = (value, tint) => {
  519. let result
  520. if (tint < 0) {
  521. // 负色调值,使颜色变暗
  522. result = value * (1 + tint)
  523. } else {
  524. // 正色调值,使颜色变亮
  525. result = value + (255 - value) * tint
  526. }
  527. return Math.max(0, Math.min(255, Math.round(result)))
  528. }
  529. // 应用色调到每个RGB通道
  530. const r = adjustColor(rgb.r, tint)
  531. const g = adjustColor(rgb.g, tint)
  532. const b = adjustColor(rgb.b, tint)
  533. // 返回调整后的颜色
  534. return tinycolor({ r, g, b }).toHexString()
  535. } catch (e) {
  536. console.warn('应用色调调整出错:', e)
  537. return hexColor
  538. }
  539. },
  540. // 处理列宽
  541. processColumns (sheet, sheetData) {
  542. const maxCols = Math.min(sheet.columnCount || 26, 50)
  543. const colWidthCache = new Map()
  544. const mergedCols = new Set()
  545. // 收集合并单元格信息
  546. if (sheet._merges) {
  547. Object.values(sheet._merges).forEach((merge) => {
  548. // 记录被合并的列
  549. for (let col = merge.left; col <= merge.right; col++) {
  550. mergedCols.add(col - 1)
  551. }
  552. })
  553. }
  554. // 先收集所有单元格的文本宽度
  555. sheet.eachRow((row) => {
  556. for (let colIndex = 0; colIndex < maxCols; colIndex++) {
  557. try {
  558. const cell = row.getCell(colIndex + 1)
  559. if (!cell) continue
  560. // 安全获取单元格文本
  561. let cellText = ''
  562. try {
  563. if (cell.text !== undefined && cell.text !== null) {
  564. cellText = String(cell.text)
  565. } else if (cell.value !== undefined && cell.value !== null) {
  566. if (typeof cell.value === 'object') {
  567. cellText = cell.value?.richText
  568. ? cell.value.richText
  569. .map((t) => String(t?.text || ''))
  570. .join('')
  571. : cell.value?.hyperlink
  572. ? '[链接]'
  573. : cell.value?.image
  574. ? '[图片]'
  575. : ''
  576. } else {
  577. cellText = String(cell.value)
  578. }
  579. }
  580. } catch (e) {
  581. cellText = ''
  582. }
  583. // 计算文本宽度
  584. if (cellText) {
  585. // 检查是否是合并单元格的一部分
  586. const isMerged = Object.values(sheet._merges || {}).some(
  587. (merge) =>
  588. row.number >= merge.top &&
  589. row.number <= merge.bottom &&
  590. colIndex + 1 >= merge.left &&
  591. colIndex + 1 <= merge.right
  592. )
  593. // 如果是合并单元格,根据合并的列数调整宽度计算
  594. if (isMerged) {
  595. const merge = Object.values(sheet._merges || {}).find(
  596. (m) =>
  597. row.number >= m.top &&
  598. row.number <= m.bottom &&
  599. colIndex + 1 >= m.left &&
  600. colIndex + 1 <= m.right
  601. )
  602. if (merge && colIndex + 1 === merge.left) {
  603. // 只为合并单元格的第一列计算宽度
  604. const mergeWidth = merge.right - merge.left + 1
  605. const textWidth = [...cellText].reduce((width, char) => {
  606. return width + (/[\u4e00-\u9fa5]/.test(char) ? 2 : 1)
  607. }, 0)
  608. // 平均分配到每一列
  609. const avgWidth = Math.ceil(textWidth / mergeWidth)
  610. colWidthCache.set(
  611. colIndex,
  612. Math.max(colWidthCache.get(colIndex) || 0, avgWidth)
  613. )
  614. }
  615. } else {
  616. // 普通单元格
  617. const textWidth = [...cellText].reduce((width, char) => {
  618. return width + (/[\u4e00-\u9fa5]/.test(char) ? 2 : 1)
  619. }, 0)
  620. colWidthCache.set(
  621. colIndex,
  622. Math.max(colWidthCache.get(colIndex) || 0, textWidth)
  623. )
  624. }
  625. }
  626. } catch (e) {
  627. // 忽略错误
  628. }
  629. }
  630. })
  631. // 设置列宽
  632. for (let colIndex = 0; colIndex < maxCols; colIndex++) {
  633. let maxWidth = colWidthCache.get(colIndex) || 0
  634. // 对于合并单元格的列,限制最大宽度
  635. if (mergedCols.has(colIndex)) {
  636. maxWidth = Math.min(maxWidth, 30) // 限制合并列的宽度
  637. }
  638. // 设置合理的列宽范围
  639. const width = Math.max(40, Math.min(300, maxWidth * 9))
  640. sheetData.cols[colIndex] = {
  641. width: width
  642. }
  643. }
  644. },
  645. // ARGB 转 Hex 颜色方法
  646. convertArgbToHex (argb) {
  647. if (!argb) return null
  648. try {
  649. const matches =
  650. /^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(argb)
  651. if (!matches) return null
  652. return `#${matches[2]}${matches[3]}${matches[4]}`
  653. } catch (e) {
  654. console.error('Color conversion error:', e)
  655. return null
  656. }
  657. },
  658. columnIndexToLetter (index) {
  659. let temp
  660. let letter = ''
  661. // 修复列索引转字母的算法
  662. if (index < 0) return ''
  663. do {
  664. temp = index % 26
  665. letter = String.fromCharCode(temp + 65) + letter
  666. index = Math.floor(index / 26) - 1
  667. } while (index >= 0)
  668. return letter
  669. },
  670. parseTheme () {
  671. const theme = this.workbook._themes?.theme1
  672. if (!theme) {
  673. // Office默认主题颜色
  674. this.themeColors = [
  675. 'FFFFFF', // 白色 - 浅色1
  676. '000000', // 黑色 - 深色1
  677. 'EEECE1', // 浅灰 - 浅色2
  678. '1F497D', // 深灰 - 深色2
  679. '4F81BD', // 蓝色 - 强调色1
  680. 'C0504D', // 红色 - 强调色2
  681. '9BBB59', // 绿色 - 强调色3
  682. '8064A2', // 紫色 - 强调色4
  683. '4BACC6', // 青色 - 强调色5
  684. 'F79646' // 橙色 - 强调色6
  685. ]
  686. return
  687. }
  688. try {
  689. const parser = new DOMParser()
  690. const doc = parser.parseFromString(theme, 'text/xml')
  691. // 获取颜色方案元素
  692. const clrScheme = doc.getElementsByTagName('a:clrScheme')[0]
  693. if (!clrScheme) {
  694. throw new Error('找不到颜色方案元素')
  695. }
  696. // 初始化主题颜色数组
  697. this.themeColors = []
  698. const colorElements = Array.from(clrScheme.children)
  699. // 处理每个颜色元素
  700. for (const element of colorElements) {
  701. let colorValue = null
  702. // 查找颜色定义元素
  703. const srgbClr = element.getElementsByTagName('a:srgbClr')[0]
  704. const sysClr = element.getElementsByTagName('a:sysClr')[0]
  705. if (srgbClr) {
  706. colorValue = srgbClr.getAttribute('val')
  707. } else if (sysClr) {
  708. colorValue =
  709. sysClr.getAttribute('lastClr') || sysClr.getAttribute('val')
  710. }
  711. this.themeColors.push(colorValue || 'FFFFFF')
  712. }
  713. } catch (error) {
  714. console.error('解析主题颜色出错:', error)
  715. this.themeColors = [
  716. 'FFFFFF', // 白色 - 浅色1
  717. '000000', // 黑色 - 深色1
  718. 'EEECE1', // 浅灰 - 浅色2
  719. '1F497D', // 深灰 - 深色2
  720. '4F81BD', // 蓝色 - 强调色1
  721. 'C0504D', // 红色 - 强调色2
  722. '9BBB59', // 绿色 - 强调色3
  723. '8064A2', // 紫色 - 强调色4
  724. '4BACC6', // 青色 - 强调色5
  725. 'F79646' // 橙色 - 强调色6
  726. ]
  727. }
  728. }
  729. }
  730. }
  731. </script>
  732. <style scoped>
  733. .spreadsheet-container {
  734. width: 100%;
  735. height: calc(100vh - 120px);
  736. position: relative;
  737. }
  738. .spreadsheet-container.loading::after {
  739. content: "加载中...";
  740. position: absolute;
  741. top: 0;
  742. left: 0;
  743. right: 0;
  744. bottom: 0;
  745. background: rgba(255, 255, 255, 0.7);
  746. display: flex;
  747. align-items: center;
  748. justify-content: center;
  749. font-size: 18px;
  750. z-index: 1000;
  751. }
  752. .sheet-btn {
  753. border: 1px solid #ccc;
  754. background-color: #f0f0f0;
  755. cursor: pointer;
  756. margin-right: 5px;
  757. border-radius: 4px;
  758. padding: 5px 15px;
  759. }
  760. .sheet-btn.active {
  761. background-color: #4caf50;
  762. color: white;
  763. border-color: #388e3c;
  764. }
  765. .btn-group {
  766. margin-bottom: 10px;
  767. display: flex;
  768. flex-wrap: wrap;
  769. gap: 5px;
  770. padding: 8px;
  771. background-color: #f8f8f8;
  772. border-radius: 4px;
  773. }
  774. </style>