feat(statistic): statistic component (#1491)

* fix: add new component statistic

* fix: add statistic renderless

* feat: add statistic theme

* feat: add statistic component

* fix: update review

* fix: update review

* fix: update review
This commit is contained in:
James 2024-03-30 10:04:06 +08:00 committed by GitHub
parent 772ef20270
commit ecba505e95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 790 additions and 2 deletions

View File

@ -0,0 +1,144 @@
export default {
mode: ['pc'],
apis: [
{
name: 'statistic',
type: 'component',
props: [
{
name: 'value',
type: 'number',
defaultValue: '0',
desc: {
'zh-CN': '数字显示内容',
'en-US': 'Digital display content'
},
mode: ['pc'],
pcDemo: 'basic-usage',
mfDemo: ''
},
{
name: 'precision',
type: 'number',
defaultValue: '0',
desc: {
'zh-CN': '精度值',
'en-US': 'Take precision value'
},
mode: ['pc'],
pcDemo: 'basic-usage',
mfDemo: ''
},
{
name: 'title',
type: 'string | array',
defaultValue: '',
desc: {
'zh-CN': '设置数字内容标题',
'en-US': 'Set digital content titles'
},
mode: ['pc'],
pcDemo: 'basic-usage',
mfDemo: ''
},
{
name: 'group-separator',
type: 'string',
defaultValue: ',',
desc: {
'zh-CN': '设置千分位标志符',
'en-US': 'Set Millennial Flag'
},
mode: ['pc'],
pcDemo: 'basic-usage',
mfDemo: ''
},
{
name: 'prefix',
type: 'string',
defaultValue: '',
desc: {
'zh-CN': '设置数字内容前缀',
'en-US': 'Set numerical content prefix'
},
mode: ['pc'],
pcDemo: 'basic-usage',
mfDemo: ''
},
{
name: 'suffix',
type: 'string',
defaultValue: '',
desc: {
'zh-CN': '设置数字内容后缀',
'en-US': 'Set numeric content suffix'
},
mode: ['pc'],
pcDemo: 'basic-usage',
mfDemo: ''
},
{
name: 'formatter',
type: '(value) => {}',
defaultValue: '',
desc: {
'zh-CN': '设置自定义数字格式化',
'en-US': 'Set custom number formatting'
},
mode: ['pc'],
pcDemo: 'basic-usage',
mfDemo: ''
},
{
name: 'value-style',
type: 'string | object | array',
defaultValue: '',
desc: {
'zh-CN': '设置数字样式',
'en-US': 'Set Number Style'
},
mode: ['pc'],
pcDemo: 'basic-usage',
mfDemo: ''
}
],
events: [],
methods: [],
slots: [
{
name: 'prefix',
type: '',
defaultValue: '',
desc: {
'zh-CN': '数字内容前置插槽',
'en-US': 'Digital content front slot'
},
mode: ['pc'],
mfDemo: ''
},
{
name: 'suffix',
type: '',
defaultValue: '',
desc: {
'zh-CN': '数字内容后置插槽',
'en-US': 'Digital content rear slot'
},
mode: ['pc'],
mfDemo: ''
},
{
name: 'title',
type: '',
defaultValue: '',
desc: {
'zh-CN': '数字内容标题插槽',
'en-US': 'Digital content title slot'
},
mode: ['pc'],
mfDemo: ''
}
]
}
]
}

View File

@ -0,0 +1,26 @@
<template>
<div>
<tiny-layout>
<tiny-row :flex="true" class="row-bg">
<tiny-col :span="3">
<tiny-statistic :value="num" :precision="0" title="用户活跃度"></tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="num" :precision="2" title="消费额度"></tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="num" :precision="0" title="来访数量" prefix="view:"></tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="123" :precision="0" title="男女占比例" suffix="/100"></tiny-statistic>
</tiny-col>
</tiny-row>
</tiny-layout>
</div>
</template>
<script setup lang="jsx">
import { Statistic as TinyStatistic, Layout as TinyLayout, Row as TinyRow, Col as TinyCol } from '@opentiny/vue'
const num = 306526.23
</script>

View File

@ -0,0 +1,20 @@
import { test, expect } from '@playwright/test'
test('基本用法', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('statistic#basic-usage')
await page
.locator('div')
.filter({ hasText: /123\/100$/ })
.first()
.click()
await page
.locator('div')
.filter({ hasText: /^基本用法$/ })
.first()
.click()
await page
.locator('div')
.filter({ hasText: /^306,526\.23$/ })
.click()
})

View File

