位置: IT常识 - 正文

Vue使用pdf-lib为文件流添加水印并预览(vue显示pdf)

编辑:rootadmin
Vue使用pdf-lib为文件流添加水印并预览

推荐整理分享Vue使用pdf-lib为文件流添加水印并预览(vue显示pdf),希望有所帮助,仅作参考,欢迎阅读内容。

文章相关热门搜索词:vue3.0 pdf,vue实现pdf预览功能,vue pdf,vue 展示pdf文件内容,vue pdf,vue pdf,vue3 pdf,vue 使用pdf.js,内容如对您有帮助,希望把文章链接给更多的朋友!

之前也写过两篇预览pdf的,但是没有加水印,这是链接:Vue使用vue-pdf实现PDF文件预览,使用pdfobject预览pdf。这次项目中又要预览pdf了,要求还要加水印,做的时候又发现了一种预览pdf的方式,这种方式我觉的更好一些,并且还有个要求就是添加水印,当然水印后端也是可以加的,但是后端说了一堆...反正就是要让前端做,在我看来就是借口、不想做,最近也不忙,那我就给他搞出来好了。下面来介绍一下 

首先预览pdf就很简单了,我们只需要通过window.URL.createObjectURL(new Blob(file))转为一个路径fileSrc后,再通过window.open(fileSrc)就可以了,window.open方法第二个参数默认就是打开一个新页签,这样就可以直接预览了,很方便!就是下面这样子:

并且右上角自动给我们提供了下载、打印等功能。 

但是要加上水印的话,可能会稍微复杂一点点,我也百度找了好多,发现好多都是在项目里直接预览的,也就是在当前页面或者一个div有个容器用来专门预览pdf的,然后水印的话也是appendChild到容器div中进行的。这不是我想要的,并且也跟我现在预览的方式不一样,所以我的思路就是如何给文件的那个二进制blob流上加上水印,这样预览的时候也是用这个文件流,以后不想预览了、直接下载也要水印也是很方便的。找来找去找到了pdf-lib库,然后就去https://www.npmjs.com/package/pdf-lib这里去看了下使用示例,看了两个例子,发现好像这个很合适哦,终于一波操作拿下了,这就是我想要的。

我这里添加水印共三种方式,第一种就是可以直接传入文本,将文本添加进去作为水印 ;第二种是将图片的ArrayBuffer传递进去,将图片作为水印;因为第一种方式直接传文本只能传英文,我传入汉字就报错了,npm官网好像也有写,这是不可避免的,所以才有了第三种方式,就是也是传入文本,不过我们通过canvas画出来,然后通过toDataURL转为base64路径,然后再通过XHR去加载该图片拿到图片的Blob,再调用Blob的arrayBuffer方法拿到buffer传递进去作为水印,其实第三种和第二种都是图片的形式,第三种会更灵活一些。下面上代码

