背景
和hqin数字人项目,该项目采用 vite + vue3 技术开发的h5项目,放公众号打开。录音和保存视频跳转小程序,调用小程序的接口。对方要求代码风格使用 ESLint + Airbnb风格,然后,再设置一下gitHooks,提交git时,校验一下代码规范。项目引入了神策埋点和阿里arms监控。
技术点
1. ESLint + Airbnb 代码风格
1.1 安装与配置
参考文档, 综合分析普遍使用的 eslint 依赖包后,推荐下面这一整套依赖包:
推荐必配 9 个依赖包:
- eslint: ESLint 是一种用于识别和报告在 ECMAScript/JavaScript 代码中发现的模式的工具。 必须首先安装这个依赖包,因为其他的依赖包建立在它之上。
- eslint-define-config:为 .eslintrc.js 文件提供 defineConfig 功能。
- eslint-plugin-vue:Vue.js 的官方 ESLint 插件,它允许我们使用 ESLint 检查文件中的 Vue 代码。
- eslint-plugin-prettier: 将 Prettier 作为 ESLint 规则运行。 如果你想禁用与代码格式相关的所有其他 ESLint 规则,并且仅启用检测潜在错误的规则,则此插件效果最佳。 如果你安装了 eslint 那么你应该会遇到 eslint 规则和 prettier 规则冲突。可用 eslint-config-prettier 解决 eslint 规则和 prettier 规则的冲突。
- eslint-config-prettier:关闭所有不必要或可能与 Prettier 冲突的规则。
- vue-eslint-parser:文件的 ESLint 自定义解析器.vue。
- @typescript-eslint/parser:一个 ESLint 解析器,它利用TypeScript ESTree允许 ESLint 对 TypeScript 源代码进行 lint。
- @typescript-eslint/eslint-plugin:一个为 TypeScript 代码库提供 lint 规则的 ESLint 插件。
- prettier: 代码格式化程序。 Prettier 2.5 发布:支持 TypeScript 4.5 新语法和 MDX v2 注释语法
推荐可选 2 个依赖包:
-
eslint-plugin-import:支持 ES2015+ (ES6+) import / export 语法的规则,并防止文件路径和导入名称拼写错误的问题。
-
eslint-config-airbnb-base: 这个包提供了 Airbnb 的基本 JS .eslintrc(没有 React 插件)作为可扩展的共享配置。 安装 eslint-config-airbnb-base 之前请先安装 eslint-plugin-import。
因为只有开发阶段需要 eslint,所以将 eslint 的这些依赖添加到开发阶段的依赖 devDependencies 中即可。
步骤一:安装
//npm i -D eslint eslint-config-airbnb-base eslint-plugin-import eslint-plugin-vue
npm i -D eslint
npx eslint --init //选择
步骤二: 添加 Vite 运行的时候自动检测 eslint 规范
npm install -D vite-plugin-eslint
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import eslintPlugin from 'vite-plugin-eslint'
export default defineConfig({
plugins: [
vue(),
// 增加下面的配置项,这样在运行时就能检查 eslint 规范
eslintPlugin({
include: ['src/**/*.js', 'src/**/*.vue', 'src/*.js', 'src/*.vue']
})
]
})
步骤三:配置 eslintrc.js 文件
3.1:安装 babel 插件
3.2:配置 Eslint 基本配置
module.exports = {
env: {
browser: true,
node: true
},
extends: [
'airbnb-base',
'plugin:vue/vue3-recommended' // 使用插件支持vue3
],
// 页面引用"@"报错的配置
settings: {
'import/extensions': [
'.js', // 如果你只使用纯 JavaScript 文件
'.ts', // 如果你在项目中使用 TypeScript
'.vue', // 如果你在项目中使用 Vue 单文件组件
],
'import/resolver': {
alias: {
map: [['@', './src']], // 设置别名解析
extensions: ['.js', '.ts', '.vue'], // 设置支持的扩展名
},
},
},
parserOptions: {
parser: '@babel/eslint-parser',
sourceType: 'module',
ecmaVersion: 12,
allowImportExportEverywhere: true, // 不限制eslint对import使用位置
ecmaFeatures: {
modules: true,
legacyDecorators: true
},
requireConfigFile: false // 解决报错:vue--Parsing error: No Babel config file detected for
},
plugins: [
'vue'
],
// 省略其他配置
globals: {
wx: true,
},
rules: {
...
}
}
3.3:配置 Eslint rules 规则
'semi': ['warn', 'never'], // 禁止尾部使用分号
'no-console': 'warn', // 禁止出现console
'no-debugger': 'warn', // 禁止出现debugger
'no-duplicate-case': 'warn', // 禁止出现重复case
'no-empty': 'warn', // 禁止出现空语句块
'no-extra-parens': 'warn', // 禁止不必要的括号
'no-func-assign': 'warn', // 禁止对Function声明重新赋值
'no-unreachable': 'warn', // 禁止出现[return|throw]之后的代码块
'no-else-return': 'warn', // 禁止if语句中return语句之后有else块
'no-empty-function': 'warn', // 禁止出现空的函数块
'no-lone-blocks': 'warn', // 禁用不必要的嵌套块
'no-multi-spaces': 'warn', // 禁止使用多个空格
'no-redeclare': 'warn', // 禁止多次声明同一变量
'no-return-assign': 'warn', // 禁止在return语句中使用赋值语句
'no-return-await': 'warn', // 禁用不必要的[return/await]
'no-self-compare': 'warn', // 禁止自身比较表达式
'no-useless-catch': 'warn', // 禁止不必要的catch子句
'no-useless-return': 'warn', // 禁止不必要的return语句
'no-mixed-spaces-and-tabs': 'warn', // 禁止空格和tab的混合缩进
'no-multiple-empty-lines': 'warn', // 禁止出现多行空行
'no-trailing-spaces': 'warn', // 禁止一行结束后面不要有空格
'no-useless-call': 'warn', // 禁止不必要的.call()和.apply()
'no-var': 'warn', // 禁止出现var用let和const代替
'no-delete-var': 'off', // 允许出现delete变量的使用
'no-shadow': 'off', // 允许变量声明与外层作用域的变量同名
...
步骤四:启动项目,测试效果
"scripts": {
// eslint --fix自动修复所有格式问题
"lint": "eslint --fix --ext .js,.vue src",
},
Vue 使用 Eslint 和 Prettier 并采用 Airbnb 规范
1.2 如何设置一下gitHooks,提交git时,校验一下代码规范
方法一:
当我们创建了一个Git的本地仓库后,项目的根目录下看到一个.git文件夹,在文件夹下的hooks目录下有很多的.sample 为后缀的钩子文件,如下所示。这些文件就是我们要改造的脚本,这个以.sample后缀结束的脚本文件是不会执行的,如果需要执行,我们需要去掉.sample后缀。参考文档1、
参考文档2
方法二:
配置husky(官网): husky是Git hooks工具,可以防止使用Git hooks不好的commit或者push。使用方法如下:
- 安装husky:
npm install --save-dev husky; - 初始化:
npx husky init:可以看到在在我们项目的根目录出现了一个.husky文件夹,.husky/目录下新增了一个名为pre-commit的shell脚本。也就是说在在执行git commit命令时会先执行 pre-commit 这个脚本。 - 修改.husky/pre-commit文件的执行语句:
亦可如下配置:
如何在vscode中把换行的默认方式改为LF?
设置 -> 搜索files:eol进行设置 -> 选择:\n
\n 对应的是 LF
\r\n对应的是CRLF
2. 配置不同环境
项目分三个环境:本地开发环境(developmen)、测试环境(uat)、生成环境(production)。域名和接口不统一且不同环境也不一样,所以通过加几个不同环境的.env文件,在不同的环境调用。可通过import.meta.env.MODE获取所处环境。参考vite官网配置。
注意:为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理的代码。
import.meta.env.MODE == 'development'; // 本地开发环境
import.meta.env.MODE == 'staging'; // uat测试环境
import.meta.env.MODE == 'production'; // 生产环境
.env 文件:本地运行环境
VITE_BASE = "./"
# 【神策】上报地址 (uat环境)
VITE_SENSOES_SERVER_URL = "https://sensorsdata.xxxxx.com/sa?project=default"
# 多域名打通(uat环境)
VITE_IHOMEPRO_WEB_DOAMIN = "ihomepro-uat.xxxxx.com"
.env.staging 文件:测试环境
NODE_ENV = test
# vite配置base
VITE_BASE = "/digital-person"
# h5跳转小程序的版本类型:所需跳转的小程序版本 env-version(正式版release、开发版develop、体验版trial)
VITE_MINI_ENV = "trial"
# 【申朴】接口域名 (uat环境)
VITE_API_DOAMIN = "https://hq-test.xxxxxxx.com/api"
# 【hqin】接口域名 (uat环境)
VITE_HQAPI_DOAMIN = "https://hqins-api-uat.xxxxx.com/api"
# 【神策】上报地址 (uat环境)
VITE_SENSOES_SERVER_URL = "https://sensorsdata.xxxx.com/sa?project=default"
# 多域名打通(uat环境)
VITE_IHOMEPRO_WEB_DOAMIN = "ihomepro-uat.xxxx.com"
.env.production 文件:生成环境
NODE_ENV = production
# vite配置base
VITE_BASE = "/digital-person"
# h5跳转小程序的版本类型:所需跳转的小程序版本 env-version(正式版release、开发版develop、体验版trial)
VITE_MINI_ENV = "release"
#【申朴】接口域名 (生产环境)
VITE_API_DOAMIN = "https://hq.xxxxxx.com/api"
#【hqin】接口域名 (生产环境)
VITE_HQAPI_DOAMIN = "https://hqins-api.xxxxx.com/api"
# 【神策】上报地址 (生产环境)
VITE_SENSOES_SERVER_URL = "https://sensorsdata.xxxxx.com/sa?project=HQ_Data_Analytics"
# 多域名打通(生产环境)
VITE_IHOMEPRO_WEB_DOAMIN = "ihomepro.xxxxx.com"
调用方法:
/*
**【hqin】接口域名
*/
export const requestHqinDomin = import.meta.env.VITE_HQAPI_DOAMIN || '';
/*
**【申扑】接口域名
*/
export const requestDomin = import.meta.env.VITE_API_DOAMIN || '';
/*
**【神策】上报地址
*/
export const sensorsServerUrl = import.meta.env.VITE_SENSOES_SERVER_URL || '';
// 多域名打通
export const iHomeProWebDomin = import.meta.env.VITE_IHOMEPRO_WEB_DOAMIN || '';
/*
** 【wx-open-launch-weapp】
** h5跳转小程序的版本类型:所需跳转的小程序版本 env-version:
** @param:合法值为:正式版release、开发版develop、体验版trial(支持的微信版本:iOS 8.0.18及以上、Android 8.0.19及以上)
*/
export const getMiniEnvVesion = () => import.meta.env.VITE_MINI_ENV;
运行和打包配置:
3. h5跳转小程序
3.1 使用方法
官方文档,步骤:
-
绑定域名
登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。
-
引入js文件
在需要调用JS接口的页面引入如下JS文件:res.wx.qq.com/open/js/jwe… (支持https)
如需进一步提升服务稳定性,当上述资源不可访问时,可改访问:res2.wx.qq.com/open/js/jwe… (支持https))
-
通过config接口注入权限验证配置并申请所需开放标签
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印
appId: '', // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳
nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名
jsApiList: [], // 必填,需要使用的JS接口列表
openTagList: ['wx-open-launch-weapp'] // 可选,需要使用的开放标签列表,例如['wx-open-launch-app']
});
- 通过ready接口处理成功验证
wx.ready(function () {
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中
});
- 使用开放标签 wx-open-launch-weapp
官网这种写法只是针对vue2,在vue3中会报错。
<div class="uploader-audio">
<img src="https://***.png" alt="" />
<div class="audios-txt">录制配音</div>
<wx-open-launch-weapp
id="launch-btn"
appid="wx6abc************"
:path="miniUrl"
:env-version="miniEnv"
style="position: absolute;left: 0;top: 0;width: 100%;height: 100%;"
>
<component is="script" type="text/wxtag-template">
<div class="audios-txt">录制配音</div>
<component is="style">
.audios-txt {
text-align: center;
margin-top: 2px;
color: #018aff;
font-weight: bold;
font-size: 13px;
}
</component>
</component>
</wx-open-launch-weapp>
</div>
这里 使用 is=“‘script’” 来转义 script 标签, 在vue2中, component 是可以直接使用 script 标签的,在vue3中已经不支持直接使用 script 标签了(直接报错),所有这里用 is=“‘script’” 来转义 。
运行报警告,解决办法:在vite.config.js文件进行配置
样式设置:建议写成内联样式,wx-open-launch-weapp⽤来做这个的遮罩就好。
3.2 分享卡片标题描述未获取到
问题描述:从分享页进去再次分享页面,分享出来的卡片信息获取不成功。
导致原因:从首页进去详情页分享出来的页面正常是因为页面一进去就注入wxjssdk,等到了详情页肯定注入成功;但是直接点击分享出来的详情页存在异步请求(详情页接口和jssdk获取参数接口),可能不成功,
解决办法:给获取详情页信息接口添加一个延迟操作。等注入成功后再请求信息,再设置卡片信息。
const pageStart = () => {
isShare.value = route.query.isshare || false;
if (isShare.value) {
setTimeout(() => {
getDetail();
}, 500);
} else {
// 安卓跳转不成功出现一串代码问题:手动注册一次
// wxShare.register();
// getDetail();
wxShare.register('', {
success: () => {
getDetail();
},
fail: () => {
// 错误,是否触发重新注册
},
});
}
};
3.3 安卓跳转小程序出现一串代码问题
问题描述:点击【上传录音】按钮跳转小程序,出现一串代码。
导致原因:注入时机问题(页面可能渲染完成了,微信jssdk还没注入)。
解决办法:等注入成功后再请求接口。代码如上
4. 视频详情页访问埋点
背景:视频制作者分享视频出去,访客打开页面后,进行埋点。因为也要统计访客的访问时长,所以需在访客离开页面的时候调用访问接口。
// 处理页面可见性变化的回调函数(ios存在兼容问题,点击X关闭按钮不触发visibilitychange)
// const handleVisibilityChange = async() => {
// // console.log("document.visibilityState----", document.visibilityState)
// if (document.visibilityState === 'hidden') {
// // 页面不可见,执行离开页面的操作,比如调用埋点接口
// await postVideoLog();
// document.removeEventListener('visibilitychange', handleVisibilityChange);
// }
// }
onBeforeUnmount(async() => {
// // if(!isShare.value) return
// // await postVideoLog();
// // 解绑事件监听
// // document.removeEventListener('visibilitychange', handleVisibilityChange);
// lifecycle.removeEventListener('statechange', handleVisibilityChange);
ViewRecordUtil.clearStillTime();
});
4.1 关于Page lifecycle
W3C 最新的规范 Page Lifecycle。Page Lifecycle API 试图通过以下方式解决性能瓶颈:
- 引入标准化的页面生命周期状态和概念;
- 定义新的、由系统发起的变更状态,允许浏览器在 Tab 隐藏或者不活跃时限制其占用的系统资源;
- 创建新的 API 和 Event 来让开发者捕获和响应由系统引起的状态变化;
对于老式浏览器可以使用谷歌开发的兼容库 PageLifecycle.js进行管理。page-lifecycle.js:
4.2 navigator.sendBeacon埋点问题
一开始直接在onBeforeUnMount里面调用该方法,发现数据丢失严重,定位发现关闭页面根本不会走到onBeforeUnmount/onUnmount生命周期里面。于是换成使用navigator.sendBeacon,安装 npm install page-lifecycle;
import lifecycle from 'page-lifecycle';
const handleVisibilityChange = async (event) => {
const { oldState, newState } = event;
if (oldState == 'passive' && newState == 'hidden') { // 关闭页面时候触发
ViewRecordUtil.clearStillTime();
await postVideoLog();
lifecycle.removeEventListener('statechange', handleVisibilityChange);
}
};
onMounted(() => {
// 【进入分享页】且【非本人】打开添加【statechange】事件
if (isShare.value && !isMakerSelf.value) {
// logLocalStorageVal('sendBeacon_state', {name: '非本人'});
lifecycle.addEventListener('statechange', handleVisibilityChange);
}
});
const navigatorSendBeacon = (params) => {
const logsApi = `${requestDomin}/visit_logs`;
//= ===测试=====
// logsApi = requestDomin + '/api/visit_logs';
const formData = new FormData();
Tool.convertDataToFormData(params, formData);
navigator.sendBeacon(logsApi, formData);
// const headers = {
// type: 'application/json',
// };
// let beParams= filterParams(params);
// // navigator.sendBeacon(logsApi, new URLSearchParams(beParams));
// const blob = new Blob([JSON.stringify(beParams)], headers);
// // logLocalStorageVal('sendBeacon_state', {name: '发送sendBeacon请求'});
// navigator.sendBeacon(logsApi, blob);
};
/*
** 视频访问记录
** @isOpenPolling 是否调用轮询记录
*/
const postVideoLog = async (isOpenPolling) => {
// 定义两个日期
const date2 = moment();
// 计算两个日期之间的秒数差
const secondsDiff = date2.diff(date1, 'seconds');
date1 = moment();
try {
// 轮询最长时间(到点清定时器): 时长 + 10分钟
const pollingMaxTime = tmplData.value.duration + 10 * 60;
let params = {
dataType: 1, // 类型1:视频
duration: secondsDiff > 0 ? secondsDiff : 1,
pollingMaxTime,
// ====测试====
// pollingMaxTime: 10,
};
params = Object.assign(visitParams, params);
if (isOpenPolling) {
// 访问埋点
params.startLoop = true;
await ViewRecordUtil.reportVisit(params);
} else {
const postParams = cloneDeep(params);
postParams.sync_content = {
url: postParams.url,
nickName: postParams.nickName,
headPhoto: postParams.headPhoto,
};
delete postParams.url;
delete postParams.nickName;
delete postParams.headPhoto;
delete postParams.pollingMaxTime;
if (postParams.visitorId == 0) return;
navigatorSendBeacon(postParams);
}
} catch (err) {
toastError(err);
}
};
问题一: 安卓左滑 & X 关闭,以及ios左滑关闭页面都能监听到页面状态改变,但是ios点击左上角X关闭监听不到页面状态变化。(解决方案:采用3秒轮询调用接口上报)。
问题二:上报的时长是3的倍数,为了使数据更准确,关闭页面仍然调用navigatorSendBeacon()函数。一开始接口参数都是字符串,传的是URLSearchParams类型的参数;接口调整后参数有对象,于是改成了Blob类型的参数,存在的问题是关闭页面接口会调用2次(一次是跨域options请求)导致上报失败,后面查阅得知FormData类型的参数不存在options跨域请求,于是换成FormData传参。
4.2 post请求FormData二级参数格式封装函数:
// 递归函数,将多层结构的数据转换为 FormData 对象
function convertDataToFormData(data, formData, parentKey) {
forEach(data, (value, key) => {
const newKey = parentKey ? `${parentKey}[${key}]` : key;
if (isObject(value)) {
convertDataToFormData(value, formData, newKey);
} else if (value !== '') {
formData.append(newKey, value);
}
});
}
5. 实现多图片链接下载到本地的脚本
旧项目图片链接域名要改,首先要把所有的图片下载到本地后重新上传,手动一张张下载慢,写一个脚本根据收集的链接数组程序自动下载。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>下载图片</title>
</head>
<body>
<button id="downloadBtn">下载图片</button>
<script>
// 图片链接数组
var imageUrls = [
'https://static.jiabaometa.com/hengqin_test/3/2024-02-29/1709178303197487.png',
'https://static.jiabaometa.com/hengqin_test/3/2024-02-29/1709178853268432.png',
];
// 下载图片函数
function downloadImage(url, index) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onload = function() {
var reader = new FileReader();
reader.onload = function() {
// 创建临时链接并下载图片
var downloadLink = document.createElement('a');
downloadLink.href = reader.result;
downloadLink.download = 'image' + index + '.jpg';
downloadLink.click();
};
reader.readAsDataURL(xhr.response);
};
xhr.send();
}
// 点击事件处理
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('downloadBtn').addEventListener('click', function() {
for (var i = 0; i < imageUrls.length; i++) {
downloadImage(imageUrls[i], i + 1); // 传入图片链接和索引
}
});
});
</script>
</body>
</html>
PC后台管理(vue2+elementUI)
1. vue引入自定义标签的 Textarea 组件
1.1 需求
数字人计划书模板,文案后台配置,动态信息插入字段动态获取,实现文本输入框,点击可编辑,在文本任何处可以选择插入标记对应的模版词组(不可编辑),在删除这个模版词组的时候,可以整个删除。引入vue-tag-textarea插件实现。参考使用。
github地址:github.com/wisewrong/v…
实现后的效果:
接口返回的数据:
实现代码:
<div class="page-list">
<div class="page-item" v-for="(item, index) in pageList" :key="index">
<div class="flex item-head">
<div class="page-title">P{{item.page }}-{{ item.title }}</div>
</div>
<w-textarea v-model="item.content" :tools="toolParams" ref="textAreaEle" maxlength="200" @add="handleAdd(index)">
<!-- 【插入参数】级联模板 -->
<div class="cascader-dialog" v-if="item.showInsetDialog">
<el-cascader-panel
:props="{
label: 'name',
value: 'name',
}"
:options="fieldOptions"
@change="handleChange"></el-cascader-panel>
</div>
</w-textarea>
</div>
</div>
将后台放回的字段code替换成对应中文并加上标注标签展示在页面上,提交的时候再将中文转回英文字段code并去除标签提交。处理方法:
/*
** 匹配大括号里的内容
** @【params】 type:1-页面数据展示处理(code->中文名称);2-提交数据处理(中文名称->code)
*/
regexContent({ regex, pages, type = 1}) {
pages.forEach(ele => {
if(type == 1) {
ele.showInsetDialog = false;
}
const timestamp = new Date().getTime();
// $& 插入匹配的子串
// ele.content = ele.content.replace(regex, `<wise id='r${timestamp}'>$&</wise>`);
ele.content = ele.content.replace(regex, (match, p1) => {
// 将英文属性名翻译为中文
const valProperty = this.getValByKey(p1, type);
// 返回替换后的字符串
let eleVal = valProperty ? `<wise id="r${timestamp}d${parseInt(Math.random()*1000)}">${valProperty}</wise>` : '';
if (type == 2) {
eleVal = `${valProperty}`;
}
return eleVal
});
});
return pages
},
getDetail() {
...
// 定义正则表达式,匹配大括号内的内容
const regex = /{([^}]+)}/g;
this.regexContent({ regex, pages, type: 1 });
...
},
postData() {
const regex = /<wise id=".*?">{(.*?)}<\/wise>/g;
let params = oCopy(this.pageList);
params = this.regexContent({ regex, pages: params, type: 2 });
...
},
1.2 问题:插入标签后光标位置问题
问题描述:点击插入参数后,光标就移开插入位置了,这时连续插入参数,则会默认添加在开头。
解决办法:找到源码中对应的代码方法,在插入新标签文本后,将光标设置到新插入内容的后面,可以继续插入标签文本或者直接输入。
2. 前端导出table列表的excel表
file.js文件封装方法:
const getExcelList = async (url, obj) => {
const res = await $ajax.get(url, {
params: obj,
});
return res.data.data;
};
const getAllData = async (url, params, callback) => {
try {
let page = 1;
params.page = page;
const myData = await getExcelList(url, params);
let total = myData.total;
let allData = callback ? callback(myData.data) : myData.data;
while (allData.length < total) {
page++;
params.page = page;
const currentData = await getExcelList(url, params);
const data = callback ? callback(currentData.data) : currentData.data;
allData = allData.concat(data);
}
return allData;
} catch(err) {
}
}
/*
** 导出excel(前端处理)
** @params: url 【String】- 请求链接
** obj 【Object】- 请求参数
** thObj 【Object】- 导出的表头名
** fileName 【String】- 文件名
** callback 【Function】 -数据处理
*/
export const exportToExcel = async ({url, params, thObj, fileName, callback}) => {
const allData = await getAllData(url, params, callback);
const arrTitle = [];
for(let key in thObj) {
arrTitle.push(thObj[key]);
}
// 将数据格式化为二维数组
const dataArray = allData.map(item => {
const arrVal = [];
for(let key in thObj) {
arrVal.push(item[key]);
}
return arrVal
});
dataArray.unshift(arrTitle);
// console.log('dataArray----', dataArray)
// 创建 Excel 文件
const csvContent = "data:text/csv;charset=utf-8," + dataArray.map(row => row.join(",")).join("\n");
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
let file_name = fileName ? fileName: '列表';
let deFileName = moment().format('YYYYMMDDHHmm');
link.setAttribute("href", encodedUri);
link.setAttribute("download", `${file_name}${deFileName}.csv`);
document.body.appendChild(link);
link.click();
}
页面调用:
exportExcel() {
const that = this;
this.$confirm("是否确定导出模板真人视频列表?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
let obj = Object.assign({}, this.formData);
let subDates = this.subDates;
if (subDates) {
obj.created_at_start = subDates[0];
obj.created_at_end = subDates[1];
}
obj = tool.filterParams(obj);
if(this.formData.status !== '') {
obj.status = this.formData.status;
}
const thObj = {
'user_id': '用户ID',
'user_name': '用户昵称',
'tmpl_name': '所属模板',
'title': '视频标题',
'duration': '视频时长(秒)',
'video': '原始视频',
'url': '成果视频',
'status_name': '视频状态',
'created_at': '创建时间',
'upload_at': '上传时间',
'finish_at': '发布时间',
};
this.dataLoading = true;
this.$util.exportToExcel({
url: this.getListUrl,
params: obj,
thObj,
fileName: '模板真人视频列表',
callback: function(oldList) {
const newList = oldList.map((item) => {
item.tmpl_name = item.tmpl ? item.tmpl.title : '';
item.user_name = item.user ? item.user.name : '';
item.status_name = that.statusName[item.status];
return { ...item };
});
return newList
},
}).finally(()=> {
this.dataLoading = false;
});
})
.catch(() => {});
},