前端将html导出为word文档

[复制链接]
发表于 2025-10-14 11:06:35 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
媒介

本次功能的引出是由于博主所做的功能业务为统计分析页面,须要将图表及分析数据导出到word文档,使体系利用职员可以在页面表现的底子上举行修改。按理说页面导出的数据是不允许修改的,以是博主向产物司理提出直接将页面导出为图片,在博主的据理力图下,乐成的被否决了,那就来好好研究一下吧
正文

在通过多方搜索以及实践之下,终于找到了办理方案,原理就是先将页面上的元素样式转换为行内样式,然后再加上可以被word文档辨认的标头,然后通过html-docx-js将html文档转为blob流,如许就可以通过点击a标签的情势将该文件导出
将页面元素样式转化为行内样式

起首,须要天生一个空iframe元素 我们利用的数据都在这个元素内,如许就不会影响页面上的数据
  1.     window.contIframe = document.createElement('iframe');
  2.     window.contIframe.style = 'display: none; width:100%;';
  3.     document.body.appendChild(window.contIframe);
复制代码
然后将须要导出的元素插入到这个iframe中
  1.   // contEl 需要导出的元素
  2.   let cloneEl = contEl.cloneNode(true);
  3.   cloneEl.style.width = getComputedStyle(contEl).width;
  4.   window.contIframe.contentDocument.body.appendChild(cloneEl);
