Compare commits

..

No commits in common. "dev" and "dev" have entirely different histories.
dev ... dev

184 changed files with 548 additions and 2749 deletions

View File

@ -708,15 +708,6 @@
"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,7 +181,6 @@ 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,10 +1,6 @@
import { cmpMenus } from '../../sites/demos/mobile-first/menus.js'
export const demoStr = import.meta.glob('../../sites/demos/mobile-first/app/**/*.vue', {
eager: false,
query: '?raw',
import: 'default'
})
export const demoStr = import.meta.glob('../../sites/demos/mobile-first/app/**/*.vue', { eager: false, as: 'raw' })
export const demoVue = import.meta.glob('../../sites/demos/mobile-first/app/**/*.vue', { eager: false })
// demos配置

View File

@ -5,11 +5,7 @@
// 同web-doc的菜单资源
import { cmpMenus } from '../../sites/demos/pc/menus.js'
export const demoStr = import.meta.glob('../../sites/demos/pc/app/**/*.vue', {
eager: false,
query: '?raw',
import: 'default'
})
export const demoStr = import.meta.glob('../../sites/demos/pc/app/**/*.vue', { eager: false, as: 'raw' })
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',
extends: 'stylelint-config-standard', //stylelint-config-airbnb
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
'indentation': 4 // http://cui.ulanqab.huawei.com/#/articalDetail?id=b76da810d8ed8
}
}

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-close'
pcDemo: 'message-closable'
},
{
name: 'min-height',

View File

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

View File

@ -86,17 +86,6 @@ 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.26.0'
stable: '3.24.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 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 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'
},
mode: ['mobile-first']
},

View File

@ -199,17 +199,6 @@ 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',
@ -260,15 +249,6 @@ 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,12 +1,5 @@
<template>
<tiny-steps
ref="steps"
advanced
:item-style="{ disabled: { background: 'yellow', maxWidth: '360px' } }"
:data="data"
:active="advancedActive"
@click="advancedClick"
></tiny-steps>
<tiny-steps ref="steps" advanced :data="data" :active="advancedActive" @click="advancedClick"></tiny-steps>
</template>
<script>

View File

@ -1,5 +1,5 @@
<template>
<tiny-color-picker @confirm="onConfirm" v-model="color" @cancel="onCancel" alpha />
<tiny-color-picker @confirm="onConfirm" @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('#66CCFFFF').click()
await page.getByText('#804040FF').click()
})

View File

@ -1,5 +1,5 @@
<template>
<tiny-color-picker @confirm="onConfirm" v-model="color" @cancel="onCacnel" alpha />
<tiny-color-picker @confirm="onConfirm" @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 v-model="color" @confirm="onConfirm" @cancel="onCancel" />
<tiny-color-picker @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('.tiny-color-picker__inner').click()
await page.locator('.black').click()
await page.getByRole('button', { name: '确定' }).click()
await page.getByText('用户点击了确定').click()
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()
})

View File

@ -1,5 +1,5 @@
<template>
<tiny-color-picker v-model="color" @confirm="onConfirm" @cancel="onCacnel" />
<tiny-color-picker @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.locator('.tiny-input__suffix-inner svg').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.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.locator('.tiny-input__suffix-inner svg').click()
await page.getByRole('textbox', { name: '请选择' }).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.locator('.tiny-input__suffix-inner svg').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.getByLabel('示例', { exact: true }).getByRole('paragraph')).toContainText('颜色值hsv')
})

View File

@ -5,5 +5,11 @@ 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 page.getByRole('button', { name: '确定' }).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()
})

View File

@ -5,16 +5,23 @@ 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.getByRole('button', { name: '确定' }).click()
await page.getByText('取消选择预定义颜色Append predefine').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.getByRole('button', { name: '选择' }).click()
await page.locator('.tiny-color-picker__inner').click()
await page.getByText('取消选择预定义颜色Append predefine').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', '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')
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')
})

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,6 +20,12 @@ 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()
})
@ -30,6 +36,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,6 +5,8 @@ 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,12 +6,15 @@ 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

@ -1,40 +0,0 @@
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

@ -1,53 +0,0 @@
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

@ -1,25 +0,0 @@
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

