retention.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. <template>
  2. <!-- 对应页面:设备统计-留存 -->
  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">
  7. <!-- <view class="uni-title">用户留存</view> -->
  8. <view class="uni-sub-title hide-on-phone">设备留存趋势分析</view>
  9. </view>
  10. </view>
  11. <view class="uni-container">
  12. <view class="uni-stat--x flex">
  13. <uni-data-select collection="opendb-app-list" field="appid as value, name as text" orderby="text asc"
  14. :defItem="1" label="应用选择" @change="changeAppid" v-model="query.appid" :clear="false" />
  15. <uni-data-select collection="opendb-app-versions" :where="versionQuery"
  16. field="_id as value, version as text" orderby="text asc" label="版本选择" v-model="query.version_id" />
  17. <view class="flex">
  18. <uni-stat-tabs label="日期选择" :current="currentDateTab" mode="date" :yesterday="false"
  19. @change="changeTimeRange" />
  20. <uni-datetime-picker type="daterange" :end="new Date().getTime()" v-model="query.start_time"
  21. returnType="timestamp" :clearIcon="false" class="uni-stat-datetime-picker"
  22. :class="{'uni-stat__actived': currentDateTab < 0 && !!query.start_time.length}"
  23. @change="useDatetimePicker" />
  24. </view>
  25. </view>
  26. <view class="uni-stat--x">
  27. <uni-stat-tabs label="平台选择" type="boldLine" mode="platform" v-model="query.platform_id"
  28. @change="changePlatform" />
  29. <uni-data-select v-if="query.platform_id && query.platform_id.indexOf('==') === -1"
  30. :localdata="channelData" label="渠道/场景值选择" v-model="query.channel_id"></uni-data-select>
  31. </view>
  32. <view class="uni-stat--x mb-m" style="padding-top: 0;">
  33. <view class="mb-m line-bottom">
  34. <uni-stat-tabs type="boldLine" :tabs="fields" v-model="field" tooltip
  35. style="line-height: 40px; margin-bottom: -17px;" />
  36. </view>
  37. <uni-stat-tabs type="box" :tabs="keys" v-model="key" class="mb-l" />
  38. <view class="p-m">
  39. <view class="uni-charts-box">
  40. <qiun-data-charts type="area" :chartData="chartData" echartsH5 echartsApp
  41. tooltipFormat="tooltipCustom" />
  42. </view>
  43. </view>
  44. </view>
  45. <view class="uni-stat--x p-m">
  46. <view class="uni-tips mb-s flex">
  47. <uni-icons type="info"></uni-icons>
  48. 表格中显示为空,表示留存为 0 或无数据
  49. </view>
  50. <uni-table :loading="loading" stripe :emptyText="$t('common.empty')">
  51. <uni-tr style="background-color: #eee;">
  52. <template v-for="(mapper, index) in fieldsMap">
  53. <uni-th v-if="mapper.title" :key="index" align="center">{{mapper.title}}</uni-th>
  54. </template>
  55. </uni-tr>
  56. <uni-tr v-for="(item ,i) in tableData" :key="i">
  57. <template v-for="(mapper, index) in fieldsMap">
  58. <uni-td v-if="mapper.title" :key="index" align="center"
  59. :class="/[d|w|m]_\d/.test(mapper.field)&&[item[mapper.field] ? 'uni-stat-table-bg' : '']">
  60. {{item[mapper.field] ? item[mapper.field] : ''}}
  61. </uni-td>
  62. </template>
  63. </uni-tr>
  64. </uni-table>
  65. <view class="uni-pagination-box">
  66. <uni-pagination show-icon show-page-size :page-size="options.pageSize"
  67. :current="options.pageCurrent" :total="options.total" @change="changePageCurrent"
  68. @pageSizeChange="changePageSize" />
  69. </view>
  70. </view>
  71. </view>
  72. <!-- #ifndef H5 -->
  73. <fix-window />
  74. <!-- #endif -->
  75. </view>
  76. </template>
  77. <script>
  78. import {
  79. mapfields,
  80. stringifyQuery,
  81. stringifyField,
  82. stringifyGroupField,
  83. getTimeOfSomeDayAgo,
  84. division,
  85. format,
  86. formatDate,
  87. debounce
  88. } from '@/js_sdk/uni-stat/util.js'
  89. import fieldsFactory from './fieldsMap.js'
  90. export default {
  91. data() {
  92. return {
  93. query: {
  94. dimension: "day",
  95. appid: '',
  96. platform_id: '',
  97. uni_platform: '',
  98. version_id: '',
  99. channel_id: '',
  100. start_time: [],
  101. },
  102. options: {
  103. pageSize: 20,
  104. pageCurrent: 1, // 当前页
  105. total: 0, // 数据总量
  106. },
  107. loading: false,
  108. currentDateTab: 0,
  109. tableData: [],
  110. chartData: {},
  111. field: 'new_device',
  112. fields: [{
  113. _id: 'new_device',
  114. name: '新增留存',
  115. tooltip: '指定时间新增(即首次访问应用)用户,在之后的第N天,再次访问应用的用户数占比'
  116. }, {
  117. _id: 'active_device',
  118. name: '活跃留存',
  119. tooltip: '指定时间活跃(即访问应用)用户,在之后的第N天,再次访问应用的用户数占比'
  120. }],
  121. key: 1,
  122. channelData: []
  123. }
  124. },
  125. computed: {
  126. fieldsMap() {
  127. const title = this.field === 'active_device' ? '活跃用户' : '新增用户'
  128. const maps = [{
  129. title,
  130. field: `${this.field}_count`,
  131. stat: 0
  132. }]
  133. return fieldsFactory(maps)
  134. },
  135. fieldName() {
  136. let name = ''
  137. this.fields.forEach(item => {
  138. if (item._id === this.field) {
  139. name = item.name
  140. }
  141. })
  142. return name
  143. },
  144. keyName() {
  145. return this.keys.forEach(item => {
  146. if (item._id === this.key) {
  147. return item.name
  148. }
  149. })
  150. },
  151. keys() {
  152. const values = [1, 2, 3, 4, 5, 6, 7, 14, 30]
  153. return values.map(val => {
  154. return {
  155. _id: val,
  156. name: `${val}天后`
  157. }
  158. })
  159. },
  160. channelQuery() {
  161. const platform_id = this.query.platform_id
  162. return stringifyQuery({
  163. platform_id
  164. })
  165. },
  166. versionQuery() {
  167. const {
  168. appid,
  169. uni_platform
  170. } = this.query
  171. const query = stringifyQuery({
  172. appid,
  173. uni_platform,
  174. })
  175. return query
  176. }
  177. },
  178. created() {
  179. this.debounceGet = debounce(() => this.getAllData(this.query))
  180. this.getChannelData()
  181. },
  182. watch: {
  183. query: {
  184. deep: true,
  185. handler(val) {
  186. this.options.pageCurrent = 1 // 重置分页
  187. this.debounceGet()
  188. }
  189. },
  190. key() {
  191. this.debounceGet()
  192. },
  193. field() {
  194. this.debounceGet()
  195. }
  196. },
  197. methods: {
  198. useDatetimePicker() {
  199. this.currentDateTab = -1
  200. },
  201. changeAppid(id) {
  202. this.getChannelData(id, false)
  203. },
  204. changePlatform(id, index, name, item) {
  205. this.getChannelData(null, id)
  206. this.query.version_id = 0
  207. this.query.uni_platform = item.code
  208. },
  209. changeTimeRange(id, index) {
  210. this.currentDateTab = index
  211. const start = getTimeOfSomeDayAgo(id),
  212. end = getTimeOfSomeDayAgo(0) - 1
  213. this.query.start_time = [start, end]
  214. },
  215. changePageCurrent(e) {
  216. this.options.pageCurrent = e.current
  217. this.getTabelData(this.query)
  218. },
  219. changePageSize(pageSize) {
  220. this.options.pageSize = pageSize
  221. this.options.pageCurrent = 1 // 重置分页
  222. this.getTabelData(this.query)
  223. },
  224. // 此处 util 中的 stringifyField 不满足需求,特殊处理 stringifyField
  225. stringifyField(mapping, goal, prop) {
  226. if (goal) {
  227. mapping = mapping.filter(f => f.field === goal)
  228. }
  229. if (prop) {
  230. mapping = mapping.filter(f => f.field && f.hasOwnProperty(prop))
  231. }
  232. const fields = mapping.map(f => {
  233. if (f.stat === -1) {
  234. return f.field
  235. } else if (f.stat === 0) {
  236. return `${f.field} as ${ 'temp_' + f.field}`
  237. } else {
  238. return `retention.${this.field}.${f.field}.device_count as ${ 'temp_' + f.field}`
  239. }
  240. }).join()
  241. return fields
  242. },
  243. // 此处 util 中的 groupField 不满足需求,特殊处理 groupField
  244. createStr(type = "device_count", vals, fields, tail) {
  245. const value = vals || [1, 2, 3, 4, 5, 6, 7, 14, 30]
  246. const p = 'd'
  247. const f = this.fields.map(item => item._id)
  248. fields = fields || f
  249. const strArr = value.map(item => {
  250. return fields.map(field => {
  251. return `retention.${field}.${p + '_' + item}.${type} as ${p + '_' + item}`
  252. })
  253. })
  254. if (tail) {
  255. strArr.push(tail)
  256. }
  257. const str = strArr.join()
  258. return str
  259. },
  260. getAllData(query) {
  261. this.getChartData(query, this.key, this.keyName)
  262. this.getTabelData(query)
  263. },
  264. getChartData(query, key = this.key, name = '访问人数') {
  265. // this.chartData = {}
  266. const {
  267. pageCurrent
  268. } = this.options
  269. query = stringifyQuery(query, null, ['uni_platform'])
  270. const groupField = this.createStr("device_count", [key], [this.field])
  271. const db = uniCloud.database()
  272. db.collection('uni-stat-result')
  273. .where(query)
  274. .field(`${this.stringifyField(this.fieldsMap, `d_${key}`)}, start_time`)
  275. .groupBy('start_time')
  276. .groupField(stringifyGroupField(this.fieldsMap, `d_${key}`))
  277. .orderBy('start_time', 'asc')
  278. .get({
  279. getCount: true
  280. })
  281. .then(res => {
  282. let {
  283. count,
  284. data
  285. } = res.result
  286. const options = {
  287. categories: [],
  288. series: [{
  289. name: `${key}天后${this.fieldName}`,
  290. data: []
  291. }]
  292. }
  293. for (const item of data) {
  294. const x = formatDate(item.start_time, 'day')
  295. const y = item[`d_${key}`]
  296. options.series[0].data.push(y)
  297. options.categories.push(x)
  298. }
  299. this.chartData = options
  300. }).catch((err) => {
  301. console.error(err)
  302. // err.message 错误信息
  303. // err.code 错误码
  304. }).finally(() => {
  305. this.loading = false
  306. })
  307. },
  308. getTabelData(query) {
  309. const {
  310. pageCurrent
  311. } = this.options
  312. query = stringifyQuery(query, null, ['uni_platform'])
  313. const tail = this.field + "_count"
  314. const groupField = this.createStr('user_rate', '', [this.field], tail)
  315. this.loading = true
  316. const db = uniCloud.database()
  317. db.collection('uni-stat-result')
  318. .where(query)
  319. .field(this.stringifyField(this.fieldsMap))
  320. .groupBy('start_time')
  321. .groupField(stringifyGroupField(this.fieldsMap))
  322. .orderBy('start_time', 'desc')
  323. .skip((pageCurrent - 1) * this.options.pageSize)
  324. .limit(this.options.pageSize)
  325. .get({
  326. getCount: true
  327. })
  328. .then(res => {
  329. const {
  330. count,
  331. data
  332. } = res.result
  333. for (const item of data) {
  334. mapfields(this.fieldsMap, item, item)
  335. }
  336. this.options.total = count
  337. this.tableData = []
  338. this.tableData = data
  339. }).catch((err) => {
  340. console.error(err)
  341. // err.message 错误信息
  342. // err.code 错误码
  343. }).finally(() => {
  344. this.loading = false
  345. })
  346. },
  347. //获取渠道信息
  348. getChannelData(appid, platform_id) {
  349. this.query.channel_id = ''
  350. const db = uniCloud.database()
  351. const condition = {}
  352. //对应应用
  353. appid = appid ? appid : this.query.appid
  354. if (appid) {
  355. condition.appid = appid
  356. }
  357. //对应平台
  358. platform_id = platform_id ? platform_id : this.query.platform_id
  359. if (platform_id) {
  360. condition.platform_id = platform_id
  361. }
  362. let platformTemp = db.collection('uni-stat-app-platforms')
  363. .field('_id, name')
  364. .getTemp()
  365. let channelTemp = db.collection('uni-stat-app-channels')
  366. .where(condition)
  367. .field('_id, channel_name, create_time, platform_id')
  368. .getTemp()
  369. db.collection(channelTemp, platformTemp)
  370. .orderBy('platform_id', 'asc')
  371. .get()
  372. .then(res => {
  373. let data = res.result.data
  374. let channels = []
  375. if (data.length > 0) {
  376. let channelName
  377. for (let i in data) {
  378. channelName = data[i].channel_name ? data[i].channel_name : '默认'
  379. if (data[i].platform_id.length > 0) {
  380. channelName = data[i].platform_id[0].name + '-' + channelName
  381. }
  382. channels.push({
  383. value: data[i]._id,
  384. text: channelName
  385. })
  386. }
  387. }
  388. this.channelData = channels
  389. })
  390. .catch((err) => {
  391. console.error(err)
  392. // err.message 错误信息
  393. // err.code 错误码
  394. }).finally(() => {})
  395. }
  396. }
  397. }
  398. </script>
  399. <style lang="scss">
  400. .flex {
  401. display: flex;
  402. flex-wrap: wrap;
  403. align-items: center;
  404. }
  405. .label-text {
  406. font-size: 14px;
  407. color: #666;
  408. margin: auto 0;
  409. margin-right: 5px;
  410. }
  411. .line-bottom {
  412. border-bottom: 2px solid #eee;
  413. }
  414. .uni-stat-table-bg {
  415. background-color: #4e82d9;
  416. color: #fff;
  417. }
  418. </style>