1. 安装 npm i pdf-lib2. 引入 //我的需求里只用到这么多就够了,其他的按需引入import { degrees, PDFDocument, rgb, StandardFonts } from 'pdf-lib';3. 添加水印使用  3.1 添加文本水印import { degrees, PDFDocument, rgb, StandardFonts } from 'pdf-lib';// This should be a Uint8Array or ArrayBuffer// This data can be obtained in a number of different ways// If your running in a Node environment, you could use fs.readFile()// In the browser, you could make a fetch() call and use res.arrayBuffer()const existingPdfBytes = ...// Load a PDFDocument from the existing PDF bytesconst pdfDoc = await PDFDocument.load(existingPdfBytes)// Embed the Helvetica fontconst helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)// Get the first page of the documentconst pages = pdfDoc.getPages()const firstPage = pages[0]// Get the width and height of the first pageconst { width, height } = firstPage.getSize()// Draw a string of text diagonally across the first pagefirstPage.drawText('This text was added with JavaScript!', { x: 5, y: height / 2 + 300, size: 50, font: helveticaFont, color: rgb(0.95, 0.1, 0.1), rotate: degrees(-45),})// Serialize the PDFDocument to bytes (a Uint8Array)const pdfBytes = await pdfDoc.save()// For example, `pdfBytes` can be:// • Written to a file in Node// • Downloaded from the browser// • Rendered in an <iframe>3.2 添加图片文本 import { PDFDocument } from 'pdf-lib'// These should be Uint8Arrays or ArrayBuffers// This data can be obtained in a number of different ways// If your running in a Node environment, you could use fs.readFile()// In the browser, you could make a fetch() call and use res.arrayBuffer()const jpgImageBytes = ...const pngImageBytes = ...// Create a new PDFDocumentconst pdfDoc = await PDFDocument.create()// Embed the JPG image bytes and PNG image bytesconst jpgImage = await pdfDoc.embedJpg(jpgImageBytes)const pngImage = await pdfDoc.embedPng(pngImageBytes)// Get the width/height of the JPG image scaled down to 25% of its original sizeconst jpgDims = jpgImage.scale(0.25)// Get the width/height of the PNG image scaled down to 50% of its original sizeconst pngDims = pngImage.scale(0.5)// Add a blank page to the documentconst page = pdfDoc.addPage()// Draw the JPG image in the center of the pagepage.drawImage(jpgImage, { x: page.getWidth() / 2 - jpgDims.width / 2, y: page.getHeight() / 2 - jpgDims.height / 2, width: jpgDims.width, height: jpgDims.height,})// Draw the PNG image near the lower right corner of the JPG imagepage.drawImage(pngImage, { x: page.getWidth() / 2 - pngDims.width / 2 + 75, y: page.getHeight() / 2 - pngDims.height, width: pngDims.width, height: pngDims.height,})// Serialize the PDFDocument to bytes (a Uint8Array)const pdfBytes = await pdfDoc.save()// For example, `pdfBytes` can be:// • Written to a file in Node// • Downloaded from the browser// • Rendered in an <iframe>

canvas那个也是用的这个这个通过图片添加水印 

上面这些都是官网上给的一些示例,我当时看到上面这两个例子,灵感瞬间就来了,然后测试,测试成功没问题,就开始整理代码,封装。结合自己的业务需求和可以复用通用的思想进行封装。下面贴一下最终的成功

