forked from opentiny/tiny-vue
feat(skeleton): skeleton component (#1345)
* feat(skeleton): skeleton component * fix(skeleton): skeleton css style
This commit is contained in:
parent
1380f4e931
commit
47c6f176c2
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-skeleton></tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Skeleton as TinySkeleton } from '@opentiny/vue'
|
||||
</script>
|
|
@ -0,0 +1,14 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('基本用法', async ({ page }) => {
|
||||
page.on('pageerror', (exception) => expect(exception).toBeNull())
|
||||
await page.goto('skeleton#basic')
|
||||
|
||||
const first = page.locator('.tiny-skeleton')
|
||||
const children = page.locator('.tiny-skeleton-item')
|
||||
const active = page.locator('.tiny-skeleton-item--active')
|
||||
|
||||
await expect(first).toHaveCount(1)
|
||||
await expect(children).toHaveCount(4)
|
||||
await expect(active).toHaveCount(4)
|
||||
})
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-skeleton></tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Skeleton } from '@opentiny/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinySkeleton: Skeleton
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-skeleton avatar></tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Skeleton as TinySkeleton } from '@opentiny/vue'
|
||||
</script>
|
|
@ -0,0 +1,14 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('段落显示左侧头像', async ({ page }) => {
|
||||
page.on('pageerror', (exception) => expect(exception).toBeNull())
|
||||
await page.goto('skeleton#complex-demo')
|
||||
|
||||
const first = page.locator('.tiny-skeleton')
|
||||
const avatar = page.locator('.tiny-skeleton__avatar')
|
||||
const medium = page.locator('.tiny-skeleton-item--medium')
|
||||
|
||||
await expect(first).toHaveCount(1)
|
||||
await expect(avatar).toHaveCount(1)
|
||||
await expect(medium).toHaveCount(1)
|
||||
})
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-skeleton avatar></tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Skeleton } from '@opentiny/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinySkeleton: Skeleton
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-skeleton>
|
||||
<template #placeholder>
|
||||
<tiny-skeleton-item variant="image" style="width: 200px"></tiny-skeleton-item>
|
||||
<br /><br />
|
||||
<tiny-skeleton-item variant="circle" size="small"></tiny-skeleton-item>
|
||||
</template>
|
||||
</tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Skeleton as TinySkeleton, SkeletonItem as TinySkeletonItem } from '@opentiny/vue'
|
||||
</script>
|
|
@ -0,0 +1,14 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('自定义排版', async ({ page }) => {
|
||||
page.on('pageerror', (exception) => expect(exception).toBeNull())
|
||||
await page.goto('skeleton#custom-layout')
|
||||
|
||||
const image = page.locator('.tiny-skeleton-item--image')
|
||||
const circle = page.locator('.tiny-skeleton-item--circle')
|
||||
|
||||
await expect(image).toHaveCount(1)
|
||||
await expect(circle).toHaveCount(1)
|
||||
await expect(circle).toHaveClass(/tiny-skeleton-item--small/)
|
||||
await expect(image).toHaveCSS('width', '200px')
|
||||
})
|
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-skeleton>
|
||||
<template #placeholder>
|
||||
<tiny-skeleton-item variant="image" style="width: 200px"></tiny-skeleton-item>
|
||||
<br /><br />
|
||||
<tiny-skeleton-item variant="circle" size="small"></tiny-skeleton-item>
|
||||
</template>
|
||||
</tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Skeleton, SkeletonItem } from '@opentiny/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinySkeleton: Skeleton,
|
||||
TinySkeletonItem: SkeletonItem
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-skeleton :rows="5" :rows-width="['200px', '100px', '50px']"></tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Skeleton as TinySkeleton } from '@opentiny/vue'
|
||||
</script>
|
|
@ -0,0 +1,16 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('自定义段落宽度', async ({ page }) => {
|
||||
page.on('pageerror', (exception) => expect(exception).toBeNull())
|
||||
await page.goto('skeleton#custom-paragraph-width')
|
||||
|
||||
const first = page.locator('.tiny-skeleton')
|
||||
const item1 = page.locator('.tiny-skeleton-item').nth(1)
|
||||
const item2 = page.locator('.tiny-skeleton-item').nth(2)
|
||||
const item3 = page.locator('.tiny-skeleton-item').nth(3)
|
||||
|
||||
await expect(first).toHaveCount(1)
|
||||
await expect(item1).toHaveCSS('width', '200px')
|
||||
await expect(item2).toHaveCSS('width', '100px')
|
||||
await expect(item3).toHaveCSS('width', '50px')
|
||||
})
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-skeleton :rows="5" :rows-width="['200px', '100px', '50px']"></tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Skeleton } from '@opentiny/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinySkeleton: Skeleton
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-skeleton :rows="4"></tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Skeleton as TinySkeleton } from '@opentiny/vue'
|
||||
</script>
|
|
@ -0,0 +1,12 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('自定义段落行数', async ({ page }) => {
|
||||
page.on('pageerror', (exception) => expect(exception).toBeNull())
|
||||
await page.goto('skeleton#custom-rows')
|
||||
|
||||
const first = page.locator('.tiny-skeleton')
|
||||
const item = page.locator('.tiny-skeleton-item')
|
||||
|
||||
await expect(first).toHaveCount(1)
|
||||
await expect(item).toHaveCount(5)
|
||||
})
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-skeleton :rows="4"></tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Skeleton } from '@opentiny/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinySkeleton: Skeleton
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<div>
|
||||
<p>大小</p>
|
||||
<tiny-radio v-model="size" label="small">Small</tiny-radio>
|
||||
<tiny-radio v-model="size" label="medium">Middle</tiny-radio>
|
||||
<tiny-radio v-model="size" label="large">Large</tiny-radio>
|
||||
<br /><br />
|
||||
<p>动画</p>
|
||||
<tiny-switch v-model="active"></tiny-switch>
|
||||
<br /><br />
|
||||
<tiny-skeleton :active="active">
|
||||
<template #placeholder>
|
||||
<tiny-skeleton-item></tiny-skeleton-item>
|
||||
<br />
|
||||
<tiny-skeleton-item variant="image" :size="size"></tiny-skeleton-item>
|
||||
<br /><br />
|
||||
<tiny-skeleton-item variant="circle" :size="size"></tiny-skeleton-item>
|
||||
</template>
|
||||
</tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
Skeleton as TinySkeleton,
|
||||
Radio as TinyRadio,
|
||||
SkeletonItem as TinySkeletonItem,
|
||||
Switch as TinySwitch
|
||||
} from '@opentiny/vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const size = ref('medium')
|
||||
const active = ref(true)
|
||||
</script>
|
|
@ -0,0 +1,50 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('细粒度模式', async ({ page }) => {
|
||||
page.on('pageerror', (exception) => expect(exception).toBeNull())
|
||||
await page.goto('skeleton#fine-grained-mode')
|
||||
|
||||
const first = page.locator('.tiny-skeleton')
|
||||
const radio1 = page.locator('.tiny-radio').nth(0)
|
||||
const radio2 = page.locator('.tiny-radio').nth(1)
|
||||
const radio3 = page.locator('.tiny-radio').nth(2)
|
||||
const activeSwitch = page.locator('.pc-demo > div > .tiny-switch')
|
||||
const image = page.locator('.tiny-skeleton-item--image')
|
||||
const circle = page.locator('.tiny-skeleton-item--circle')
|
||||
const square = page.locator('.tiny-skeleton-item--square')
|
||||
|
||||
await expect(first).toHaveCount(1)
|
||||
|
||||
// 测试动画效果
|
||||
await expect(circle).toHaveClass(/tiny-skeleton-item--active/)
|
||||
await expect(square).toHaveClass(/tiny-skeleton-item--active/)
|
||||
await expect(image).toHaveClass(/tiny-skeleton-item--active/)
|
||||
|
||||
await activeSwitch.click()
|
||||
await page.waitForTimeout(500)
|
||||
await expect(image).not.toHaveClass(/tiny-skeleton-item--active/)
|
||||
await expect(circle).not.toHaveClass(/tiny-skeleton-item--active/)
|
||||
await expect(square).not.toHaveClass(/tiny-skeleton-item--active/)
|
||||
|
||||
// 测试大小
|
||||
await radio2.click()
|
||||
await page.waitForTimeout(500)
|
||||
await expect(radio2).toHaveClass(/is-checked/)
|
||||
await expect(circle).toHaveClass(/tiny-skeleton-item--medium/)
|
||||
await expect(image).toHaveClass(/tiny-skeleton-item--medium/)
|
||||
await expect(square).not.toHaveClass(/tiny-skeleton-item--medium/)
|
||||
|
||||
await radio1.click()
|
||||
await page.waitForTimeout(500)
|
||||
await expect(radio1).toHaveClass(/is-checked/)
|
||||
await expect(circle).toHaveClass(/tiny-skeleton-item--small/)
|
||||
await expect(image).toHaveClass(/tiny-skeleton-item--small/)
|
||||
await expect(square).not.toHaveClass(/tiny-skeleton-item--small/)
|
||||
|
||||
await radio3.click()
|
||||
await page.waitForTimeout(500)
|
||||
await expect(radio3).toHaveClass(/is-checked/)
|
||||
await expect(circle).toHaveClass(/tiny-skeleton-item--large/)
|
||||
await expect(image).toHaveClass(/tiny-skeleton-item--large/)
|
||||
await expect(square).not.toHaveClass(/tiny-skeleton-item--large/)
|
||||
})
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div>
|
||||
<p>大小</p>
|
||||
<tiny-radio v-model="size" label="small">Small</tiny-radio>
|
||||
<tiny-radio v-model="size" label="medium">Middle</tiny-radio>
|
||||
<tiny-radio v-model="size" label="large">Large</tiny-radio>
|
||||
<br /><br />
|
||||
|
||||
<p>动画</p>
|
||||
<tiny-switch v-model="active"></tiny-switch>
|
||||
<br /><br />
|
||||
<tiny-skeleton :active="active">
|
||||
<template #placeholder>
|
||||
<tiny-skeleton-item></tiny-skeleton-item>
|
||||
<br />
|
||||
<tiny-skeleton-item variant="image" :size="size"></tiny-skeleton-item>
|
||||
<br /><br />
|
||||
<tiny-skeleton-item variant="circle" :size="size"></tiny-skeleton-item>
|
||||
</template>
|
||||
</tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Skeleton, Radio, SkeletonItem, Switch } from '@opentiny/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinySkeleton: Skeleton,
|
||||
TinyRadio: Radio,
|
||||
TinySkeletonItem: SkeletonItem,
|
||||
TinySwitch: Switch
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
size: 'medium',
|
||||
active: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-button @click="handler">显示/隐藏</tiny-button>
|
||||
<br /><br />
|
||||
<tiny-skeleton :loading="loading">
|
||||
<template #default>
|
||||
<tiny-user-head type="icon" round></tiny-user-head>
|
||||
<p class="paragraph">内容比较短的一段文字</p>
|
||||
<tiny-button>一个按钮</tiny-button>
|
||||
</template>
|
||||
<template #placeholder>
|
||||
<tiny-skeleton-item variant="circle" style="width: 72px; height: 72px"></tiny-skeleton-item>
|
||||
<br /><br />
|
||||
<tiny-skeleton-item style="width: 180px"></tiny-skeleton-item>
|
||||
<tiny-skeleton-item style="width: 92px; height: 28px"></tiny-skeleton-item>
|
||||
</template>
|
||||
</tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
Skeleton as TinySkeleton,
|
||||
Button as TinyButton,
|
||||
SkeletonItem as TinySkeletonItem,
|
||||
UserHead as TinyUserHead
|
||||
} from '@opentiny/vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
const handler = () => {
|
||||
loading.value = !loading.value
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,26 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('加载完成', async ({ page }) => {
|
||||
page.on('pageerror', (exception) => expect(exception).toBeNull())
|
||||
await page.goto('skeleton#loading-completed')
|
||||
|
||||
const square = page.locator('.tiny-skeleton-item--square')
|
||||
const circle = page.locator('.tiny-skeleton-item--circle')
|
||||
const button = page.getByRole('button', { name: '显示/隐藏' })
|
||||
|
||||
await expect(circle).toHaveCount(1)
|
||||
await expect(square).toHaveCount(2)
|
||||
await expect(circle).toHaveCSS('width', '72px')
|
||||
await expect(circle).toHaveCSS('height', '72px')
|
||||
|
||||
await button.click()
|
||||
await page.waitForTimeout(500)
|
||||
await expect(square).toBeHidden()
|
||||
await expect(circle).toBeHidden()
|
||||
const p = page.locator('.paragraph')
|
||||
await expect(p).toHaveText(/内容比较短的一段文字/)
|
||||
const btn = page.getByRole('button', { name: '一个按钮' })
|
||||
await expect(btn).toBeVisible()
|
||||
const head = page.locator('.tiny-user-head')
|
||||
await expect(head).toBeVisible()
|
||||
})
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div>
|
||||
<tiny-button @click="handler">显示/隐藏</tiny-button>
|
||||
<br /><br />
|
||||
<tiny-skeleton :loading="loading">
|
||||
<template #default>
|
||||
<tiny-user-head type="icon" round></tiny-user-head>
|
||||
<p class="paragraph">内容比较短的一段文字</p>
|
||||
<tiny-button>一个按钮</tiny-button>
|
||||
</template>
|
||||
<template #placeholder>
|
||||
<tiny-skeleton-item variant="circle" style="width: 72px; height: 72px"></tiny-skeleton-item>
|
||||
<br /><br />
|
||||
<tiny-skeleton-item style="width: 180px"></tiny-skeleton-item>
|
||||
<tiny-skeleton-item style="width: 92px; height: 28px"></tiny-skeleton-item>
|
||||
</template>
|
||||
</tiny-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Skeleton, Button, SkeletonItem, UserHead } from '@opentiny/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinySkeleton: Skeleton,
|
||||
TinyButton: Button,
|
||||
TinySkeletonItem: SkeletonItem,
|
||||
TinyUserHead: UserHead
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handler() {
|
||||
this.loading = !this.loading
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: Skeleton 骨架屏
|
||||
---
|
||||
|
||||
# Skeleton 骨架屏
|
||||
|
||||
用于在内容加载过程中展示一组占位图形。
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: Skeleton 骨架屏
|
||||
---
|
||||
|
||||
# Skeleton 骨架屏
|
||||
|
||||
Used to display a set of placeholder graphics while content is loading.
|
|
@ -0,0 +1,207 @@
|
|||
export default {
|
||||
column: '2',
|
||||
owner: '',
|
||||
demos: [
|
||||
{
|
||||
'demoId': 'base',
|
||||
'name': { 'zh-CN': '基本用法', 'en-US': 'Basic Usage' },
|
||||
'desc': {
|
||||
'zh-CN': '<p>基础的骨架效果。</p>\n',
|
||||
'en-US': '<p>Basic skeleton effect.</p>\n'
|
||||
},
|
||||
'codeFiles': ['base.vue']
|
||||
},
|
||||
{
|
||||
'demoId': 'complex-demo',
|
||||
'name': { 'zh-CN': '复杂的组合', 'en-US': 'Complex Demo' },
|
||||
'desc': {
|
||||
'zh-CN': '<p>更复杂的组合,通过 <code>avatar</code> 属性控制骨架段落左侧出现头像占位。</p>\n',
|
||||
'en-US':
|
||||
'<p>More complex combinations, use the <code>avatar</code> attribute to control the appearance of the avatar placeholder on the left side of the skeleton paragraph.</p>\n'
|
||||
},
|
||||
'codeFiles': ['complex-demo.vue']
|
||||
},
|
||||
{
|
||||
'demoId': 'custom-rows',
|
||||
'name': { 'zh-CN': '自定义段落行数', 'en-US': 'Custom rows' },
|
||||
'desc': {
|
||||
'zh-CN':
|
||||
'<p>段落默认渲染 4 行,通过 <code>rows</code> 属性控制段落行数,显示的数量会比传入的数量多 1,首行会被渲染一个长度 40% 的段首,末行会被渲染一个长度 60% 的段尾。</p>\n',
|
||||
'en-US':
|
||||
'<p>By default, paragraphs are rendered in 4 lines. The number of paragraph lines is controlled through the <code>rows</code> attribute. The number displayed will be 1 more than the number passed in. The first line will render the paragraph header at 40% length, and the last line will render the paragraph trailer at 60% length.</p>\n'
|
||||
},
|
||||
'codeFiles': ['custom-rows.vue']
|
||||
},
|
||||
{
|
||||
'demoId': 'custom-layout',
|
||||
'name': { 'zh-CN': '自定义排版', 'en-US': 'Custom layout' },
|
||||
'desc': {
|
||||
'zh-CN':
|
||||
'<p>当默认排版不满足需求时,可自定义排版结构,通过 <code>class</code> 和 <code>style</code> 可自定义宽高等样式。</p>\n',
|
||||
'en-US':
|
||||
'<p>When the default layout does not meet the needs, the layout structure can be customized, and styles such as width and height can be customized through <code>class</code> and <code>style</code>.</p>\n'
|
||||
},
|
||||
'codeFiles': ['custom-layout.vue']
|
||||
},
|
||||
{
|
||||
'demoId': 'loading-completed',
|
||||
'name': { 'zh-CN': '加载完成', 'en-US': 'Loading completed' },
|
||||
'desc': {
|
||||
'zh-CN':
|
||||
'<p>通过 <code>loading</code> 属性的值来表示是否加载完成。 可以通过具名插槽 <code>default</code> 来构建 <code>loading</code> 结束之后需要展示的真实 DOM 元素结构。</p>\n',
|
||||
'en-US':
|
||||
'<p>Whether the loading is completed is indicated by the value of the <code>loading</code> attribute. You can use the named slot <code>default</code> to build the real DOM element structure that needs to be displayed after <code>loading</code> ends.</p>\n'
|
||||
},
|
||||
'codeFiles': ['loading-completed.vue']
|
||||
},
|
||||
{
|
||||
'demoId': 'custom-paragraph-width',
|
||||
'name': { 'zh-CN': '自定义段落宽度', 'en-US': 'Custom paragraph width' },
|
||||
'desc': {
|
||||
'zh-CN':
|
||||
'<p><code>rows-width</code> 属性可以自定义段落宽度,数组中的每一项可以为 <code>number</code> 或 <code>string</code>,当为 <code>number</code> 时,组件会自动增加 <code>px</code> 单位。</p>\n',
|
||||
'en-US':
|
||||
'<p>The <code>rows-width</code> attribute can customize the paragraph width. Each item in the array can be <code>number</code> or <code>string</code>. When it is <code>number< /code>, the component will automatically increase the <code>px</code> unit</p>\n'
|
||||
},
|
||||
'codeFiles': ['custom-paragraph-width.vue']
|
||||
},
|
||||
{
|
||||
'demoId': 'fine-grained-mode',
|
||||
'name': { 'zh-CN': '细粒度模式', 'en-US': 'Fine-grained mode' },
|
||||
'desc': {
|
||||
'zh-CN':
|
||||
'<p>细粒度模式,<code>variant</code> 属性可以控制 <code>skeleton-item</code> 的形态,可选值:image / circle / square。<code>size</code> 属性可以控制 <code>skeleton-item</code> 的大小,可选值:medium / small / large。</p>\n',
|
||||
'en-US':
|
||||
'<p>Fine-grained mode, the <code>variant</code> attribute can control the shape of <code>skeleton-item</code>, optional values: image / circle / square. The <code>size</code> attribute can control the size of <code>skeleton-item</code>. Optional values: medium / small / large.</p>\n'
|
||||
},
|
||||
'codeFiles': ['fine-grained-mode.vue']
|
||||
}
|
||||
],
|
||||
apis: [
|
||||
{
|
||||
'name': 'skeleton',
|
||||
'type': 'component',
|
||||
'props': [
|
||||
{
|
||||
'name': 'loading',
|
||||
'type': 'boolean',
|
||||
'defaultValue': 'true',
|
||||
'desc': {
|
||||
'zh-CN': '是否显示骨架屏,传 false 时会展示加载完成后的内容',
|
||||
'en-US':
|
||||
'Customized interface. A Promise object is returned. This parameter is mandatory when the framework service is not used.'
|
||||
},
|
||||
'demoId': 'custom-layout'
|
||||
},
|
||||
{
|
||||
'name': 'active',
|
||||
'type': 'boolean',
|
||||
'defaultValue': 'true',
|
||||
'desc': {
|
||||
'zh-CN': '是否开启动画',
|
||||
'en-US':
|
||||
'Customized interface. A Promise object is returned. This parameter is mandatory when the framework service is not used.'
|
||||
},
|
||||
'demoId': 'fine-grained-mode'
|
||||
},
|
||||
{
|
||||
'name': 'avatar',
|
||||
'type': 'boolean',
|
||||
'defaultValue': 'false',
|
||||
'desc': {
|
||||
'zh-CN': '是否显示头像',
|
||||
'en-US':
|
||||
'Customized interface. A Promise object is returned. This parameter is mandatory when the framework service is not used.'
|
||||
},
|
||||
'demoId': 'complex-demo'
|
||||
},
|
||||
{
|
||||
'name': 'rows',
|
||||
'type': 'number',
|
||||
'defaultValue': '3',
|
||||
'desc': {
|
||||
'zh-CN': '默认排版,可配置段落显示行数',
|
||||
'en-US':
|
||||
'Customized interface. A Promise object is returned. This parameter is mandatory when the framework service is not used.'
|
||||
},
|
||||
'demoId': 'custom-rows'
|
||||
},
|
||||
{
|
||||
'name': 'rows-width',
|
||||
'type': 'number[] | string[]',
|
||||
'defaultValue': '[]',
|
||||
'desc': {
|
||||
'zh-CN': '自定义段落每一行的宽度',
|
||||
'en-US':
|
||||
'Customized interface. A Promise object is returned. This parameter is mandatory when the framework service is not used.'
|
||||
},
|
||||
'demoId': 'custom-paragraph-width'
|
||||
}
|
||||
],
|
||||
'slots': [
|
||||
{
|
||||
'name': 'default',
|
||||
'type': '',
|
||||
'defaultValue': '',
|
||||
'desc': {
|
||||
'zh-CN': '加载完成后显示的内容',
|
||||
'en-US': 'Option default slot'
|
||||
},
|
||||
'demoId': 'custom-layout'
|
||||
},
|
||||
{
|
||||
'name': 'placeholder',
|
||||
'type': '',
|
||||
'defaultValue': '',
|
||||
'desc': {
|
||||
'zh-CN': '自定义骨架屏结构',
|
||||
'en-US': 'Option default slot'
|
||||
},
|
||||
'demoId': 'custom-layout'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': 'skeleton-item',
|
||||
'type': 'component',
|
||||
'props': [
|
||||
{
|
||||
'name': 'variant',
|
||||
'type': 'IVariant',
|
||||
'typeAnchorName': 'IVariant',
|
||||
'defaultValue': 'square',
|
||||
'desc': {
|
||||
'zh-CN': '骨架屏形态',
|
||||
'en-US':
|
||||
'Customized interface. A Promise object is returned. This parameter is mandatory when the framework service is not used.'
|
||||
},
|
||||
'demoId': 'fine-grained-mode'
|
||||
},
|
||||
{
|
||||
'name': 'size',
|
||||
'type': 'ISize',
|
||||
'typeAnchorName': 'ISize',
|
||||
'defaultValue': 'medium',
|
||||
'desc': {
|
||||
'zh-CN': '针对 image 和 circle 形态,内置三种大小',
|
||||
'en-US':
|
||||
'Customized interface. A Promise object is returned. This parameter is mandatory when the framework service is not used.'
|
||||
},
|
||||
'demoId': 'fine-grained-mode'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
types: [
|
||||
{
|
||||
name: 'IVariant',
|
||||
type: 'type',
|
||||
code: `type IVariant = 'image' | 'circle' | 'square'`
|
||||
},
|
||||
{
|
||||
name: 'ISize',
|
||||
type: 'type',
|
||||
code: `type ISize = 'large' | 'medium' | 'small'`
|
||||
}
|
||||
]
|
||||
}
|
|
@ -208,7 +208,8 @@ export const cmpMenus = [
|
|||
{ 'nameCn': '进度条', 'name': 'Progress', 'key': 'progress' },
|
||||
{ 'nameCn': '树形控件', 'name': 'Tree', 'key': 'tree' },
|
||||
{ 'nameCn': '穿梭框', 'name': 'Transfer', 'key': 'transfer' },
|
||||
{ 'nameCn': '无限滚动', 'name': 'InfiniteScroll', 'key': 'infinite-scroll' }
|
||||
{ 'nameCn': '无限滚动', 'name': 'InfiniteScroll', 'key': 'infinite-scroll' },
|
||||
{ 'nameCn': '骨架屏', 'name': 'Skeleton', 'key': 'skeleton' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,14 @@
|
|||
import type { ISkeletonItemProps, ISharedRenderlessParamHooks, ISkeletonItemState, ISkeletonItemApi } from '@/types'
|
||||
|
||||
export const api = ['state']
|
||||
|
||||
export const renderless = (props: ISkeletonItemProps, { reactive, inject }: ISharedRenderlessParamHooks) => {
|
||||
const state: ISkeletonItemState = reactive({
|
||||
isActive: inject('active', false)
|
||||
})
|
||||
|
||||
const api: ISkeletonItemApi = {
|
||||
state
|
||||
}
|
||||
return api
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { isNumber, isNull } from '../common/type'
|
||||
|
||||
export const toPxStyle = (value: string | number): undefined | string => {
|
||||
if (isNull(value)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (isNumber(value)) {
|
||||
return `${value}px`
|
||||
}
|
||||
|
||||
return String(value)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import type { ISkeletonProps, ISkeletonApi, ISharedRenderlessParamHooks } from '@/types'
|
||||
import { toPxStyle } from './index'
|
||||
|
||||
export const api = ['toPxStyle']
|
||||
|
||||
export const renderless = (props: ISkeletonProps, { toRefs, provide }: ISharedRenderlessParamHooks): ISkeletonApi => {
|
||||
const { active } = toRefs(props)
|
||||
provide('active', active)
|
||||
|
||||
const api = {
|
||||
toPxStyle
|
||||
}
|
||||
return api
|
||||
}
|
|
@ -154,6 +154,8 @@ export * from './select-mobile.type'
|
|||
export * from './select-view.type'
|
||||
export * from './selected-box.type'
|
||||
export * from './shared.type'
|
||||
export * from './skeleton.type'
|
||||
export * from './skeleton-item.type'
|
||||
export * from './slide-bar.type'
|
||||
export * from './slider.type'
|
||||
export * from './slider-button.type'
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import type { ExtractPropTypes } from 'vue'
|
||||
import type { skeletonItemProps, $constants } from '@/skeleton-item/src'
|
||||
|
||||
export type ISkeletonItemProps = ExtractPropTypes<typeof skeletonItemProps>
|
||||
|
||||
export type ISkeletonItemConstants = typeof $constants
|
||||
|
||||
export interface ISkeletonItemState {
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
export interface ISkeletonItemApi {
|
||||
state: ISkeletonItemState
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import type { ExtractPropTypes } from 'vue'
|
||||
import type { skeletonProps, $constants } from '@/skeleton/src'
|
||||
|
||||
export type ISkeletonProps = ExtractPropTypes<typeof skeletonProps>
|
||||
|
||||
export type ISkeletonConstants = typeof $constants
|
||||
|
||||
export interface ISkeletonApi {
|
||||
toPxStyle: (value: string | number) => string | undefined
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
@import '../custom.less';
|
||||
@import './vars.less';
|
||||
|
||||
@skeleton-item-prefix-cls: ~'@{css-prefix}skeleton-item';
|
||||
|
||||
.@{skeleton-item-prefix-cls} {
|
||||
.component-css-vars-skeleton-item();
|
||||
|
||||
&--active {
|
||||
&.@{skeleton-item-prefix-cls} {
|
||||
background: linear-gradient(100deg, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0) 60%) #f2f2f3;
|
||||
background-size: 200% 100%;
|
||||
background-position-x: 180%;
|
||||
animation: 2s skeleton-loading ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
background-color: var(--ti-skeleton-item-bg-color);
|
||||
border-radius: var(--ti-skeleton-item-border-radius);
|
||||
|
||||
&--square {
|
||||
width: 100%;
|
||||
height: var(--ti-skeleton-item-square-height);
|
||||
}
|
||||
|
||||
&--circle {
|
||||
border-radius: var(--ti-skeleton-item-circle-border-radius);
|
||||
&.@{skeleton-item-prefix-cls} {
|
||||
&--small {
|
||||
width: var(--ti-skeleton-item-circle-small-size);
|
||||
height: var(--ti-skeleton-item-circle-small-size);
|
||||
}
|
||||
|
||||
&--medium {
|
||||
width: var(--ti-skeleton-item-circle-medium-size);
|
||||
height: var(--ti-skeleton-item-circle-medium-size);
|
||||
}
|
||||
|
||||
&--large {
|
||||
width: var(--ti-skeleton-item-circle-large-size);
|
||||
height: var(--ti-skeleton-item-circle-large-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
width: 40%;
|
||||
height: 40%;
|
||||
fill: var(--ti-skeleton-item-image-icon-color);
|
||||
}
|
||||
|
||||
&.@{skeleton-item-prefix-cls} {
|
||||
&--small {
|
||||
width: var(--ti-skeleton-item-image-small-size-width);
|
||||
height: var(--ti-skeleton-item-image-small-size-height);
|
||||
}
|
||||
|
||||
&--medium {
|
||||
width: var(--ti-skeleton-item-image-medium-size-width);
|
||||
height: var(--ti-skeleton-item-image-medium-size-height);
|
||||
}
|
||||
|
||||
&--large {
|
||||
width: var(--ti-skeleton-item-image-large-size-width);
|
||||
height: var(--ti-skeleton-item-image-large-size-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
to {
|
||||
background-position-x: -20%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
.component-css-vars-skeleton-item() {
|
||||
--ti-skeleton-item-bg-color: var(--ti-base-color-bg-5, #f5f5f6);
|
||||
--ti-skeleton-item-border-radius: var(--ti-common-border-radius-1, 4px);
|
||||
--ti-skeleton-item-image-icon-color: var(--ti-base-color-common-2, #adb0b8);
|
||||
--ti-skeleton-item-square-height: var(--ti-common-size-4x, 16px);
|
||||
--ti-skeleton-item-circle-border-radius: var(--ti-common-border-radius-3, 50%);
|
||||
--ti-skeleton-item-circle-large-size: var(--ti-common-size-15x, 60px);
|
||||
--ti-skeleton-item-circle-medium-size: var(--ti-common-size-10x, 40px);
|
||||
--ti-skeleton-item-circle-small-size: var(--ti-common-size-5x, 20px);
|
||||
--ti-skeleton-item-image-small-size-height: var(--ti-common-size-15x, 60px);
|
||||
--ti-skeleton-item-image-small-size-width: var(--ti-common-size-15x, 60px);
|
||||
--ti-skeleton-item-image-medium-size-height: var(--ti-common-size-25x, 100px);
|
||||
--ti-skeleton-item-image-medium-size-width: var(--ti-common-size-25x, 100px);
|
||||
--ti-skeleton-item-image-large-size-height: var(--ti-common-size-50x, 200px);
|
||||
--ti-skeleton-item-image-large-size-width: var(--ti-common-size-50x, 200px);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
@import '../custom.less';
|
||||
@import './vars.less';
|
||||
|
||||
@skeleton-item-prefix-cls: ~'@{css-prefix}skeleton';
|
||||
|
||||
.@{skeleton-item-prefix-cls} {
|
||||
.component-css-vars-skeleton();
|
||||
|
||||
width: 100%;
|
||||
|
||||
&__article {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
width: var(--ti-skeleton-avatar-size);
|
||||
height: var(--ti-skeleton-avatar-size);
|
||||
margin-right: var(--ti-skeleton-avatar-margin-right);
|
||||
}
|
||||
|
||||
&__section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-item__title {
|
||||
width: var(--ti-skeleton-title-width);
|
||||
margin-bottom: var(--ti-skeleton-title-margin-bottom);
|
||||
}
|
||||
|
||||
&-item--square {
|
||||
margin-bottom: var(--ti-skeleton-row-margin-bottom);
|
||||
|
||||
&:last-child {
|
||||
width: var(--ti-skeleton-last-row-width);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
.component-css-vars-skeleton() {
|
||||
--ti-skeleton-avatar-size: var(--ti-common-size-10x, 40px);
|
||||
--ti-skeleton-avatar-background-color: var(--ti-common-color-bg-disabled, #f5f5f6);
|
||||
--ti-skeleton-avatar-margin-right: var(--ti-common-space-4x, 16px);
|
||||
--ti-skeleton-title-margin-bottom: var(--ti-common-space-4x, 16px);
|
||||
--ti-skeleton-title-width: 40%;
|
||||
--ti-skeleton-row-margin-bottom: var(--ti-common-space-3x, 12px);
|
||||
--ti-skeleton-last-row-width: 60%;
|
||||
}
|
|
@ -204,6 +204,8 @@
|
|||
"@opentiny/vue-select-mobile": "workspace:~",
|
||||
"@opentiny/vue-select-view": "workspace:~",
|
||||
"@opentiny/vue-selected-box": "workspace:~",
|
||||
"@opentiny/vue-skeleton": "workspace:~",
|
||||
"@opentiny/vue-skeleton-item": "workspace:~",
|
||||
"@opentiny/vue-slide-bar": "workspace:~",
|
||||
"@opentiny/vue-slider": "workspace:~",
|
||||
"@opentiny/vue-slider-button": "workspace:~",
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { mountPcMode } from '@opentiny-internal/vue-test-utils'
|
||||
import { test, describe, expect } from 'vitest'
|
||||
import SkeletonItem from '@opentiny/vue-skeleton-item'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
describe('PC Mode', () => {
|
||||
const mount = mountPcMode
|
||||
|
||||
test('variant', async () => {
|
||||
const wrapper = mount(() => <SkeletonItem variant="circle"></SkeletonItem>)
|
||||
await nextTick()
|
||||
expect(wrapper.classes()).toContain('tiny-skeleton-item--circle')
|
||||
})
|
||||
|
||||
test('size', async () => {
|
||||
const wrapper = mount(() => <SkeletonItem variant="image" size="small"></SkeletonItem>)
|
||||
await nextTick()
|
||||
expect(wrapper.classes()).toContain('tiny-skeleton-item--small')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,24 @@
|
|||
import SkeletonItem from './src/index'
|
||||
import '@opentiny/vue-theme/skeleton-item/index.less'
|
||||
import { version } from './package.json'
|
||||
|
||||
SkeletonItem.model = {
|
||||
prop: 'modelValue',
|
||||
event: 'update:modelValue'
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
SkeletonItem.install = function (Vue) {
|
||||
Vue.component(SkeletonItem.name, SkeletonItem)
|
||||
}
|
||||
|
||||
SkeletonItem.version = version
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (process.env.BUILD_TARGET === 'runtime') {
|
||||
if (typeof window !== 'undefined' && window.Vue) {
|
||||
SkeletonItem.install(window.Vue)
|
||||
}
|
||||
}
|
||||
|
||||
export default SkeletonItem
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@opentiny/vue-skeleton-item",
|
||||
"version": "3.13.0",
|
||||
"description": "",
|
||||
"main": "lib/index.js",
|
||||
"module": "index.ts",
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@opentiny/vue-renderless": "workspace:~",
|
||||
"@opentiny/vue-theme": "workspace:~",
|
||||
"@opentiny/vue-common": "workspace:~"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentiny-internal/vue-test-utils": "workspace:*",
|
||||
"vitest": "^0.31.0"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { $props, $setup, $prefix, defineComponent } from '@opentiny/vue-common'
|
||||
import type { PropType } from '@opentiny/vue-common'
|
||||
|
||||
import template from 'virtual-template?pc'
|
||||
|
||||
export type VariantType = 'image' | 'circle' | 'square'
|
||||
export type SizeType = 'large' | 'medium' | 'small'
|
||||
|
||||
const $constants = {}
|
||||
|
||||
export const skeletonItemProps = {
|
||||
...$props,
|
||||
_constants: {
|
||||
type: Object,
|
||||
default: () => $constants
|
||||
},
|
||||
modelValue: String,
|
||||
variant: {
|
||||
type: String as PropType<VariantType>,
|
||||
default: 'square'
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<SizeType>,
|
||||
default: 'medium'
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: $prefix + 'SkeletonItem',
|
||||
props: skeletonItemProps,
|
||||
setup(props, context) {
|
||||
return $setup({ props, context, template })
|
||||
}
|
||||
})
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div
|
||||
class="tiny-skeleton-item"
|
||||
:class="[
|
||||
variant ? 'tiny-skeleton-item--' + variant : '',
|
||||
size && variant !== 'square' ? 'tiny-skeleton-item--' + size : '',
|
||||
state.isActive ? 'tiny-skeleton-item--active' : ''
|
||||
]"
|
||||
>
|
||||
<icon-rich-text-image v-if="variant === 'image'"></icon-rich-text-image>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { renderless, api } from '@opentiny/vue-renderless/skeleton-item/vue'
|
||||
import { props, setup, defineComponent } from '@opentiny/vue-common'
|
||||
import { iconRichTextImage } from '@opentiny/vue-icon'
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['update:modelValue'],
|
||||
props: [...props, 'modelValue', 'variant', 'size'],
|
||||
components: {
|
||||
IconRichTextImage: iconRichTextImage()
|
||||
},
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api })
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,65 @@
|
|||
import { mountPcMode } from '@opentiny-internal/vue-test-utils'
|
||||
import { test, describe, expect } from 'vitest'
|
||||
import Skeleton from '@opentiny/vue-skeleton'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
describe('PC Mode', () => {
|
||||
const mount = mountPcMode
|
||||
|
||||
test('base usage', () => {
|
||||
const wrapper = mount(() => <Skeleton></Skeleton>)
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
test('active', async () => {
|
||||
const wrapper = mount(() => <Skeleton></Skeleton>)
|
||||
await nextTick()
|
||||
expect(wrapper.find('.tiny-skeleton-item').classes()).toContain('tiny-skeleton-item--active')
|
||||
})
|
||||
|
||||
test('loading & rows', async () => {
|
||||
const wrapper = mount(() => <Skeleton loading rows={3}></Skeleton>)
|
||||
await nextTick()
|
||||
expect(wrapper.find('.tiny-skeleton').exists()).toBe(true)
|
||||
expect(wrapper.findAll('.tiny-skeleton-item').length).toBe(4)
|
||||
})
|
||||
|
||||
test('slot', async () => {
|
||||
const wrapper = mount(() => (
|
||||
<Skeleton
|
||||
v-slots={{
|
||||
placeholder: () => <div class="tiny-placeholder">加载中</div>
|
||||
}}></Skeleton>
|
||||
))
|
||||
await nextTick()
|
||||
expect(wrapper.find('.tiny-placeholder').exists()).toBe(true)
|
||||
})
|
||||
|
||||
test('avatar', async () => {
|
||||
const wrapper = mount(() => <Skeleton avatar></Skeleton>)
|
||||
await nextTick()
|
||||
expect(wrapper.find('.tiny-skeleton__avatar').exists()).toBe(true)
|
||||
})
|
||||
|
||||
test('rows-width', async () => {
|
||||
const widths = ['200px', '100px', '50px']
|
||||
const wrapper = mount(<Skeleton></Skeleton>, {
|
||||
props: {
|
||||
'rows-width': widths
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
const skeletonItems = wrapper.findAll('.tiny-skeleton-item')
|
||||
|
||||
expect(skeletonItems).toHaveLength(4)
|
||||
|
||||
skeletonItems.shift()
|
||||
|
||||
skeletonItems.forEach((item, index) => {
|
||||
const computedStyles = getComputedStyle(item.element)
|
||||
const width = computedStyles.width
|
||||
|
||||
expect(width).toBe(widths[index])
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,24 @@
|
|||
import Skeleton from './src/index'
|
||||
import '@opentiny/vue-theme/skeleton/index.less'
|
||||
import { version } from './package.json'
|
||||
|
||||
Skeleton.model = {
|
||||
prop: 'modelValue',
|
||||
event: 'update:modelValue'
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
Skeleton.install = function (Vue) {
|
||||
Vue.component(Skeleton.name, Skeleton)
|
||||
}
|
||||
|
||||
Skeleton.version = version
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (process.env.BUILD_TARGET === 'runtime') {
|
||||
if (typeof window !== 'undefined' && window.Vue) {
|
||||
Skeleton.install(window.Vue)
|
||||
}
|
||||
}
|
||||
|
||||
export default Skeleton
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@opentiny/vue-skeleton",
|
||||
"version": "3.13.0",
|
||||
"description": "",
|
||||
"main": "lib/index.js",
|
||||
"module": "index.ts",
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@opentiny/vue-common": "workspace:~",
|
||||
"@opentiny/vue-renderless": "workspace:~",
|
||||
"@opentiny/vue-theme": "workspace:~",
|
||||
"@opentiny/vue-skeleton-item": "workspace:~"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@opentiny-internal/vue-test-utils": "workspace:*",
|
||||
"vitest": "0.31.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { $props, $setup, $prefix, defineComponent } from '@opentiny/vue-common'
|
||||
import type { PropType } from '@opentiny/vue-common'
|
||||
import template from 'virtual-template?pc'
|
||||
|
||||
const $constants = {}
|
||||
|
||||
export const skeletonProps = {
|
||||
...$props,
|
||||
_constants: {
|
||||
type: Object,
|
||||
default: () => $constants
|
||||
},
|
||||
modelValue: String,
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
avatar: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rowsWidth: {
|
||||
type: Array as PropType<(string | number)[]>,
|
||||
default: () => []
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: $prefix + 'Skeleton',
|
||||
props: skeletonProps,
|
||||
setup(props, context) {
|
||||
return $setup({ props, context, template })
|
||||
}
|
||||
})
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<div class="tiny-skeleton">
|
||||
<template v-if="loading">
|
||||
<slot name="placeholder">
|
||||
<div class="tiny-skeleton__article">
|
||||
<tiny-skeleton-item variant="circle" class="tiny-skeleton__avatar" v-if="avatar"> </tiny-skeleton-item>
|
||||
<div class="tiny-skeleton__section">
|
||||
<tiny-skeleton-item class="tiny-skeleton-item__title"></tiny-skeleton-item>
|
||||
<tiny-skeleton-item
|
||||
v-for="(item, index) in rows"
|
||||
:key="item"
|
||||
:style="{ width: toPxStyle(rowsWidth[index]) }"
|
||||
></tiny-skeleton-item>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { renderless, api } from '@opentiny/vue-renderless/skeleton/vue'
|
||||
import { props, setup, defineComponent } from '@opentiny/vue-common'
|
||||
import SkeletonItem from '@opentiny/vue-skeleton-item'
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['update:modelValue'],
|
||||
props: [...props, 'modelValue', 'loading', 'rows', 'avatar', 'rowsWidth', 'active'],
|
||||
components: {
|
||||
TinySkeletonItem: SkeletonItem
|
||||
},
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api })
|
||||
}
|
||||
})
|
||||
</script>
|
Loading…
Reference in New Issue