复制代码
删除埋伏元素并将元素样式转换为行内样式
  1. [/code] [code]let domWrap = cloneEl.cloneNode(true)
  2.   // 1. 删除隐藏的元素, 并且将元素样式设置为行内样式, 方便word识别
  3.   Array.from(domWrap.querySelectorAll('*')).forEach(item => {
  4.   // 这个是需要特殊处理的元素 这个是需要将这个元素转换为canvas然后再导出,这里只提供这种思路 如若需要可以按我这个方法走,不然就直接删掉这个if分支
  5.     if (item.className.includes('fishbone_main') && canvas) {
  6.       item.childNodes[0].remove();
  7.       item.appendChild(canvas);
  8.     }
  9.     let attr = item.getAttribute('data-toword');
  10.     let originItem = contEl.querySelector('[data-toword="' + attr + '"]');
  11.     if (originItem) {
  12.       let sty = getComputedStyle(originItem);
  13.       if (sty.display == 'none ') return item.remove();
  14.       if (sty.opacity === '0') return item.remove();
  15.       setStyle(item, sty);
  16.     }
  17.    // 当页面中存在表格时 需要稍微处理一下,因为直接导出会导致单元格过宽,本次使用的是element的table表格
  18.     if (item.className.includes('department_content_table')) {
  19.       const table = convertElTableToHtmlTableWord(item.childNodes[0], '');
  20.       item.childNodes[0].remove();
  21.       item.innerHTML = table;
  22.     }
  23.   });
  24.   
  25. function setStyle(ele, sty) {
  26.   if (ele.nodeName.toLowerCase() != 'img') {
  27.     // let sty = getComputedStyle(ele)
  28.     ele.setAttribute(
  29.       'style',
  30.       (ele.getAttribute('style') || '') +
  31.         `;font-size: ${sty.fontSize};color: ${sty.color};font-style: ${sty.fontStyle};line-height: ${sty.lineHeight};font-weight: ${sty.fontWeight};
  32.       font-family: ${sty.fontFamily};text-align: ${sty.textAlign};text-indent: ${sty.textIndent}; margin: ${sty.margin}; padding: ${sty.padding};width: ${sty.width}; height: ${sty.height};
  33.       white-space:${sty.whiteSpace};word-break:${sty.wordBreak};display:${sty.display}`
  34.     );
  35.   }
  36. }
  37. // 处理table转换为html
  38. function convertElTableToHtmlTableWord(elTable, title, infoLeft = '', infoRight = '') {
  39.   if (!elTable) return '';
  40.   // 获取 el-table 的表头数据,包括多级表头
  41.   const theadRows = elTable.querySelectorAll('thead tr');
  42.   // 获取 el-table 的数据行
  43.   const tbodyRows = elTable.querySelectorAll('tbody tr');
  44.   let length = getTotalColumnCount(theadRows[0]);
  45.   // 开始构建 HTML 表格的字符串,设置表格整体样式和边框样式
  46.   let htmlTable = '<table style="border-collapse: collapse; border: 1px solid black;"><thead>';
  47.   htmlTable += '<tr>';
  48.   if (infoRight != '') {
  49.     htmlTable += `<th colspan="${Math.floor(length / 2)}" style="width:306.2000pt;padding:0.0000pt 5.4000pt 0.0000pt 5.4000pt ;border-left:1.0000pt solid rgb(255,255,255);
  50. mso-border-left-alt:0.5000pt solid rgb(255,255,255);border-right:1.0000pt solid rgb(255,255,255);mso-border-right-alt:0.5000pt solid rgb(255,255,255);
  51. border-top:1.0000pt solid rgb(255,255,255);mso-border-top-alt:0.5000pt solid rgb(255,255,255);border-bottom:none;
  52. mso-border-bottom-alt:none;background:rgb(255,255,255);text-align: right;">单位:${infoRight}</th>`;
  53.   } else {
  54.     htmlTable += `<th colspan="${Math.floor(length / 2)}" style="width:306.2000pt;padding:0.0000pt 5.4000pt 0.0000pt 5.4000pt ;border-left:1.0000pt solid rgb(255,255,255);
  55. mso-border-left-alt:0.5000pt solid rgb(255,255,255);border-right:1.0000pt solid rgb(255,255,255);mso-border-right-alt:0.5000pt solid rgb(255,255,255);
  56. border-top:1.0000pt solid rgb(255,255,255);mso-border-top-alt:0.5000pt solid rgb(255,255,255);border-bottom:none;
  57. mso-border-bottom-alt:none;background:rgb(255,255,255);text-align: right;"></th>`;
  58.   }
  59.   htmlTable += '</tr>';
  60.   // 处理多级表头
  61.   theadRows.forEach(row => {
  62.     htmlTable += '<tr>';
  63.     const columns = row.querySelectorAll('th');
  64.     columns.forEach(column => {
  65.       const colspan = column.getAttribute('colspan') || '1';
  66.       const rowspan = column.getAttribute('rowspan') || '1';
  67.       htmlTable += `<th colspan="${colspan}" rowspan="${rowspan}" style="border: 1px solid black;">${column.innerText}</th>`;
  68.     });
  69.     htmlTable += '</tr>';
  70.   });
  71.   htmlTable += '</thead><tbody>';
  72.   // 构建数据行
  73.   tbodyRows.forEach(row => {
  74.     htmlTable += '<tr>';
  75.     const cells = row.querySelectorAll('td');
  76.     cells.forEach(cell => {
  77.       if (cell.querySelector('div')) {
  78.         htmlTable += `<td style="border: 1px solid black;">${cell.querySelector('div').innerHTML}</td>`;
  79.       } else {
  80.         htmlTable += `<td style="border: 1px solid black;">${cell.innerText}</td>`;
  81.       }
  82.     });
  83.     htmlTable += '</tr>';
  84.   });
  85.   htmlTable += '</tbody></table>';
  86.   return htmlTable;
  87. }
复制代码

如许整个html文档的样式就转换为行内样式了
下面就须要将图片转换为base64用于导出
  1. let imgList = domWrap.querySelectorAll('img');
  2.   console.log('加载图片数量: ', imgList.length);
  3.   await Promise.all(
  4.     Array.from(imgList)
  5.       .filter(x => !x.src.startsWith('data'))
  6.       .map(tempimg => {
  7.         let img = new Image();
  8.         img.setAttribute('crossOrigin', 'anonymous');
  9.         img.src = options.proxyHost ? tempimg.src.replace(location.host, options.proxyHost) : tempimg.src;
  10.         return new Promise((resolve, reject) => {
  11.           try {
  12.             img.onload = function() {
  13.               img.onload = null;
  14.               const cw = Math.min(img.width, options.maxWidth);
  15.               const ch = img.height * (cw / img.width);
  16.               const canvas = document.createElement('CANVAS');
  17.               canvas.width = cw;
  18.               canvas.height = ch;
  19.               const context = canvas.getContext('2d');
  20.               context?.drawImage(img, 0, 0, cw, ch);
  21.               const uri = canvas.toDataURL('image/jpg', 0.8);
  22.               tempimg.src = uri;
  23.               const w = Math.min(img.width, 550, options.maxWidth); // word图片最大宽度
  24.               tempimg.width = w;
  25.               tempimg.height = img.height * (w / img.width);
  26.               console.log('img onload...', options.fileName, img.src, img.width, img.height, cw, ch, w, tempimg.height);
  27.               canvas.remove();
  28.               resolve(img.src);
  29.             };
  30.             img.onerror = function() {
  31.               console.log('img load error, ', img.src);
  32.               resolve('');
  33.             };
  34.           } catch (e) {
  35.             console.log(e);
  36.             resolve('');
  37.           }
  38.         });
  39.       })
  40.   );
