Compare commits

...

32 Commits
dev ... dev

Author SHA1 Message Date
申君健 4b56f19814
fix(lang): fix to be compatible with aui (#3628)
* fix(lang): fix to be compatible with aui

* fix(locals): fix locals key
2025-08-04 09:54:04 +08:00
liukun a8f429bef7
fix:修复saas模式下按钮间距 (#3630) 2025-08-01 11:18:35 +08:00
liukun db3f74b418
fix:修改默认字体大小 (#3629) 2025-07-31 16:45:26 +08:00
gimmyhehe 919f9bbffb
feat(grid): saas theme add filter select style (#3626)
* feat(grid): saas theme add filter select style

* test: fix e2e test case
2025-07-31 09:45:18 +08:00
gimmyhehe f3c2499ab7
fix(grid): fix index not update at drag row (#3622) 2025-07-31 09:26:15 +08:00
liukun db03876549
fix(input):fix textarea height in Multiple line placeholders (#3624)
* Update App.vue

* fix:多行占位符导致文本域高度计算错误
2025-07-30 11:44:26 +08:00
申君健 b7aa885dc9
fix(input): add pre=true for tiny-tooltip (#3625) 2025-07-30 11:35:25 +08:00
申君健 22b3099cb8
fix(PropType): fix import of PropType (#3620)
* fix(PropType):  fix import of PropType

* fix(propType): fix
2025-07-30 11:07:49 +08:00
申君健 96cd780f26
fix(site): add MCP tools for query examples and jump examples (#3623) 2025-07-30 10:20:15 +08:00
liukun 41b9fbaade
fix:修复saas模式下文本域高度问题 (#3608) 2025-07-28 14:15:23 +08:00
Davont 984b5e13ce
fix(chart): fix chart bug, resolve memory leakage issues (#3610)
* fix: 【Charts】update charts snapshots

* fix: 修复图表height为100%时的高度问题

* fix: 优化图表extend逻辑

* fix: 修复图表extend失效问题

* fix: 删除注释

* fix: 新增图表echartOption变量,以便用户获取echart配置

* fix: 修复playground百度地图案例错误问题

* fix: 修复图表option复杂变量失效问题

* fix: 修改高德百度地图案例注释

* fix: chart-core添加cloneDeep引入

* fix: 修改chart组件getFormatted函数判断逻辑

* fix: 更新chart-core包版本为3.17.1

* fix: 修复箱型图data为空数组时的报错问题

* fix: 添加图标属性逻辑的深拷贝

* fix: 删除注释文件

* fix: 修改图表打包

* fix: 图表打包指令修改

* fix: huiCharts打包替换为Chart

* fix: 新增图表解绑,解决内存泄漏问题
2025-07-28 14:13:28 +08:00
申君健 4487170e58
fix(dropdown): modify the responsive adaptation of the drop-down arrow in the mobile first template (#3614) 2025-07-28 11:47:21 +08:00
gimmyhehe 5f51bdde9f
fix(grid): optimize render count (#3613)
* fix(grid): optimize render count

* fix(grid): optimize render count

* docs(pager): optimize pager demos
2025-07-28 11:46:53 +08:00
liukun c5e112a0b7
fix:修复saas模式表格图标引用问题 (#3618) 2025-07-28 11:45:51 +08:00
gimmyhehe 87e9491ff7
fix(grid): grid promise validate return value back to undefined (#3616) 2025-07-28 11:40:26 +08:00
ajaxzheng 12d236492e
feat(site): connect next-sdk and ai dialog box to realize dynamic switching routing function of large models (#3619)
* feat: 对接next-sdk和ai对话框

* feat: 实现大模型动态切换路由功能

* feat: 添加LLM信息填写对话框及相关逻辑,更新依赖版本
2025-07-28 11:35:40 +08:00
ajaxzheng eaeb9325da
feat: modify the resource file loading mode and add postcss plugin configuration. (#3615) 2025-07-28 11:34:47 +08:00
gimmyhehe 6b27c3076a
docs(pager): optimize pager demos (#3612) 2025-07-25 15:36:27 +08:00
ajaxzheng 875322c4e4
Revert "fix(tabs): 优化多端缓存逻辑,添加增删操作触发子组件销毁重建逻辑 (#3601)" (#3606)
This reverts commit 337ac61d71.
2025-07-24 14:46:41 +08:00
liukun 532c8a7ee1
fix:修改saas模式表格排序按钮间距 (#3603) 2025-07-24 10:42:26 +08:00
chenxi-20 337ac61d71
fix(tabs): 优化多端缓存逻辑,添加增删操作触发子组件销毁重建逻辑 (#3601) 2025-07-24 10:40:44 +08:00
ajaxzheng 53ba501691
docs(float-settings): 修改浮动设置组件的触发方式为点击,并移除不必要的状态管理 (#3602) 2025-07-24 10:38:28 +08:00
James 80a5340dd8
fix: internationalization-related modifications, temporarily hide the entry point (#3597)
* fix: add entrance in western portuguese

* fix: revise inspection comments

* fix: optimized code

* fix: revise inspection comments
2025-07-21 14:14:07 +08:00
wuyiping eb2ce6583f
feat(color-picker): refactoring the ColorPicker component style (#3595)
* feat(color-picker): refactoring the ColorPicker component style

* feat(color-picker): refactoring the ColorPicker component style

* feat(color-picker): refactoring the ColorPicker component style

* feat(color-picker): refactoring the ColorPicker component style

* feat(color-picker): color-picker component add e2e

* feat(color-select-panel): color-select-panel component add e2e
2025-07-21 14:13:29 +08:00
James 043e6b7305
fix(modal): [modal] modify the messageClosable in Vue2 version to not display the close button (#3600) 2025-07-21 09:58:56 +08:00
liukun 14ebc4711a
fix:图标fill无法透传,以及图标引用错误 (#3599) 2025-07-21 09:31:37 +08:00
Kagol 10a395984a
chore: add svg folder-solid (#3598)
* chore: add folder-solid svgs

* chore: pnpm create:icon-saas
2025-07-18 17:24:07 +08:00
chenxi-20 1900da82e4
feat(steps): Add wizard style step bar itemStyle differentiated configuration (#3594)
* fix: 去掉步骤条最大宽度限制

* fix: 增加向导型步骤条itemStyle差异化配置
2025-07-18 14:29:29 +08:00
liukun 06a3daee6c
fix:修复日历视图设置高度不生效 (#3592) 2025-07-18 14:28:21 +08:00
liukun 93f5a61ae2
fix:修复select默认尺寸多选tag颜色 (#3591) 2025-07-18 14:22:22 +08:00
gimmyhehe ccbacd97b2
fix: adapt mf list view when has not grid column (#3593) 2025-07-18 14:22:01 +08:00
allcontributors[bot] 71617af484
docs: add Lingchen111 as a contributor for code (#3596)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-07-18 09:54:27 +08:00
184 changed files with 2749 additions and 548 deletions

View File

@ -708,6 +708,15 @@
"contributions": [
"doc"
]
},
{
"login": "Lingchen111",
"name": "Lingchen111",
"avatar_url": "https://avatars.githubusercontent.com/u/123021749?v=4",
"profile": "https://github.com/Lingchen111",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 8,

View File

@ -181,6 +181,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="12.5%"><a href="https://github.com/lcy0620"><img src="https://avatars.githubusercontent.com/u/188683944?v=4?s=100" width="100px;" alt="lcy0620"/><br /><sub><b>lcy0620</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=lcy0620" title="Code">💻</a></td>
<td align="center" valign="top" width="12.5%"><a href="https://github.com/sakurajiajia"><img src="https://avatars.githubusercontent.com/u/37933037?v=4?s=100" width="100px;" alt="木斯佳"/><br /><sub><b>木斯佳</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=sakurajiajia" title="Documentation">📖</a></td>
<td align="center" valign="top" width="12.5%"><a href="https://github.com/552847957"><img src="https://avatars.githubusercontent.com/u/8729901?v=4?s=100" width="100px;" alt="552847957"/><br /><sub><b>552847957</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=552847957" title="Documentation">📖</a></td>
<td align="center" valign="top" width="12.5%"><a href="https://github.com/Lingchen111"><img src="https://avatars.githubusercontent.com/u/123021749?v=4?s=100" width="100px;" alt="Lingchen111"/><br /><sub><b>Lingchen111</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=Lingchen111" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -1,6 +1,10 @@
import { cmpMenus } from '../../sites/demos/mobile-first/menus.js'
export const demoStr = import.meta.glob('../../sites/demos/mobile-first/app/**/*.vue', { eager: false, as: 'raw' })
export const demoStr = import.meta.glob('../../sites/demos/mobile-first/app/**/*.vue', {
eager: false,
query: '?raw',
import: 'default'
})
export const demoVue = import.meta.glob('../../sites/demos/mobile-first/app/**/*.vue', { eager: false })
// demos配置

View File

@ -5,7 +5,11 @@
// 同web-doc的菜单资源
import { cmpMenus } from '../../sites/demos/pc/menus.js'
export const demoStr = import.meta.glob('../../sites/demos/pc/app/**/*.vue', { eager: false, as: 'raw' })
export const demoStr = import.meta.glob('../../sites/demos/pc/app/**/*.vue', {
eager: false,
query: '?raw',
import: 'default'
})
export const demoVue = import.meta.glob('../../sites/demos/pc/app/**/*.vue', { eager: false })
// demos配置

View File

@ -1,11 +1,11 @@
module.exports = {
extends: 'stylelint-config-standard', //stylelint-config-airbnb
extends: 'stylelint-config-standard',
rules: {
'string-quotes': 'single',
'property-no-unknown': true,
'selector-pseudo-class-no-unknown': true,
'at-rule-empty-line-before': 'always',
'block-no-empty': true,
'indentation': 4 // http://cui.ulanqab.huawei.com/#/articalDetail?id=b76da810d8ed8
'indentation': 4
}
}

View File

@ -241,7 +241,7 @@ export default {
'en-US': "Whether the 'message' type pop-up window displays a close button"
},
mode: ['pc'],
pcDemo: 'message-closable'
pcDemo: 'message-close'
},
{
name: 'min-height',

View File

@ -460,13 +460,13 @@ export default {
},
{
name: 'input',
type: 'Function(value)',
type: '(event: InputEvent) => void',
defaultValue: '',
desc: {
'zh-CN': '输入值时触发事件',
'en-US': ''
'en-US': 'Trigger event when input value is entered '
},
mode: ['mobile-first'],
mode: ['pc', 'mobile-first'],
mfDemo: ''
}
],

View File

@ -86,6 +86,17 @@ export default {
mode: ['pc'],
pcDemo: 'three-areas'
},
{
name: 'trigger-simple',
type: 'boolean',
defaultValue: 'false',
desc: {
'zh-CN': '是否启用简易模式',
'en-US': 'Whether to enable simplified mode.'
},
mode: ['pc'],
pcDemo: 'trigger-simple'
},
{
name: 'border',
type: 'boolean',

View File

@ -120,13 +120,13 @@ export default {
type: 'Object',
defaultValue: '{}',
meta: {
stable: '3.24.0'
stable: '3.26.0'
},
desc: {
'zh-CN':
'自定义单链型步骤条块的内联样式,数据类型为{ [statusName: string]: styleObject },不同状态可根据key值差异化配置 key值为status字段的值value值为对应节点的样式对象',
'步骤条块的内联样式,数据类型为{ [statusName: string]: styleObject },不同状态可根据key值差异化配置 key值为status字段的值value值为对应节点的样式对象',
'en-US':
'Customize the inline style of single chain step blocks, with data type {[statusName: string]: styleObject}. Different states can be configured differently based on key values, where the key value is the value of the status field and the value value is the style object of the corresponding node'
'Customize the inline style of step blocks, with data type {[statusName: string]: styleObject}. Different states can be configured differently based on key values, where the key value is the value of the status field and the value value is the style object of the corresponding node'
},
mode: ['mobile-first']
},

View File

@ -199,6 +199,17 @@ export default {
mode: ['mobile-first'],
mfDemo: 'sub-field'
},
{
name: 'text-position',
type: 'string',
defaultValue: '',
desc: {
'zh-CN': `节点文案位置。默认名称和时间分别展示在图标上下方;可选值:'right',只有名称展示名称在右方`,
'en-US': `Node copy position. The default name and time are displayed above and below the icon, respectively; optional value: 'right', where only the name is displayed on the right side. `
},
mode: ['pc'],
pcDemo: 'text-position'
},
{
name: 'time-field',
type: 'string',
@ -249,6 +260,15 @@ export default {
],
methods: [],
slots: [
{
name: 'default',
desc: {
'zh-CN': '组件默认插槽。组件显示为插槽内容',
'en-US': 'Component default slot. The component displays as the slot content. '
},
mode: ['pc'],
pcDemo: 'slot-default'
},
{
name: 'bottom',
desc: {

View File

@ -1,5 +1,12 @@
<template>
<tiny-steps ref="steps" advanced :data="data" :active="advancedActive" @click="advancedClick"></tiny-steps>
<tiny-steps
ref="steps"
advanced
:item-style="{ disabled: { background: 'yellow', maxWidth: '360px' } }"
:data="data"
:active="advancedActive"
@click="advancedClick"
></tiny-steps>
</template>
<script>

View File

@ -1,5 +1,5 @@
<template>
<tiny-color-picker @confirm="onConfirm" @cancel="onCancel" alpha />
<tiny-color-picker @confirm="onConfirm" v-model="color" @cancel="onCancel" alpha />
</template>
<script setup>

View File

@ -8,7 +8,7 @@ test('测试 Alpha', async ({ page }) => {
await page.getByRole('button', { name: '取消' }).click()
await page.getByText('用户选择了取消').click()
await page.locator('.tiny-color-picker__inner').click()
await page.getByRole('button', { name: '选择' }).click()
await page.getByRole('button', { name: '确定' }).click()
// default is hex
await page.getByText('#804040FF').click()
await page.getByText('#66CCFFFF').click()
})

View File

@ -1,5 +1,5 @@
<template>
<tiny-color-picker @confirm="onConfirm" @cancel="onCacnel" alpha />
<tiny-color-picker @confirm="onConfirm" v-model="color" @cancel="onCacnel" alpha />
</template>
<script>

View File

@ -5,8 +5,8 @@ test('基本用法', async ({ page }) => {
await page.goto('color-picker#basic-usage')
await page.locator('.tiny-color-picker__inner').click()
await page.locator('.black').click()
await page.getByRole('button', { name: '选择' }).click()
await page.getByRole('button', { name: '确定' }).click()
await page.locator('.tiny-color-picker__inner').click()
await page.locator('.tiny-color-select-panel__inner__color-select').click()
await page.getByRole('button', { name: '选择' }).click()
await page.getByRole('button', { name: '确定' }).click()
})

View File

@ -4,5 +4,5 @@ test('默认显示色盘', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('color-picker#default-visible')
await page.locator('.tiny-color-select-panel__inner__hue-select').click()
await page.getByRole('button', { name: '选择' }).click()
await page.getByRole('button', { name: '确定' }).click()
})

View File

@ -1,5 +1,5 @@
<template>
<tiny-color-picker @confirm="onConfirm" @cancel="onCancel" />
<tiny-color-picker v-model="color" @confirm="onConfirm" @cancel="onCancel" />
</template>
<script setup>
@ -15,7 +15,7 @@ const onConfirm = (hex) => {
TinyNotify({
type: 'success',
position: 'top-right',
title: '用户点击了选择',
title: '用户点击了确定',
message: hex
})
}

View File

@ -3,8 +3,8 @@ import { test, expect } from '@playwright/test'
test('事件触发', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('color-picker#event')
await page.locator('#event').getByRole('img').click()
await page.getByRole('button', { name: '选择' }).click()
await page.locator('#event').getByRole('img').first().click()
await page.getByRole('button', { name: '取消' }).click()
await page.locator('.tiny-color-picker__inner').click()
await page.locator('.black').click()
await page.getByRole('button', { name: '确定' }).click()
await page.getByText('用户点击了确定').click()
})

View File

@ -1,5 +1,5 @@
<template>
<tiny-color-picker @confirm="onConfirm" @cancel="onCacnel" />
<tiny-color-picker v-model="color" @confirm="onConfirm" @cancel="onCacnel" />
</template>
<script>

View File

@ -12,9 +12,9 @@ test('hex 时应该为#xxx', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('color-picker#format')
await page.locator('.tiny-color-picker__inner').click()
await page.getByRole('textbox', { name: '请选择' }).click()
await page.locator('.tiny-input__suffix-inner svg').click()
await page.getByRole('list').getByText('hex').click()
await page.getByRole('button', { name: '选择' }).click()
await page.getByRole('button', { name: '确定' }).click()
await expect(page.getByLabel('示例', { exact: true }).getByRole('paragraph')).toContainText('颜色值:#66CCFF')
})
@ -22,9 +22,9 @@ test('hsl 时应该为 hsl(xxx,xxx,xxx)', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('color-picker#format')
await page.locator('.tiny-color-picker__inner').click()
await page.getByRole('textbox', { name: '请选择' }).click()
await page.locator('.tiny-input__suffix-inner svg').click()
await page.getByRole('list').getByText('hsl').click()
await page.getByRole('button', { name: '选择' }).click()
await page.getByRole('button', { name: '确定' }).click()
await expect(page.getByLabel('示例', { exact: true }).getByRole('paragraph')).toContainText('颜色值hsl')
})
@ -32,8 +32,8 @@ test('hsv 时候应该为 hsv(xx,xx,xx)', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('color-picker#format')
await page.locator('.tiny-color-picker__inner').click()
await page.getByRole('textbox', { name: '请选择' }).click()
await page.locator('.tiny-input__suffix-inner svg').click()
await page.getByRole('list').getByText('hsv').click()
await page.getByRole('button', { name: '选择' }).click()
await page.getByRole('button', { name: '确定' }).click()
await expect(page.getByLabel('示例', { exact: true }).getByRole('paragraph')).toContainText('颜色值hsv')
})

View File

@ -5,11 +5,5 @@ test('测试历史记录', async ({ page }) => {
await page.goto('color-picker#history')
await page.getByRole('button', { name: 'Toggle History visibility' }).click()
await page.locator('.tiny-color-picker__inner').click()
await expect(page.getByRole('button', { name: '历史记录' })).toBeVisible()
await page.getByRole('button', { name: '历史记录' }).click()
await expect(page.getByText('暂无', { exact: true })).toBeVisible()
await page.getByRole('button', { name: '选择' }).click()
await page.getByRole('button', { name: 'Append history color' }).click()
await page.locator('.tiny-color-picker__inner').click()
await page.getByRole('button', { name: '历史记录' }).click()
await page.getByRole('button', { name: '确定' }).click()
})

View File

@ -5,23 +5,16 @@ test('测试预定义颜色', async ({ page }) => {
await page.goto('color-picker#predefine')
await page.getByRole('button', { name: 'Toggle predefine visibility' }).click()
await page.locator('.tiny-color-picker__inner').click()
await expect(page.getByRole('button', { name: '预定义颜色' })).toBeVisible()
await page.getByRole('button', { name: '预定义颜色' }).click()
await expect(page.locator('.tiny-color-select-panel__predefine > div:nth-child(8)')).toBeVisible()
await page.getByText('取消选择预定义颜色Append predefine').click()
await page.getByRole('button', { name: '选择' }).click()
await page.getByRole('button', { name: '确定' }).click()
await page.getByRole('button', { name: 'Append predefine color' }).click()
await page.locator('.tiny-color-picker__inner').click()
await page.getByRole('button', { name: '预定义颜色' }).click()
await expect(page.locator('.tiny-color-select-panel__predefine > div:nth-child(9)')).toBeVisible()
await page.getByRole('button', { name: '选择' }).click()
await page.getByRole('button', { name: '确定' }).click()
await page.getByRole('button', { name: 'Pop predefine color' }).click()
await page.locator('.tiny-color-picker').click()
await page.getByRole('button', { name: '预定义颜色' }).click()
await expect(page.locator('.tiny-color-select-panel__predefine > div:nth-child(9)')).not.toBeVisible()
await expect(page.locator('.tiny-color-select-panel__predefine > div:nth-child(8)')).toBeVisible()
await page.locator('.tiny-color-select-panel__predefine > div:nth-child(8)').click()
await page.getByRole('button', { name: '选择' }).click()
await page.locator('.tiny-color-picker__inner').click()
await page.getByText('取消选择预定义颜色Append predefine').click()
await page.getByRole('button', { name: '确定' }).click()
})

View File

@ -3,8 +3,8 @@ import { test, expect } from '@playwright/test'
test('测试尺寸', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('color-picker#size')
await expect(page.locator('.tiny-color-picker.tiny-color-picker--large')).toHaveCSS('width', '48px')
await expect(page.locator('.tiny-color-picker.tiny-color-picker--medium')).toHaveCSS('width', '40px')
await expect(page.locator('.tiny-color-picker.tiny-color-picker--small')).toHaveCSS('width', '28px')
await expect(page.locator('.tiny-color-picker.tiny-color-picker--mini')).toHaveCSS('width', '24px')
await expect(page.locator('.tiny-color-picker.tiny-color-picker--large')).toHaveCSS('width', '32px')
await expect(page.locator('.tiny-color-picker.tiny-color-picker--medium')).toHaveCSS('width', '24px')
await expect(page.locator('.tiny-color-picker.tiny-color-picker--small')).toHaveCSS('width', '20px')
await expect(page.locator('.tiny-color-picker.tiny-color-picker--mini')).toHaveCSS('width', '16px')
})

View File

@ -12,7 +12,7 @@ test('hex', async ({ page }) => {
await page.getByRole('button', { name: 'Toggle' }).click()
await page.getByRole('textbox', { name: '请选择' }).click()
await page.getByRole('list').getByText('hex').click()
await page.getByRole('button', { name: '选择' }).click()
await page.getByRole('button', { name: '确定' }).click()
await expect(page.locator('#format')).toContainText('颜色值:#66CCFF')
})
@ -20,12 +20,6 @@ test('hsl 时应该为 hsl(xxx,xxx,xxx)', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('color-select-panel#format')
await page.getByRole('button', { name: 'Toggle' }).click()
await page
.locator('div')
.filter({ hasText: /^rgbhexhslhsv取消选择$/ })
.getByRole('img')
.click()
await page.getByRole('textbox', { name: '请选择' }).click()
await page.getByRole('textbox', { name: '请选择' }).click()
await page.getByRole('list').getByText('hsl').click()
})
@ -36,6 +30,6 @@ test('hsv 时候应该为 hsv(xx,xx,xx)', async ({ page }) => {
await page.getByRole('button', { name: 'Toggle' }).click()
await page.getByRole('textbox', { name: '请选择' }).click()
await page.getByRole('list').getByText('hsv').click()
await page.getByRole('button', { name: '选择' }).click()
await page.getByRole('button', { name: '确定' }).click()
await expect(page.locator('#format')).toContainText('颜色值hsv(199.99999999999997, 60%, 100%)')
})

View File

@ -5,8 +5,6 @@ test('历史记录', async ({ page }) => {
await page.goto('color-select-panel#history')
await page.getByRole('button', { name: 'Toggle History visibility' }).click()
await page.getByRole('button', { name: 'Show Color select panel' }).click()
await expect(page.getByRole('button', { name: '历史记录' })).toBeVisible()
await page.getByRole('button', { name: 'Toggle History visibility' }).click()
await page.getByRole('button', { name: 'Show Color select panel' }).click()
await expect(page.getByRole('button', { name: '历史记录' })).not.toBeVisible()
})

View File

@ -6,15 +6,12 @@ test('预定义颜色', async ({ page }) => {
await page.getByRole('button', { name: 'Toggle Predefine color' }).click()
await page.getByRole('button', { name: 'Show Color select panel' }).click()
await page.getByRole('button', { name: '预定义颜色' }).click()
expect(page.locator('.tiny-color-select-panel__predefine__color-block')).toHaveCount(8)
await page.getByRole('button', { name: 'Append predefine color' }).click()
await page.getByRole('button', { name: 'Show Color select panel' }).click()
await page.getByRole('button', { name: '预定义颜色' }).click()
expect(page.locator('.tiny-color-select-panel__predefine__color-block')).toHaveCount(9)
await page.getByRole('button', { name: 'Pop predefine color' }).click()
await page.getByRole('button', { name: 'Show Color select panel' }).click()
await page.getByRole('button', { name: '预定义颜色' }).click()
expect(page.locator('.tiny-color-select-panel__predefine__color-block')).toHaveCount(8)
await page.getByRole('button', { name: 'Toggle Predefine color' }).click()
await page.getByRole('button', { name: 'Show Color select panel' }).click()

View File

@ -0,0 +1,40 @@
import { test, expect } from '@playwright/test'
test('dialogSelect 表格单选', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('dialog-select#nest-grid-single')
await page.locator('#nest-grid-single').getByRole('button', { name: '打开窗口' }).click()
await page.locator('.tiny-grid-body__row').first().waitFor()
let rows
rows = await page.locator('.tiny-grid-body__row').all()
for (const row of rows) {
const checked = await row.locator('input[type="radio"]').isChecked()
expect(checked).toBe(false)
}
await page.getByRole('row', { name: 'GFD 科技有限公司 福建 福州' }).locator('path').nth(1).click()
rows = await page.locator('.tiny-grid-body__row').all()
for (let i = 0; i < rows.length; i++) {
const checked = await rows[i].locator('input[type="radio"]').first().isChecked()
if (i === 0) {
expect(checked).toBe(true)
} else {
expect(checked).toBe(false)
}
}
await page.getByRole('row', { name: 'WWW 科技有限公司 广东 深圳' }).locator('path').nth(1).click()
rows = await page.locator('.tiny-grid-body__row').all()
for (let i = 0; i < rows.length; i++) {
const checked = await rows[i].locator('input[type="radio"]').first().isChecked()
if (i === 1) {
expect(checked).toBe(true)
} else {
expect(checked).toBe(false)
}
}
})

View File

@ -0,0 +1,53 @@
import { test, expect } from '@playwright/test'
test('dialogSelect 树多选', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('dialog-select#nest-tree-multi')
await page.locator('#nest-tree-multi').getByRole('button', { name: '打开窗口', exact: true }).click()
const nodeContent = await page.locator('.tiny-tree-node__content-left label').nth(1)
const iconNode = await nodeContent.getAttribute('class')
expect(iconNode?.includes('tiny-radio')).toBe(false)
expect(iconNode?.includes('tiny-checkbox')).toBe(true)
let isChecked
isChecked = await page
.getByRole('treeitem', { name: '三级 9' })
.locator('.tiny-checkbox input[type="checkbox"]')
.isChecked()
expect(isChecked).toBeFalsy()
let current
current = await page.getByRole('treeitem', { name: '三级 9' }).locator('path').nth(1)
await current.click()
isChecked = await page
.getByRole('treeitem', { name: '三级 9' })
.locator('.tiny-checkbox input[type="checkbox"]')
.isChecked()
expect(isChecked).toBeTruthy()
await page
.locator('div')
.filter({ hasText: /^一级 1二级 4三级 8三级 9暂无数据$/ })
.getByRole('img')
.nth(3)
.click()
isChecked = await page
.getByRole('treeitem', { name: '三级 9' })
.locator('.tiny-checkbox input[type="checkbox"]')
.isChecked()
expect(isChecked).toBeFalsy()
await page.getByRole('treeitem', { name: '三级 9' }).locator('path').nth(1).click()
isChecked = await page
.getByRole('treeitem', { name: '三级 9' })
.locator('.tiny-checkbox input[type="checkbox"]')
.isChecked()
expect(isChecked).toBeTruthy()
})

View File

@ -0,0 +1,25 @@
import { test, expect } from '@playwright/test'
test('dialogSelect 树单选', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('dialog-select#nest-tree-single')
await page.locator('#nest-tree-single').getByRole('button', { name: '打开窗口' }).click()
const nodeContent = await page.locator('.tiny-tree-node__content-left label').nth(1)
const iconNode = await nodeContent.getAttribute('class')
expect(iconNode?.includes('tiny-radio')).toBe(true)
expect(iconNode?.includes('tiny-checkbox')).toBe(false)
let current
current = await page.getByText('201一级')
await current.click()
expect(await current.locator('input[type="radio"]').isChecked()).toBe(true)
current = await page.getByRole('treeitem', { name: '二级 6' }).locator('label')
await current.click()
expect(await current.locator('input[type="radio"]').isChecked()).toBe(true)
current = await page.getByText('201一级')
expect(await current.locator('input[type="radio"]').isChecked()).toBe(false)
})

View File

@ -0,0 +1,20 @@
import { test, expect } from '@playwright/test'
test('dialogSelect 设置多选状态', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('dialog-select#set-selection')
await page.locator('#set-selection').getByRole('button', { name: '打开窗口' }).click()
await page.getByRole('button', { name: 'Close' }).click()
await page.getByRole('button', { name: '切换第二行选中状态' }).click()
await page.locator('#set-selection').getByRole('button', { name: '打开窗口' }).click()
const trs = await page.locator('.tiny-grid table tbody tr').all()
for (let i = 0; i < trs.length; i++) {
const classes = await trs[i].getAttribute('class')
if (i === 1) {
expect(classes?.includes('row__selected')).toBeTruthy()
} else {
expect(classes?.includes('row__selected')).toBeFalsy()
}
}
})

View File

@ -4,7 +4,6 @@ test('自动加载数据', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('grid-data-source#data-source-auto-load')
// 判断 auto-load 为 false 时不加载数据
await page.getByRole('paragraph').nth(1).click()
const demo = page.locator('#data-source-auto-load')
await expect(demo.getByText('暂无数据')).toHaveText('暂无数据')
})

View File

@ -3,7 +3,8 @@ import { test, expect } from '@playwright/test'
test('根据日期动态生成列', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('grid-dynamically-columns#dynamically-columns-dynamically-columns')
await page.getByRole('textbox').nth(1).click()
const demo = page.locator('#dynamically-columns-dynamically-columns')
await demo.locator('.tiny-picker.tiny-date-container').click()
await page.getByText('12 月').first().click()
await page.getByText('2 月').nth(2).click()
await expect(page.getByRole('cell', { name: '12' }).first()).toBeVisible()

View File

@ -4,9 +4,9 @@ test('列筛选规则', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('grid-filter#filter-custom-filter')
await page.getByRole('cell', { name: '名称' }).getByRole('img').click()
await page.getByRole('textbox').nth(1).click()
await page.getByRole('textbox').nth(1).press('CapsLock')
await page.getByRole('textbox').nth(1).fill('WWW')
await page.locator('.tiny-grid__filter-wrapper.filter__active input').click()
await page.locator('.tiny-grid__filter-wrapper.filter__active input').press('CapsLock')
await page.locator('.tiny-grid__filter-wrapper.filter__active input').fill('WWW')
await page.getByRole('button', { name: 'confirm' }).click()
await expect(page.getByRole('cell', { name: 'WWW 科技 YX 公司' })).toBeVisible()
})

View File

@ -6,11 +6,12 @@ test('简化版筛选面板 - 单选/多选菜单', async ({ page }) => {
await page.getByRole('cell', { name: '公司名称' }).getByRole('img').click()
// 筛选面板搜索功能
await page.getByRole('textbox').nth(1).click()
await page.getByRole('textbox').nth(1).fill('a')
const filterInput = page.locator('.tiny-grid__filter-wrapper.filter__active input')
await filterInput.click()
await filterInput.fill('a')
await expect(page.getByRole('listitem').filter({ hasText: '暂无数据' })).toBeVisible()
await page.getByRole('textbox').nth(1).click()
await page.getByRole('textbox').nth(1).fill('')
await filterInput.click()
await filterInput.fill('')
await page.getByTitle('GFD 科技 YX 公司').click()
await page.getByTitle('WWW 科技 YX 公司').click()

View File

@ -71,7 +71,8 @@ export const iconGroups = {
'IconUp',
'IconUpdate',
'IconUpO',
'IconUpWard'
'IconUpWard',
'IconTriangleUp'
],
Downward: [
'IconArrowBottom',
@ -388,7 +389,11 @@ export const iconGroups = {
'IconSmile',
'IconStarActive',
'IconStarDisable',
'IconStarO'
'IconStarO',
'IconBadgeHotCn',
'IconBadgeHotEn',
'IconBadgeNewCn',
'IconBadgeNewEn'
],
Tool: [
'IconConfig',

View File

@ -11,15 +11,15 @@ import { TinyLocales } from '@opentiny/vue'
function getLocale() {
// resolve key
return Promise.resolve(['zh_CN', 'en_US', 'zh_TW'])
return Promise.resolve(['zhCN', 'enUS', 'zhTW'])
}
function getCurrentLocale() {
return Promise.resolve(['zh_CN'])
return Promise.resolve(['zhCN'])
}
function getChangeLocaleUrl(targetLocale) {
if (targetLocale === 'en_US') {
if (targetLocale === 'enUS') {
return Promise.resolve(`${window.location.origin}/#/webenglish/en-US/component/locales/custom-service`)
} else {
return Promise.resolve(`${window.location.origin}/#/zh-CN/component/custom-service`)

View File

@ -16,13 +16,13 @@ export default {
methods: {
getLocale() {
// resolve key
return Promise.resolve(['zh_CN', 'en_US', 'zh_TW'])
return Promise.resolve(['zhCN', 'enUS', 'zhTW'])
},
getCurrentLocale() {
return Promise.resolve(['zh_CN'])
return Promise.resolve(['zhCN'])
},
getChangeLocaleUrl(targetLocale) {
if (targetLocale === 'en_US') {
if (targetLocale === 'enUS') {
return Promise.resolve(`${window.location.origin}/#/webenglish/en-US/component/locales/custom-service`)
} else {
return Promise.resolve(`${window.location.origin}/#/zh-CN/component/custom-service`)

View File

@ -0,0 +1,26 @@
<template>
<div>
<tiny-numeric v-model="value" @input="onInput"></tiny-numeric>
<div>
input事件触发了:<span class="count">{{ inputCount }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { TinyModal, TinyNumeric } from '@opentiny/vue'
const value = ref(1)
const inputCount = ref(0)
const onInput = (event: InputEvent) => {
const currentValue = (event.target as HTMLInputElement).value
TinyModal.message({
message: `新值: ${currentValue}`,
status: 'info'
})
inputCount.value++
}
</script>

View File

@ -0,0 +1,15 @@
import { expect, test } from '@playwright/test'
test('输入事件', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('numeric#input-event')
let count
for (let i = 0; i < 5; i++) {
await page.locator('.tiny-numeric__input-inner').fill(String(Math.random()))
count = await page.locator('.count').textContent()
expect(Number(count)).toBe(i + 1)
}
})

View File

@ -0,0 +1,34 @@
<template>
<div>
<tiny-numeric v-model="value" @input="onInput"></tiny-numeric>
<div>
input事件触发了:<span class="count">{{ inputCount }}</span>
</div>
</div>
</template>
<script lang="ts">
import { TinyModal, TinyNumeric } from '@opentiny/vue'
export default {
components: {
TinyNumeric
},
data() {
return {
value: 1,
inputCount: 0
}
},
methods: {
onInput(event: InputEvent) {
const currentValue = (event.target as HTMLInputElement).value
TinyModal.message({
message: `新值: ${currentValue}`,
status: 'info'
})
this.inputCount++
}
}
}
</script>

View File

@ -165,6 +165,18 @@ export default {
},
codeFiles: ['change-event.vue']
},
{
demoId: 'input-event',
name: {
'zh-CN': '输入事件',
'en-US': 'Input Event'
},
desc: {
'zh-CN': '<p>输入时触发<code>input</code>事件。<p>',
'en-US': '<p>Trigger the <code>input</code> event upon input. </p>'
},
codeFiles: ['input-event.vue']
},
{
demoId: 'focus-event',
name: {

View File

@ -1,20 +1,17 @@
<template>
<div class="content">
<tiny-pager align="left" :total="100"></tiny-pager>
<tiny-pager align="center" :total="100"></tiny-pager>
<tiny-pager align="right" :total="100"></tiny-pager>
<TinyRadioGroup v-model="state.align" type="button" :options="state.options"></TinyRadioGroup>
<TinyPager :align="state.align" :total="100"></TinyPager>
</div>
</template>
<script setup>
import { TinyPager } from '@opentiny/vue'
</script>
import { reactive } from 'vue'
<style scoped>
.content {
margin-bottom: 20px;
}
.tiny-radio-group {
margin-bottom: 10px;
}
</style>
import { TinyPager, TinyRadioGroup } from '@opentiny/vue'
const state = reactive({
align: 'left',
options: ['left', 'center', 'right'].map((item) => ({ label: item, text: item }))
})
</script>

View File

@ -7,7 +7,12 @@ test('对齐方式', async ({ page }) => {
const demo = page.locator('#align')
const pager = demo.locator('.tiny-pager')
await expect(pager.first()).toHaveCSS('text-align', 'left')
await expect(pager.nth(1)).toHaveCSS('text-align', 'center')
await expect(pager.nth(2)).toHaveCSS('text-align', 'right')
await expect(pager).toHaveCSS('text-align', 'left')
await page.click('text=center')
await expect(pager).toHaveCSS('text-align', 'center')
await page.click('text=right')
await expect(pager).toHaveCSS('text-align', 'right')
await page.click('text=left')
await expect(pager).toHaveCSS('text-align', 'left')
})

View File

@ -1,17 +1,23 @@
<template>
<div class="content">
<tiny-pager align="left" :total="100"></tiny-pager>
<tiny-pager align="center" :total="100"></tiny-pager>
<tiny-pager align="right" :total="100"></tiny-pager>
<TinyRadioGroup v-model="align" type="button" :options="options"></TinyRadioGroup>
<tiny-pager :align="align" :total="100"></tiny-pager>
</div>
</template>
<script>
import { TinyPager } from '@opentiny/vue'
import { TinyPager, TinyRadioGroup } from '@opentiny/vue'
export default {
components: {
TinyPager
TinyPager,
TinyRadioGroup
},
data() {
return {
align: 'left',
options: ['left', 'center', 'right'].map((item) => ({ label: item, text: item }))
}
}
}
</script>

View File

@ -1,7 +1,7 @@
<template>
<tiny-pager mode="number" :page-size="5" :page-sizes="[5, 7, 10, 20, 50]" :total="100"> </tiny-pager>
<tiny-pager mode="number" :page-size="20" :page-sizes="[10, 20, 50, 100]" :total="100"> </tiny-pager>
</template>
<script setup>
import { TinyPager, TinyModal } from '@opentiny/vue'
import { TinyPager } from '@opentiny/vue'
</script>

View File

@ -7,7 +7,7 @@ test('每页显示数量', async ({ page }) => {
const preview = page.locator('#page-size')
const pager = preview.locator('.tiny-pager')
const total = 100
const initPageSize = 5
const initPageSize = 20
const getPageCount = (pageSize: number) => String(Math.ceil(total / pageSize))
const sizeChange = pager.locator('.tiny-pager__page-size')
const sizeSelect = page.locator('.tiny-pager__selector')

View File

@ -1,9 +1,9 @@
<template>
<tiny-pager mode="number" :page-size="5" :page-sizes="[5, 7, 10, 20, 50]" :total="100"> </tiny-pager>
<tiny-pager mode="number" :page-size="20" :page-sizes="[10, 20, 50, 100]" :total="100"> </tiny-pager>
</template>
<script>
import { TinyPager, TinyModal } from '@opentiny/vue'
import { TinyPager } from '@opentiny/vue'
export default {
components: {

View File

@ -27,7 +27,6 @@
:current-page="custPager.currentPage"
:page-size="custPager.pageSize"
:total="custPager.total"
:page-sizes="[5, 10, 20, 50]"
@current-change="currentChange"
@size-change="sizeChange"
layout="total, sizes, prev, pager, next, jumper"

View File

@ -27,7 +27,6 @@
:current-page="custPager.currentPage"
:page-size="custPager.pageSize"
:total="custPager.total"
:page-sizes="[5, 10, 20, 50]"
@current-change="currentChange"
@size-change="sizeChange"
layout="total, sizes, prev, pager, next, jumper"

View File

@ -1,11 +1,5 @@
<template>
<tiny-pager
:popper-append-to-body="false"
layout="sizes,prev, pager, next"
:page-size="5"
:page-sizes="[5, 10, 20, 30, 40, 50, 100]"
:total="50"
></tiny-pager>
<tiny-pager :popper-append-to-body="false" layout="sizes,prev, pager, next" :total="50"></tiny-pager>
</template>
<script setup>

View File

@ -1,11 +1,5 @@
<template>
<tiny-pager
:popper-append-to-body="false"
layout="sizes,prev, pager, next"
:page-size="5"
:page-sizes="[5, 10, 20, 30, 40, 50, 100]"
:total="50"
></tiny-pager>
<tiny-pager :popper-append-to-body="false" layout="sizes,prev, pager, next" :total="50"></tiny-pager>
</template>
<script>

View File

@ -1,11 +1,5 @@
<template>
<tiny-pager
popper-class="custom-pager"
layout="sizes,prev, pager, next"
:page-size="5"
:page-sizes="[5, 10, 20, 30, 40, 50, 100]"
:total="50"
></tiny-pager>
<tiny-pager popper-class="custom-pager" layout="sizes,prev, pager, next" :total="50"></tiny-pager>
</template>
<script setup>

View File

@ -1,11 +1,5 @@
<template>
<tiny-pager
popper-class="custom-pager"
layout="sizes,prev, pager, next"
:page-size="5"
:page-sizes="[5, 10, 20, 30, 40, 50, 100]"
:total="50"
></tiny-pager>
<tiny-pager popper-class="custom-pager" layout="sizes,prev, pager, next" :total="50"></tiny-pager>
</template>
<script>

View File

@ -1,16 +1,30 @@
<template>
<div class="qr-code-attr">
iconSize:
<tiny-numeric v-model="params.iconSize" :min="1" :max="params.size * 0.3" />
size: <tiny-numeric v-model="params.size" :min="1" :max="1e4" />
</div>
<tiny-qr-code v-bind="params"></tiny-qr-code>
</template>
<script setup>
import { TinyQrCode } from '@opentiny/vue'
import { reactive } from 'vue'
import { TinyNumeric, TinyQrCode } from '@opentiny/vue'
const params = {
const params = reactive({
value: '测试二维码数据',
icon: import.meta.env.VITE_APP_BUILD_BASE_URL
? `${import.meta.env.VITE_APP_BUILD_BASE_URL}static/images/mountain.png`
: 'https://res.hc-cdn.com/tinyui-design-common/1.0.5.20230707170109/assets/tinyvue.svg',
iconSize: 60,
size: 250
}
})
</script>
<style scoped>
.qr-code-attr {
display: flex;
align-items: center;
gap: 20px;
}
</style>

View File

@ -8,4 +8,23 @@ test('自定义 icon', async ({ page }) => {
const canvasImg = page.locator('.tiny-qr-code .mask-icon img')
await expect(canvas).toBeVisible()
await expect(canvasImg).toBeVisible()
await page.getByLabel('示例', { exact: true }).getByRole('button').nth(1).click()
const iconSize = await canvasImg.evaluate((el) => {
return window.getComputedStyle(el)
})
const inputIconSizeWidth = await page.getByRole('spinbutton').first().inputValue()
expect(iconSize.width === `${inputIconSizeWidth}px`).toBe(true)
expect(iconSize.height === `${inputIconSizeWidth}px`).toBe(true)
await page.getByLabel('示例', { exact: true }).getByRole('button').nth(3).click()
const [qrWidth, qrHeight] = await page.locator('.tiny-qr-code').evaluate((el) => {
const style = window.getComputedStyle(el)
return [style.width, style.height]
})
const inputSizeWidth = await page.getByRole('spinbutton').nth(1).inputValue()
expect(qrWidth === `${inputSizeWidth}px`).toBe(true)
expect(qrHeight === `${inputSizeWidth}px`).toBe(true)
})

View File

@ -1,25 +1,46 @@
<template>
<div class="qr-code-attr">
iconSize:
<tiny-numeric v-model="iconSize" :min="1" :max="size * 0.3" />
size: <tiny-numeric v-model="size" :min="1" :max="1e4" />
</div>
<tiny-qr-code v-bind="params"></tiny-qr-code>
</template>
<script>
import { TinyQrCode } from '@opentiny/vue'
import { TinyNumeric, TinyQrCode } from '@opentiny/vue'
export default {
components: {
TinyNumeric,
TinyQrCode
},
data() {
return {
params: {
size: 250,
iconSize: 60
}
},
computed: {
params() {
return {
value: '测试二维码数据',
icon: import.meta.env.VITE_APP_BUILD_BASE_URL
? `${import.meta.env.VITE_APP_BUILD_BASE_URL}static/images/mountain.png`
: 'https://res.hc-cdn.com/tinyui-design-common/1.0.5.20230707170109/assets/tinyvue.svg',
iconSize: 60,
size: 250
iconSize: this.iconSize,
size: this.size
}
}
}
}
</script>
<style scoped>
.qr-code-attr {
display: flex;
align-items: center;
gap: 20px;
}
</style>

View File

@ -1,15 +1,12 @@
<template>
<div class="qr-code-container">
<div>
<tiny-button @click="changeColor">改变颜色</tiny-button>
</div>
<br />
<tiny-color-picker v-model="params.color" />
<tiny-qr-code v-bind="params"></tiny-qr-code>
</div>
</template>
<script setup>
import { TinyQrCode, TinyButton } from '@opentiny/vue'
import { TinyQrCode, TinyColorPicker } from '@opentiny/vue'
import { ref } from 'vue'
const params = ref({
@ -18,8 +15,4 @@ const params = ref({
size: 250,
style: { background: '#f5f5f5', padding: '24px' }
})
const changeColor = () => {
params.value.color = '#666'
}
</script>

View File

@ -6,4 +6,35 @@ test('自定义样式', async ({ page }) => {
const canvas = page.locator('.tiny-qr-code canvas')
await expect(canvas).toBeVisible()
const backgroundColor0 = await canvas.evaluate(
(el: any, { x, y }) => {
const ctx = el.getContext('2d')
const pixel = ctx.getImageData(x, y, 1, 1).data
const toHex = (num: number) => num.toString(16).padStart(2, '0')
return `#${toHex(pixel[0])}${toHex(pixel[1])}${toHex(pixel[2])}`
},
{ x: 1, y: 1 }
)
expect(backgroundColor0 === '#1677ff').toBeTruthy()
await page.locator('.tiny-color-picker__inner').click()
await page.locator('.black').click()
await page.locator('.tiny-color-select-panel__inner__hue-select').click()
await page.getByRole('button', { name: '选择' }).click()
const backgroundColor1 = await page.locator('.tiny-color-picker__inner').evaluate((el) => {
return window.getComputedStyle(el).backgroundColor
})
const backgroundColor2 = await canvas.evaluate(
(el: any, { x, y }) => {
const ctx = el.getContext('2d')
const pixel = ctx.getImageData(x, y, 1, 1).data
return `rgb(${pixel[0]}, ${pixel[1]}, ${pixel[2]})`
},
{ x: 1, y: 1 }
)
expect(backgroundColor1 === backgroundColor2).toBeTruthy()
})

View File

@ -1,26 +1,29 @@
<template>
<div class="qr-code-container">
<div>
<tiny-button @click="changeColor">改变颜色</tiny-button>
</div>
<br />
<div>改变颜色<tiny-color-picker v-model="color" /></div>
<tiny-qr-code v-bind="params"></tiny-qr-code>
</div>
</template>
<script>
import { TinyQrCode, TinyButton } from '@opentiny/vue'
import { TinyQrCode, TinyColorPicker } from '@opentiny/vue'
export default {
components: {
TinyQrCode,
TinyButton
TinyColorPicker
},
data() {
return {
params: {
color: '#1677ff'
}
},
computed: {
params() {
return {
value: '测试二维码数据',
color: '#1677ff',
color: this.color,
size: 250,
style: { background: '#f5f5f5', padding: '24px' }
}

View File

@ -0,0 +1,81 @@
<template>
<div class="demo-timeline-default-slot">
<tiny-time-line>
<template #default>
<ol>
<li v-for="(item, index) in items" :key="index" :class="item.status">
<div>
<div class="index-icon">{{ index + 1 }}</div>
<hr />
</div>
<div>{{ item.desc }}</div>
<div v-if="item.user">{{ item.user }}</div>
<div v-if="item.datetime">{{ item.datetime }}</div>
</li>
</ol>
</template>
</tiny-time-line>
</div>
</template>
<script setup>
import { TinyTimeLine } from '@opentiny/vue'
const items = [
{ desc: '提交申请', user: '张三', datetime: new Date(Date.now() - 60 * 60 * 1e3).toLocaleString(), status: 'done' },
{ desc: '直接主管', user: '李四', datetime: new Date().toLocaleString(), status: 'done' },
{ desc: '部门主管', user: '王五' },
{ desc: '完成' }
]
</script>
<style scoped lang="less">
.demo-timeline-default-slot {
--color: #d3d5d6;
.done {
--color: #5073e5;
}
--size: 20px;
}
ol {
display: flex;
}
li {
margin: 10px 0;
color: var(--color);
min-width: 200px;
}
div {
position: relative;
color: var(--active-color);
&:not(:first-of-type) {
margin: 5px 0;
display: flex;
justify-content: center;
}
}
hr {
position: absolute;
top: 15%;
width: 100%;
height: 5px;
background-color: var(--color);
}
.index-icon {
width: var(--size);
height: var(--size);
border-radius: var(--size);
background-color: var(--color);
color: #fff;
padding: 1px;
font-size: calc(var(--size) - 2px);
text-align: center;
z-index: 1;
margin: auto;
}
</style>

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('time-line#slot-default')
const timeline = await page.locator('.tiny-timeline')
await page.waitForSelector('.tiny-timeline')
expect(await timeline.locator('.tiny-timeline-item').count()).toBe(0)
expect(await timeline.locator('ol').count()).toBeGreaterThan(0)
})

View File

@ -0,0 +1,95 @@
<template>
<div class="demo-timeline">
<tiny-time-line vertical shape="dot">
<template #default>
<ol>
<li v-for="(item, index) in items" :key="index" :class="item.status">
<div>
<div class="index-icon">{{ index + 1 }}</div>
<hr />
</div>
<div>{{ item.desc }}</div>
<div v-if="item.user">{{ item.user }}</div>
<div v-if="item.datetime">{{ item.datetime }}</div>
</li>
</ol>
</template>
</tiny-time-line>
</div>
</template>
<script>
import { TinyTimeLine } from '@opentiny/vue'
export default {
components: {
TinyTimeLine
},
data() {
return {
items: [
{
desc: '提交申请',
user: '张三',
datetime: new Date(Date.now() - 60 * 60 * 1e3).toLocaleString(),
status: 'done'
},
{ desc: '直接主管', user: '李四', datetime: new Date().toLocaleString(), status: 'done' },
{ desc: '部门主管', user: '王五' },
{ desc: '完成' }
]
}
}
}
</script>
<style scoped lang="less">
.demo-timeline {
--color: #d3d5d6;
.done {
--color: #5073e5;
}
--size: 20px;
}
ol {
display: flex;
}
li {
margin: 10px 0;
color: var(--color);
min-width: 200px;
}
div {
position: relative;
color: var(--active-color);
&:not(:first-of-type) {
margin: 5px 0;
display: flex;
justify-content: center;
}
}
hr {
position: absolute;
top: 15%;
width: 100%;
height: 5px;
background-color: var(--color);
}
.index-icon {
width: var(--size);
height: var(--size);
border-radius: var(--size);
background-color: var(--color);
color: #fff;
padding: 1px;
font-size: calc(var(--size) - 2px);
text-align: center;
z-index: 1;
margin: auto;
}
</style>

View File

@ -186,6 +186,18 @@ export default {
'en-US': '<p>Add description information for a single node through the <code>description</code> slot.</p>'
},
codeFiles: ['slot-description.vue']
},
{
demoId: 'slot-default',
name: {
'zh-CN': '默认插槽',
'en-US': 'Default Slot'
},
desc: {
'zh-CN': '组件默认插槽',
'en-US': 'Component Default Slot'
},
codeFiles: ['slot-default.vue']
}
],
features: [

View File

@ -6,3 +6,6 @@ VITE_APP_MODE='pc'
VITE_APP_BUILD_BASE_URL='/'
VITE_PLAYGROUND_URL=/playground.html
VITE_LLM_API_KEY=
VITE_LLM_URL=https://api.deepseek.com/v1

View File

@ -27,6 +27,10 @@
"@docsearch/css": "^3.8.0",
"@docsearch/js": "^3.8.0",
"@docsearch/react": "npm:@docsearch/css",
"@opentiny/next-sdk": "0.0.1-alpha.5",
"@opentiny/tiny-robot": "0.3.0-alpha.3",
"@opentiny/tiny-robot-kit": "0.3.0-alpha.3",
"@opentiny/tiny-robot-svgs": "0.3.0-alpha.3",
"@opentiny/tiny-vue-mcp": "^0.0.2",
"@opentiny/utils": "workspace:~",
"@opentiny/vue": "workspace:~",
@ -62,7 +66,8 @@
"tailwindcss": "^3.2.4",
"vue": "^3.4.31",
"vue-i18n": "~9.14.3",
"vue-router": "4.1.5"
"vue-router": "4.1.5",
"zod": "^3.24.4"
},
"devDependencies": {
"@opentiny-internal/unplugin-virtual-template": "workspace:~",

View File

@ -1,5 +1,6 @@
module.exports = {
plugins: {
'tailwindcss/nesting': 'postcss-nesting',
tailwindcss: {}
}
}

View File

@ -7,57 +7,120 @@
<iframe v-if="modalSHow" width="100%" height="100%" :src="previewUrl" frameborder="0"></iframe>
</tiny-modal>
</tiny-config-provider>
<div class="right-panel" :class="{ collapsed: !showTinyRobot }">
<tiny-robot-chat />
</div>
<IconAi v-show="!showTinyRobot" @click="handleShowTinyRobot" class="style-settings-icon"></IconAi>
<tiny-dialog-box
v-model:visible="boxVisibility"
:close-on-click-modal="false"
title="请填写您的LLM信息, 否则无法体验智能化能力"
width="30%"
>
<div>
<tiny-form ref="formRef" :model="createData" label-width="120px">
<tiny-form-item label="LLM URL" prop="llmUrl" :rules="{ required: true, messages: '必填', trigger: 'blur' }">
<tiny-input v-model="createData.llmUrl"></tiny-input>
</tiny-form-item>
<tiny-form-item
label="API Key"
prop="llmApiKey"
:rules="{ required: true, messages: '必填', trigger: 'blur' }"
>
<tiny-input v-model="createData.llmApiKey"></tiny-input>
</tiny-form-item>
<tiny-form-item>
<tiny-button @click="submit" type="primary">保存</tiny-button>
</tiny-form-item>
</tiny-form>
</div>
</tiny-dialog-box>
</div>
</template>
<script>
import { defineComponent, onMounted, provide, ref } from 'vue'
import { ConfigProvider, Modal } from '@opentiny/vue'
import { iconClose } from '@opentiny/vue-icon'
import { appData } from './tools'
<script setup lang="ts">
import { onMounted, provide, ref, reactive } from 'vue'
import {
TinyConfigProvider,
TinyModal,
TinyDialogBox,
TinyForm,
TinyFormItem,
TinyInput,
TinyButton
} from '@opentiny/vue'
import useTheme from './tools/useTheme'
import TinyRobotChat from './components/tiny-robot-chat.vue'
import { IconAi } from '@opentiny/tiny-robot-svgs'
import { showTinyRobot } from './composable/utils'
import { createServer, createInMemoryTransport } from '@opentiny/next-sdk'
import { createGlobalMcpTool } from './tools/globalMcpTool'
import { $local, isEnvLLMDefined, isLocalLLMDefined } from './composable/utils'
export default defineComponent({
name: 'AppVue',
props: [],
components: {
TinyConfigProvider: ConfigProvider,
TinyModal: Modal,
TinyIconClose: iconClose()
const boxVisibility = ref(false)
const formRef = ref()
const createData = reactive({
llmUrl: $local.llmUrl || import.meta.env.VITE_LLM_URL,
llmApiKey: $local.llmApiKey || import.meta.env.VITE_LLM_API_KEY
})
const submit = () => {
formRef.value.validate().then(() => {
$local.llmUrl = createData.llmUrl
$local.llmApiKey = createData.llmApiKey
boxVisibility.value = false
window.location.reload()
})
}
const previewUrl = ref(import.meta.env.VITE_PLAYGROUND_URL)
const modalSHow = ref(false)
const server = createServer(
{
name: 'comprehensive-config',
version: '1.0.0'
},
setup() {
const previewUrl = ref(import.meta.env.VITE_PLAYGROUND_URL)
const modalSHow = ref(false)
onMounted(() => {
// header
const common = new window.TDCommon(['#header'], {
allowDarkTheme: true,
searchConfig: {
show: true
},
menuCollapse: {
useCollapse: true, // 1024
menuId: '#layoutSider'
}
})
common.renderHeader()
})
const { designConfig, currentThemeKey } = useTheme()
provide('showPreview', (url) => {
previewUrl.value = url
modalSHow.value = true
})
return {
appData,
designConfig,
currentThemeKey,
previewUrl,
modalSHow
{
capabilities: {
logging: {},
resources: { subscribe: true, listChanged: true }
}
}
)
server.use(createInMemoryTransport())
createGlobalMcpTool(server)
onMounted(() => {
server.connectTransport()
// header
const common = new window.TDCommon(['#header'], {
allowDarkTheme: true,
searchConfig: {
show: true
},
menuCollapse: {
useCollapse: true, // 1024
menuId: '#layoutSider'
}
})
common.renderHeader()
})
const { designConfig, currentThemeKey } = useTheme()
provide('showPreview', (url) => {
previewUrl.value = url
modalSHow.value = true
})
const handleShowTinyRobot = () => {
if (!isEnvLLMDefined && !isLocalLLMDefined) {
boxVisibility.value = true
} else {
showTinyRobot.value = !showTinyRobot.value
}
}
</script>
<style scoped lang="less">
@ -73,4 +136,19 @@ export default defineComponent({
padding: 34px 0 0;
}
}
.right-panel:not(.collapsed) {
:deep(.tr-container) {
z-index: 9999;
}
}
.style-settings-icon {
position: fixed;
bottom: 100px;
right: 100px;
font-size: 24px;
z-index: 19999;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,12 @@
<template>
<div v-html="markdown"></div>
</template>
<script setup lang="ts">
import { BubbleMarkdownMessageRenderer } from '@opentiny/tiny-robot'
import { computed } from 'vue'
const props = defineProps<{ content: string }>()
const markdownRenderer = new BubbleMarkdownMessageRenderer()
const markdown = computed(() => markdownRenderer.md.render(props.content))
</script>

View File

@ -123,7 +123,7 @@ const showPreview = inject('showPreview')
const state = reactive({
tabValue: 'tab0',
cmpId: router.currentRoute.value.params.cmpId,
langKey: getWord('zh-CN', 'en-US'),
langKey: getWord('zh-CN', 'en-US', 'es-LA', 'pt-BR'),
copyTip: i18nByKey('copyCode'),
copyIcon: 'i-ti-copy'
})

View File

@ -43,9 +43,8 @@
<tiny-popover
width="180"
placement="left-end"
trigger="manual"
trigger="click"
:visible-arrow="false"
v-model="demoStyleVisible"
popper-class="opt-menu style-settings-menu theme-settings-popover"
>
<div v-for="(item, index) in styleSettings" :key="index" class="style-settings-item">
@ -60,11 +59,7 @@
</div>
<template #reference>
<div>
<div
class="settings-btn style-settings-btn"
@click="demoStyleVisible = !demoStyleVisible"
@blur="demoStyleVisible = false"
>
<div class="settings-btn style-settings-btn">
<style-settings-icon class="settings-icon style-settings-icon"></style-settings-icon>
</div>
</div>
@ -121,7 +116,6 @@ export default defineComponent({
const floatSettings = ref(null)
const state = reactive({
demoStyleVisible: false,
themeData: [],
styleSettings: getStyleSettings(i18nByKey),
settingsStyle: {

View File

@ -0,0 +1,187 @@
<template>
<!-- mcp-robot弹窗 -->
<tr-container v-model:show="showTinyRobot" v-model:fullscreen="fullscreen">
<tr-bubble-provider :message-renderers="messageRenderers">
<div class="robot-top-msg" v-if="showMessages.length === 0">
<tr-welcome title="智能助手" description="您好我是Opentiny AI智能助手" :icon="welcomeIcon">
<template #footer>
<div class="welcome-footer"></div>
</template>
</tr-welcome>
<tr-prompts
:items="promptItems"
:wrap="true"
item-class="prompt-item"
class="tiny-prompts"
@item-click="handlePromptItemClick"
></tr-prompts>
</div>
<tr-bubble-list v-else class="robot-top-msg markdown-body" :items="showMessages" :roles="roles" auto-scroll>
</tr-bubble-list>
</tr-bubble-provider>
<template #footer>
<div class="chat-input">
<TrSuggestionPills :items="suggestionPillItems" @item-click="handleSuggestionPillItemClick" /><br />
<tr-sender
ref="senderRef"
mode="single"
v-model="inputMessage"
:placeholder="GeneratingStatus.includes(messageState.status) ? '正在思考中...' : '请输入您的问题'"
:clearable="!!inputMessage"
:loading="GeneratingStatus.includes(messageState.status)"
:showWordLimit="true"
:maxLength="1000"
:template="currentTemplate"
@submit="handleSendMessage"
@cancel="abortRequest"
@keydown="handleMessageKeydown($event, onTrigger, onKeyDown)"
@reset-template="clearTemplate"
></tr-sender>
</div>
</template>
</tr-container>
</template>
<script setup lang="ts">
import {
TrBubbleList,
TrContainer,
TrPrompts,
TrSender,
TrWelcome,
TrSuggestionPills,
TrBubbleProvider,
BubbleMarkdownMessageRenderer,
BubbleChainMessageRenderer
} from '@opentiny/tiny-robot'
import { GeneratingStatus, STATUS } from '@opentiny/tiny-robot-kit'
import { useTinyRobot } from '../composable/useTinyRobot'
import { showTinyRobot } from '../composable/utils'
import ReactiveMarkdown from './ReactiveMarkdown.vue'
import { computed, nextTick, watch } from 'vue'
const mdRenderer = new BubbleMarkdownMessageRenderer()
const messageRenderers = {
markdown: ReactiveMarkdown,
chain: {
component: BubbleChainMessageRenderer,
defaultProps: {
contentRenderer: (content: string) => mdRenderer.md.render(content)
}
}
}
const {
fullscreen,
welcomeIcon,
promptItems,
messages,
messageState,
inputMessage,
abortRequest,
roles,
handlePromptItemClick,
senderRef,
currentTemplate,
clearTemplate,
handleSendMessage,
handleMessageKeydown,
suggestionPillItems,
handleSuggestionPillItemClick
} = useTinyRobot()
const showMessages = computed(() => {
if (messageState.status === STATUS.PROCESSING) {
return [
...messages.value,
{
role: 'assistant',
content: '正在思考中...',
loading: true
}
]
}
return messages.value
})
const scrollToBottom = () => {
const containerBody = document.querySelector('div.ai-console-content-wrap')
if (containerBody) {
nextTick(() => {
containerBody.scrollTo({
top: containerBody.scrollHeight,
behavior: 'smooth'
})
})
}
}
//
watch(() => messages.value[messages.value.length - 1]?.content, scrollToBottom)
</script>
<style scoped lang="less">
.chat-input {
margin-top: 8px;
padding: 10px 15px;
}
.tr-container {
container-type: inline-size;
:deep(.tr-welcome__title-wrapper) {
display: flex;
align-items: center;
justify-content: center;
}
}
.welcome-footer {
margin-top: 12px;
color: rgb(128, 128, 128);
font-size: 12px;
line-height: 20px;
}
.tiny-prompts {
padding: 16px 24px;
:deep(.prompt-item) {
width: 100%;
box-sizing: border-box;
@container (width >=64rem) {
width: calc(50% - 8px);
}
.tr-prompt__content-label {
font-size: 14px;
line-height: 24px;
}
}
}
.tr-history-demo {
position: absolute;
right: 100%;
top: 100%;
z-index: 100;
width: 300px;
height: 600px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
}
/** 聊天顶部要撑开空间 */
.robot-top-msg {
flex: 1;
}
</style>
<style>
.tr-chain-item__body .tr-chain-item__content {
word-break: break-all;
}
</style>

View File

@ -22,7 +22,7 @@
</template>
<script lang="ts">
import type { PropType } from 'vue'
import { type PropType } from '@opentiny/vue-common'
import { defineComponent, computed } from 'vue'
import { Tag as TinyTag, Alert as TinyAlert, Tooltip as TinyTooltip } from '@opentiny/vue'
import { getWord } from '@/tools'

View File

@ -0,0 +1,112 @@
import { createClient, createInMemoryTransport, createMCPHost } from '@opentiny/next-sdk'
import type { ChatCompletionResponse } from '@opentiny/tiny-robot-kit'
import type { ChatCompletionRequest } from '@opentiny/tiny-robot-kit'
import type { StreamHandler } from '@opentiny/tiny-robot-kit'
import { BaseModelProvider } from '@opentiny/tiny-robot-kit'
import type { AIModelConfig } from '@opentiny/tiny-robot-kit'
import { reactive } from 'vue'
import { $local, isEnvLLMDefined } from './utils'
// 创建nextClient
const nextClient = createClient(
{
name: 'next-sdk',
version: '1.0.0'
},
{
capabilities: {
roots: { listChanged: true },
sampling: { createMessage: true },
elicitation: { elicit: true }
}
}
)
nextClient.use(createInMemoryTransport())
nextClient.connectTransport()
let lastContent: any
let lastToolCall: any
const onToolCallChain = (extra: any, handler: StreamHandler) => {
lastContent = null
const { delta } = extra
const infoItem = reactive({
id: delta.toolCall.id,
title: delta.toolCall.function.name,
content: delta.toolCall.callToolContent
? '工具调用结果:' + delta.toolCall.callToolContent
: `\n正在调用工具${delta.toolCall.function.name}...`
})
if (!lastToolCall || lastToolCall.items?.[0]?.id !== infoItem.id) {
lastToolCall = {
type: 'chain',
items: [infoItem]
}
handler.onMessage(lastToolCall)
} else {
const find = lastToolCall.items.find((item: any) => item.id === infoItem.id)
if (find) {
find.content = infoItem.content
} else {
lastToolCall.items.push(infoItem)
}
}
}
const mcpHost = createMCPHost({
llmOption: {
url: isEnvLLMDefined ? import.meta.env.VITE_LLM_URL : $local.llmUrl || '',
apiKey: isEnvLLMDefined ? import.meta.env.VITE_LLM_API_KEY : $local.llmApiKey || '',
dangerouslyAllowBrowser: true,
model: 'deepseek-chat',
llm: 'deepseek'
},
mcpClients: [nextClient]
})
export class AgentModelProvider extends BaseModelProvider {
constructor(config: AIModelConfig) {
super(config)
}
/** 同步请示不需要实现 */
chat(request: ChatCompletionRequest): Promise<ChatCompletionResponse> {
throw new Error('Method not implemented.')
}
async chatStream(request: ChatCompletionRequest, handler: StreamHandler): Promise<void> {
// 验证请求的messages属性必须是数组且每个消息必须有role\content属性
const lastMessage = request.messages[request.messages.length - 1].content
lastToolCall = null
await mcpHost.chatStream(lastMessage, {
onData: (data: any) => {
if (data.delta.role === 'tool') {
onToolCallChain(data, handler)
} else {
if (!lastContent) {
lastContent = reactive({
type: 'markdown',
content: data.delta.content
})
handler.onMessage(lastContent)
} else {
lastContent.content += data.delta.content
}
}
},
onDone: () => {
lastContent = null
lastToolCall = null
handler.onDone()
},
onError: (error: any) => {
lastContent = null
lastToolCall = null
handler.onError(error)
}
})
}
}

View File

@ -0,0 +1,178 @@
import { AIClient, useConversation } from '@opentiny/tiny-robot-kit'
import { IconAi, IconUser } from '@opentiny/tiny-robot-svgs'
import { h, nextTick, onMounted, ref, watch } from 'vue'
import type { SuggestionItem } from '@opentiny/tiny-robot'
import { AgentModelProvider } from './agentModelProvider'
import { BubbleMarkdownMessageRenderer } from '@opentiny/tiny-robot'
const mdRenderer = new BubbleMarkdownMessageRenderer()
export const useTinyRobot = () => {
const client = new AIClient({
providerImplementation: new AgentModelProvider({ provider: 'custom' }),
provider: 'custom'
})
const fullscreen = ref(false)
const show = ref(true)
const aiAvatar = h(IconAi, { style: { fontSize: '32px' } })
const userAvatar = h(IconUser, { style: { fontSize: '32px' } })
const welcomeIcon = h(IconAi, { style: { fontSize: '48px' } })
const promptItems = [
{
label: '快速跳到文档',
description: '帮我切换到国际化指南',
icon: h('span', { style: { fontSize: '18px' } }, '🕹')
},
{
label: '快速跳到组件',
description: '帮我切换到 Select 组件',
icon: h('span', { style: { fontSize: '18px' } }, '🕹')
}
]
const handlePromptItemClick = (ev, item) => {
sendMessage(item.description)
}
const { messageManager } = useConversation({ client })
const { messageState, inputMessage, sendMessage, abortRequest, messages } = messageManager
const roles = {
assistant: {
type: 'markdown',
placement: 'start',
avatar: aiAvatar,
maxWidth: '80%',
contentRenderer: mdRenderer
},
user: {
placement: 'end',
avatar: userAvatar,
maxWidth: '80%',
contentRenderer: mdRenderer
}
}
// 建议按钮组,设置对话的模板
const suggestionPillItems = [
{
id: '1',
text: 'Select',
icon: h('span', { style: { fontSize: '18px' } }, '🧩')
},
{
id: '1',
text: '表格',
icon: h('span', { style: { fontSize: '18px' } }, '🧩')
},
{
id: '1',
text: '树组件',
icon: h('span', { style: { fontSize: '18px' } }, '🧩')
}
]
function handleSuggestionPillItemClick(item: SuggestionItem) {
if (item.id === '1') {
let templateText = `帮我跳转到 [目标组件]`
let currentInitialValue = { 目标组件: item.text, : '' }
if (senderRef.value) {
senderRef.value.setTemplate(templateText, currentInitialValue)
}
} else {
senderRef.value?.setTemplate(item.text, {})
}
}
const senderRef = ref(null)
const currentTemplate = ref('')
const suggestionOpen = ref(false)
// 清除当前指令
const clearTemplate = () => {
// 清空指令相关状态
currentTemplate.value = ''
// 确保重新聚焦到输入框
nextTick(() => {
senderRef.value?.focus()
})
}
// 发送消息
const handleSendMessage = () => {
sendMessage(inputMessage.value)
clearTemplate()
}
const handleMessageKeydown = (event, triggerFn, suggestionKeyDown) => {
// 如果指令面板已打开,交给 suggestion 组件处理键盘事件
if (suggestionOpen.value) {
suggestionKeyDown(event)
return
}
// 如果按下斜杠键并且不在指令编辑模式,触发指令面板
if (event.key === '/' && !currentTemplate.value) {
triggerFn({
text: '',
position: 0
})
}
// ESC 键清除当前指令
if (event.key === 'Escape' && currentTemplate.value) {
event.preventDefault()
clearTemplate()
}
}
watch(
() => inputMessage.value,
(value) => {
// 如果指令面板已打开,并且指令为空,关闭指令面板
if (suggestionOpen.value && value === '') {
suggestionOpen.value = false
}
}
)
// 页面加载完成后自动聚焦输入框
onMounted(() => {
setTimeout(() => {
senderRef.value?.focus()
}, 500)
})
return {
client,
fullscreen,
show,
aiAvatar,
userAvatar,
welcomeIcon,
promptItems,
messageManager,
messages,
messageState,
inputMessage,
sendMessage,
abortRequest,
roles,
handlePromptItemClick,
senderRef,
currentTemplate,
suggestionOpen,
clearTemplate,
handleSendMessage,
handleMessageKeydown,
suggestionPillItems,
handleSuggestionPillItemClick
}
}

View File

@ -3,10 +3,14 @@
*
*/
import { reactive } from 'vue'
import { ref } from 'vue'
export { $local, $session } from './storage'
import { $local, $session } from './storage'
export const globalConversation = reactive({
id: ''
})
export { $local, $session }
export const showTinyRobot = ref(false)
// 如果环境变量和本地变量都未定义,则提示用户填写
export const isEnvLLMDefined = Boolean(import.meta.env.VITE_LLM_API_KEY && import.meta.env.VITE_LLM_URL)
export const isLocalLLMDefined = Boolean($local.llmUrl && $local.llmApiKey)

View File

@ -5,10 +5,15 @@ export const LANG_KEY = '_lang'
// localStorage中保存语言的value
export const ZH_CN_LANG = 'zhCN'
export const EN_US_LANG = 'enUS'
export const ES_LA_LANG = 'esLA'
export const PT_BR_LANG = 'ptBR'
// 语言key值对应的路由
export const LANG_PATH_MAP = {
[ZH_CN_LANG]: 'zh-CN',
[EN_US_LANG]: 'en-US'
[EN_US_LANG]: 'en-US',
[ES_LA_LANG]: 'es-LA',
[PT_BR_LANG]: 'pt-BR'
}
export const CURRENT_THEME_KEY = 'tiny-current-theme'

View File

@ -1,6 +1,8 @@
{
"zh-cn": "Chinese",
"en-us": "English",
"es-la": "Spanish",
"pt-br": "Portuguese",
"localeType": "Language Selection",
"dark": "Dark",
"light": "Light",

View File

@ -0,0 +1,47 @@
{
"zh-cn": "Chinese",
"en-us": "English",
"es-la": "Spanish",
"pt-br": "Portuguese",
"localeType": "Language Selection",
"dark": "Dark",
"light": "Light",
"searchPlaceholder": "Search",
"home": "Home",
"doc": "Docs",
"component": "Components",
"common": "Common",
"apiPreference": "Framework",
"apiTiny": "Vue",
"yan-shi": "Demo",
"demos": "Demos",
"api": "API",
"name": "Name",
"propType": "Type",
"defValue": "Default",
"typeValue": "Option Value",
"desc": "Description",
"showCode": "Show Code",
"hideCode": "Hide Code",
"copyCode": "Copy Code",
"doc-owner": "Owner",
"copyCodeOk": "Copy Success",
"frameAngular": "Angular",
"playground": "Open Playground",
"changeLanguage": "Change Language",
"changeTheme": "Change Components Theme",
"changeApiType": "Change Api Type",
"backTop": "Back To Top",
"overview": "Components Overview",
"overviewDesc": "TinyVue provides a wealth of basic UI components for web applications, and we will continue to explore the best UI practices for enterprise-level applications. Welcome to try TinyVue.",
"overviewDescPlus": "TinyVuePlus is a component library for Cloud business scenarios based on TinyVue, following the new design specifications of Cloud CloudDesign and utilizing Vite+Vue3+TypeScript technology stack.",
"searchComponents": "search components",
"apiType": "Components demos code style",
"apiStyleOptions": "Options",
"apiStyleComposition": "Composition",
"demoMode": "Demo display mode",
"demoModeSingle": "Single",
"demoModeMultiple": "Multiple",
"contributor": "Contributors",
"noData": "No Data"
}

View File

@ -3,10 +3,21 @@ import { initI18n, t } from '@opentiny/vue-locale'
import { $local } from '../tools'
import zh from './zh.json'
import en from './en.json'
import esLA from './es.json'
import ptBR from './pt.json'
import { zhCN, enUS } from '@opentiny/tiny-vue-mcp'
const messages = { enUS: { ...en, ...enUS }, zhCN: { ...zh, ...zhCN }, esLA: { ...esLA }, ptBR: { ...ptBR } }
const langMap = new Map([
['zhCN', 'zhCN'],
['enUS', 'enUS'],
['esLA', 'esLA'],
['ptBR', 'ptBR']
])
$local._lang = langMap.get($local._lang) || 'zhCN'
const messages = { enUS: { ...en }, zhCN: { ...zh } }
// $local._lang = $local._lang !== 'zhCN' && $local._lang !== 'enUS' ? 'zhCN' : $local._lang
$local._lang = 'zhCN'
const customCreateI18n = ({ locale, messages }) =>
createI18n({
locale, // set locale
@ -14,7 +25,6 @@ const customCreateI18n = ({ locale, messages }) =>
fallbackLocale: 'zhCN', // set fallback locale
messages // set locale messages
})
const i18n = initI18n({
createI18n: customCreateI18n,
i18n: {
@ -23,7 +33,17 @@ const i18n = initI18n({
messages
})
const i18nByKey = i18n.global.t
const getWord = (cn, en) => (i18n.global.locale === 'zhCN' ? cn : en)
const getWord = (cn, en, es, pt) => {
const localeMap = new Map([
['zhCN', cn], // 简体中文
['enUS', en], // 英语
['esLA', es], // 西班牙语
['ptBR', pt] // 葡萄牙语
])
const currentLocale = i18n.global.locale
return localeMap.get(currentLocale) ?? cn
}
export { i18n, i18nByKey, getWord }

View File

@ -0,0 +1,47 @@
{
"zh-cn": "Chinese",
"en-us": "English",
"es-la": "Spanish",
"pt-br": "Portuguese",
"localeType": "Language Selection",
"dark": "Dark",
"light": "Light",
"searchPlaceholder": "Search",
"home": "Home",
"doc": "Docs",
"component": "Components",
"common": "Common",
"apiPreference": "Framework",
"apiTiny": "Vue",
"yan-shi": "Demo",
"demos": "Demos",
"api": "API",
"name": "Name",
"propType": "Type",
"defValue": "Default",
"typeValue": "Option Value",
"desc": "Description",
"showCode": "Show Code",
"hideCode": "Hide Code",
"copyCode": "Copy Code",
"doc-owner": "Owner",
"copyCodeOk": "Copy Success",
"frameAngular": "Angular",
"playground": "Open Playground",
"changeLanguage": "Change Language",
"changeTheme": "Change Components Theme",
"changeApiType": "Change Api Type",
"backTop": "Back To Top",
"overview": "Components Overview",
"overviewDesc": "TinyVue provides a wealth of basic UI components for web applications, and we will continue to explore the best UI practices for enterprise-level applications. Welcome to try TinyVue.",
"overviewDescPlus": "TinyVuePlus is a component library for Cloud business scenarios based on TinyVue, following the new design specifications of Cloud CloudDesign and utilizing Vite+Vue3+TypeScript technology stack.",
"searchComponents": "search components",
"apiType": "Components demos code style",
"apiStyleOptions": "Options",
"apiStyleComposition": "Composition",
"demoMode": "Demo display mode",
"demoModeSingle": "Single",
"demoModeMultiple": "Multiple",
"contributor": "Contributors",
"noData": "No Data"
}

View File

@ -1,6 +1,8 @@
{
"zh-cn": "中文",
"en-us": "英文",
"es-la": "西班牙语",
"pt-br": "葡萄牙语",
"localeType": "语言选择",
"dark": "深色",
"light": "浅色",

View File

@ -20,7 +20,7 @@ import { i18n } from './i18n/index'
import { router } from './router'
import App from './App.vue'
import { appData } from './tools'
import { ZH_CN_LANG, EN_US_LANG, LANG_PATH_MAP } from './const'
import { ZH_CN_LANG, EN_US_LANG, LANG_PATH_MAP, ES_LA_LANG, PT_BR_LANG } from './const'
import demoConfig from '@demos/config.js'
import hljs from 'highlight.js/lib/core'
@ -31,7 +31,14 @@ import tsPath from 'highlight.js/lib/languages/typescript'
import docsearch from '@docsearch/js'
import '@docsearch/css'
import { doSearchEverySite } from './tools/docsearch'
import { getLocaleMode } from './tools/utils.js'
import '@opentiny/vue-theme/dark-theme-index.css'
import { createMcpTools, getTinyVueMcpConfig } from '@opentiny/tiny-vue-mcp'
import { t } from '@opentiny/vue-locale'
import { registerMcpConfig } from '@opentiny/vue-common'
// tiny-robot 对话框
import '@opentiny/tiny-robot/dist/style.css'
const envTarget = import.meta.env.VITE_BUILD_TARGET || 'open'
@ -65,12 +72,19 @@ setTimeout(() => {
const zhPath = LANG_PATH_MAP[ZH_CN_LANG]
const enPath = LANG_PATH_MAP[EN_US_LANG]
const esPath = LANG_PATH_MAP[ES_LA_LANG]
const ptPath = LANG_PATH_MAP[PT_BR_LANG]
const isZhCn = location.href.includes(`/${zhPath}`)
const isEnUs = location.href.includes(`/${enPath}`)
const notMatchLang = (isZhCn && appData.lang !== ZH_CN_LANG) || (isEnUs && appData.lang !== EN_US_LANG)
const isEsLa = location.href.includes(`/${esPath}`)
const isPtBr = location.href.includes(`/${ptPath}`)
const notMatchLang =
(isZhCn && appData.lang !== ZH_CN_LANG) ||
(isEnUs && appData.lang !== EN_US_LANG) ||
(isEsLa && appData.lang !== ES_LA_LANG) ||
(isPtBr && appData.lang !== PT_BR_LANG)
if (notMatchLang) {
// appData.lang = isEnUs ? EN_US_LANG : ZH_CN_LANG 官网先屏蔽英文内容
appData.lang = isEnUs ? ZH_CN_LANG : ZH_CN_LANG
appData.lang = getLocaleMode()
i18n.global.locale = appData.lang
}
@ -82,6 +96,8 @@ app.config.globalProperties.tiny_theme = { value: import.meta.env.VITE_TINY_THEM
if (import.meta.env.VITE_TINY_THEME === 'saas') {
import('./tailwind.css')
}
// 注册TinyVue组件mcp配置
registerMcpConfig(getTinyVueMcpConfig({ t }), createMcpTools)
app.use(router).use(i18n).use(createHead()) // 支持md修改title

View File

@ -50,7 +50,6 @@ function genMenus() {
type: 'components'
}))
}))
return [...standaloneOptions, ...docOptions, ...cmpOptions]
}

View File

@ -9,11 +9,10 @@ const Overview = () => import('@/views/overview.vue')
const Features = () => import('@/views/features.vue')
const context = import.meta.env.VITE_CONTEXT
let routes = [
// 组件总览
{
path: `${context}:all?/zh-CN/:theme/overview`,
path: `${context}:all?/${LANG_PATH_MAP[appData.lang] || 'zh-CN'}/:theme/overview`,
component: Layout,
name: 'overview',
children: [{ name: 'Overview', path: '', component: Overview, meta: { title: '组件总览 | TinyVue' } }]
@ -27,7 +26,7 @@ let routes = [
},
// 组件
{
path: `${context}:all?/zh-CN/:theme/components/:cmpId`,
path: `${context}:all?/${LANG_PATH_MAP[appData.lang] || 'zh-CN'}/:theme/components/:cmpId`,
component: Layout,
name: 'components',
children: [{ name: 'Components', path: '', component: Components }]
@ -43,7 +42,7 @@ let routes = [
{
path: '/:pathMatch(.*)*',
redirect: () => {
const langPath = LANG_PATH_MAP[ZH_CN_LANG]
const langPath = LANG_PATH_MAP[appData.lang] || LANG_PATH_MAP[ZH_CN_LANG]
return { path: `${context}${langPath}/${DEFAULT_THEME}/overview` }
}
}
@ -58,7 +57,5 @@ router.afterEach((to, from) => {
if (to.meta.title) {
document.title = to.meta.title
}
// tiny-robot 通过路由,确定浮动区是否显示AI按钮
appData.hasFloatRobot = to.path.endsWith('components/grid')
})
export { router }

View File

@ -1,10 +1,8 @@
import { reactive, computed } from 'vue'
import { useAutoStore } from './storage'
import { useMediaQuery } from './useMediaQuery'
import { ZH_CN_LANG, EN_US_LANG, LANG_KEY, LANG_PATH_MAP } from '../const'
import { ZH_CN_LANG, LANG_KEY, LANG_PATH_MAP } from '../const'
const zhPath = LANG_PATH_MAP[ZH_CN_LANG]
const enPath = LANG_PATH_MAP[EN_US_LANG]
const appData = reactive({
lang: useAutoStore('local', LANG_KEY, ZH_CN_LANG),
theme: useAutoStore('local', '_theme', 'light'),
@ -16,9 +14,10 @@ const appFn = {
if (name !== appData.lang) {
let url = location.href
url = location.href.replace(LANG_PATH_MAP[appData.lang], LANG_PATH_MAP[name])
// appData.lang = name 官网先屏蔽英文内容
// appData.lang = name // 官网先屏蔽切换语言,默认中文
appData.lang = ZH_CN_LANG
location.replace(url)
history.replaceState({}, '', url)
location.reload()
}
},
toggleTheme() {

View File

@ -0,0 +1,122 @@
import { genMenus } from '../menus'
import { z } from 'zod'
import { useRouter } from 'vue-router'
// 组件页面的右上导航的数据回调函数
export const cmpAnchorDataCallback = { value: null }
export const createGlobalMcpTool = (server) => {
const router = useRouter()
server.registerResource(
'site-menus',
'site-menus://app',
{
title: 'TinyVue官网的菜单数据',
description: 'TinyVue官网的菜单数据其中"key"为路由路径,"name"为菜单名称,"children"为子菜单',
mimeType: 'text/plain'
},
async (uri) => ({
contents: [
{
uri: uri.href,
text: JSON.stringify(genMenus())
}
]
})
)
// 帮我查看button组件的API
server.registerTool(
'swtich-router',
{
title: 'router',
description: '可以帮用户跳转到文档页面组件示例的总页面或组件API文档页面或组件库的概览页面',
inputSchema: {
key: z.string().describe('跳转页面路径'),
type: z
.enum(['components', 'docs', 'overview', 'features'])
.describe('跳转页面类型,比如:组件的页面,文档的页面,组件的概览页面'),
isOpenApi: z.boolean().describe('跳转到组件页面时是否打开API文档')
}
},
async ({ key, type, isOpenApi }) => {
const { params, fullPath } = router.currentRoute.value
const { theme } = params
const themeIndex = fullPath.indexOf(theme)
const linkUrl =
fullPath.slice(0, themeIndex) + `${theme}/${type}/${key === 'overview' ? '' : key}${isOpenApi ? '#api' : ''}`
router.push(linkUrl)
return {
content: [
{
type: 'text',
text: `跳转页面成功: ${key}`
}
]
}
}
)
server.registerTool(
'get-component-demos',
{
title: '查询全部示例的信息',
description:
'查询当前组件的全部示例信息demos信息。返回值是一个数组其中每一项的 demoId 属性是示例的键通过键可以跳转到该示例。desc属性是示例的详细描述。',
inputSchema: {}
},
async () => {
// 通知组件页面返回右侧导航的数据
if (cmpAnchorDataCallback.value != null) {
const links = cmpAnchorDataCallback.value()
return {
content: [{ type: 'text', text: JSON.stringify(links) }]
}
} else {
return {
content: [{ type: 'text', text: '找不到示例' }]
}
}
}
)
server.registerTool(
'jump-to-demo',
{
title: '跳转到组件的示例demo',
description: '根据参数demoId, 跳转到指定的示例demo。',
inputSchema: {
demoId: z.string().describe('示例的id,唯一标识。')
}
},
async ({ demoId }) => {
// 通知组件页面返回右侧导航的数据
location.hash = '#' + demoId
return {
content: [{ type: 'text', text: '跳转示例成功' }]
}
}
)
// 长任务示例
server.registerTool(
'long-task',
{
title: 'long-task',
description: '可以帮用户订机票'
},
async () => {
// 执行一个长任务
await new Promise((resolve) => setTimeout(resolve, 10000))
return {
content: [
{
type: 'text',
text: '执行一个长任务,执行完成'
}
]
}
}
)
}

View File

@ -1,4 +1,5 @@
import { ref, watch } from 'vue'
import { getLocaleMode } from './utils'
function parse(str) {
if (str === null) return undefined
@ -58,13 +59,14 @@ const typeMatcher = { session: $session, local: $local, api: null }
* @returns 响应式ref
*/
const useAutoStore = (type, key, defaultValue) => {
let refVar = ref(typeMatcher[type][key])
let refVar = ref(getLocaleMode())
typeMatcher[type][key] = refVar.value
watch(refVar, (curr, prev) => {
typeMatcher[type][key] = curr
})
refVar.value = refVar.value ?? defaultValue
return refVar
}

View File

@ -1,11 +1,12 @@
import { reactive } from 'vue'
import { $local } from './storage'
import { appFn } from './appData'
import { appFn, appData } from './appData'
const _modeKey = 'tiny-vue-api-mode'
const _demoModeKey = 'tiny-vue-demo-mode'
const apiModeState = reactive({
localeMode: location.href.includes('en-US') ? 'enUS' : 'zhCN',
localeMode: appData.lang,
apiMode: $local[_modeKey] || 'Composition', // 示例风格: Options: 组合式; Composition: 选项式
demoMode: $local[_demoModeKey] || 'default' // 示例展示: default:多示例, single:单示例
})

View File

@ -22,15 +22,16 @@ export function useBulletin() {
onMounted(() => {
Modal.confirm({
title: '公告',
message: (<div style="font-size:16px;line-height:1.5;">
<div>尊敬的 TinyVue 用户</div>
<p style="text-indent: 2em;" v-html={lastBulletin.content}>
</p>
<div style="text-align:right;margin-top:20px">TinyVue 团队</div>
<div style="text-align:right;">{ lastBulletin.time }</div>
</div>),
message: (
<div style="font-size:16px;line-height:1.5;">
<div>尊敬的 TinyVue 用户</div>
<p style="text-indent: 2em;" v-html={lastBulletin.content}></p>
<div style="text-align:right;margin-top:20px">TinyVue 团队</div>
<div style="text-align:right;">{lastBulletin.time}</div>
</div>
),
status: null,
width:'760',
width: '760',
confirmContent: '我知道了',
cancelContent: '关闭'
}).then((res) => {

View File

@ -15,6 +15,14 @@ const getStyleSettings = (i18nByKey) => {
// {
// value: 'enUS',
// text: i18nByKey('en-us')
// },
// {
// value: 'esLA',
// text: i18nByKey('es-la')
// },
// {
// value: 'ptBR',
// text: i18nByKey('pt-br')
// }
// ]
// },

View File

@ -1,6 +1,6 @@
import { reactive, computed, watch } from 'vue'
import { router } from '@/router.js'
import { getAllComponents } from '@/menus.jsx'
import { getAllComponents } from '@/menus'
import demoConfig from '@demos/config.js'
import { staticDemoPath } from '../views/components-doc/cmp-config'

View File

@ -1,5 +1,6 @@
import Contributors from '@/data/contributors'
import ContributorMap from '@/data/contributorMap'
import { ZH_CN_LANG, EN_US_LANG, ES_LA_LANG, PT_BR_LANG, LANG_PATH_MAP } from '../const'
const baseUrl = import.meta.env.BASE_URL
@ -103,4 +104,34 @@ const getCmpContributors = (cmpId) => {
return contributorInfo
}
export { $clone, $split, $delay, $idle, pubUrl, fetchDemosFile, getCmpContributors }
const getLocaleMode = () => {
const { href, pathname } = location
const DEFAULT_LANG = ZH_CN_LANG // 默认语言
const langCheckMap = new Map([
[
EN_US_LANG,
() => href.includes(`/${LANG_PATH_MAP[EN_US_LANG]}`) || pathname.includes(`/${LANG_PATH_MAP[EN_US_LANG]}/`)
],
[
ZH_CN_LANG,
() => href.includes(`/${LANG_PATH_MAP[ZH_CN_LANG]}`) || pathname.includes(`/${LANG_PATH_MAP[ZH_CN_LANG]}/`)
],
[
ES_LA_LANG,
() => href.includes(`/${LANG_PATH_MAP[ES_LA_LANG]}`) || pathname.includes(`/${LANG_PATH_MAP[ES_LA_LANG]}/`)
],
[
PT_BR_LANG,
() => href.includes(`/${LANG_PATH_MAP[PT_BR_LANG]}`) || pathname.includes(`/${LANG_PATH_MAP[PT_BR_LANG]}/`)
]
])
for (const [lang, checkFn] of langCheckMap) {
if (checkFn()) return lang
}
return DEFAULT_LANG // 无匹配时返回默认
}
export { $clone, $split, $delay, $idle, pubUrl, fetchDemosFile, getCmpContributors, getLocaleMode }

View File

@ -91,7 +91,7 @@
</template>
<script setup lang="ts">
import { reactive, computed, watch, onMounted, nextTick, ref } from 'vue'
import { reactive, computed, watch, onMounted, nextTick, ref, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { TinyTabs, TinyTabItem } from '@opentiny/vue'
import { debounce } from '@opentiny/utils'
@ -106,6 +106,7 @@ import ApiDocs from '../../components/api-docs.vue'
import McpDocs from '../../components/mcp-docs.vue'
import useTasksFinish from '../../composable/useTasksFinish'
import { appData } from '../../tools/appData'
import { cmpAnchorDataCallback } from '../../tools/globalMcpTool'
const props = defineProps({ loadData: {}, appMode: {}, demoKey: {} })
@ -121,7 +122,7 @@ const isRunningTest = localStorage.getItem('tiny-e2e-test') === 'true'
const anchorRefreshKey = ref(0)
const route = useRoute()
const state = reactive({
langKey: getWord('zh-CN', 'en-US'),
langKey: getWord('zh-CN', 'en-US', 'es-LA', 'pt-BR'),
cmpId: '',
observer: null,
currJson: { column: 1, demos: [], apis: [], types: {} },
@ -282,7 +283,7 @@ const demoMounted = () => {
}
const loadPage = () => {
const lang = getWord('cn', 'en')
const lang = getWord('cn', 'en', 'es', 'pt')
state.cmpId = router.currentRoute.value.params.cmpId
state.chartCode = getWebdocPath(state.cmpId) === 'chart'
@ -442,6 +443,11 @@ const handleAnchorClick = (e, data) => {
}
}
cmpAnchorDataCallback.value = () => state.currJson.demos
onUnmounted(() => {
cmpAnchorDataCallback.value = null
})
defineExpose({ loadPage })
</script>

View File

@ -59,7 +59,7 @@
import { useRoute } from 'vue-router'
import { defineComponent, reactive, computed, toRefs, watch, onMounted, onUnmounted } from 'vue'
import { TreeMenu, Dropdown, DropdownMenu, Tooltip, Tag, Radio, RadioGroup, Button } from '@opentiny/vue'
import { genMenus, getMenuIcons } from '@/menus.jsx'
import { genMenus, getMenuIcons } from '@/menus'
import { router } from '@/router.js'
import { getWord, i18nByKey, appData, appFn, useApiMode, useTemplateMode } from '@/tools'
import useTheme from '@/tools/useTheme'
@ -96,7 +96,7 @@ export default defineComponent({
expandKeys: []
})
const lang = getWord('zh-CN', 'en-US')
const lang = getWord('zh-CN', 'en-US', 'es-LA', 'pt-BR')
const route = useRoute()
const { all: allPathParam, theme = defaultTheme } = useRoute().params
const allPath = allPathParam ? allPathParam + '/' : ''

View File

@ -121,7 +121,7 @@ export default defineComponent({
.filter((item) => item.children.length > 0)
state.searchMenus = searchMenus
}
const lang = getWord('zh-CN', 'en-US')
const lang = getWord('zh-CN', 'en-US', 'es-LA', 'pt-BR')
const { defaultTheme } = useTheme()
const { all: allPathParam, theme = defaultTheme } = useRoute().params
const allPath = allPathParam ? allPathParam + '/' : ''

View File

@ -6,7 +6,5 @@
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts"
]
}
"include": ["vite.config.ts"]
}

View File

@ -14,7 +14,8 @@
"test:e2e": "playwright test",
"test:unit": "vitest",
"install:browser": "playwright install",
"codegen": "playwright codegen localhost:3101"
"codegen": "playwright codegen localhost:3101",
"open:report": "playwright show-report"
},
"devDependencies": {
"@opentiny-internal/playwright-config": "workspace:^1.0.1-beta.0",

View File

@ -1,5 +1,6 @@
module.exports = {
plugins: {
'tailwindcss/nesting': 'postcss-nesting',
tailwindcss: {},
autoprefixer: {}
}

View File

@ -0,0 +1,8 @@
global.ResizeObserver = class ResizeObserver {
constructor(callback) {
this.callback = callback
}
observe() {}
unobserve() {}
disconnect() {}
}

Some files were not shown because too many files have changed in this diff Show More