@ -0,0 +1,38 @@
<template>
<div>
<tiny-layout>
<tiny-row :flex="true" class="row-bg">
<tiny-col :span="3">
<tiny-statistic :value="num" :precision="0" title="用户活跃度"></tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="num" :precision="2" title="消费额度"></tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="num" :precision="0" title="来访数量" prefix="view:"></tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="123" :precision="0" title="男女占比例" suffix="/100"></tiny-statistic>
</tiny-col>
</tiny-row>
</tiny-layout>
</div>
</template>
<script>
import { Statistic, Layout, Row, Col } from '@opentiny/vue'
export default {
components: {
TinyStatistic: Statistic,
TinyLayout: Layout,
TinyRow: Row,
TinyCol: Col
},
data() {
return {
num: 306526.23
}
}
}
</script>

View File

@ -0,0 +1,39 @@
<template>
<div>
<tiny-layout>
<tiny-row :flex="true" class="row-bg">
<tiny-col :span="3">
<tiny-statistic :value="10010258" :precision="0">
<template #title> 活跃度 </template>
</tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="num" :precision="2" :title="msg"> </tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="num" :precision="2" :title="money">
<template #title:data="{ scoped }">{{ scoped }}</template>
</tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="num" :precision="0" title="点赞数量">
<template #prefix> Like:</template>
</tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="600" :precision="0" title="队伍比分">
<template #suffix>/220</template>
</tiny-statistic>
</tiny-col>
</tiny-row>
</tiny-layout>
</div>
</template>
<script setup lang="jsx">
import { Statistic as TinyStatistic, Layout as TinyLayout, Row as TinyRow, Col as TinyCol } from '@opentiny/vue'
const num = 306526.23
const msg = { value: '额度' }
const money = { value: '存款' }
</script>

View File

@ -0,0 +1,13 @@
import { test, expect } from '@playwright/test'
test('插槽用法', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('statistic#statistic-slot')
await page.locator('div').filter({ hasText: /^10,010,258$/ })
await page
.locator('div')
.filter({ hasText: /^306,526\.23$/ })
.first()
await page.getByText('Like:306,526').click()
await page.getByText('600/').click()
})

View File

@ -0,0 +1,51 @@
<template>
<div>
<tiny-layout>
<tiny-row :flex="true" class="row-bg">
<tiny-col :span="3">
<tiny-statistic :value="10010258" :precision="0">
<template #title> 活跃度 </template>
</tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="num" :precision="2" :title="msg"> </tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="num" :precision="2" :title="money">
<template #title:data="{ scoped }">{{ scoped }}</template>
</tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="num" :precision="0" title="点赞数量">
<template #prefix> Like:</template>
</tiny-statistic>
</tiny-col>
<tiny-col :span="3">
<tiny-statistic :value="600" :precision="0" title="队伍比分">
<template #suffix>/220</template>
</tiny-statistic>
</tiny-col>
</tiny-row>
</tiny-layout>
</div>
</template>
<script>
import { Statistic, Layout, Row, Col } from '@opentiny/vue'
export default {
components: {
TinyStatistic: Statistic,
TinyLayout: Layout,
TinyRow: Row,
TinyCol: Col
},
data() {
return {
num: 306526.23,
msg: { value: '额度' },
money: { value: '存款' }
}
}
}
</script>

View File

@ -0,0 +1,29 @@
<template>
<div>
<tiny-layout>
<tiny-row :flex="true" class="row-bg">
<tiny-col :span="6">
<tiny-statistic :value="num" :value-style="{ color: '#b12220' }" :precision="0" title="点赞数量">
<template #prefix> Like:</template>
</tiny-statistic>
</tiny-col>
<tiny-col :span="6">
<tiny-statistic :value="num" :value-style="{ 'color': '#3ac295' }" :precision="0" title="点赞数量">
<template #prefix> Like:</template>
</tiny-statistic>
</tiny-col>
<tiny-col :span="6">
<tiny-statistic :value="num" :value-style="[{ 'color': '#e37d29' }]" :precision="0" title="点赞数量">
<template #prefix> Like:</template>
</tiny-statistic>
</tiny-col>
</tiny-row>
</tiny-layout>
</div>
</template>
<script setup lang="jsx">
import { Statistic as TinyStatistic, Layout as TinyLayout, Row as TinyRow, Col as TinyCol } from '@opentiny/vue'
const num = 306526.23
</script>

View File

@ -0,0 +1,7 @@
import { test, expect } from '@playwright/test'
test('样式用法', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('statistic#statistic-style')
await expect(page.getByText('Like:306,526').first()).toHaveClass(/tiny-statistic__slots/)
})

View File