复制代码
还须要将canvas转换为 base64,实在可以利用这一步将页面中不想让用户更改的元素先转化为canvas,然后再导出
  1. [/code] [code]  let canvasList = domWrap.querySelectorAll('canvas');
  2.   console.log('加载canvas数量: ', canvasList.length);
  3.   await Promise.all(
  4.     Array.from(canvasList).map(tempCanvas => {
  5.       let img = new Image();
  6.       img.setAttribute('crossOrigin', 'anonymous');
  7.       return new Promise((resolve, reject) => {
  8.         try {
  9.           let attr = tempCanvas.getAttribute('data-toword');
  10.           let cvs = contEl.querySelector('[data-toword="' + attr + '"]');
  11.           // 由于该canvas是我再导出之前生成,所以需要单独处理一下
  12.           if (!cvs && tempCanvas.className === 'fishbone_canvas') {
  13.             cvs = tempCanvas;
  14.           }
  15.           if (!cvs) return resolve();
  16.           img.src = cvs.toDataURL('image/jpg', 0.8);
  17.           const w = Math.min(cvs.width, options.maxWidth);
  18.           const h = cvs.height * (w / cvs.width);
  19.           img.width = w;
  20.           img.height = h;
  21.           const parent = tempCanvas.parentNode;
  22.           if (tempCanvas.nextSibling) {
  23.             parent.insertBefore(img, tempCanvas.nextSibling);
  24.           } else {
  25.             parent.appendChild(img);
  26.           }
  27.           tempCanvas.remove();
  28.           resolve('');
  29.         } catch (e) {
  30.           console.log(e);
  31.           resolve('');
  32.         }
  33.       });
  34.     })
  35.   );
复制代码
至此准备工作完全竣事 下面就须要将文件导出为word文档 起首须要引入html-docx-js包
  1. [/code] [code]  const htmlContent = domWrap.innerHTML;
  2.   // 将html数据转化为blob
  3.   const docxBlob = htmlDocx.asBlob(htmlContent, { tableCellMargin: 0 });
  4.   console.log('即将生成文件大小: ', docxBlob.size, (docxBlob.size / 1024 / 1024).toFixed(2) + 'M');
  5.   // 移除iframe内部元素, 方便下次导出
  6.   if (!window.devs) domWrap.remove();
  7.   saveAs(docxBlob, options.fileName + '.docx');
  8.   
  9.   //--------------
  10.   
  11.   function saveAs(blob, fileName) {
  12.   var URL = window.URL || window.webkitURL;
  13.   var a = document.createElement('a');
  14.   fileName = fileName || blob.name || 'download';
  15.   a.download = fileName;
  16.   a.rel = 'noopener';
  17.   a.target = '_blank';
  18.   if (typeof blob === 'string') {
  19.     a.href = blob;
  20.     a.click();
  21.   } else {
  22.     a.href = URL.createObjectURL(blob);
  23.     setTimeout(() => a.click(), 0);
  24.     setTimeout(() => URL.revokeObjectURL(a.href), 2e4); // 20s
  25.   }
  26. }
复制代码


报错题目 
  1. With statements cannot be used with the "esm" output format due to strict mode
复制代码
博主在利用html-docx-js包时发现了报错题目导致整个流程卡在了末了一步,在颠末长达一天的研究之后,发现可以通过script方式引入 第一步 先将包下载到当地 npm install html-docx-js 将包内关键js文件拿到当地

 博主是拿出之后并修改了名字, 然后在index.html文件中导入该js文件 肯定要在head标签内导入 否则是不收效的

