Skip to content

Commit bcf0096

Browse files
committed
FEAT:Time Limit (#15)
1 parent 35fbec1 commit bcf0096

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1225
-210
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@
4040
},
4141
"dependencies": {
4242
"axios": "^0.21.1",
43+
"clipboardy": "^2.3.0",
4344
"echarts": "^5.1.2",
4445
"element-plus": "^1.0.2-beta.48",
4546
"vue": "^3.0.11",
4647
"vue-router": "^4.0.8"
4748
}
48-
}
49+
}

src/app/components/clear/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { computed, defineComponent, h, ref, Ref } from "vue"
44
import { getUsedStorage } from "../../../database/memory-detector"
55
import './style'
66
import { t } from "../../locale"
7+
import { renderContentContainer } from "../common/content-container"
78

89
const byte2Mb = (size: number) => Math.round((size || 0) / 1024.0 / 1024.0 * 1000) / 1000
910

@@ -65,4 +66,4 @@ const firstRow = () => h(ElRow, { gutter: 20 },
6566
]
6667
)
6768

68-
export default defineComponent(() => () => h('div', { class: 'content-container' }, firstRow()))
69+
export default defineComponent(() => renderContentContainer(firstRow))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { h, VNode } from "vue"
2+
3+
/**
4+
* @returns the render function of content container
5+
*/
6+
export function renderContentContainer(childNodes: () => VNode[] | VNode): () => VNode {
7+
return () => h('div', { class: 'content-container' }, childNodes())
8+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { ElButton, ElInput, ElSwitch } from "element-plus"
2+
import { h, Ref, VNode } from "vue"
3+
import { I18nKey, t } from "../../locale"
4+
import { QueryData } from "./constants"
5+
6+
/**
7+
* @returns the render function of filtr containers
8+
*/
9+
export function renderFilterContainer<Props>(childNodes: (props: Props) => VNode[]): (props: Props) => VNode {
10+
// The container render function
11+
return (props: Props) => h(
12+
'div',
13+
{ class: 'filter-container' },
14+
childNodes(props)
15+
)
16+
}
17+
18+
type _I18nKey = I18nKey | string
19+
20+
const processI18nKey = (key: _I18nKey) => typeof key !== 'string' ? t(key) : key
21+
22+
/**
23+
* @returns filter item input VNode
24+
*/
25+
export const inputFilterItem = (modelValue: Ref<string>, placeholder: _I18nKey, queryData?: QueryData) => h(ElInput,
26+
{
27+
class: 'filter-item',
28+
modelValue: modelValue.value,
29+
placeholder: processI18nKey(placeholder),
30+
clearable: true,
31+
onClear: () => modelValue.value = '',
32+
onInput: (val: string) => modelValue.value = val.trim(),
33+
onKeyup: (event: KeyboardEvent) => event.key === 'Enter' && queryData && queryData()
34+
}
35+
)
36+
37+
const switchLabel = (label: string) => h('a', { class: 'filter-name' }, label)
38+
39+
const elSwitch = (modelValue: Ref<boolean>, queryData?: QueryData) => h(ElSwitch,
40+
{
41+
class: 'filter-item',
42+
modelValue: modelValue.value,
43+
onChange: (val: boolean) => {
44+
modelValue.value = val
45+
queryData && queryData()
46+
}
47+
}
48+
)
49+
50+
export const switchFilterItem = (modelValue: Ref<boolean>, labelName: _I18nKey, queryData?: QueryData) => [
51+
switchLabel(processI18nKey(labelName)),
52+
elSwitch(modelValue, queryData)
53+
]
54+
55+
export type FilterButtonProps = {
56+
onClick: () => void
57+
type?: 'primary' | 'info' | 'success' | 'warning' | 'danger'
58+
label: _I18nKey
59+
icon?: string
60+
}
61+
62+
export const buttonFilterItem = ({ onClick, type, label, icon }: FilterButtonProps) => h(ElButton, {
63+
class: 'filter-item',
64+
type,
65+
icon: `el-icon-${icon}`,
66+
onClick
67+
}, () => processI18nKey(label))

src/app/components/habit/chart/option.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const formatTimeOfEchart = (params: EChartOption.Tooltip.Format | EChartOption.T
1313
return `${formatPeriodCommon(value[1] * 1000)}<br/>${start}-${end}`
1414
}
1515

16-
const title = t(msg => msg.period.chart.title)
16+
const title = t(msg => msg.habit.chart.title)
1717
const baseOptions: EChartOption<EChartOption.SeriesBar> = {
1818
title: {
1919
text: title,
@@ -25,7 +25,7 @@ const baseOptions: EChartOption<EChartOption.SeriesBar> = {
2525
feature: {
2626
saveAsImage: {
2727
show: true,
28-
title: t(msg => msg.period.chart.saveAsImageTitle),
28+
title: t(msg => msg.habit.chart.saveAsImageTitle),
2929
name: title, // file name
3030
excludeComponents: ['toolbox'],
3131
pixelRatio: 1
@@ -40,7 +40,7 @@ const baseOptions: EChartOption<EChartOption.SeriesBar> = {
4040
}
4141
},
4242
yAxis: {
43-
name: t(msg => msg.period.chart.yAxisName),
43+
name: t(msg => msg.habit.chart.yAxisName),
4444
type: 'value'
4545
},
4646
series: [{

src/app/components/habit/filter.ts

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { ElDatePicker, ElOption, ElSelect, ElSwitch } from "element-plus"
22
import { ref, Ref, h } from "vue"
33
import { daysAgo } from "../../../util/time"
44
import { t } from "../../locale"
5-
import { PeriodMessage } from "../../locale/components/period"
5+
import { HabitMessage } from "../../locale/components/habit"
6+
import { renderFilterContainer, switchFilterItem } from "../common/filter"
67

7-
const datePickerShortcut = (msg: keyof PeriodMessage['dateRange'], agoOfStart: number) => {
8+
const datePickerShortcut = (msg: keyof HabitMessage['dateRange'], agoOfStart: number) => {
89
return {
9-
text: t(messages => messages.period.dateRange[msg]),
10+
text: t(messages => messages.habit.dateRange[msg]),
1011
value: daysAgo(agoOfStart, 0)
1112
}
1213
}
@@ -21,7 +22,7 @@ export type FilterProps = _Props
2122

2223
const trenderSearchingRef: Ref<boolean> = ref(false)
2324

24-
const options: { [size: string]: keyof PeriodMessage['sizes'] } = {
25+
const options: { [size: string]: keyof HabitMessage['sizes'] } = {
2526
1: 'fifteen',
2627
2: 'halfHour',
2728
4: 'hour',
@@ -30,7 +31,7 @@ const options: { [size: string]: keyof PeriodMessage['sizes'] } = {
3031

3132
// Period size select
3233
const selectOptions = () => Object.entries(options)
33-
.map(([size, msg]) => h(ElOption, { label: t(root => root.period.sizes[msg]), value: size }))
34+
.map(([size, msg]) => h(ElOption, { label: t(root => root.habit.sizes[msg]), value: size }))
3435
const periodSizeSelect = (periodSizeRef: Ref<string>) => h(ElSelect,
3536
{
3637
placeholder: t(msg => msg.trender.hostPlaceholder),
@@ -41,7 +42,7 @@ const periodSizeSelect = (periodSizeRef: Ref<string>) => h(ElSelect,
4142
onChange: (val: string) => periodSizeRef.value = val,
4243
}, selectOptions)
4344

44-
type ShortCutProp = [label: keyof PeriodMessage['dateRange'], dayAgo: number]
45+
type ShortCutProp = [label: keyof HabitMessage['dateRange'], dayAgo: number]
4546
const shortcutProps: ShortCutProp[] = [
4647
['latestDay', 1],
4748
['latest3Days', 3],
@@ -69,29 +70,17 @@ const picker = (dateRangeRef: Ref<Date[]>) => h(ElDatePicker, {
6970
})
7071
const datePickerItem = (dateRangeRef: Ref<Date[]>) => h('span', { class: 'filter-item' }, picker(dateRangeRef))
7172

72-
const averageName = () => h('a', { class: 'filter-name' }, t(msg => msg.period.average.label))
73-
const averageSwitch = (averageRef: Ref<boolean>) => h(ElSwitch,
74-
{
75-
class: 'filter-item',
76-
modelValue: averageRef.value,
77-
onChange: (val: boolean) => averageRef.value = val
78-
}
79-
)
80-
81-
const filterContainer = ({
73+
const childNodes = ({
8274
dateRangeRef,
8375
periodSizeRef,
8476
averageRef
85-
}: _Props) => h('div',
86-
{ class: 'filter-container' },
87-
[
77+
}: _Props) => [
8878
// Size select
8979
periodSizeSelect(periodSizeRef),
9080
// Date range picker
9181
datePickerItem(dateRangeRef),
9282
// Average by date
93-
averageName(), averageSwitch(averageRef)
83+
...switchFilterItem(averageRef, msg => msg.habit.average.label)
9484
]
95-
)
9685

97-
export default filterContainer
86+
export default renderFilterContainer(childNodes)

src/app/components/habit/index.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { computed, ComputedRef, defineComponent, h, onMounted, ref, Ref, watch }
33
import { MAX_PERIOD_ORDER, PeriodKey } from "../../../entity/dto/period-info"
44
import periodService, { PeriodQueryParam } from "../../../service/period-service"
55
import { daysAgo, isSameDay } from "../../../util/time"
6+
import { renderContentContainer } from "../common/content-container"
67
import chart, { ChartProps } from "./chart"
78
import generateOptions from "./chart/option"
89
import filter from './filter'
@@ -25,8 +26,15 @@ const queryParamRef: ComputedRef<PeriodQueryParam> = computed(() => {
2526
const now = new Date()
2627
const endIsToday = isSameDay(now, endDate)
2728

28-
let periodEnd = endIsToday ? PeriodKey.of(now).before(1) : PeriodKey.of(endDate, MAX_PERIOD_ORDER)
29-
let periodStart = endIsToday ? PeriodKey.of(startDate, periodEnd.order).after(1) : PeriodKey.of(startDate, 0)
29+
let periodEnd: PeriodKey, periodStart: PeriodKey
30+
if (endIsToday) {
31+
periodEnd = PeriodKey.of(now)
32+
periodStart = PeriodKey.of(startDate, periodEnd.order)
33+
periodEnd = periodEnd.before(1)
34+
} else {
35+
periodEnd = PeriodKey.of(endDate, MAX_PERIOD_ORDER)
36+
periodStart = PeriodKey.of(startDate, 0)
37+
}
3038

3139
const remainder = (periodEnd.order + 1) % periodSizeNumberRef.value
3240
if (remainder) {
@@ -51,14 +59,15 @@ const queryAndRenderChart = () => periodService.list(queryParamRef.value)
5159

5260
watch([dateRangeRef, averageRef, periodSizeRef], () => queryAndRenderChart())
5361

62+
const handleMounted = () => {
63+
bar = init(chartRef.value)
64+
queryAndRenderChart()
65+
}
5466

5567
const _default = defineComponent(() => {
56-
onMounted(() => {
57-
bar = init(chartRef.value)
58-
queryAndRenderChart()
59-
})
60-
61-
return () => h('div', { class: 'content-container' }, [filter(filterProps), chart(chartProps)])
68+
// Must use closure, not functaion variable
69+
onMounted(() => handleMounted())
70+
return renderContentContainer(() => [filter(filterProps), chart(chartProps)])
6271
})
6372

6473
export default _default
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ElButton } from "element-plus"
2+
import { defineComponent, h, SetupContext } from "vue"
3+
import { t } from "../../../locale"
4+
5+
const saveButton = (onClick: () => void) => h(ElButton,
6+
{
7+
onClick,
8+
type: 'primary',
9+
icon: 'el-icon-check'
10+
},
11+
() => t(msg => msg.limit.button.save)
12+
)
13+
14+
const _default = defineComponent((_props, context: SetupContext) => {
15+
const onSave: () => void = (context.attrs.onSave || (() => { })) as () => void
16+
return () => h('span', {}, [
17+
saveButton(onSave)
18+
])
19+
})
20+
21+
export default _default
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { ElForm } from "element-plus"
2+
import { computed, ComputedRef, defineComponent, h, ref, Ref, useContext } from "vue"
3+
import '../style/el-input'
4+
import pathEdit from "./path-edit"
5+
import timeFormItem from "./time-limit"
6+
import urlFormItem, { FormUrlProps, Protocol } from './url'
7+
import UrlPathItem from "./url-path-item"
8+
9+
// Limitted time
10+
const hourRef: Ref<number> = ref()
11+
const minuteRef: Ref<number> = ref()
12+
const secondRef: Ref<number> = ref()
13+
14+
// Limitted url
15+
const protocolRef: Ref<string> = ref()
16+
const pathItemsRef: Ref<UrlPathItem[]> = ref([])
17+
const urlRef: ComputedRef<string> = computed(() => pathItemsRef.value.map(i => i.toString()).join('/'))
18+
19+
const timeProps = { hourRef, minuteRef, secondRef }
20+
const urlProps: FormUrlProps = { protocolRef, pathItemsRef, urlRef }
21+
const pathEditProps = { pathItemsRef }
22+
const render = () => h(ElForm,
23+
{ labelWidth: '100px' },
24+
() => [timeFormItem(timeProps), urlFormItem(urlProps), pathEdit(pathEditProps)]
25+
)
26+
27+
function init() {
28+
hourRef.value = 1
29+
minuteRef.value = undefined
30+
secondRef.value = undefined
31+
protocolRef.value = Protocol.ALL
32+
pathItemsRef.value = []
33+
}
34+
35+
init()
36+
37+
export type FormData = {
38+
/**
39+
* Time / seconds
40+
*/
41+
timeLimit: number
42+
/**
43+
* Protocol + url
44+
*/
45+
url: string
46+
}
47+
48+
const handleGetData = () => {
49+
let timeLimit = 0
50+
timeLimit += (hourRef.value || 0) * 3600
51+
timeLimit += (minuteRef.value || 0) * 60
52+
timeLimit += (secondRef.value || 0)
53+
const url = urlRef.value || ''
54+
const protocol = protocolRef.value
55+
const result: FormData = {
56+
timeLimit,
57+
url: url ? protocol + url : ''
58+
}
59+
return result
60+
}
61+
62+
const _default = defineComponent(() => {
63+
useContext().expose({
64+
getData: () => handleGetData(),
65+
clean: () => init()
66+
})
67+
return render
68+
})
69+
70+
export default _default

0 commit comments

Comments
 (0)