Skip to content

Commit bbb1d19

Browse files
authored
Site analysis(#201)
Site analysis(#201)
2 parents eeafbf1 + f7978ff commit bbb1d19

File tree

43 files changed

+1456
-791
lines changed

Some content is hidden

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

43 files changed

+1456
-791
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright (c) 2023 Hengyang Zhang
3+
*
4+
* This software is released under the MIT License.
5+
* https://opensource.org/licenses/MIT
6+
*/
7+
8+
import type { I18nKey } from "@app/locale"
9+
import type { PropType, VNode } from "vue"
10+
11+
import { tN } from "@app/locale"
12+
import { defineComponent, h } from "vue"
13+
14+
export type IndicatorProps = {
15+
mainName: string
16+
mainValue: string
17+
subTips?: I18nKey
18+
subValue?: string
19+
}
20+
21+
function renderChildren(props: IndicatorProps): VNode[] {
22+
const { mainName, subTips, mainValue, subValue } = props
23+
const children = [
24+
h('div', { class: 'indicator-name' }, mainName),
25+
h('div', { class: 'indicator-value' }, mainValue || '-'),
26+
]
27+
const subTipsLine = []
28+
if (subTips || subValue) {
29+
const subValueSpan = h('span', { class: 'indicator-sub-value' }, subValue || '-')
30+
if (subTips) {
31+
subTipsLine.push(...tN(subTips, { value: subValueSpan }))
32+
} else {
33+
subTipsLine.push(subValueSpan)
34+
}
35+
} else {
36+
subTipsLine.push('')
37+
}
38+
children.push(h('div', { class: 'indicator-sub-tip' }, subTipsLine))
39+
return children
40+
}
41+
42+
const _default = defineComponent({
43+
props: {
44+
mainName: String,
45+
mainValue: String,
46+
subTips: Function as PropType<I18nKey>,
47+
subValue: String,
48+
},
49+
setup(props) {
50+
return () => h('div', { class: 'analysis-indicator-container' }, renderChildren(props))
51+
}
52+
})
53+
54+
export default _default
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright (c) 2023 Hengyang Zhang
3+
*
4+
* This software is released under the MIT License.
5+
* https://opensource.org/licenses/MIT
6+
*/
7+
8+
import { ElCard } from "element-plus"
9+
import { defineComponent, h } from "vue"
10+
11+
const _default = defineComponent({
12+
props: {
13+
title: String
14+
},
15+
setup(props, ctx) {
16+
const slots = ctx.slots
17+
const { default: default_ } = slots
18+
return () => {
19+
const title = h('div', { class: 'analysis-row-title' }, props.title)
20+
return h(ElCard, { class: 'analysis-row-card' }, () => [title, h(default_, { class: 'analysis-row-body' })])
21+
}
22+
}
23+
})
24+
25+
export default _default
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Copyright (c) 2021 Hengyang Zhang
3+
*
4+
* This software is released under the MIT License.
5+
* https://opensource.org/licenses/MIT
6+
*/
7+
8+
import type { Ref, PropType, VNode } from "vue"
9+
10+
import { ElOption, ElSelect, ElTag } from "element-plus"
11+
import { ref, h, defineComponent } from "vue"
12+
import statService, { HostSet } from "@service/stat-service"
13+
import { t } from "@app/locale"
14+
import SelectFilterItem from "@app/components/common/select-filter-item"
15+
import { labelOfHostInfo } from "../util"
16+
17+
async function handleRemoteSearch(queryStr: string, trendDomainOptions: Ref<timer.site.SiteKey[]>, searching: Ref<boolean>) {
18+
if (!queryStr) {
19+
trendDomainOptions.value = []
20+
return
21+
}
22+
searching.value = true
23+
const domains: HostSet = await statService.listHosts(queryStr)
24+
const options: timer.site.SiteKey[] = []
25+
const { origin, merged, virtual } = domains
26+
origin.forEach(host => options.push({ host }))
27+
merged.forEach(host => options.push({ host, merged: true }))
28+
virtual.forEach(host => options.push({ host, virtual: true }))
29+
trendDomainOptions.value = options
30+
searching.value = false
31+
}
32+
33+
const HOST_PLACEHOLDER = t(msg => msg.analysis.common.hostPlaceholder)
34+
35+
const TIME_FORMAT_LABELS: { [key in timer.app.TimeFormat]: string } = {
36+
default: t(msg => msg.timeFormat.default),
37+
second: t(msg => msg.timeFormat.second),
38+
minute: t(msg => msg.timeFormat.minute),
39+
hour: t(msg => msg.timeFormat.hour)
40+
}
41+
42+
function keyOfHostInfo(option: timer.site.SiteKey): string {
43+
const { merged, virtual, host } = option
44+
let prefix = '_'
45+
merged && (prefix = 'm')
46+
virtual && (prefix = 'v')
47+
return `${prefix}${host || ''}`
48+
}
49+
50+
function hostInfoOfKey(key: string): timer.site.SiteKey {
51+
if (!key?.length) return undefined
52+
const prefix = key.charAt(0)
53+
return { host: key.substring(1), merged: prefix === 'm', virtual: prefix === 'v' }
54+
}
55+
56+
const MERGED_TAG_TXT = t(msg => msg.analysis.common.merged)
57+
const VIRTUAL_TAG_TXT = t(msg => msg.analysis.common.virtual)
58+
function renderHostLabel(hostInfo: timer.site.SiteKey): VNode[] {
59+
const result = [
60+
h('span', {}, hostInfo.host)
61+
]
62+
hostInfo.merged && result.push(
63+
h(ElTag, { size: 'small' }, () => MERGED_TAG_TXT)
64+
)
65+
hostInfo.virtual && result.push(
66+
h(ElTag, { size: 'small' }, () => VIRTUAL_TAG_TXT)
67+
)
68+
return result
69+
}
70+
71+
const _default = defineComponent({
72+
name: "TrendFilter",
73+
props: {
74+
site: Object as PropType<timer.site.SiteKey>,
75+
timeFormat: String as PropType<timer.app.TimeFormat>
76+
},
77+
emits: {
78+
siteChange: (_site: timer.site.SiteKey) => true,
79+
timeFormatChange: (_format: timer.app.TimeFormat) => true,
80+
},
81+
setup(props, ctx) {
82+
const domainKey: Ref<string> = ref('')
83+
const trendSearching: Ref<boolean> = ref(false)
84+
const trendDomainOptions: Ref<timer.site.SiteKey[]> = ref([])
85+
const defaultSite: timer.site.SiteKey = props.site
86+
const timeFormat: Ref<timer.app.TimeFormat> = ref(props.timeFormat)
87+
if (defaultSite) {
88+
domainKey.value = keyOfHostInfo(defaultSite)
89+
trendDomainOptions.value.push(defaultSite)
90+
}
91+
92+
function handleSiteChange() {
93+
const siteInfo: timer.site.SiteInfo = hostInfoOfKey(domainKey.value)
94+
ctx.emit('siteChange', siteInfo)
95+
}
96+
97+
return () => [
98+
h(ElSelect, {
99+
placeholder: HOST_PLACEHOLDER,
100+
class: 'filter-item',
101+
modelValue: domainKey.value,
102+
filterable: true,
103+
remote: true,
104+
loading: trendSearching.value,
105+
clearable: true,
106+
remoteMethod: (query: string) => handleRemoteSearch(query, trendDomainOptions, trendSearching),
107+
onChange: (key: string) => {
108+
domainKey.value = key
109+
handleSiteChange()
110+
},
111+
onClear: () => {
112+
domainKey.value = undefined
113+
handleSiteChange()
114+
}
115+
}, () => (trendDomainOptions.value || [])?.map(
116+
hostInfo => h(ElOption, {
117+
value: keyOfHostInfo(hostInfo),
118+
label: labelOfHostInfo(hostInfo),
119+
}, () => renderHostLabel(hostInfo))
120+
)),
121+
h(SelectFilterItem, {
122+
historyName: 'timeFormat',
123+
defaultValue: timeFormat.value,
124+
options: TIME_FORMAT_LABELS,
125+
onSelect: (newVal: timer.app.TimeFormat) => ctx.emit('timeFormatChange', timeFormat.value = newVal)
126+
})
127+
]
128+
}
129+
})
130+
131+
export default _default
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Copyright (c) 2023 Hengyang Zhang
3+
*
4+
* This software is released under the MIT License.
5+
* https://opensource.org/licenses/MIT
6+
*/
7+
import type { PropType, Ref, VNode } from "vue"
8+
9+
import { defineComponent, h, ref, watch } from "vue"
10+
import siteService from "@service/site-service"
11+
import Site from "./site"
12+
import RowCard from "../common/row-card"
13+
import Indicator from "../common/indicator"
14+
import "./summary.sass"
15+
import { ElCol, ElRow } from "element-plus"
16+
import { t } from "@app/locale"
17+
import { cvt2LocaleTime, periodFormatter } from "@app/util/time"
18+
19+
type Summary = {
20+
focus: number
21+
visit: number
22+
day: number
23+
firstDay?: string
24+
}
25+
26+
function computeSummary(site: timer.site.SiteKey, rows: timer.stat.Row[]): Summary {
27+
if (!site) return undefined
28+
29+
const summary: Summary = { focus: 0, visit: 0, day: 0 }
30+
summary.firstDay = rows?.[0]?.date
31+
rows.forEach(({ focus, time: visit }) => {
32+
summary.focus += focus
33+
summary.visit += visit
34+
focus && (summary.day += 1)
35+
})
36+
return summary
37+
}
38+
39+
const DAYS_LABEL = t(msg => msg.analysis.summary.day)
40+
const FOCUS_LABEL = t(msg => msg.analysis.common.focusTotal)
41+
const VISIT_LABEL = t(msg => msg.analysis.common.visitTotal)
42+
43+
function renderContent(siteInfo: timer.site.SiteInfo, summary: Summary, timeFormat: timer.app.TimeFormat): VNode {
44+
const { day, firstDay, focus, visit } = summary || {}
45+
return h(ElRow, { class: "analysis-summary-container" }, () => [
46+
h(ElCol, { span: 6 }, () => h(Site, { site: siteInfo })),
47+
h(ElCol, { span: 6 }, () => h(Indicator, {
48+
mainName: DAYS_LABEL,
49+
mainValue: day?.toString() || '-',
50+
subTips: msg => msg.analysis.summary.firstDay,
51+
subValue: firstDay ? `@${cvt2LocaleTime(firstDay)}` : ''
52+
})),
53+
h(ElCol, { span: 6 }, () => h(Indicator, {
54+
mainName: FOCUS_LABEL,
55+
mainValue: focus === undefined ? '-' : periodFormatter(focus, timeFormat, false),
56+
})),
57+
h(ElCol, { span: 6 }, () => h(Indicator, {
58+
mainName: VISIT_LABEL,
59+
mainValue: visit?.toString() || '-',
60+
})),
61+
])
62+
}
63+
64+
const _default = defineComponent({
65+
props: {
66+
site: Object as PropType<timer.site.SiteKey>,
67+
timeFormat: String as PropType<timer.app.TimeFormat>,
68+
rows: Array as PropType<timer.stat.Row[]>,
69+
},
70+
setup(props) {
71+
const siteInfo: Ref<timer.site.SiteInfo> = ref()
72+
const timeFormat: Ref<timer.app.TimeFormat> = ref(props.timeFormat)
73+
const summaryInfo: Ref<Summary> = ref(computeSummary(props.site, props.rows))
74+
75+
const querySiteInfo = async () => {
76+
const siteKey = props.site
77+
if (!siteKey) {
78+
siteInfo.value = undefined
79+
} else {
80+
siteInfo.value = (await siteService.get(siteKey)) || siteKey
81+
}
82+
}
83+
84+
watch(() => props.timeFormat, () => timeFormat.value = props.timeFormat)
85+
watch(() => props.site, querySiteInfo)
86+
watch(() => props.rows, () => summaryInfo.value = computeSummary(props.site, props.rows))
87+
88+
querySiteInfo()
89+
90+
return () => h(RowCard, {
91+
title: t(msg => msg.analysis.summary.title)
92+
}, () => renderContent(siteInfo.value, summaryInfo.value, timeFormat.value))
93+
}
94+
})
95+
96+
export default _default
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Copyright (c) 2023 Hengyang Zhang
3+
*
4+
* This software is released under the MIT License.
5+
* https://opensource.org/licenses/MIT
6+
*/
7+
8+
import { PropType, defineComponent, h } from "vue"
9+
import { labelOfHostInfo } from "../../util"
10+
import { t } from "@app/locale"
11+
12+
const renderIcon = (iconUrl: string) => h('img', { src: iconUrl, width: 24, height: 24 })
13+
const renderTitle = (title: string) => h('h1', { class: 'site-alias' }, title)
14+
const renderSubtitle = (subtitle: string) => h('p', { class: 'site-host' }, subtitle)
15+
16+
const EMPTY_DESC = t(msg => msg.analysis.common.emptyDesc)
17+
18+
function renderChildren(site: timer.site.SiteInfo) {
19+
if (!site) {
20+
return renderTitle(EMPTY_DESC)
21+
}
22+
const result = []
23+
24+
const { iconUrl, alias } = site
25+
const label = labelOfHostInfo(site)
26+
const title: string = alias ? alias : label
27+
const subtitle: string = alias ? label : undefined
28+
29+
iconUrl && result.push(renderIcon(iconUrl))
30+
result.push(renderTitle(title))
31+
subtitle && result.push(renderSubtitle(subtitle))
32+
return result
33+
}
34+
35+
const _default = defineComponent({
36+
props: {
37+
site: Object as PropType<timer.site.SiteInfo>,
38+
},
39+
setup(props) {
40+
return () => h('div', { class: 'site-container' }, renderChildren(props.site))
41+
}
42+
})
43+
44+
export default _default
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
.analysis-summary-container
3+
height: 140px
4+
>.el-col:not(:first-child)
5+
border-left: 1px var(--el-border-color) var(--el-border-style)
6+
.site-container
7+
position: relative
8+
top: 50%
9+
transform: translateY(-50%)
10+
text-align: center
11+
padding: 0 25px
12+
.site-alias
13+
font-size: 26px
14+
margin-block-start: 0.2em
15+
margin-block-end: 0.5em
16+
.site-host
17+
font-size: 14px
18+
color: var(--el-text-color-secondary)
19+
.site-host,.site-alias
20+
white-space: nowrap
21+
overflow: hidden
22+
text-overflow: ellipsis

0 commit comments

Comments
 (0)