自此导出完毕
效果展示




完备代码

toWord.js完备代码
  1. import { formatDate, setStyle, saveAs, convertElTableToHtmlTableWord } from './util';// maxWidth?: number, title?: string, fileName?: string, time?: string, proxyHost?: string, exclude?: arrayexport async function toWord(contEl, option, canvas) {  let options = Object.assign(    {      fileName: `word_${formatDate('yyyy-MM-dd hh:mm:ss')}`, // 导出文件名      maxWidth: 550, // 图片最大宽度,      title: '', // 导出添加一级标题      time: '', // 导出添加文章时间      blob: false, // 返回效果为blob      exclude: [] // 清除元素选择器    },    option || {}  );  if (!contEl) return console.warn('未传入导出元素');  if (typeof contEl === 'string') contEl = document.getElementById(contEl) || document.querySelector(contEl);  // 设置标记, 方便复制样式  Array.from(contEl.querySelectorAll('*')).forEach(item => {    item.setAttribute(      'data-toword',      Math.random()        .toString(32)        .slice(-5)    );  });  if (!window.contIframe) {    window.contIframe = document.createElement('iframe');
  2.     window.contIframe.style = 'display: none; width:100%;';
  3.     document.body.appendChild(window.contIframe);
  4.   }  let cloneEl = contEl.cloneNode(true);  cloneEl.style.width = getComputedStyle(contEl).width;  window.contIframe.contentDocument.body.appendChild(cloneEl);  let domWrap = cloneEl; // .cloneNode(true)  // 1. 删除埋伏的元素, 而且将元素样式设置为行内样式, 方便word辨认  Array.from(domWrap.querySelectorAll('*')).forEach(item => {    if (item.className.includes('fishbone_main') && canvas) {      item.childNodes[0].remove();      item.appendChild(canvas);    }    let attr = item.getAttribute('data-toword');    let originItem = contEl.querySelector('[data-toword="' + attr + '"]');    if (originItem) {      let sty = getComputedStyle(originItem);      if (sty.display == 'none ') return item.remove();      if (sty.opacity === '0') return item.remove();      setStyle(item, sty);    }    if (item.className.includes('department_content_table')) {      const table = convertElTableToHtmlTableWord(item.childNodes[0], '');      item.childNodes[0].remove();      item.innerHTML = table;    }  });  //  // 1.1 删除清除的元素  if (Array.isArray(options.exclude) && options.exclude.length) {    options.exclude.forEach(ext => {      Array.from(domWrap.querySelectorAll(ext)).forEach(item => item.remove());    });  }  // 2. 将图片转为Base64编码, 方便word生存  let imgList = domWrap.querySelectorAll('img');
  5.   console.log('加载图片数量: ', imgList.length);
  6.   await Promise.all(
  7.     Array.from(imgList)
  8.       .filter(x => !x.src.startsWith('data'))
  9.       .map(tempimg => {
  10.         let img = new Image();
  11.         img.setAttribute('crossOrigin', 'anonymous');
  12.         img.src = options.proxyHost ? tempimg.src.replace(location.host, options.proxyHost) : tempimg.src;
  13.         return new Promise((resolve, reject) => {
  14.           try {
  15.             img.onload = function() {
  16.               img.onload = null;
  17.               const cw = Math.min(img.width, options.maxWidth);
  18.               const ch = img.height * (cw / img.width);
  19.               const canvas = document.createElement('CANVAS');
  20.               canvas.width = cw;
  21.               canvas.height = ch;
  22.               const context = canvas.getContext('2d');
  23.               context?.drawImage(img, 0, 0, cw, ch);
  24.               const uri = canvas.toDataURL('image/jpg', 0.8);
  25.               tempimg.src = uri;
  26.               const w = Math.min(img.width, 550, options.maxWidth); // word图片最大宽度
  27.               tempimg.width = w;
  28.               tempimg.height = img.height * (w / img.width);
  29.               console.log('img onload...', options.fileName, img.src, img.width, img.height, cw, ch, w, tempimg.height);
  30.               canvas.remove();
  31.               resolve(img.src);
  32.             };
  33.             img.onerror = function() {
  34.               console.log('img load error, ', img.src);
  35.               resolve('');
  36.             };
  37.           } catch (e) {
  38.             console.log(e);
  39.             resolve('');
  40.           }
  41.         });
  42.       })
  43.   );
  44.   // 3. 将canvas转为Base64编码, 方便word生存  let canvasList = domWrap.querySelectorAll('canvas');  console.log('加载canvas数目: ', canvasList.length);  await Promise.all(    Array.from(canvasList).map(tempCanvas => {      let img = new Image();      img.setAttribute('crossOrigin', 'anonymous');      return new Promise((resolve, reject) => {        try {          let attr = tempCanvas.getAttribute('data-toword');          let cvs = contEl.querySelector('[data-toword="' + attr + '"]');          if (!cvs && tempCanvas.className === 'fishbone_canvas') {            cvs = tempCanvas;          }          if (!cvs) return resolve();          img.src = cvs.toDataURL('image/jpg', 0.8);          const w = Math.min(cvs.width, options.maxWidth);          const h = cvs.height * (w / cvs.width);          img.width = w;          img.height = h;          const parent = tempCanvas.parentNode;          if (tempCanvas.nextSibling) {            parent.insertBefore(img, tempCanvas.nextSibling);          } else {            parent.appendChild(img);          }          tempCanvas.remove();          resolve('');        } catch (e) {          console.log(e);          resolve('');        }      });    })  );  Array.from(contEl.querySelectorAll('*')).forEach(item => {    item.removeAttribute('data-toword');  });  const htmlContent = domWrap.innerHTML;  const docxBlob = htmlDocx.asBlob(htmlContent, { tableCellMargin: 0 });  console.log('即将天生文件巨细: ', docxBlob.size, (docxBlob.size / 1024 / 1024).toFixed(2) + 'M');  // 移除iframe内部元素, 方便下次导出  if (!window.devs) domWrap.remove();  saveAs(docxBlob, options.fileName + '.docx');}
