Skip to content

Commit 2f8be55

Browse files
committed
feature: Show user habit
1 parent 9133292 commit 2f8be55

File tree

25 files changed

+768
-84
lines changed

25 files changed

+768
-84
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ElCard } from 'element-plus'
2+
import { h, Ref } from 'vue'
3+
4+
export type ChartProps = {
5+
chartRef: Ref<HTMLDivElement>
6+
}
7+
8+
const _default = (props: ChartProps) => h(ElCard,
9+
{ class: 'chart-container-card' },
10+
() => h('div', { class: 'chart-container', ref: props.chartRef })
11+
)
12+
13+
export default _default
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { EChartOption } from "echarts"
2+
import { PeriodKey, PERIODS_PER_DATE } from "../../../../entity/dto/period-info"
3+
import PeriodResult from "../../../../entity/dto/period-result"
4+
import { formatPeriodCommon, formatTime } from "../../../../util/time"
5+
import { t } from "../../../locale"
6+
7+
const formatTimeOfEchart = (params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[], merge: boolean) => {
8+
const format: EChartOption.Tooltip.Format = params instanceof Array ? params[0] : params
9+
const { value } = format
10+
// If merge, don't show the date
11+
const start = formatTime(value[2], merge ? '{h}:{i}' : '{m}-{d} {h}:{i}')
12+
const end = formatTime(value[3], '{h}:{i}')
13+
return `${formatPeriodCommon(value[1] * 1000)}<br/>${start}-${end}`
14+
}
15+
16+
const title = t(msg => msg.period.chart.title)
17+
const baseOptions: EChartOption<EChartOption.SeriesBar> = {
18+
title: {
19+
text: title,
20+
left: 'center'
21+
},
22+
tooltip: {},
23+
toolbox: {
24+
show: true,
25+
feature: {
26+
saveAsImage: {
27+
show: true,
28+
title: t(msg => msg.period.chart.saveAsImageTitle),
29+
name: title, // file name
30+
excludeComponents: ['toolbox'],
31+
pixelRatio: 1
32+
}
33+
}
34+
},
35+
xAxis: {
36+
axisLabel: {/** placeholder */ },
37+
type: 'time',
38+
axisLine: {
39+
show: false
40+
}
41+
},
42+
yAxis: {
43+
name: t(msg => msg.period.chart.yAxisName),
44+
type: 'value'
45+
},
46+
series: [{
47+
type: "bar",
48+
large: true,
49+
data: [],
50+
barGap: '0%', // Make series be overlap
51+
barCategoryGap: '0%'
52+
}]
53+
}
54+
55+
type _Props = {
56+
data: PeriodResult[]
57+
merge: boolean
58+
periodSize: number
59+
}
60+
61+
export type OptionProps = _Props
62+
63+
const merge2OneDay = (data: PeriodResult[], periodSize: number) => {
64+
const map: Map<number, number> = new Map()
65+
data.forEach(item => {
66+
const key = Math.floor(item.getStartOrder() / periodSize)
67+
const val = map.get(key) || 0
68+
map.set(key, val + item.millseconds)
69+
})
70+
const result = []
71+
let period = PeriodKey.of(new Date(), 0)
72+
for (let i = 0; i < PERIODS_PER_DATE / periodSize; i++) {
73+
const key = period.order / periodSize
74+
result.push(PeriodResult.of(period.after(periodSize - 1), periodSize, map.get(key) || 0))
75+
period = period.after(periodSize)
76+
}
77+
return result
78+
}
79+
80+
function formatXAxis(ts: number) {
81+
const date = new Date(ts)
82+
if (date.getHours() === 0 && date.getMinutes() === 0) {
83+
return formatTime(date, '{m}-{d}')
84+
} else {
85+
return formatTime(date, '{h}:{i}')
86+
}
87+
}
88+
89+
const generateOptions = ({ data, merge, periodSize }: _Props) => {
90+
const periodData: PeriodResult[] = merge ? merge2OneDay(data, periodSize) : data
91+
const valueData: any[] = []
92+
periodData.forEach((item) => {
93+
const startTime = item.startTime.getTime()
94+
const endTime = item.endTime.getTime()
95+
const seconds = Math.floor(item.millseconds / 1000)
96+
const x = (startTime + endTime) / 2
97+
valueData.push([x, seconds, startTime, endTime])
98+
})
99+
const xAxis = baseOptions.xAxis as EChartOption.XAxis
100+
xAxis.min = periodData[0].startTime.getTime()
101+
xAxis.max = periodData[periodData.length - 1].endTime.getTime()
102+
xAxis.axisLabel.formatter = merge ? '{HH}:{mm}' : formatXAxis
103+
baseOptions.series[0].data = valueData
104+
baseOptions.tooltip.formatter = params => formatTimeOfEchart(params, merge)
105+
106+
return baseOptions
107+
}
108+
109+
export default generateOptions
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { ElDatePicker, ElOption, ElSelect, ElSwitch } from "element-plus"
2+
import { ref, Ref, h } from "vue"
3+
import { daysAgo } from "../../../util/time"
4+
import { t } from "../../locale"
5+
import { PeriodMessage } from "../../locale/components/period"
6+
7+
const datePickerShortcut = (msg: keyof PeriodMessage['dateRange'], agoOfStart: number) => {
8+
return {
9+
text: t(messages => messages.period.dateRange[msg]),
10+
value: daysAgo(agoOfStart, 0)
11+
}
12+
}
13+
14+
type _Props = {
15+
dateRangeRef: Ref<Date[]>
16+
periodSizeRef: Ref<string>
17+
mergeRef: Ref<boolean>
18+
}
19+
20+
export type FilterProps = _Props
21+
22+
const trenderSearchingRef: Ref<boolean> = ref(false)
23+
24+
const options: { [size: string]: keyof PeriodMessage['sizes'] } = {
25+
1: 'fifteen',
26+
2: 'halfHour',
27+
4: 'hour'
28+
}
29+
30+
// Period size select
31+
const selectOptions = () => Object.entries(options)
32+
.map(([size, msg]) => h(ElOption, { label: t(root => root.period.sizes[msg]), value: size }))
33+
const periodSizeSelect = (periodSizeRef: Ref<string>) => h(ElSelect,
34+
{
35+
placeholder: t(msg => msg.trender.hostPlaceholder),
36+
class: 'filter-item',
37+
modelValue: periodSizeRef.value,
38+
filterable: true,
39+
loading: trenderSearchingRef.value,
40+
onChange: (val: string) => periodSizeRef.value = val,
41+
}, selectOptions)
42+
43+
const shortcuts = [
44+
datePickerShortcut('latestDay', 1),
45+
datePickerShortcut('latestWeek', 7),
46+
datePickerShortcut('latest15Days', 15),
47+
]
48+
// Date picker
49+
const picker = (dateRangeRef: Ref<Date[]>) => h(ElDatePicker, {
50+
modelValue: dateRangeRef.value,
51+
type: 'daterange',
52+
format: 'YYYY/MM/DD',
53+
clearable: false,
54+
rangeSeparator: '-',
55+
startPlaceholder: t(msg => msg.trender.startDate),
56+
endPlaceholder: t(msg => msg.trender.endDate),
57+
unlinkPanels: true,
58+
disabledDate: (date: Date) => date.getTime() > new Date().getTime(),
59+
shortcuts,
60+
'onUpdate:modelValue': (newVal: Date[]) => dateRangeRef.value = newVal
61+
})
62+
const datePickerItem = (dateRangeRef: Ref<Date[]>) => h('span', { class: 'filter-item' }, picker(dateRangeRef))
63+
64+
const mergeName = () => h('a', { class: 'filter-name' }, t(msg => msg.period.merge.label))
65+
const mergeSwitch = (mergeRef: Ref<boolean>) => h(ElSwitch,
66+
{
67+
class: 'filter-item',
68+
modelValue: mergeRef.value,
69+
onChange: (val: boolean) => mergeRef.value = val
70+
}
71+
)
72+
73+
const filterContainer = ({
74+
dateRangeRef,
75+
periodSizeRef,
76+
mergeRef
77+
}: _Props) => h('div',
78+
{ class: 'filter-container' },
79+
[
80+
// Size select
81+
periodSizeSelect(periodSizeRef),
82+
// Date range picker
83+
datePickerItem(dateRangeRef),
84+
// Merge date
85+
mergeName(), mergeSwitch(mergeRef)
86+
]
87+
)
88+
89+
export default filterContainer

