From ecba505e95835ca96c705488499c7d4603db44fb Mon Sep 17 00:00:00 2001
From: James <72028410+James-9696@users.noreply.github.com>
Date: Sat, 30 Mar 2024 10:04:06 +0800
Subject: [PATCH] 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
---
examples/sites/demos/apis/statistic.js | 144 ++++++++++++++++++
.../statistic/basic-usage-composition-api.vue | 26 ++++
.../pc/app/statistic/basic-usage.spec.ts | 20 +++
.../demos/pc/app/statistic/basic-usage.vue | 38 +++++
.../statistic-slot-composition-api.vue | 39 +++++
.../pc/app/statistic/statistic-slot.spec.ts | 13 ++
.../demos/pc/app/statistic/statistic-slot.vue | 51 +++++++
.../statistic-style-composition-api.vue | 29 ++++
.../pc/app/statistic/statistic-style.spec.ts | 7 +
.../pc/app/statistic/statistic-style.vue | 41 +++++
.../pc/app/statistic/webdoc/statistic.cn.md | 7 +
.../pc/app/statistic/webdoc/statistic.en.md | 7 +
.../pc/app/statistic/webdoc/statistic.js | 46 ++++++
examples/sites/demos/pc/menus.js | 3 +-
examples/sites/demos/saas/menus.js | 3 +-
packages/modules.json | 13 ++
packages/renderless/src/statistic/index.ts | 22 +++
packages/renderless/src/statistic/vue.ts | 21 +++
packages/renderless/types/index.ts | 1 +
packages/renderless/types/statistic.type.ts | 20 +++
packages/theme/src/statistic/index.less | 48 ++++++
packages/theme/src/statistic/smb-theme.js | 6 +
packages/theme/src/statistic/vars.less | 28 ++++
.../statistic/__tests__/statistic.test.tsx | 3 +
packages/vue/src/statistic/index.ts | 35 +++++
packages/vue/src/statistic/package.json | 26 ++++
packages/vue/src/statistic/src/index.ts | 44 ++++++
packages/vue/src/statistic/src/pc.vue | 51 +++++++
28 files changed, 790 insertions(+), 2 deletions(-)
create mode 100644 examples/sites/demos/apis/statistic.js
create mode 100644 examples/sites/demos/pc/app/statistic/basic-usage-composition-api.vue
create mode 100644 examples/sites/demos/pc/app/statistic/basic-usage.spec.ts
create mode 100644 examples/sites/demos/pc/app/statistic/basic-usage.vue
create mode 100644 examples/sites/demos/pc/app/statistic/statistic-slot-composition-api.vue
create mode 100644 examples/sites/demos/pc/app/statistic/statistic-slot.spec.ts
create mode 100644 examples/sites/demos/pc/app/statistic/statistic-slot.vue
create mode 100644 examples/sites/demos/pc/app/statistic/statistic-style-composition-api.vue
create mode 100644 examples/sites/demos/pc/app/statistic/statistic-style.spec.ts
create mode 100644 examples/sites/demos/pc/app/statistic/statistic-style.vue
create mode 100644 examples/sites/demos/pc/app/statistic/webdoc/statistic.cn.md
create mode 100644 examples/sites/demos/pc/app/statistic/webdoc/statistic.en.md
create mode 100644 examples/sites/demos/pc/app/statistic/webdoc/statistic.js
create mode 100644 packages/renderless/src/statistic/index.ts
create mode 100644 packages/renderless/src/statistic/vue.ts
create mode 100644 packages/renderless/types/statistic.type.ts
create mode 100644 packages/theme/src/statistic/index.less
create mode 100644 packages/theme/src/statistic/smb-theme.js
create mode 100644 packages/theme/src/statistic/vars.less
create mode 100644 packages/vue/src/statistic/__tests__/statistic.test.tsx
create mode 100644 packages/vue/src/statistic/index.ts
create mode 100644 packages/vue/src/statistic/package.json
create mode 100644 packages/vue/src/statistic/src/index.ts
create mode 100644 packages/vue/src/statistic/src/pc.vue
diff --git a/examples/sites/demos/apis/statistic.js b/examples/sites/demos/apis/statistic.js
new file mode 100644
index 000000000..587313f22
--- /dev/null
+++ b/examples/sites/demos/apis/statistic.js
@@ -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: ''
+ }
+ ]
+ }
+ ]
+}
diff --git a/examples/sites/demos/pc/app/statistic/basic-usage-composition-api.vue b/examples/sites/demos/pc/app/statistic/basic-usage-composition-api.vue
new file mode 100644
index 000000000..782bdd2fc
--- /dev/null
+++ b/examples/sites/demos/pc/app/statistic/basic-usage-composition-api.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/statistic/basic-usage.spec.ts b/examples/sites/demos/pc/app/statistic/basic-usage.spec.ts
new file mode 100644
index 000000000..2498e0328
--- /dev/null
+++ b/examples/sites/demos/pc/app/statistic/basic-usage.spec.ts
@@ -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()
+})
diff --git a/examples/sites/demos/pc/app/statistic/basic-usage.vue b/examples/sites/demos/pc/app/statistic/basic-usage.vue
new file mode 100644
index 000000000..43f2975dc
--- /dev/null
+++ b/examples/sites/demos/pc/app/statistic/basic-usage.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/statistic/statistic-slot-composition-api.vue b/examples/sites/demos/pc/app/statistic/statistic-slot-composition-api.vue
new file mode 100644
index 000000000..cc6b90ea4
--- /dev/null
+++ b/examples/sites/demos/pc/app/statistic/statistic-slot-composition-api.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ 活跃度
+
+
+
+
+
+
+
+ {{ scoped }}
+
+
+
+
+ Like:
+
+
+
+
+ /220
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/statistic/statistic-slot.spec.ts b/examples/sites/demos/pc/app/statistic/statistic-slot.spec.ts
new file mode 100644
index 000000000..d6179bc93
--- /dev/null
+++ b/examples/sites/demos/pc/app/statistic/statistic-slot.spec.ts
@@ -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()
+})
diff --git a/examples/sites/demos/pc/app/statistic/statistic-slot.vue b/examples/sites/demos/pc/app/statistic/statistic-slot.vue
new file mode 100644
index 000000000..7dc9c38a0
--- /dev/null
+++ b/examples/sites/demos/pc/app/statistic/statistic-slot.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+ 活跃度
+
+
+
+
+
+
+
+ {{ scoped }}
+
+
+
+
+ Like:
+
+
+
+
+ /220
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/statistic/statistic-style-composition-api.vue b/examples/sites/demos/pc/app/statistic/statistic-style-composition-api.vue
new file mode 100644
index 000000000..7d30b7a63
--- /dev/null
+++ b/examples/sites/demos/pc/app/statistic/statistic-style-composition-api.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Like:
+
+
+
+
+ Like:
+
+
+
+
+ Like:
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/statistic/statistic-style.spec.ts b/examples/sites/demos/pc/app/statistic/statistic-style.spec.ts
new file mode 100644
index 000000000..aabb4b3cf
--- /dev/null
+++ b/examples/sites/demos/pc/app/statistic/statistic-style.spec.ts
@@ -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/)
+})
diff --git a/examples/sites/demos/pc/app/statistic/statistic-style.vue b/examples/sites/demos/pc/app/statistic/statistic-style.vue
new file mode 100644
index 000000000..1a07aed3e
--- /dev/null
+++ b/examples/sites/demos/pc/app/statistic/statistic-style.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+ Like:
+
+
+
+
+ Like:
+
+
+
+
+ Like:
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/statistic/webdoc/statistic.cn.md b/examples/sites/demos/pc/app/statistic/webdoc/statistic.cn.md
new file mode 100644
index 000000000..f2f676707
--- /dev/null
+++ b/examples/sites/demos/pc/app/statistic/webdoc/statistic.cn.md
@@ -0,0 +1,7 @@
+---
+title: Statistic 统计组件
+---
+
+# Statistic 统计组件
+
+显示统计。
diff --git a/examples/sites/demos/pc/app/statistic/webdoc/statistic.en.md b/examples/sites/demos/pc/app/statistic/webdoc/statistic.en.md
new file mode 100644
index 000000000..53958248a
--- /dev/null
+++ b/examples/sites/demos/pc/app/statistic/webdoc/statistic.en.md
@@ -0,0 +1,7 @@
+---
+title: Statistics component
+---
+
+# Statistics component
+
+Display statistical data.
diff --git a/examples/sites/demos/pc/app/statistic/webdoc/statistic.js b/examples/sites/demos/pc/app/statistic/webdoc/statistic.js
new file mode 100644
index 000000000..c1e92de10
--- /dev/null
+++ b/examples/sites/demos/pc/app/statistic/webdoc/statistic.js
@@ -0,0 +1,46 @@
+export default {
+ column: '2',
+ owner: '',
+ demos: [
+ {
+ demoId: 'basic-usage',
+ name: {
+ 'zh-CN': '基本用法',
+ 'en-US': 'Basic Usage'
+ },
+ desc: {
+ 'zh-CN':
+ '通过value
设置数字内容,precision
设置数字精度值,title
设置数字内容标题,prefix
设置数字内容前置插槽,suffix
设置数字内容后置插槽。',
+ 'en-US':
+ 'Set digital content throughvalue
,precision
set digital precision value,title
set digital content title,prefix
set digital content front slot, andsuffix
set digital content rear slot.'
+ },
+ codeFiles: ['basic-usage.vue']
+ },
+ {
+ demoId: 'statistic-slot',
+ name: {
+ 'zh-CN': '插槽用法',
+ 'en-US': 'Slot Usage'
+ },
+ desc: {
+ 'zh-CN':
+ '通过title
设置标题插槽,prefix
设置数字前缀插槽,suffix
设置数字后缀插槽。',
+ 'en-US':
+ 'Set the title slot throughtitle
, set the number prefix slot throughprefix
, and set the number suffix slot throughsuffix
.'
+ },
+ codeFiles: ['statistic-slot.vue']
+ },
+ {
+ demoId: 'statistic-style',
+ name: {
+ 'zh-CN': '样式用法',
+ 'en-US': 'Style Usage'
+ },
+ desc: {
+ 'zh-CN': '通过value-style
设置数字样式。',
+ 'en-US': 'Set the number style throughvalue style
.'
+ },
+ codeFiles: ['statistic-style.vue']
+ }
+ ]
+}
diff --git a/examples/sites/demos/pc/menus.js b/examples/sites/demos/pc/menus.js
index 3f4fdb121..d81ac5de4 100644
--- a/examples/sites/demos/pc/menus.js
+++ b/examples/sites/demos/pc/menus.js
@@ -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' }
]
},
{
diff --git a/examples/sites/demos/saas/menus.js b/examples/sites/demos/saas/menus.js
index e9e7fa25c..24d426abe 100644
--- a/examples/sites/demos/saas/menus.js
+++ b/examples/sites/demos/saas/menus.js
@@ -26,7 +26,8 @@ const noSaasComponents = [
'TopBox',
'Watermark',
'Wheel',
- 'Skeleton'
+ 'Skeleton',
+ 'Statistic'
]
// mobile-first上所有分类,pc上都有,因此可以用pc端menu分类进行合并
diff --git a/packages/modules.json b/packages/modules.json
index c9dedb6f0..c3d431804 100644
--- a/packages/modules.json
+++ b/packages/modules.json
@@ -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",
diff --git a/packages/renderless/src/statistic/index.ts b/packages/renderless/src/statistic/index.ts
new file mode 100644
index 000000000..b7308a4c5
--- /dev/null
+++ b/packages/renderless/src/statistic/index.ts
@@ -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 ? '.' : '')
+ }
diff --git a/packages/renderless/src/statistic/vue.ts b/packages/renderless/src/statistic/vue.ts
new file mode 100644
index 000000000..a36c2a671
--- /dev/null
+++ b/packages/renderless/src/statistic/vue.ts
@@ -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
+}
diff --git a/packages/renderless/types/index.ts b/packages/renderless/types/index.ts
index a3d8d5fd7..fee4190cc 100644
--- a/packages/renderless/types/index.ts
+++ b/packages/renderless/types/index.ts
@@ -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'
diff --git a/packages/renderless/types/statistic.type.ts b/packages/renderless/types/statistic.type.ts
new file mode 100644
index 000000000..212c43226
--- /dev/null
+++ b/packages/renderless/types/statistic.type.ts
@@ -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
+
+export type IStatisticConstants = typeof $constants
+
+export interface IStatisticState {
+ getIntegerAndDecimal: number | string
+}
+export interface IStatisticApi {
+ getIntegerAndDecimal: (value: string | number) => string | undefined
+}
+
+export type IStatisticPcRenderlessParams = ISharedRenderlessFunctionParams & {
+ state: IStatisticState
+ props: IStatisticProps
+ api: IStatisticApi
+}
diff --git a/packages/theme/src/statistic/index.less b/packages/theme/src/statistic/index.less
new file mode 100644
index 000000000..312a9ca6c
--- /dev/null
+++ b/packages/theme/src/statistic/index.less
@@ -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;
+ }
+}
diff --git a/packages/theme/src/statistic/smb-theme.js b/packages/theme/src/statistic/smb-theme.js
new file mode 100644
index 000000000..c6341fb7c
--- /dev/null
+++ b/packages/theme/src/statistic/smb-theme.js
@@ -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)'
+}
diff --git a/packages/theme/src/statistic/vars.less b/packages/theme/src/statistic/vars.less
new file mode 100644
index 000000000..2849fae15
--- /dev/null
+++ b/packages/theme/src/statistic/vars.less
@@ -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);
+}
diff --git a/packages/vue/src/statistic/__tests__/statistic.test.tsx b/packages/vue/src/statistic/__tests__/statistic.test.tsx
new file mode 100644
index 000000000..b526946df
--- /dev/null
+++ b/packages/vue/src/statistic/__tests__/statistic.test.tsx
@@ -0,0 +1,3 @@
+import { describe } from 'vitest'
+
+describe('PC Mode', () => {})
diff --git a/packages/vue/src/statistic/index.ts b/packages/vue/src/statistic/index.ts
new file mode 100644
index 000000000..c98829b3f
--- /dev/null
+++ b/packages/vue/src/statistic/index.ts
@@ -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
diff --git a/packages/vue/src/statistic/package.json b/packages/vue/src/statistic/package.json
new file mode 100644
index 000000000..bb1be9233
--- /dev/null
+++ b/packages/vue/src/statistic/package.json
@@ -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"
+}
diff --git a/packages/vue/src/statistic/src/index.ts b/packages/vue/src/statistic/src/index.ts
new file mode 100644
index 000000000..fe53422a4
--- /dev/null
+++ b/packages/vue/src/statistic/src/index.ts
@@ -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 = (val: any): PropType => val
+
+export const statisticProps = {
+ ...$props,
+ _constants: {
+ type: Object,
+ default: () => $constants
+ },
+ precision: {
+ type: Number,
+ default: 0
+ },
+ formatter: Function,
+ value: {
+ type: definePropType([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 })
+ }
+})
diff --git a/packages/vue/src/statistic/src/pc.vue b/packages/vue/src/statistic/src/pc.vue
new file mode 100644
index 000000000..799c36ddd
--- /dev/null
+++ b/packages/vue/src/statistic/src/pc.vue
@@ -0,0 +1,51 @@
+
+
+
+
+ {{ title }}
+
+
+
+ {{ title }}
+
+
+
+
+
+
+ {{ prefix }}
+
+
+
+ {{ state.value }}
+
+
+
+ {{ suffix }}
+
+
+
+
+
+
+
+
+ {{ title.value }}
+
+
+
+
+
+