复制代码
 
util完备代码
  1. [/code] [code]export function formatDate(fmt, date) {
  2.   if (!date) date = new Date();
  3.   if (!fmt) fmt = 'yyyy-MM-dd hh:mm:ss';
  4.   if (/(y+)/.test(fmt)) {
  5.     fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
  6.   }
  7.   const o = {
  8.     'M+': date.getMonth() + 1,
  9.     'd+': date.getDate(),
  10.     'h+': date.getHours(),
  11.     'm+': date.getMinutes(),
  12.     's+': date.getSeconds()
  13.   };
  14.   for (const k in o) {
  15.     if (new RegExp(`(${k})`).test(fmt)) {
  16.       const str = o[k] + '';
  17.       fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? str : ('00' + str).substr(str.length));
  18.     }
  19.   }
  20.   return fmt;
  21. }
  22. export function setStyle(ele, sty) {
  23.   if (ele.nodeName.toLowerCase() != 'img') {
  24.     // let sty = getComputedStyle(ele)
  25.     ele.setAttribute(
  26.       'style',
  27.       (ele.getAttribute('style') || '') +
  28.         `;font-size: ${sty.fontSize};color: ${sty.color};font-style: ${sty.fontStyle};line-height: ${sty.lineHeight};font-weight: ${sty.fontWeight};
  29.       font-family: ${sty.fontFamily};text-align: ${sty.textAlign};text-indent: ${sty.textIndent}; margin: ${sty.margin}; padding: ${sty.padding};width: ${sty.width}; height: ${sty.height};
  30.       white-space:${sty.whiteSpace};word-break:${sty.wordBreak};display:${sty.display}`
  31.     );
  32.   }
  33. }
  34. export function saveAs(blob, fileName) {
  35.   var URL = window.URL || window.webkitURL;
  36.   var a = document.createElement('a');
  37.   fileName = fileName || blob.name || 'download';
  38.   a.download = fileName;
  39.   a.rel = 'noopener';
  40.   a.target = '_blank';
  41.   if (typeof blob === 'string') {
  42.     a.href = blob;
  43.     a.click();
  44.   } else {
  45.     a.href = URL.createObjectURL(blob);
  46.     setTimeout(() => a.click(), 0);
  47.     setTimeout(() => URL.revokeObjectURL(a.href), 2e4); // 20s
  48.   }
  49. }
  50. //导出word   e-table转html
  51. export function convertElTableToHtmlTableWord(elTable, title, infoLeft = '', infoRight = '') {
  52.   if (!elTable) return '';
  53.   // 获取 el-table 的表头数据,包括多级表头
  54.   const theadRows = elTable.querySelectorAll('thead tr');
  55.   // 获取 el-table 的数据行
  56.   const tbodyRows = elTable.querySelectorAll('tbody tr');
  57.   let length = getTotalColumnCount(theadRows[0]);
  58.   // 开始构建 HTML 表格的字符串,设置表格整体样式和边框样式
  59.   let htmlTable = '<table style="border-collapse: collapse; border: 1px solid black;"><thead>';
  60.   htmlTable += '<tr>';
  61.   if (infoRight != '') {
  62.     htmlTable += `<th colspan="${Math.floor(length / 2)}" style="width:306.2000pt;padding:0.0000pt 5.4000pt 0.0000pt 5.4000pt ;border-left:1.0000pt solid rgb(255,255,255);
  63. mso-border-left-alt:0.5000pt solid rgb(255,255,255);border-right:1.0000pt solid rgb(255,255,255);mso-border-right-alt:0.5000pt solid rgb(255,255,255);
  64. border-top:1.0000pt solid rgb(255,255,255);mso-border-top-alt:0.5000pt solid rgb(255,255,255);border-bottom:none;
  65. mso-border-bottom-alt:none;background:rgb(255,255,255);text-align: right;">单位:${infoRight}</th>`;
  66.   } else {
  67.     htmlTable += `<th colspan="${Math.floor(length / 2)}" style="width:306.2000pt;padding:0.0000pt 5.4000pt 0.0000pt 5.4000pt ;border-left:1.0000pt solid rgb(255,255,255);
  68. mso-border-left-alt:0.5000pt solid rgb(255,255,255);border-right:1.0000pt solid rgb(255,255,255);mso-border-right-alt:0.5000pt solid rgb(255,255,255);
  69. border-top:1.0000pt solid rgb(255,255,255);mso-border-top-alt:0.5000pt solid rgb(255,255,255);border-bottom:none;
  70. mso-border-bottom-alt:none;background:rgb(255,255,255);text-align: right;"></th>`;
  71.   }
  72.   htmlTable += '</tr>';
  73.   // 处理多级表头
  74.   theadRows.forEach(row => {
  75.     htmlTable += '<tr>';
  76.     const columns = row.querySelectorAll('th');
  77.     columns.forEach(column => {
  78.       const colspan = column.getAttribute('colspan') || '1';
  79.       const rowspan = column.getAttribute('rowspan') || '1';
  80.       htmlTable += `<th colspan="${colspan}" rowspan="${rowspan}" style="border: 1px solid black;">${column.innerText}</th>`;
  81.     });
  82.     htmlTable += '</tr>';
  83.   });
  84.   htmlTable += '</thead><tbody>';
  85.   // 构建数据行
  86.   tbodyRows.forEach(row => {
  87.     htmlTable += '<tr>';
  88.     const cells = row.querySelectorAll('td');
  89.     cells.forEach(cell => {
  90.       if (cell.querySelector('div')) {
  91.         htmlTable += `<td style="border: 1px solid black;">${cell.querySelector('div').innerHTML}</td>`;
  92.       } else {
  93.         htmlTable += `<td style="border: 1px solid black;">${cell.innerText}</td>`;
  94.       }
  95.     });
  96.     htmlTable += '</tr>';
  97.   });
  98.   htmlTable += '</tbody></table>';
  99.   return htmlTable;
  100. }
  101. //获取总列数
  102. function getTotalColumnCount(theadRows) {
  103.   let rowColumns = 0;
  104.   const columns = theadRows.querySelectorAll('th');
  105.   columns.forEach(column => {
  106.     const colspan = column.getAttribute('colspan') || 1;
  107.     rowColumns += Number(colspan);
  108.   });
  109.   return rowColumns;
  110. }
复制代码
 


结语

本文竣事,如果有更好的导出方式接待批评区讨论

原文:

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表