src/app/components/period/index.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { ECharts, init } from "echarts"
2+
import { computed, ComputedRef, defineComponent, h, onMounted, ref, Ref, watch } from "vue"
3+
import { MAX_PERIOD_ORDER, PeriodKey } from "../../../entity/dto/period-info"
4+
import periodService, { PeriodQueryParam } from "../../../service/period-service"
5+
import { daysAgo, isSameDay } from "../../../util/time"
6+
import chart, { ChartProps } from "./chart"
7+
import generateOptions from "./chart/option"
8+
import filter from './filter'
9+
10+
const periodSizeRef: Ref<string> = ref('1')
11+
const dateRangeRef: Ref<Date[]> = ref(daysAgo(1, 0))
12+
const mergeRef: Ref<boolean> = ref(false)
13+
const periodSizeNumberRef: ComputedRef<number> = computed(() => Number.parseInt(periodSizeRef.value))
14+
const chartRef: Ref<HTMLDivElement> = ref()
15+
let bar: ECharts
16+
17+
const filterProps = { dateRangeRef, periodSizeRef, mergeRef }
18+
const chartProps: ChartProps = { chartRef }
19+
20+
const queryParamRef: ComputedRef<PeriodQueryParam> = computed(() => {
21+
let dateRange = dateRangeRef.value
22+
if (dateRange.length !== 2) dateRange = daysAgo(1, 0)
23+
const endDate = dateRange[1]
24+
const startDate = dateRange[0]
25+
const now = new Date()
26+
const endIsToday = isSameDay(now, endDate)
27+
28+
let periodEnd = endIsToday ? PeriodKey.of(now) : PeriodKey.of(endDate, MAX_PERIOD_ORDER)
29+
let periodStart = endIsToday ? PeriodKey.of(startDate, periodEnd.order).after(1) : PeriodKey.of(startDate, 0)
30+
31+
const remainder = (periodEnd.order + 1) % periodSizeNumberRef.value
32+
33+
if (remainder) {
34+
periodEnd = periodEnd.before(remainder)
35+
periodStart = periodStart.before(remainder)
36+
}
37+
38+
return {
39+
dateRange: dateRangeRef.value,
40+
// Must query one by one, if merged
41+
periodSize: mergeRef.value ? 1 : periodSizeNumberRef.value,
42+
periodStart,
43+
periodEnd
44+
}
45+
})
46+
47+
const queryAndRenderChart = () => periodService.list(queryParamRef.value)
48+
.then(val => {
49+
const newOptions = generateOptions({ data: val, merge: mergeRef.value, periodSize: periodSizeNumberRef.value })
50+
bar.setOption(newOptions, true, false)
51+
})
52+
53+
watch([dateRangeRef, mergeRef, periodSizeRef], () => queryAndRenderChart())
54+
55+
56+
const _default = defineComponent(() => {
57+
onMounted(() => {
58+
bar = init(chartRef.value)
59+
queryAndRenderChart()
60+
})
61+
62+
return () => h('div', { class: 'content-container' }, [filter(filterProps), chart(chartProps)])
63+
})
64+
65+
export default _default

