js.vue 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019
  1. <template>
  2. <!-- 对应页面: js报错 -->
  3. <view class="fix-top-window">
  4. <view class="uni-header">
  5. <uni-stat-breadcrumb class="uni-stat-breadcrumb-on-phone" />
  6. <view class="uni-group hide-on-phone">
  7. <view class="uni-sub-title">开发者可以在这里快速查询应用最近出现的具体错误内容,了解错误概况信息,以便快速修复问题</view>
  8. </view>
  9. </view>
  10. <view class="uni-container">
  11. <view class="uni-stat--x flex p-1015">
  12. <uni-data-select collection="opendb-app-list" field="appid as value, name as text" orderby="text asc" :defItem="1" label="应用选择" v-model="query.appid" :clear="false" />
  13. <uni-data-select collection="opendb-app-versions" :where="versionQuery" class="ml-m" field="_id as value, version as text, uni_platform as label, create_date as date" format="{label} - {text}" orderby="date desc" label="版本选择" v-model="query.version_id" />
  14. </view>
  15. <view class="uni-stat--x flex">
  16. <uni-stat-tabs label="日期选择" :current="currentDateTab" :yesterday="false" mode="date"
  17. @change="changeTimeRange" />
  18. <uni-datetime-picker type="datetimerange" :end="new Date().getTime()" v-model="query.start_time"
  19. returnType="timestamp" :clearIcon="false" class="uni-stat-datetime-picker"
  20. :class="{'uni-stat__actived': currentDateTab < 0 && !!query.start_time.length}"
  21. @change="useDatetimePicker" />
  22. </view>
  23. <view class="uni-stat--x">
  24. <uni-stat-tabs label="平台选择" type="boldLine" mode="platform" v-model="query.platform_id" @change="changePlatform"/>
  25. <uni-data-select ref="version-select" v-if="query.platform_id && query.platform_id.indexOf('==') === -1" collection="uni-stat-app-channels" :where="channelQuery" class="p-channel" field="_id as value, channel_name as text" orderby="text asc" label="渠道/场景值选择" v-model="query.channel_id" />
  26. </view>
  27. <view class="uni-stat--x" style="padding: 15px 0;">
  28. <uni-stat-panel :items="panelData" class="uni-stat-panel" />
  29. <uni-stat-tabs type="box" v-model="chartTab" :tabs="chartTabs" class="mb-l" />
  30. <view class="uni-charts-box">
  31. <qiun-data-charts type="area" :chartData="chartData" :eopts="{notMerge:true}" echartsH5 echartsApp tooltipFormat="tooltipCustom" :errorMessage="errorMessage"/>
  32. </view>
  33. </view>
  34. <view class="uni-stat--x p-m">
  35. <view class="flex-between">
  36. <view class="uni-stat-card-header">信息列表</view>
  37. <view class="uni-group">
  38. <!-- #ifdef H5 -->
  39. <button v-if="sourceMapEnabled" class="uni-button" type="primary" size="mini"
  40. @click="openUploadPopup">上传
  41. sourceMap</button>
  42. <!-- #endif -->
  43. </view>
  44. </view>
  45. <uni-table :loading="loading" border stripe :emptyText="errorMessage || $t('common.empty')">
  46. <uni-tr>
  47. <block v-for="(mapper, index) in fieldsMap" :key="index">
  48. <uni-th v-if="mapper.title" :key="index" align="center">
  49. <!-- #ifdef MP -->
  50. {{mapper.title}}
  51. <!-- #endif -->
  52. <!-- #ifndef MP -->
  53. <uni-tooltip>
  54. {{mapper.title}}
  55. <uni-icons v-if="mapper.tooltip" type="help" color="#666" />
  56. <template v-if="mapper.tooltip" v-slot:content>
  57. <view class="uni-stat-tooltip-s">
  58. {{mapper.tooltip}}
  59. </view>
  60. </template>
  61. </uni-tooltip>
  62. <!-- #endif -->
  63. </uni-th>
  64. </block>
  65. <uni-th align="center" v-if="sourceMapEnabled">
  66. 操作
  67. </uni-th>
  68. </uni-tr>
  69. <uni-tr v-for="(item ,i) in tableData" :key="i">
  70. <block v-for="(mapper, index) in fieldsMap" :key="index">
  71. <uni-td v-if="mapper.field === 'count'" :key="mapper.field" align="center">
  72. <text class="link-btn" @click="navTo('detail', item)">
  73. {{item[mapper.field] !== undefined ? item[mapper.field] : '-'}}
  74. </text>
  75. </uni-td>
  76. <uni-td v-else :key="mapper.field" align="center">
  77. {{item[mapper.field] !== undefined ? item[mapper.field] : '-'}}
  78. </uni-td>
  79. </block>
  80. <uni-td v-if="sourceMapEnabled">
  81. <button size="mini" type="primary" style="white-space: nowrap;"
  82. @click="openErrPopup(item)">详 情</button>
  83. </uni-td>
  84. </uni-tr>
  85. </uni-table>
  86. <view class="uni-pagination-box">
  87. <uni-pagination show-icon show-page-size :page-size="options.pageSize"
  88. :current="options.pageCurrent" :total="options.total" @change="changePageCurrent"
  89. @pageSizeChange="changePageSize" />
  90. </view>
  91. </view>
  92. </view>
  93. <uni-popup ref="errMsg" type="center" :animation="false" :maskClick="true" @change="errMsgPopupChange">
  94. <view class="modal black-theme">
  95. <view class="modal-header">
  96. 错误详情
  97. </view>
  98. <scroll-view scroll-x="true" scroll-y="true">
  99. <view class="modal-content" style="padding: 20px 30px;">
  100. <view v-if="msgLoading" style="margin: 150px 0;text-align: center;font-size: 14px;">
  101. <uni-load-more class="mb-m" :showText="false" status="loading" />
  102. <view>正在解析,请稍等...</view>
  103. </view>
  104. <text>{{errMsg}}</text>
  105. </view>
  106. </scroll-view>
  107. <view class="dialog-close" @click="closeErrPopup">
  108. <view class="dialog-close-plus" data-id="close"></view>
  109. <view class="dialog-close-plus dialog-close-rotate" data-id="close"></view>
  110. </view>
  111. </view>
  112. </uni-popup>
  113. <!-- #ifdef H5 -->
  114. <uni-drawer class="sourcemap-drawser" ref="upload" mode="right" :mask-click="true" :width="340">
  115. <view class="modal" style="max-width: none; min-width: auto;padding: 0 10px;">
  116. <view class="modal-header">
  117. 上传 sourceMap
  118. </view>
  119. <view class="modal-content" style="height: 300px;padding: 0;">
  120. <view style="margin-top: 10px;">
  121. <uni-data-select collection="opendb-app-list" field="appid as value, name as text" orderby="text asc" label="应用" v-model="uploadOptions.appid" />
  122. </view>
  123. <view style="margin-top: 10px;">
  124. <uni-data-select collection="uni-stat-app-platforms" field="code as value, name as text" orderby="text asc" label="平台" v-model="uploadOptions.uni_platform" />
  125. </view>
  126. <view style="margin-top: 10px;">
  127. <uni-data-select collection="opendb-app-versions" :where="uploadVersionQuery" field="version as value, version as text" orderby="text desc" label="版本" v-model="uploadOptions.version" />
  128. </view>
  129. <view class="flex m-m">
  130. <view class="label-text">选择文件:</view>
  131. <button class="uni-button ml-m" type="primary" @click="choosefile">选择文件并上传</button>
  132. </view>
  133. <view v-if="!vaildate" class="upload-msg-warning">
  134. {{uploadMsg}}
  135. </view>
  136. </view>
  137. <view class="dialog-close" @click="closeUploadPopup">
  138. <view class="dialog-close-plus" style="background-color: #333;" data-id="close"></view>
  139. <view class="dialog-close-plus dialog-close-rotate" style="background-color: #333;" data-id="close">
  140. </view>
  141. </view>
  142. </view>
  143. <view class="upload-task-header">
  144. <text>上传任务:{{uploadSuccessTasks.length}}/{{uploadFile.tempFileTasks.length}}</text>
  145. </view>
  146. <scroll-view v-if="uploadFile.tempFileTasks.length" style="height: calc(100vh - 362px);" scroll-y="true">
  147. <view v-if="uploadFile.tempFileTasks.length > uploadSuccessTasks.length">
  148. <view class="upload-task-header">
  149. <text>正在上传</text>
  150. </view>
  151. <uploadTask :uploadTasks="sortUploadFileTempFileTasks"></uploadTask>
  152. </view>
  153. <view v-if="uploadSuccessTasks.length">
  154. <view class="upload-task-header">
  155. <text style="color:#42b983;">上传成功</text>
  156. </view>
  157. <uploadTask :uploadTasks="uploadSuccessTasks" :showProgress="false"></uploadTask>
  158. </view>
  159. </scroll-view>
  160. </uni-drawer>
  161. <!-- #endif -->
  162. <!-- #ifndef H5 -->
  163. <fix-window />
  164. <!-- #endif -->
  165. </view>
  166. </template>
  167. <script>
  168. import {
  169. mapfields,
  170. stringifyQuery,
  171. getTimeOfSomeDayAgo,
  172. division,
  173. format,
  174. formatDate,
  175. parseDateTime,
  176. fileToUrl,
  177. debounce,
  178. getAllDateCN,
  179. createUniStatQuery
  180. } from '@/js_sdk/uni-stat/util.js'
  181. import {
  182. fieldsMap,
  183. popupFieldsMap
  184. } from './fieldsMap.js'
  185. import uploadTask from './uploadTask.vue'
  186. import {
  187. stacktracey,
  188. uniStracktraceyPreset
  189. } from '@dcloudio/uni-stacktracey';
  190. import adminConfig from '@/admin.config.js'
  191. const panelOption = [{
  192. title: '错误总数',
  193. value: 0,
  194. tooltip: '指应用在某个时间段内出现错误的总数'
  195. }, {
  196. title: '错误率',
  197. value: 0,
  198. tooltip: '时间范围内的总错误数/应用启动次数,如果小于0.01%,默认显示为0'
  199. }]
  200. const db = uniCloud.database(); // 初始化数据库实例
  201. const _ = db.command; // 定义数据库操作符
  202. const dbNameSourceMap = `uni-stat-error-source-map`; // sourceMap资源存放的表名
  203. const sourcemapPrefix = `__UNI__/uni-stat/sourcemap`;
  204. let sourcemapFileCache = {};
  205. export default {
  206. data() {
  207. return {
  208. uniStat: adminConfig.uniStat,
  209. fieldsMap,
  210. popupFieldsMap,
  211. query: {
  212. // type: "js",
  213. dimension: "day",
  214. appid: "",
  215. platform_id: '',
  216. uni_platform: '',
  217. version_id: '',
  218. start_time: []
  219. },
  220. uploadOptions: createUniStatQuery({
  221. appid: "",
  222. uni_platform: '',
  223. }),
  224. uploadMsg: '',
  225. options: {
  226. pageSize: 20,
  227. pageCurrent: 1, // 当前页
  228. total: 0, // 数据总量
  229. },
  230. loading: false,
  231. popupLoading: false,
  232. currentDateTab: 0,
  233. // currentChartTab: ,
  234. tableData: [],
  235. popupTableData: [],
  236. panelData: JSON.parse(JSON.stringify(panelOption)),
  237. chartData: {},
  238. chartTab: 'errorCount',
  239. chartTabs: [{
  240. _id: 'errorCount',
  241. name: '错误次数'
  242. }, {
  243. _id: 'errorRate',
  244. name: '错误率'
  245. }],
  246. errMsg: '',
  247. msgLoading: false,
  248. uploadFile: {
  249. tempFileTasks: [],
  250. tempFiles: [],
  251. clear() {
  252. this.tempFileTasks.length = this.tempFiles.length = 0
  253. }
  254. },
  255. uploadSuccessTaskNames: [],
  256. errorItem: '',
  257. errorMessage: "",
  258. }
  259. },
  260. components: {
  261. uploadTask
  262. },
  263. computed: {
  264. queryStr() {
  265. return stringifyQuery(this.query)
  266. },
  267. versionQuery() {
  268. const {
  269. appid,
  270. uni_platform
  271. } = this.query
  272. const query = stringifyQuery(createUniStatQuery({
  273. appid,
  274. uni_platform
  275. }))
  276. return query
  277. },
  278. uploadVersionQuery() {
  279. const {
  280. appid,
  281. uni_platform
  282. } = this.uploadOptions
  283. const query = stringifyQuery(createUniStatQuery({
  284. appid,
  285. uni_platform
  286. }))
  287. return query
  288. },
  289. vaildate() {
  290. // 检验 this.uploadOptions 所有项都有值
  291. const allItemHasVaule = Object.keys(this.uploadOptions).every(k => this.uploadOptions[k])
  292. if (allItemHasVaule && this.uploadMsg) {
  293. this.uploadMsg = ''
  294. }
  295. return allItemHasVaule
  296. },
  297. uploadSuccessTasks() {
  298. return this.uploadFile.tempFileTasks.filter(task => task.state === 1)
  299. },
  300. sortUploadFileTempFileTasks() {
  301. return this.uploadFile.tempFileTasks.filter(task => task.state !== 1).sort((a, b) => a.state - b.state)
  302. },
  303. sourceMapEnabled() {
  304. //return !!this.uniStat.uploadSourceMapCloudSpaceId
  305. return true;
  306. },
  307. channelQuery() {
  308. const platform_id = this.query.platform_id
  309. return stringifyQuery({
  310. platform_id
  311. })
  312. },
  313. },
  314. created() {
  315. this.parsedErrors = {}
  316. if (this.sourceMapEnabled) {
  317. // sourceMap 功能需初始化上传的目标云空间
  318. if (this.uniStat.uploadSourceMapCloudSpaceId) {
  319. if (this.uniStat.uploadSourceMapCloudPlatform === "aliyun") {
  320. // 使用阿里云服务空间
  321. let endpoint = this.uniStat.uploadSourceMapCloudSpaceId.indexOf("mp-") === 0 ? 'https://api.next.bspapp.com':'https://api.bspapp.com';
  322. this.uploadSourcemapCloud = uniCloud.init({
  323. provider: 'aliyun',
  324. spaceId: this.uniStat.uploadSourceMapCloudSpaceId,
  325. clientSecret: this.uniStat.uploadSourceMapCloudClientSecret,
  326. endpoint
  327. })
  328. } else {
  329. // 使用腾讯云服务空间
  330. this.uploadSourcemapCloud = uniCloud.init({
  331. provider: 'tencent',
  332. spaceId: this.uniStat.uploadSourceMapCloudSpaceId
  333. })
  334. }
  335. } else {
  336. // 使用本项目绑定的服务空间
  337. this.uploadSourcemapCloud = uniCloud;
  338. }
  339. }
  340. this.getCloudDataDebounce = debounce(() => {
  341. this.getAllData(this.queryStr)
  342. }, 300);
  343. this.getCloudDataDebounce();
  344. },
  345. watch: {
  346. query: {
  347. deep: true,
  348. handler(val, old) {
  349. this.options.pageCurrent = 1 // 重置分页
  350. this.getCloudDataDebounce()
  351. }
  352. },
  353. chartTab(val) {
  354. this.getChartData(this.queryStr)
  355. }
  356. },
  357. methods: {
  358. useDatetimePicker(res) {
  359. this.currentDateTab = -1
  360. },
  361. changePlatform(id, index, name, item) {
  362. this.query.version_id = 0
  363. this.uploadOptions.uni_platform = item.code
  364. this.query.uni_platform = item.code
  365. },
  366. changeTimeRange(id, index) {
  367. this.currentDateTab = index
  368. const start = getTimeOfSomeDayAgo(id),
  369. end = getTimeOfSomeDayAgo(0) - 1
  370. this.query.start_time = [start, end]
  371. },
  372. changePageCurrent(e) {
  373. this.options.pageCurrent = e.current
  374. this.getTableData(this.queryStr)
  375. },
  376. changePageSize(pageSize) {
  377. this.options.pageSize = pageSize
  378. this.options.pageCurrent = 1 // 重置分页
  379. this.getTableData(this.queryStr)
  380. },
  381. getAllData(query) {
  382. if (query.indexOf("appid") === -1) {
  383. this.errorMessage = "请先选择应用";
  384. return; // 如果appid为空,则不进行查询
  385. }
  386. this.errorMessage = "";
  387. this.getChartData(query);
  388. this.getTableData(query);
  389. },
  390. getChartData(query, field = 'day_count') {
  391. let querystr = stringifyQuery(this.query, false, ['uni_platform'])
  392. this.chartData = {}
  393. const {
  394. pageCurrent
  395. } = this.options
  396. const db = uniCloud.database()
  397. const [start_time, end_tiem] = this.query.start_time
  398. const timeAll = getAllDateCN(new Date(start_time), new Date(end_tiem))
  399. db.collection('uni-stat-error-result')
  400. .where(querystr)
  401. .groupBy('start_time')
  402. .groupField('sum(count) as total_day_count')
  403. .orderBy('start_time', 'desc')
  404. .get({
  405. getCount: true
  406. })
  407. .then(async res => {
  408. const count = res.result.count
  409. const resData = res.result.data
  410. let data = []
  411. timeAll.forEach(v => {
  412. let item = resData.find(item => item.start_time === v)
  413. if (item) {
  414. data.push(item)
  415. } else {
  416. data.push({
  417. start_time: v,
  418. total_day_count: 0
  419. })
  420. }
  421. })
  422. const options = {
  423. categories: [],
  424. series: [{
  425. name: '暂无数据',
  426. data: []
  427. }]
  428. }
  429. if (this.chartTab === 'errorCount') {
  430. const countLine = options.series[0] = {
  431. name: '错误次数',
  432. data: []
  433. }
  434. const xAxis = options.categories
  435. for (const item of data) {
  436. let date = item.start_time
  437. const x = formatDate(date, 'day')
  438. const countY = item[`total_${field}`]
  439. xAxis.push(x)
  440. countLine.data.push(countY)
  441. }
  442. this.chartData = options
  443. } else {
  444. let dayAppLaunchs = await this.getDayLaunch(querystr)
  445. const rateLine = options.series[0] = {
  446. name: '错误率(%)',
  447. data: [],
  448. lineStyle: {
  449. color: '#EE6666',
  450. width: 1,
  451. },
  452. itemStyle: {
  453. borderWidth: 1,
  454. borderColor: '#EE6666',
  455. color: '#EE6666'
  456. },
  457. areaStyle: {
  458. color: {
  459. colorStops: [{
  460. offset: 0,
  461. color: '#EE6666', // 0% 处的颜色
  462. }, {
  463. offset: 1,
  464. color: '#FFFFFF' // 100% 处的颜色
  465. }]
  466. }
  467. }
  468. }
  469. const xAxis = options.categories
  470. for (const item of data) {
  471. let date = item.start_time
  472. const x = formatDate(date, 'day')
  473. const countY = item[`total_${field}`]
  474. xAxis.push(x)
  475. if (dayAppLaunchs.length) {
  476. const day = dayAppLaunchs.find(day => day.start_time === item.start_time)
  477. const index = xAxis.indexOf(x)
  478. if (day) {
  479. let rateY = (countY * 100) / day.day_app_launch_count
  480. rateY = rateY.toFixed(2)
  481. rateLine.data[index] = rateY
  482. } else {
  483. rateLine.data[index] = 0
  484. }
  485. }
  486. }
  487. this.chartData = options
  488. }
  489. }).catch((err) => {
  490. console.error(err)
  491. // err.message 错误信息
  492. // err.code 错误码
  493. }).finally(() => {})
  494. },
  495. getTotalCount(query) {
  496. const db = uniCloud.database()
  497. return db.collection('uni-stat-error-result')
  498. .where(query)
  499. .groupBy('appid')
  500. .groupField('sum(count) as total_count')
  501. .get()
  502. },
  503. getTotalLaunch(query) {
  504. const db = uniCloud.database()
  505. return db.collection('uni-stat-result')
  506. .where(query)
  507. .groupBy('appid')
  508. .groupField('sum(app_launch_count) as total_app_launch_count')
  509. .get()
  510. },
  511. /**
  512. * 从结果表里获取范围时间内的启动次数
  513. * @param {Object} query
  514. */
  515. async getDayLaunch(query) {
  516. const db = uniCloud.database()
  517. const res = await db.collection('uni-stat-result')
  518. .where(query)
  519. .groupBy('start_time')
  520. .groupField('sum(app_launch_count) as day_app_launch_count')
  521. .orderBy('start_time', 'asc')
  522. .get()
  523. return res.result.data || []
  524. },
  525. getTableData(query = stringifyQuery(this.query)) {
  526. let querystr = stringifyQuery(this.query, false, ['uni_platform'])
  527. const {
  528. pageCurrent
  529. } = this.options
  530. this.loading = true
  531. const db = uniCloud.database()
  532. const filterAppid = stringifyQuery(createUniStatQuery({
  533. appid: this.query.appid
  534. }))
  535. const mainTableTemp = db.collection('uni-stat-error-result').where(querystr).getTemp()
  536. const versions = db.collection('opendb-app-versions')
  537. .where(filterAppid)
  538. .getTemp()
  539. const platforms = db.collection('uni-stat-app-platforms')
  540. .getTemp()
  541. db.collection(mainTableTemp, versions, platforms)
  542. .orderBy('count', 'desc')
  543. .skip((pageCurrent - 1) * this.options.pageSize)
  544. .limit(this.options.pageSize)
  545. .get({
  546. getCount: true
  547. })
  548. .then(res => {
  549. const {
  550. count,
  551. data
  552. } = res.result
  553. const tempData = []
  554. this.panelData = JSON.parse(JSON.stringify(panelOption))
  555. for (const item of data) {
  556. item.last_time = parseDateTime(item.last_time, 'dateTime')
  557. item.msgTooltip = item.msg
  558. item.msg = !item.msg ? '' : item.msg.substring(0, 100) + '...'
  559. const v = item.version_id[0]
  560. const p = item.platform_id[0]
  561. item.version = v && v.version
  562. item.platform = p && p.name
  563. item.platform_code = p && p.code
  564. tempData.push(item)
  565. }
  566. this.getTotalCount(querystr).then(res => {
  567. const total = res.result.data[0]
  568. const total_count = total && total.total_count
  569. if (total_count) {
  570. tempData.forEach(item => item.total_count = Number(total_count))
  571. this.panelData[0].value = total_count
  572. }
  573. let launch_count = ''
  574. this.getTotalLaunch(querystr).then(res => {
  575. const total = res.result.data[0]
  576. launch_count = total && total.total_app_launch_count
  577. if (total_count && launch_count) {
  578. let errorRate = total_count / launch_count
  579. errorRate = (errorRate * 100).toFixed(2) + '%'
  580. this.panelData[1].value = errorRate
  581. }
  582. })
  583. }).finally(() => {
  584. this.tableData = []
  585. this.options.total = count
  586. tempData.forEach(item => mapfields(fieldsMap, item, item))
  587. this.tableData = tempData
  588. })
  589. }).catch((err) => {
  590. console.error(err)
  591. // err.message 错误信息
  592. // err.code 错误码
  593. }).finally(() => {
  594. this.loading = false
  595. })
  596. },
  597. navTo(url, item) {
  598. if (url.indexOf('http') > -1) {
  599. window.open(url)
  600. } else {
  601. if (item) {
  602. url = `${url}?error_hash=${item.hash}&create_time=${item.start_time}`
  603. }
  604. uni.navigateTo({
  605. url
  606. })
  607. }
  608. },
  609. closeErrPopup() {
  610. this.$refs.errMsg.close()
  611. },
  612. errMsgPopupChange(res) {
  613. if (res.show) {
  614. const err = this.errorItem.msgTooltip
  615. if (this.msgLoading) {
  616. this.closeErrPopup()
  617. return;
  618. }
  619. if (!err) {
  620. this.errMsg = '暂无错误数据'
  621. }
  622. this.errMsg = ''
  623. const oldMsg = this.parsedErrors[err]
  624. // || oldMsg === err
  625. this.msgLoading = true
  626. this.parseError(this.errorItem)
  627. return
  628. if (!oldMsg) {
  629. this.msgLoading = true
  630. this.parseError(this.errorItem)
  631. } else {
  632. this.errMsg = oldMsg
  633. }
  634. } else {
  635. this.msgLoading = false
  636. }
  637. },
  638. async parseError(item) {
  639. let {
  640. msgTooltip: err,
  641. appid,
  642. platform_code,
  643. version
  644. } = item
  645. //console.log('item: ', item)
  646. let base = `/${appid}/${platform_code}/${version}/`
  647. let fileList;
  648. if (sourcemapFileCache[base] && sourcemapFileCache[base].length > 0) {
  649. fileList = sourcemapFileCache[base];
  650. } else {
  651. fileList = await this.getSourceMapFileList({
  652. base
  653. });
  654. if (fileList && fileList.length > 0) {
  655. sourcemapFileCache[base] = fileList;
  656. } else {
  657. console.error(`缺少${base}对应的sourceMap,请先上传sourceMap`)
  658. }
  659. }
  660. //console.log('fileList: ', fileList)
  661. try {
  662. err = JSON.parse(err)
  663. } catch (e) {}
  664. // console.log("originalErrMsg: ", err);
  665. const stacktraceyOptions = {
  666. base,
  667. uniPlatform: platform_code,
  668. splitThirdParty: true
  669. }
  670. if (['ios', 'android', 'app'].indexOf(platform_code)>-1) {
  671. stacktraceyOptions.lineOffset = -1
  672. }
  673. stacktracey(err, {
  674. preset: {
  675. ...uniStracktraceyPreset(stacktraceyOptions),
  676. /**
  677. *
  678. * 微信特殊处理
  679. * 微信解析步骤:
  680. * 1. //usr/app-service.js -> 'weixin/__APP__/app-service.map.map'
  681. * 2. //usr/pages/API/app-service.js -> 'weixin/pages/API/app-service.map.map'
  682. * 3. uni-list-item/uni-list-item.js -> ${base}/uni-list-item/uni-list-item.js.map
  683. */
  684. parseSourceMapUrl(file, fileName, fileRelative){
  685. // 组合 sourceMapUrl
  686. if (fileRelative.indexOf('(') !== -1){
  687. let fileRelativeMatch = fileRelative.match(/\((.*)/);
  688. fileRelative = fileRelativeMatch && fileRelativeMatch[1];
  689. }
  690. if (!base || !fileRelative) return ''
  691. if (typeof sourceRoot !== "undefined") {
  692. return `${fileRelative.replace(sourceRoot, base + '/')}.map`
  693. }
  694. let baseAfter = ''
  695. if (platform_code.indexOf("mp-")>-1) {
  696. if (fileRelative.indexOf('app-service.js') !== -1) {
  697. baseAfter = (base.match(/\w$/) ? '/' : '') + '__WEIXIN__'
  698. if (fileRelative === fileName) {
  699. baseAfter += '/__APP__'
  700. }
  701. // fileRelative = fileRelative.replace('.js', '.map')
  702. }
  703. }
  704. if (baseAfter && !!fileRelative.match(/^\w/)) baseAfter += '/'
  705. let path = `${base}${baseAfter}${fileRelative}.map`;
  706. let cloud_path = `${sourcemapPrefix}${path}`;
  707. let cloud_path_web;
  708. if (platform_code === "web") {
  709. // 尝试去掉第一级的项目名(web端可能会设置h5基础运行路径)
  710. let fileRelativeWeb = fileRelative.substring(fileRelative.indexOf("/")+1);
  711. let pathWeb = `${base}${baseAfter}${fileRelativeWeb}.map`;
  712. cloud_path_web = `${sourcemapPrefix}${pathWeb}`;
  713. }
  714. let fileItem = fileList.find((item) => {
  715. return [cloud_path, cloud_path_web].indexOf(item.cloud_path) > -1;
  716. });
  717. //console.log('cloud_path: ', cloud_path, fileItem.url)
  718. return fileItem ? fileItem.url : cloud_path;
  719. }
  720. }
  721. }).then(res => {
  722. const {
  723. userError,
  724. thirdParty
  725. } = res
  726. const separate = userError.length && thirdParty.length ?
  727. `\n\n------------${platform_code.indexOf('mp-') !== -1 ? platform_code : 'uni-app'} runtime error------------\n\n` :
  728. ''
  729. this.errMsg = `${userError}${separate}${thirdParty}`
  730. this.parsedErrors[err] = this.errMsg
  731. }).finally(() => {
  732. this.msgLoading = false
  733. });
  734. },
  735. openUploadPopup() {
  736. const {
  737. appid,
  738. uni_platform
  739. } = this.query
  740. this.uploadOptions = {
  741. appid,
  742. uni_platform
  743. }
  744. this.$refs.upload.open()
  745. },
  746. closeUploadPopup() {
  747. this.$refs.upload.close()
  748. },
  749. createUploadFileTask(prefix, fileDiskPath, filePath, onUploadProgress) {
  750. const cloudPath = prefix + fileDiskPath
  751. return this.uploadSourcemapCloud.uploadFile({
  752. filePath,
  753. cloudPath,
  754. onUploadProgress
  755. })
  756. },
  757. async choosefile() {
  758. if (!this.vaildate) {
  759. this.uploadMsg = '请先将应用、平台、版本填写完整'
  760. return
  761. }
  762. const {
  763. appid,
  764. uni_platform,
  765. version
  766. } = this.uploadOptions
  767. const base = `/${appid}/${uni_platform}/${version}/`;
  768. const prefix = `${sourcemapPrefix}${base}`
  769. // 原生 input 上传逻辑
  770. const inputEl = document.createElement('input')
  771. inputEl.type = 'file'
  772. inputEl.directory = true
  773. inputEl.webkitdirectory = true
  774. inputEl.click()
  775. inputEl.addEventListener('change', async () => {
  776. this.uploadFile.clear()
  777. const fileList = inputEl.files; /* now you can work with the file list */
  778. if (!fileList.length) return
  779. Array.prototype.forEach.call(fileList, (file) => {
  780. const path = fileToUrl(file)
  781. this.uploadFile.tempFileTasks.push({
  782. fileDiskPath: file.webkitRelativePath.split('/').slice(1).join('/'),
  783. path,
  784. size: `${(file.size / 1024).toFixed(2)}kb`,
  785. name: file.name,
  786. state: 0,
  787. progress: 0,
  788. file
  789. })
  790. Object.defineProperty(file, 'path', {
  791. get() {
  792. return path
  793. },
  794. })
  795. this.uploadFile.tempFiles.push(file)
  796. })
  797. // 因为上传有并发限制,故还是一个一个上传靠谱
  798. let dataArr = [];
  799. for (let i = 0; i < this.uploadFile.tempFileTasks.length; i++) {
  800. let cur = this.uploadFile.tempFileTasks[i];
  801. let res = await new Promise((resolve, reject) => {
  802. // 已上传的文件
  803. if (this.uploadSuccessTaskNames.indexOf(cur.name) !== -1) {
  804. cur.progress = 1
  805. setTimeout(() => {
  806. cur.state = 1
  807. resolve()
  808. }, 200)
  809. } else {
  810. this.createUploadFileTask(
  811. prefix,
  812. cur.fileDiskPath,
  813. cur.path,
  814. (OnUploadProgressRes) => {
  815. const {
  816. loaded,
  817. total
  818. } = OnUploadProgressRes
  819. cur.progress = loaded / total
  820. }
  821. ).then((uploadRes) => {
  822. const cloudPath = prefix + cur.fileDiskPath
  823. let fileID = uploadRes.fileID;
  824. uniCloud.getTempFileURL({
  825. fileList: [fileID]
  826. })
  827. .then(res => {
  828. let url = res.fileList[0].tempFileURL;
  829. let file = {
  830. appid,
  831. uni_platform,
  832. version,
  833. file_id: fileID,
  834. url,
  835. name: cur.name,
  836. size: cur.file.size,
  837. cloud_path: cloudPath,
  838. base
  839. };
  840. setTimeout(() => {
  841. this.uploadSuccessTaskNames.push(name)
  842. cur.state = 1
  843. resolve(file)
  844. }, 100)
  845. });
  846. }).catch((err) => {
  847. cur.state = -1
  848. reject(`${cur.name} 上传失败:` + JSON.stringify(err))
  849. })
  850. }
  851. })
  852. if (res) dataArr.push(res);
  853. }
  854. if (dataArr && dataArr.length>0){
  855. // 将sourceMap资源文件写入数据库
  856. await this.addSourceMapFile(dataArr);
  857. }
  858. })
  859. },
  860. createStr(maps, fn, prefix = 'total_') {
  861. const strArr = []
  862. maps.forEach(mapper => {
  863. if (field.hasOwnProperty('value')) {
  864. const fieldName = mapper.field
  865. strArr.push(`${fn}(${fieldName}) as ${prefix + fieldName}`)
  866. }
  867. })
  868. return strArr.join()
  869. },
  870. openErrPopup(item) {
  871. this.errorItem = item
  872. this.$refs.errMsg.open()
  873. },
  874. // 将sourceMap资源文件写入数据库
  875. async addSourceMapFile(dataArr){
  876. let cloud_path_arr = [];
  877. dataArr.map((item, index) => {
  878. cloud_path_arr.push(item.cloud_path);
  879. });
  880. // 先删除同版本的文件(避免重复上传时出现错误)
  881. await db.collection(dbNameSourceMap).where({
  882. cloud_path: _.in(cloud_path_arr)
  883. }).remove();
  884. // 再添加
  885. await db.collection(dbNameSourceMap).add(dataArr);
  886. },
  887. // 从数据库中获取sourceMap资源文件
  888. async getSourceMapFileList(obj){
  889. let {
  890. base
  891. } = obj;
  892. let fileListRes = await db.collection(dbNameSourceMap).where({
  893. base
  894. }).limit(1000).get();
  895. return fileListRes.result.data || [];
  896. },
  897. }
  898. }
  899. </script>
  900. <style>
  901. .flex-between {
  902. margin-bottom: 10px;
  903. display: flex;
  904. justify-content: space-between;
  905. align-items: center;
  906. }
  907. .uni-stat-panel {
  908. box-shadow: unset;
  909. border-bottom: 1px solid #eee;
  910. padding: 0;
  911. margin: 0 15px;
  912. }
  913. .uni-stat-tooltip-s {
  914. width: 160px;
  915. white-space: normal;
  916. }
  917. .black-theme {
  918. background-color: #333;
  919. color: #fff;
  920. }
  921. .dialog-close {
  922. cursor: pointer;
  923. position: absolute;
  924. top: 0;
  925. right: 0;
  926. /* #ifndef APP-NVUE */
  927. display: flex;
  928. /* #endif */
  929. flex-direction: row;
  930. align-items: center;
  931. padding: 20px;
  932. margin-top: 10px;
  933. }
  934. .dialog-close-plus {
  935. width: 20px;
  936. height: 2px;
  937. background-color: #fff;
  938. border-radius: 2px;
  939. transform: rotate(45deg);
  940. }
  941. .dialog-close-rotate {
  942. position: absolute;
  943. transform: rotate(-45deg);
  944. }
  945. .upload-msg-warning {
  946. padding: 0px 15px;
  947. color: red;
  948. font-size: 14px;
  949. }
  950. ::v-deep .sourcemap-drawser .uni-select {
  951. flex: 1;
  952. }
  953. ::v-deep .sourcemap-drawser .uni-select .uni-select__input-text {
  954. width: 100%;
  955. }
  956. .upload-task-header {
  957. font-size: 14px;
  958. color: #666;
  959. padding: 15rpx 25rpx;
  960. border-top: 1px solid #eee;
  961. border-bottom: 1px solid #eee;
  962. }
  963. </style>