@ -0,0 +1,41 @@
<template>
<div>
<tiny-layout>
<tiny-row :flex="true" class="row-bg">
<tiny-col :span="6">
<tiny-statistic :value="num" :value-style="{ color: '#b12220' }" :precision="0" title="点赞数量">
<template #prefix> Like:</template>
</tiny-statistic>
</tiny-col>
<tiny-col :span="6">
<tiny-statistic :value="num" :value-style="{ 'color': '#3ac295' }" :precision="0" title="点赞数量">
<template #prefix> Like:</template>
</tiny-statistic>
</tiny-col>
<tiny-col :span="6">
<tiny-statistic :value="num" :value-style="[{ 'color': '#e37d29' }]" :precision="0" title="点赞数量">
<template #prefix> Like:</template>
</tiny-statistic>
</tiny-col>
</tiny-row>
</tiny-layout>
</div>
</template>
<script>
import { Statistic, Layout, Row, Col } from '@opentiny/vue'
export default {
components: {
TinyStatistic: Statistic,
TinyLayout: Layout,
TinyRow: Row,
TinyCol: Col
},
data() {
return {
num: 306526.23
}
}
}
</script>

View File

@ -0,0 +1,7 @@
---
title: Statistic 统计组件
---
# Statistic 统计组件
显示统计。

View File

@ -0,0 +1,7 @@
---
title: Statistics component
---
# Statistics component
Display statistical data.

View File

@ -0,0 +1,46 @@
export default {
column: '2',
owner: '',
demos: [
{
demoId: 'basic-usage',
name: {
'zh-CN': '基本用法',
'en-US': 'Basic Usage'
},
desc: {
'zh-CN':
'通过<code>value</code>设置数字内容,<code>precision</code>设置数字精度值,<code>title</code>设置数字内容标题,<code>prefix</code>设置数字内容前置插槽,<code>suffix</code>设置数字内容后置插槽。',
'en-US':
'Set digital content through<code>value</code>,<code>precision</code>set digital precision value,<code>title</code>set digital content title,<code>prefix</code>set digital content front slot, and<code>suffix</code>set digital content rear slot.'
},
codeFiles: ['basic-usage.vue']
},
{
demoId: 'statistic-slot',
name: {
'zh-CN': '插槽用法',
'en-US': 'Slot Usage'
},
desc: {
'zh-CN':
'通过<code>title</code>设置标题插槽,<code>prefix</code>设置数字前缀插槽,<code>suffix</code>设置数字后缀插槽。',
'en-US':
'Set the title slot through<code>title</code>, set the number prefix slot through<code>prefix</code>, and set the number suffix slot through<code>suffix</code>.'
},
codeFiles: ['statistic-slot.vue']
},
{
demoId: 'statistic-style',
name: {
'zh-CN': '样式用法',
'en-US': 'Style Usage'
},
desc: {
'zh-CN': '通过<code>value-style</code>设置数字样式。',
'en-US': 'Set the number style through<code>value style</code>.'
},
codeFiles: ['statistic-style.vue']
}
]
}

View File