src/app/components/report/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import timerService, { SortDirect } from "../../../service/timer-service"
55
import whitelistService from "../../../service/whitelist-service"
66
import { formatTime } from "../../../util/time"
77
import './styles/element'
8-
import './styles/filter'
98
import table, { ElSortDirect, SortInfo, TableProps } from "./table"
109
import filter, { FilterProps } from "./filter"
1110
import pagination, { PaginationProps } from "./pagination"

src/app/components/report/styles/filter.sass

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/app/components/trender/components/domain-trender.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const options: EChartOption<EChartOption.SeriesLine> = {
3636
show: true,
3737
title: t(msg => msg.trender.saveAsImageTitle),
3838
excludeComponents: ['toolbox'],
39-
pixelRatio: 2,
39+
pixelRatio: 1,
4040
backgroundColor: '#fff'
4141
}
4242
}
@@ -179,7 +179,7 @@ const _default = defineComponent((_, context: SetupContext) => {
179179
queryData()
180180
})
181181

182-
return () => h(ElCard, { style: 'margin-top: 25px;' }, () => h('div', { style: 'width:100%;height:600px;', ref: chartRef }))
182+
return () => h(ElCard, { class: 'chart-container-card' }, () => h('div', { class: 'chart-container', ref: chartRef }))
183183
})
184184