@ -1,20 +0,0 @@
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,6 +4,7 @@ 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,8 +3,7 @@ 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')
const demo = page.locator('#dynamically-columns-dynamically-columns')
await demo.locator('.tiny-picker.tiny-date-container').click()
await page.getByRole('textbox').nth(1).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.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('textbox').nth(1).click()
await page.getByRole('textbox').nth(1).press('CapsLock')
await page.getByRole('textbox').nth(1).fill('WWW')
await page.getByRole('button', { name: 'confirm' }).click()
await expect(page.getByRole('cell', { name: 'WWW 科技 YX 公司' })).toBeVisible()
})

View File

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

View File

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

View File

@ -11,15 +11,15 @@ import { TinyLocales } from '@opentiny/vue'
function getLocale() {
// resolve key
return Promise.resolve(['zhCN', 'enUS', 'zhTW'])
return Promise.resolve(['zh_CN', 'en_US', 'zh_TW'])
}
function getCurrentLocale() {
return Promise.resolve(['zhCN'])
return Promise.resolve(['zh_CN'])
}
function getChangeLocaleUrl(targetLocale) {
if (targetLocale === 'enUS') {
if (targetLocale === 'en_US') {
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(['zhCN', 'enUS', 'zhTW'])
return Promise.resolve(['zh_CN', 'en_US', 'zh_TW'])
},
getCurrentLocale() {
return Promise.resolve(['zhCN'])
return Promise.resolve(['zh_CN'])
},
getChangeLocaleUrl(targetLocale) {
if (targetLocale === 'enUS') {
if (targetLocale === 'en_US') {
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

@ -1,26 +0,0 @@
<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

@ -1,15 +0,0 @@
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

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

View File

@ -7,12 +7,7 @@ test('对齐方式', async ({ page }) => {
const demo = page.locator('#align')
const pager = demo.locator('.tiny-pager')
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')
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')
})

View File

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

View File

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

View File

@ -27,6 +27,7 @@
: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,6 +27,7 @@
: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,5 +1,11 @@
<template>
<tiny-pager :popper-append-to-body="false" layout="sizes,prev, pager, next" :total="50"></tiny-pager>
<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>
</template>
<script setup>

View File

@ -1,5 +1,11 @@
<template>
<tiny-pager :popper-append-to-body="false" layout="sizes,prev, pager, next" :total="50"></tiny-pager>
<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>
</template>
<script>

View File

@ -1,5 +1,11 @@
<template>
<tiny-pager popper-class="custom-pager" layout="sizes,prev, pager, next" :total="50"></tiny-pager>
<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>
</template>
<script setup>

View File

@ -1,5 +1,11 @@
<template>
<tiny-pager popper-class="custom-pager" layout="sizes,prev, pager, next" :total="50"></tiny-pager>
<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>
</template>
<script>

View File

@ -1,30 +1,16 @@
<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 { reactive } from 'vue'
import { TinyNumeric, TinyQrCode } from '@opentiny/vue'
import { TinyQrCode } from '@opentiny/vue'
const params = reactive({
const params = {
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>
</script>

View File

@ -8,23 +8,4 @@ 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,46 +1,25 @@
<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 { TinyNumeric, TinyQrCode } from '@opentiny/vue'
import { TinyQrCode } from '@opentiny/vue'
export default {
components: {
TinyNumeric,
TinyQrCode
},
data() {
return {
size: 250,
iconSize: 60
}
},
computed: {
params() {
return {
params: {
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: this.iconSize,
size: this.size
iconSize: 60,
size: 250
}
}
}
}
</script>
<style scoped>
.qr-code-attr {
display: flex;
align-items: center;
gap: 20px;
}
</style>

View File

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

View File

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

View File

@ -1,81 +0,0 @@
<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

@ -1,13 +0,0 @@
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

@ -1,95 +0,0 @@
<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,18 +186,6 @@ 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,6 +6,3 @@ 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,10 +27,6 @@
"@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:~",
@ -66,8 +62,7 @@
"tailwindcss": "^3.2.4",
"vue": "^3.4.31",
"vue-i18n": "~9.14.3",
"vue-router": "4.1.5",
"zod": "^3.24.4"
"vue-router": "4.1.5"
},
"devDependencies": {
"@opentiny-internal/unplugin-virtual-template": "workspace:~",

View File

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

View File

@ -7,120 +7,57 @@
<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 setup lang="ts">
import { onMounted, provide, ref, reactive } from 'vue'
import {
TinyConfigProvider,
TinyModal,
TinyDialogBox,
TinyForm,
TinyFormItem,
TinyInput,
TinyButton
} from '@opentiny/vue'
<script>
import { defineComponent, onMounted, provide, ref } from 'vue'
import { ConfigProvider, Modal } from '@opentiny/vue'
import { iconClose } from '@opentiny/vue-icon'
import { appData } from './tools'
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'
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'
export default defineComponent({
name: 'AppVue',
props: [],
components: {
TinyConfigProvider: ConfigProvider,
TinyModal: Modal,
TinyIconClose: iconClose()
},
{
capabilities: {
logging: {},
resources: { subscribe: true, listChanged: true }
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
}
}
)
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">
@ -136,19 +73,4 @@ const handleShowTinyRobot = () => {
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

@ -1,12 +0,0 @@
<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', 'es-LA', 'pt-BR'),
langKey: getWord('zh-CN', 'en-US'),
copyTip: i18nByKey('copyCode'),
copyIcon: 'i-ti-copy'
})

View File

@ -43,8 +43,9 @@
<tiny-popover
width="180"
placement="left-end"
trigger="click"
trigger="manual"
: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">
@ -59,7 +60,11 @@
</div>
<template #reference>
<div>
<div class="settings-btn style-settings-btn">
<div
class="settings-btn style-settings-btn"
@click="demoStyleVisible = !demoStyleVisible"
@blur="demoStyleVisible = false"
>
<style-settings-icon class="settings-icon style-settings-icon"></style-settings-icon>
</div>
</div>
@ -116,6 +121,7 @@ export default defineComponent({
const floatSettings = ref(null)
const state = reactive({
demoStyleVisible: false,
themeData: [],
styleSettings: getStyleSettings(i18nByKey),
settingsStyle: {

View File

@ -1,187 +0,0 @@
<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 '@opentiny/vue-common'
import type { PropType } from 'vue'
import { defineComponent, computed } from 'vue'
import { Tag as TinyTag, Alert as TinyAlert, Tooltip as TinyTooltip } from '@opentiny/vue'
import { getWord } from '@/tools'

View File

@ -1,112 +0,0 @@
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

@ -1,178 +0,0 @@
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,14 +3,10 @@
*
*/
import { ref } from 'vue'
import { reactive } from 'vue'
import { $local, $session } from './storage'
export { $local, $session } from './storage'
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)
export const globalConversation = reactive({
id: ''
})

View File

@ -5,15 +5,10 @@ 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',
[ES_LA_LANG]: 'es-LA',
[PT_BR_LANG]: 'pt-BR'
[EN_US_LANG]: 'en-US'
}
export const CURRENT_THEME_KEY = 'tiny-current-theme'

View File

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

View File

@ -1,47 +0,0 @@
{
"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,21 +3,10 @@ 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
@ -25,6 +14,7 @@ const customCreateI18n = ({ locale, messages }) =>
fallbackLocale: 'zhCN', // set fallback locale
messages // set locale messages
})
const i18n = initI18n({
createI18n: customCreateI18n,
i18n: {
@ -33,17 +23,7 @@ const i18n = initI18n({
messages
})
const i18nByKey = i18n.global.t
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
}
const getWord = (cn, en) => (i18n.global.locale === 'zhCN' ? cn : en)
export { i18n, i18nByKey, getWord }

View File

@ -1,47 +0,0 @@
{
"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,8 +1,6 @@
{
"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, ES_LA_LANG, PT_BR_LANG } from './const'
import { ZH_CN_LANG, EN_US_LANG, LANG_PATH_MAP } from './const'
import demoConfig from '@demos/config.js'
import hljs from 'highlight.js/lib/core'
@ -31,14 +31,7 @@ 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'
@ -72,19 +65,12 @@ 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 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)
const notMatchLang = (isZhCn && appData.lang !== ZH_CN_LANG) || (isEnUs && appData.lang !== EN_US_LANG)
if (notMatchLang) {
appData.lang = getLocaleMode()
// appData.lang = isEnUs ? EN_US_LANG : ZH_CN_LANG 官网先屏蔽英文内容
appData.lang = isEnUs ? ZH_CN_LANG : ZH_CN_LANG
i18n.global.locale = appData.lang
}
@ -96,8 +82,6 @@ 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,6 +50,7 @@ function genMenus() {
type: 'components'
}))
}))
return [...standaloneOptions, ...docOptions, ...cmpOptions]
}

View File

@ -9,10 +9,11 @@ const Overview = () => import('@/views/overview.vue')
const Features = () => import('@/views/features.vue')
const context = import.meta.env.VITE_CONTEXT
let routes = [
// 组件总览
{
path: `${context}:all?/${LANG_PATH_MAP[appData.lang] || 'zh-CN'}/:theme/overview`,
path: `${context}:all?/zh-CN/:theme/overview`,
component: Layout,
name: 'overview',
children: [{ name: 'Overview', path: '', component: Overview, meta: { title: '组件总览 | TinyVue' } }]
@ -26,7 +27,7 @@ let routes = [
},
// 组件
{
path: `${context}:all?/${LANG_PATH_MAP[appData.lang] || 'zh-CN'}/:theme/components/:cmpId`,
path: `${context}:all?/zh-CN/:theme/components/:cmpId`,
component: Layout,
name: 'components',
children: [{ name: 'Components', path: '', component: Components }]
@ -42,7 +43,7 @@ let routes = [
{
path: '/:pathMatch(.*)*',
redirect: () => {
const langPath = LANG_PATH_MAP[appData.lang] || LANG_PATH_MAP[ZH_CN_LANG]
const langPath = LANG_PATH_MAP[ZH_CN_LANG]
return { path: `${context}${langPath}/${DEFAULT_THEME}/overview` }
}
}
@ -57,5 +58,7 @@ 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,8 +1,10 @@
import { reactive, computed } from 'vue'
import { useAutoStore } from './storage'
import { useMediaQuery } from './useMediaQuery'
import { ZH_CN_LANG, LANG_KEY, LANG_PATH_MAP } from '../const'
import { ZH_CN_LANG, EN_US_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'),
@ -14,10 +16,9 @@ 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
history.replaceState({}, '', url)
location.reload()
location.replace(url)
}
},
toggleTheme() {

View File

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

View File

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

View File

@ -22,16 +22,15 @@ 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,14 +15,6 @@ 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'
import { getAllComponents } from '@/menus.jsx'
import demoConfig from '@demos/config.js'
import { staticDemoPath } from '../views/components-doc/cmp-config'

View File

@ -1,6 +1,5 @@
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
@ -104,34 +103,4 @@ const getCmpContributors = (cmpId) => {
return contributorInfo
}
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 }
export { $clone, $split, $delay, $idle, pubUrl, fetchDemosFile, getCmpContributors }

View File

@ -91,7 +91,7 @@
</template>
<script setup lang="ts">
import { reactive, computed, watch, onMounted, nextTick, ref, onUnmounted } from 'vue'
import { reactive, computed, watch, onMounted, nextTick, ref } from 'vue'
import { useRoute } from 'vue-router'
import { TinyTabs, TinyTabItem } from '@opentiny/vue'
import { debounce } from '@opentiny/utils'
@ -106,7 +106,6 @@ 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: {} })
@ -122,7 +121,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', 'es-LA', 'pt-BR'),
langKey: getWord('zh-CN', 'en-US'),
cmpId: '',
observer: null,
currJson: { column: 1, demos: [], apis: [], types: {} },
@ -283,7 +282,7 @@ const demoMounted = () => {
}
const loadPage = () => {
const lang = getWord('cn', 'en', 'es', 'pt')
const lang = getWord('cn', 'en')
state.cmpId = router.currentRoute.value.params.cmpId
state.chartCode = getWebdocPath(state.cmpId) === 'chart'
@ -443,11 +442,6 @@ 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'
import { genMenus, getMenuIcons } from '@/menus.jsx'
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', 'es-LA', 'pt-BR')
const lang = getWord('zh-CN', 'en-US')
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', 'es-LA', 'pt-BR')
const lang = getWord('zh-CN', 'en-US')
const { defaultTheme } = useTheme()
const { all: allPathParam, theme = defaultTheme } = useRoute().params
const allPath = allPathParam ? allPathParam + '/' : ''

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
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