@ -211,7 +211,8 @@ export const cmpMenus = [
{ 'nameCn': '树形控件', 'name': 'Tree', 'key': 'tree' },
{ 'nameCn': '穿梭框', 'name': 'Transfer', 'key': 'transfer' },
{ 'nameCn': '无限滚动', 'name': 'InfiniteScroll', 'key': 'infinite-scroll' },
{ 'nameCn': '骨架屏', 'name': 'Skeleton', 'key': 'skeleton' }
{ 'nameCn': '骨架屏', 'name': 'Skeleton', 'key': 'skeleton' },
{ 'nameCn': '统计', 'name': 'Statistic', 'key': 'statistic' }
]
},
{

View File

@ -26,7 +26,8 @@ const noSaasComponents = [
'TopBox',
'Watermark',
'Wheel',
'Skeleton'
'Skeleton',
'Statistic'
]
// mobile-first上所有分类pc上都有因此可以用pc端menu分类进行合并

View File

@ -2583,6 +2583,19 @@
"type": "template",
"exclude": false
},
"Statistic": {
"path": "vue/src/statistic/index.ts",
"type": "component",
"exclude": false,
"mode": [
"pc"
]
},
"StatisticPc": {
"path": "vue/src/statistic/src/pc.vue",
"type": "template",
"exclude": false
},
"SlideBar": {
"path": "vue/src/slide-bar/index.ts",
"type": "component",

View File

@ -0,0 +1,22 @@
import { isFunction } from '../common/type'
export const isNumber =
({ props }) =>
() => {
return typeof props.value === 'number'
}
export const getIntegerAndDecimal =
({ props }) =>
() => {
if (isFunction(props.formatter)) {
return props.formatter(props.value)
}
if (!isNumber(props.value)) {
return props.value
}
let displayValue = props.value ? String(props.value).split('.') : ''
let integer = displayValue[0]?.replace(/\B(?=(\d{3})+(?!\d))/g, props.groupSeparator)
let decimal = displayValue[1]?.padEnd(props.precision, '0').slice(0, props.precision > 0 ? props.precision : 0)
return [integer, decimal].join(decimal ? '.' : '')
}

View File

@ -0,0 +1,21 @@
import { getIntegerAndDecimal } from './index'
import type { IStatisticApi, IStatisticState } from '@/types'
export const api = ['state', 'getIntegerAndDecimal']
export const renderless = (props, hooks): IStatisticApi => {
const api: IStatisticApi = {
getIntegerAndDecimal: getIntegerAndDecimal({ props })
}
const { reactive, computed } = hooks
const state: IStatisticState = reactive({
value: computed(() => api.getIntegerAndDecimal(props))
})
Object.assign(api, {
state
})
return api
}

View File

@ -157,6 +157,7 @@ export * from './selected-box.type'
export * from './shared.type'
export * from './skeleton.type'
export * from './skeleton-item.type'
export * from './statistic.type'
export * from './slide-bar.type'
export * from './slider.type'
export * from './slider-button.type'

View File

@ -0,0 +1,20 @@
import type { ExtractPropTypes } from 'vue'
import type { statisticProps, $constants } from '@/statistic/src'
import type { ISharedRenderlessFunctionParams } from './shared.type'
export type IStatisticProps = ExtractPropTypes<typeof statisticProps>
export type IStatisticConstants = typeof $constants
export interface IStatisticState {
getIntegerAndDecimal: number | string
}
export interface IStatisticApi {
getIntegerAndDecimal: (value: string | number) => string | undefined
}
export type IStatisticPcRenderlessParams = ISharedRenderlessFunctionParams<never> & {
state: IStatisticState
props: IStatisticProps
api: IStatisticApi
}

View File

@ -0,0 +1,48 @@
@import '../custom.less';
@import './vars.less';
@statistic-item-prefix-cls: ~'@{css-prefix}statistic';
.@{statistic-item-prefix-cls} {
.component-css-vars-statistic();
width: 100%;
text-align: center;
&__title {
font-size: var(--ti-statistic-font-size);
color: var(--ti-statistic-font-color);
font-weight: var(--ti-statistic-title-font-weight);
line-height: var(--ti-statistic-title-line-height);
margin-bottom: var(--ti-statistic-title-margin-bottom);
}
&__footer-title {
margin-top: var(--ti-statistic-title-margin-bottom);
}
&__slots {
font-weight: var(--ti-statistic-font-weight);
font-size: var(--ti-statistic-font-size);
color: var(--ti-statistic-font-color);
}
&__prefix {
margin-right: var(--ti-statistic-prefix-margin-right);
font-weight: var(--ti-statistic-prefix-font-weight);
display: inline-block;
font-size: var(--ti-statistic-prefix-font-size);
}
&__description {
font-size: var(--ti-statistic-description-font-size);
display: inline-block;
}
&__suffix {
font-size: var(--ti-statistic-suffix-font-size);
margin-left: var(--ti-statistic-suffix-margin-left);
font-weight: var(--ti-statistic-suffix-font-weight);
display: inline-block;
}
}

View File

@ -0,0 +1,6 @@
export const tinyStatisticSmbTheme = {
'ti-statistic-font-size': '24px',
'ti-statistic-prefix-font-size': '24px',
'ti-statistic-font-color': '#191919',
'ti-statistic-suffix-font-size': 'var(--ti-common-font-size-4)'
}

View File

@ -0,0 +1,28 @@
.component-css-vars-statistic() {
// 标题字体大小
--ti-statistic-font-size: var(--ti-common-font-size-base);
// 标题字体颜色
--ti-statistic-font-color: var(--ti-common-color-text-weaken);
// 标题字体粗细
--ti-statistic-title-font-weight: var(--ti-common-font-weight-5);
// 标题下间距
--ti-statistic-title-margin-bottom: var(--ti-common-space-base);
// 标题行高
--ti-statistic-title-line-height: var(ti-common-line-height-4);
// 前缀插槽字体粗细
--ti-statistic-font-weight: var(--ti-common-font-weight-5);
// 前缀缀插槽间距值
--ti-statistic-prefix-margin-right: var(--ti-common-space-6);
// 前缀字体大小
--ti-statistic-prefix-font-size: var(--ti-common-font-size-4);
// 前缀字体粗细
--ti-statistic-prefix-font-weight: var(--ti-common-font-weight-5);
// 后缀插槽间距值
--ti-statistic-suffix-margin-left: var(--ti-common-space-6);
// 后缀字体大小
--ti-statistic-suffix-font-size: var(--ti-common-font-size-4);
// 后缀字体粗细
--ti-statistic-suffix-font-weight: var(--ti-common-font-weight-5);
// 数字内容字体
--ti-statistic-description-font-size: var(--ti-common-font-size-4);
}

View File

@ -0,0 +1,3 @@
import { describe } from 'vitest'
describe('PC Mode', () => {})

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2022 - present TinyVue Authors.
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import Statistic from './src/index'
import '@opentiny/vue-theme/statistic/index.less'
import { version } from './package.json'
Statistic.model = {
prop: 'modelValue',
event: 'update:modelValue'
}
/* istanbul ignore next */
Statistic.install = function (Vue) {
Vue.component(Statistic.name, Statistic)
}
Statistic.version = version
/* istanbul ignore next */
if (process.env.BUILD_TARGET === 'runtime') {
if (typeof window !== 'undefined' && window.Vue) {
Statistic.install(window.Vue)
}
}
export default Statistic

View File

@ -0,0 +1,26 @@
{
"name": "@opentiny/vue-statistic",
"version": "3.7.0",
"description": "",
"main": "lib/index.js",
"module": "index.ts",
"sideEffects": false,
"type": "module",
"devDependencies": {
"@opentiny-internal/vue-test-utils": "workspace:*",
"vitest": "^0.31.0"
},
"scripts": {
"build": "pnpm -w build:ui $npm_package_name",
"//postversion": "pnpm build"
},
"dependencies": {
"@opentiny/vue-renderless": "workspace:~",
"@opentiny/vue-common": "workspace:~",
"@opentiny/vue-layout": "workspace:~",
"@opentiny/vue-row": "workspace:~",
"@opentiny/vue-col": "workspace:~",
"@opentiny/vue-theme": "workspace:~"
},
"license": "MIT"
}

View File

@ -0,0 +1,44 @@
import type { PropType } from '@opentiny/vue-common'
import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common'
import template from 'virtual-template?pc'
export const $constants = {
PREFIX: 'tiny-statistic'
}
export const definePropType = <T>(val: any): PropType<T> => val
export const statisticProps = {
...$props,
_constants: {
type: Object,
default: () => $constants
},
precision: {
type: Number,
default: 0
},
formatter: Function,
value: {
type: definePropType<number | object>([Number, Object]),
default: 0
},
prefix: String,
suffix: String,
title: [String, Object],
valueStyle: {
type: [String, Object, Array]
},
groupSeparator: {
type: String,
default: ','
}
}
export default defineComponent({
name: $prefix + 'Statistic',
props: statisticProps,
setup(props, context) {
return $setup({ props, context, template })
}
})

View File

@ -0,0 +1,51 @@
<template>
<div class="tiny-statistic">
<div class="tiny-statistic__title">
<div v-if="title && typeof title === 'string'">
{{ title }}
</div>
<div v-else-if="$slots.title">
<slot name="title" :data="title">
{{ title }}
</slot>
</div>
</div>
<div class="tiny-statistic__slots">
<div v-if="$slots.prefix || prefix" class="tiny-statistic__prefix">
<slot name="prefix">
<span>{{ prefix }}</span>
</slot>
</div>
<span class="tiny-statistic__description" :style="valueStyle">
{{ state.value }}
</span>
<div v-if="$slots.suffix || suffix" class="tiny-statistic__suffix">
<slot name="suffix">
<span>{{ suffix }}</span>
</slot>
</div>
</div>
<div v-if="title && title instanceof Object" :class="['tiny-statistic__footer-title', 'tiny-statistic__title']">
<div v-if="$slots.title">
<slot name="title" :data="title"> </slot>
</div>
<div v-else>
<span>{{ title.value }}</span>
</div>
</div>
</div>
</template>
<script lang="ts">
import { renderless, api } from '@opentiny/vue-renderless/statistic/vue'
import { props, setup, defineComponent } from '@opentiny/vue-common'
export default defineComponent({
components: {},
emits: [],
props: [...props, 'formatter', 'precision', 'prefix', 'suffix', 'title', 'value', 'valueStyle', 'groupSeparator'],
setup(props, context) {
return setup({ props, context, renderless, api })
}
})
</script>