185185
export default _default

src/app/layout/menu.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { defineComponent, h, onMounted, ref, Ref } from "vue"
22
import { ElMenu, ElMenuItem, ElSubmenu } from "element-plus"
33
import { RouteLocationNormalizedLoaded, Router, useRoute, useRouter } from "vue-router"
44
import { I18nKey, t } from "../locale"
5+
import { MenuMessage } from "../locale/components/menu"
56

67
declare type MenuItem = {
7-
title: I18nKey
8+
title: keyof MenuMessage
89
icon: string
910
route: string
1011
children?: MenuItem[]
@@ -13,26 +14,30 @@ declare type MenuItem = {
1314
// All menu items
1415
const ALL_MENU: MenuItem[] = [
1516
{
16-
title: msg => msg.menu.data,
17+
title: 'data',
1718
icon: 's-platform',
1819
route: '/data',
1920
children: [
2021
{
21-
title: msg => msg.menu.dataReport,
22+
title: 'dataReport',
2223
route: '/data/report',
2324
icon: 'date'
2425
}, {
25-
title: msg => msg.menu.dataHistory,
26+
title: 'period',
27+
route: '/data/period',
28+
icon: 'aim'
29+
}, {
30+
title: 'dataHistory',
2631
route: '/data/history',
2732
icon: 'stopwatch'
2833
}, {
29-
title: msg => msg.menu.dataClear,
34+
title: 'dataClear',
3035
route: '/data/clear',
3136
icon: 'folder'
3237
}
3338
]
3439
}, {
35-
title: msg => msg.menu.setting,
40+
title: 'setting',
3641
route: '/setting',
3742
icon: 'setting'
3843
}
@@ -52,10 +57,10 @@ const openMenu = (route: string, title: I18nKey) => {
5257
}
5358

5459
const renderMenuLeaf = (menu: MenuItem) => h(ElMenuItem,
55-
{ index: menu.route, onClick: () => openMenu(menu.route, menu.title) },
60+
{ index: menu.route, onClick: () => openMenu(menu.route, msg => msg.menu[menu.title]) },
5661
{
5762
default: () => h('i', { class: `el-icon-${menu.icon}` }),
58-
title: () => h('span', t(menu.title))
63+
title: () => h('span', t(msg => msg.menu[menu.title]))
5964
}
6065
)
6166

@@ -65,7 +70,7 @@ const renderMenu = (menu: MenuItem) => {
6570

6671
const subMenuProps = { index: menu.route }
6772
const subMenuSlots = {
68-
title: () => [h('i', { class: `el-icon-${menu.icon}` }), h('span', t(menu.title))],
73+
title: () => [h('i', { class: `el-icon-${menu.icon}` }), h('span', t(msg => msg.menu[menu.title]))],
6974
default: () => menu.children.map(renderMenuLeaf)
7075
}
7176
return h(ElSubmenu, subMenuProps, subMenuSlots)

0 commit comments

Comments
 (0)