3.3 封装previewPdf.jsimport { degrees, PDFDocument, rgb, StandardFonts } from 'pdf-lib';/** * 浏览器打开新页签预览pdf * blob(必选): pdf文件信息(Blob对象)【Blob】 * docTitle(可选): 浏览器打开新页签的title 【String】 * isAddWatermark(可选,默认为false): 是否需要添加水印 【Boolean】 * watermark(必选):水印信息 【Object: { type: string, text: string, image:{ bytes: ArrayBuffer, imageType: string } }】 * watermark.type(可选):类型 可选值:text、image、canvas * watermark.text(watermark.type为image时不填,否则必填):水印文本。注意:如果watermark.type值为text,text取值仅支持拉丁字母中的218个字符。详见:https://www.npmjs.com/package/pdf-lib * watermark.image(watermark.type为image时必填,否则不填):水印图片 * watermark.image.bytes:图片ArrayBuffer * watermark.image.imageType:图片类型。可选值:png、jpg * Edit By WFT */export default class PreviewPdf { constructor({ blob, docTitle, isAddWatermark = false, watermark: { type = 'text', text = 'WFT', image } }) { const _self = this if(!blob) { return console.error('[PDF Blob Is a required parameter]') } if(!isAddWatermark) { // 不添加水印 _self.preView(blob, docTitle) } else { let bytes,imageType if(type == 'image') { if(!image) { return console.error('["image" Is a required parameter]') } bytes = image.bytes imageType = image.imageType } const map = { 'text': _self.addTextWatermark.bind(_self), 'image': _self.addImageWatermark.bind(_self), 'canvas': _self.addCanvasWatermark.bind(_self) } blob.arrayBuffer().then(async buffer => { const existingPdfBytes = buffer const pdfDoc = await PDFDocument.load(existingPdfBytes) let params if(type == 'text') params = { pdfDoc, text, docTitle } if(type == 'image') params = { pdfDoc, bytes, imageType, docTitle } if(type == 'canvas') params = { pdfDoc, text, docTitle } map[type](params) }).catch(e => console.error('[Preview Pdf Error]:', e)) } } // 添加 Text 水印 async addTextWatermark({ pdfDoc, text, docTitle }) { // console.log(StandardFonts, 'StandardFonts-->>') // 字体 const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica) const pages = pdfDoc.getPages() for(let i = 0; i < pages.length; i++) { let page = pages[i] let { width, height } = page.getSize() for(let i = 0; i < 6; i++) { for(let j = 0; j < 6; j++) { page.drawText(text, { x: j * 100, y: height / 5 + i * 100, size: 30, font: helveticaFont, color: rgb(0.95, 0.1, 0.1), opacity: 0.2, rotate: degrees(-35), }) } } } // 序列化为字节 const pdfBytes = await pdfDoc.save() this.preView(pdfBytes, docTitle) } // 添加 image 水印 async addImageWatermark({ pdfDoc, bytes, imageType, docTitle }) { // 嵌入JPG图像字节和PNG图像字节 let image const maps = { 'jpg': pdfDoc.embedJpg.bind(pdfDoc), 'png': pdfDoc.embedPng.bind(pdfDoc) } image = await maps[imageType](bytes) // 将JPG图像的宽度/高度缩小到原始大小的50% const dims = image.scale(0.5) const pages = pdfDoc.getPages() for(let i = 0; i < pages.length; i++) { let page = pages[i] let { width, height } = page.getSize() for(let i = 0; i < 6; i++) { for(let j = 0; j < 6; j++) { page.drawImage(image, { x: width / 5 - dims.width / 2 + j * 100, y: height / 5 - dims.height / 2 + i * 100, width: dims.width, height: dims.height, rotate: degrees(-35) }) } } } // 序列化为字节 const pdfBytes = await pdfDoc.save() this.preView(pdfBytes, docTitle) } // 添加 canvas 水印 addCanvasWatermark({ pdfDoc, text, docTitle }) { // 旋转角度大小 const rotateAngle = Math.PI / 6; // labels是要显示的水印文字,垂直排列 let labels = new Array(); labels.push(text); const pages = pdfDoc.getPages() const size = pages[0].getSize() let pageWidth = size.width let pageHeight = size.height let canvas = document.createElement('canvas'); let canvasWidth = canvas.width = pageWidth; let canvasHeight = canvas.height = pageHeight; const context = canvas.getContext('2d'); context.font = "15px Arial"; // 先平移到画布中心 context.translate(pageWidth / 2, pageHeight / 2 - 250); // 在绕画布逆方向旋转30度 context.rotate(-rotateAngle); // 在还原画布的坐标中心 context.translate(-pageWidth / 2, -pageHeight / 2); // 获取文本的最大长度 let textWidth = Math.max(...labels.map(item => context.measureText(item).width)); let lineHeight = 15, fontHeight = 12, positionY, i i = 0, positionY = 0 while (positionY <= pageHeight) { positionY = positionY + lineHeight * 5 i++ } canvasWidth += Math.sin(rotateAngle) * (positionY + i * fontHeight) // 给canvas加上画布向左偏移的最大距离 canvasHeight = 2 * canvasHeight for (positionY = 0, i = 0; positionY <= canvasHeight; positionY = positionY + lineHeight * 5) { // 进行画布偏移是为了让画布旋转之后水印能够左对齐; context.translate(-(Math.sin(rotateAngle) * (positionY + i * fontHeight)), 0); for (let positionX = 0; positionX < canvasWidth; positionX += 2 * textWidth) { let spacing = 0; labels.forEach(item => { context.fillText(item, positionX, positionY + spacing); context.fillStyle = 'rgba(187, 187, 187, .8)'; // 字体颜色 spacing = spacing + lineHeight; }) } context.translate(Math.sin(rotateAngle) * (positionY + i * fontHeight), 0); context.restore(); i++ } // 图片的base64编码路径 let dataUrl = canvas.toDataURL('image/png'); // 使用Xhr请求获取图片Blob let xhr = new XMLHttpRequest(); xhr.open("get", dataUrl, true); xhr.responseType = "blob"; xhr.onload = res => { const imgBlob = res.target.response // 获取Blob图片Buffer imgBlob.arrayBuffer().then(async buffer => { const pngImage = await pdfDoc.embedPng(buffer) for(let i = 0; i < pages.length; i++) { pages[i].drawImage(pngImage) } // 序列化为字节 const pdfBytes = await pdfDoc.save() this.preView(pdfBytes, docTitle) }) } xhr.send(); } // 预览 preView(stream, docTitle) { const URL = window.URL || window.webkitURL; const href = URL.createObjectURL(new Blob([stream], { type: 'application/pdf;charset=utf-8' })) const wo = window.open(href) // 设置新打开的页签 document title let timer = setInterval(() => { if(wo.closed) { clearInterval(timer) } else { wo.document.title = docTitle } }, 500) }}3.4 调用使用 

我这里将上面文件放在src/utils下 

3.4.1  预览(添加文本水印)Vue使用pdf-lib为文件流添加水印并预览(vue显示pdf)

代码: 

// 引入import PreviewPdf from '@/utils/previewPdf'// script// 实例化进行添加水印 并预览// file.raw 是要预览的pdf文件流 Blobnew PreviewPdf({ blob: file.raw, docTitle: 'window.open docTitle', isAddWatermark: true, // 是否需要添加水印 watermark: { // watermark必填 里面可以不填 type: 'text', text: 'WFT' }})

效果:

3.4.2 预览(添加图片水印) 

 代码:

// 引入import PreviewPdf from '@/utils/previewPdf'// scriptconst watermarkImage = require('@/assets/img/watermark.png') // 水印图片let xhr = new XMLHttpRequest();xhr.open("get", watermarkImage, true);xhr.responseType = "blob";xhr.onload = function (res) { const imgBlob = res.target.response // 水印图片的Blob流 imgBlob.arrayBuffer().then(buffer => { //get arraybuffer // 添加水印 预览 new PreviewPdf({ blob: file.raw, docTitle: file.name, isAddWatermark: true, watermark: { type: 'image', image: { bytes: buffer, imageType: 'png' } } }) })}xhr.send();

效果:

3.4.3 预览(添加文本canvas绘制水印) 

 代码:

// 引入import PreviewPdf from '@/utils/previewPdf'// scriptnew PreviewPdf({ blob: file.raw, docTitle: file.name, isAddWatermark: true, watermark: { type: 'canvas', text: 'WFT-CANVAS' }})

效果: 

因为有些样式调的不太好,就我目前写的我更偏向使用canvas这个,当然都是可以使用的,样式都是可以调整的。 

注意:里面的属性 isAddWatermark 设置为false或者不传该字段将不添加水印,还有watermark这个字段是必须的,穿个空对象也行像watermark:{}这样,因为我上面类中构造方法将参数结构了,可以按需调整。

整体的封装使用就是上面这样子了, 希望可以帮到有需要的伙伴~~~

再给大家一个直接往某个dom元素里面添加水印的方法 

不传参数默认为整个body添加水印 

function waterMark(text = 'WFT', dom = document.body) { if (document.getElementById('waterMark')) return // 旋转角度大小 var rotateAngle = Math.PI / 6; // labels是要显示的水印文字,垂直排列 var labels = new Array(); labels.push(text); let pageWidth = dom.clientWidth let pageHeight = dom.clientHeight let canvas = document.createElement('canvas'); let canvasWidth = canvas.width = pageWidth; let canvasHeight = canvas.height = pageHeight; var context = canvas.getContext('2d'); context.font = "15px Arial"; // 先平移到画布中心 context.translate(pageWidth / 2, pageHeight / 2 - 250); // 在绕画布逆方向旋转30度 context.rotate(-rotateAngle); // 在还原画布的坐标中心 context.translate(-pageWidth / 2, -pageHeight / 2); // 获取文本的最大长度 let textWidth = Math.max(...labels.map(item => context.measureText(item).width)); let lineHeight = 15, fontHeight = 12, positionY, i i = 0, positionY = 0 while (positionY <= pageHeight) { positionY = positionY + lineHeight * 5 i++ } canvasWidth += Math.sin(rotateAngle) * (positionY + i * fontHeight) // 给canvas加上画布向左偏移的最大距离 canvasHeight = 2 * canvasHeight for (positionY = 0, i = 0; positionY <= canvasHeight; positionY = positionY + lineHeight * 5) { // 进行画布偏移是为了让画布旋转之后水印能够左对齐; context.translate(-(Math.sin(rotateAngle) * (positionY + i * fontHeight)), 0); for (let positionX = 0; positionX < canvasWidth; positionX += 2 * textWidth) { let spacing = 0; labels.forEach(item => { context.fillText(item, positionX, positionY + spacing); spacing = spacing + lineHeight; }) } context.translate(Math.sin(rotateAngle) * (positionY + i * fontHeight), 0); context.restore(); i++ } let dataUrl = canvas.toDataURL('image/png'); let waterMarkPage = document.createElement('div'); waterMarkPage.id = "waterMark" let style = waterMarkPage.style; style.position = 'fixed'; style.overflow = "hidden"; style.left = 0; style.top = 0; style.opacity = '0.4'; style.background = "url(" + dataUrl + ")"; style.zIndex = 999; style.pointerEvents = "none"; style.width = '100%'; style.height = '100vh'; dom.appendChild(waterMarkPage);}
本文链接地址:https://www.jiuchutong.com/zhishi/295330.html 转载请保留说明!

上一篇:黄石国家公园的美洲野牛,美国怀俄明州 (© Gerald Corsi/Getty Images)(黄石国家公园的英文翻译)

下一篇:对于<router-view>标签的理解(对于异步电动机国家标准规定3kw)

  • 测温枪测不出温度是什么原因(测温枪测不出温度显示lo)

    测温枪测不出温度是什么原因(测温枪测不出温度显示lo)

  • 网络主要划分为(网络类型划分为)

    网络主要划分为(网络类型划分为)

  • 抖音app相册里的视频怎么发到抖音里(抖音相册里面的东西怎么删掉)

    抖音app相册里的视频怎么发到抖音里(抖音相册里面的东西怎么删掉)

  • 苹果6微信暗黑模式怎么设置(苹果微信暗黑模式黑色不均匀怎么办)

    苹果6微信暗黑模式怎么设置(苹果微信暗黑模式黑色不均匀怎么办)

  • 抖音小店开通条件(抖音小店开通条件及保证金)

    抖音小店开通条件(抖音小店开通条件及保证金)

  • 移动硬盘和u盘相比最大的优势是(移动硬盘和u盘的使用寿命)

    移动硬盘和u盘相比最大的优势是(移动硬盘和u盘的使用寿命)

  • 为什么手机电量一直不变(为什么手机电量不能充到100%)

    为什么手机电量一直不变(为什么手机电量不能充到100%)

  • 华为荣耀9x充电器型号(华为荣耀9x充电器多少钱)

    华为荣耀9x充电器型号(华为荣耀9x充电器多少钱)

  • 三星f7000折叠手机是双卡双待吗(三星f7000折叠手机参数)

    三星f7000折叠手机是双卡双待吗(三星f7000折叠手机参数)

  • 抖音注销7天内可以登录么(抖音注销7天内登录还会被注销么)

    抖音注销7天内可以登录么(抖音注销7天内登录还会被注销么)

  • 转转为什么提现要七天(转转为什么提现要三天)

    转转为什么提现要七天(转转为什么提现要三天)

  • mlc是什么接口(micin是什么接口)

    mlc是什么接口(micin是什么接口)

  • 华为应用市场安装失败(华为应用市场安装包在哪个目录下)

    华为应用市场安装失败(华为应用市场安装包在哪个目录下)

  • 进程有哪三种状态(进程的三种基本状态及其含义)

    进程有哪三种状态(进程的三种基本状态及其含义)

  • word强调文字颜色怎么弄(word强调文字颜色4)

    word强调文字颜色怎么弄(word强调文字颜色4)

  • 哔咔漫画苹果怎么下(哔咔漫画苹果怎么用)

    哔咔漫画苹果怎么下(哔咔漫画苹果怎么用)

  • iphone11pro怎么使用缩放功能(iphone11pro怎么使用nfc)

    iphone11pro怎么使用缩放功能(iphone11pro怎么使用nfc)

  • 华为怎样取消健康使用手机(如何关闭华为健康)

    华为怎样取消健康使用手机(如何关闭华为健康)

  • 华为mate30可以用5g吗(华为mate30可以用typec耳机吗)

    华为mate30可以用5g吗(华为mate30可以用typec耳机吗)

  • word文档网络选项卡在哪里(文档网络设置在哪2019)

    word文档网络选项卡在哪里(文档网络设置在哪2019)

  • 荣耀手环a2如何关机(荣耀手环a2功能)

    荣耀手环a2如何关机(荣耀手环a2功能)

  • 苹果11怎么插耳机(苹果11怎么插耳机蓝牙)

    苹果11怎么插耳机(苹果11怎么插耳机蓝牙)

  • 4g虚拟卡是什么意思(虚拟4g信号)

    4g虚拟卡是什么意思(虚拟4g信号)

  • 华为手机后面的进网许可可以撕吗(华为手机后面的标签怎么弄掉)

    华为手机后面的进网许可可以撕吗(华为手机后面的标签怎么弄掉)

  • 苹果切换控制无限循环(苹果切换控制无效)

    苹果切换控制无限循环(苹果切换控制无效)

  • 最全的PHPCMS漏洞总结(php5.6漏洞)

    最全的PHPCMS漏洞总结(php5.6漏洞)

  • 帝国cms怎么制作栏目(帝国cms怎么用)

    帝国cms怎么制作栏目(帝国cms怎么用)

  • 金税啥意思
  • 营业账簿印花税怎么交
  • 金蝶销售订单和采购订单关联
  • 买金税盘怎么做账
  • 银行基本户可以变更成一般户吗
  • 免征土地增值税
  • 外币结汇怎么做账
  • 土地不动产登记证办理流程
  • 拓展培训费如何开票
  • 增值税专用发票和普通发票的区别
  • 按次申报是什么意思
  • 个人应纳税所得额怎么算
  • 销售结算款扣款怎么记账?
  • 公司股东和自然人的区别
  • 预提保障金和交税的区别
  • 合同取得成本计入当期损益吗
  • 对外捐赠会计和税法差异调整
  • 房屋维修基金怎么申请使用
  • 充积分送手机
  • win11电脑下载的软件桌面没有图标怎么办
  • 方正电脑如何做系统
  • windows11 微软
  • windows11怎么设置锁屏时间
  • PHP:Memcached::deleteMulti()的用法_Memcached类
  • 个人之间股权转让印花税怎么交
  • 工程施工与工程结算会计科目
  • vue怎么拿到后端数据
  • 发票管理的基础环节
  • 如何做好记账会计
  • 呆账核销分录
  • fio命令详解
  • 股东投资款怎么存入公司
  • 转账不同银行同城转账手续费多少
  • 微信收款怎么做会计分录
  • 大额的维修费用怎么摊销
  • 顺丰快递电子运单打印模板
  • Linux下MySQL数据库的主从同步复制配置
  • discuz设置门户
  • phpcms怎么样
  • 福利费要分部门吗
  • 非营利企业的劳动力需求有哪些特点
  • 利息收入为负数的原因
  • mysql select语句操作实例
  • 软件服务费计入管理费用哪个明细
  • 工程附加税税率
  • 企业收到退回的银行汇票多余款项
  • 未确认递延所得税资产的可抵扣亏损到期年度表
  • 进口产品没有发票怎么入账
  • 律师事务所日语助理
  • 物流公司挂靠车辆如何做账?
  • 企业去银行
  • 个人交五险一金多少钱一个月
  • 销售如果对待不同客户
  • 一般要做代理,授权书有什么用
  • 企业什么情形必须签无固定期限合同
  • 长期股权投资损益调整怎么回事
  • windows 10 build 9888
  • win7guest账户有密码吗
  • 怎样让xp系统变得更加流畅
  • win8 开机
  • macbook qq截图存在哪
  • win7休眠模式在哪
  • linux系统怎么修改文件里的参数
  • win8 embedded
  • win7系统管理员密码
  • 3d引擎开发
  • unet遥感图像分割
  • jQuery Ajax File Upload实例源码
  • jquery解析html文本
  • scrollview用法
  • 详细分析使用AngularJS编程中提交表单的方式
  • jquery实现点击按钮
  • android设计模式的应用场景
  • JavaScript的removeChild()函数用法详解
  • python 判断字符串编码
  • 国税和地税比例
  • 惠州市国家税务局稽查局局长
  • 文山市税务
  • 浙江职称评审网官网
  • 北医三院预约号最晚几点取
  • 免责声明:网站部分图片文字素材来源于网络,如有侵权,请及时告知,我们会第一时间删除,谢谢! 邮箱:opceo@qq.com

    鄂ICP备2023003026号

    网站地图: 企业信息 工商信息 财税知识 网络常识 编程技术

    友情链接: 武汉网站建设