packages/ui/docs/COMPONENT_API.md
本文档记录了经过 Naive UI 重构后的所有核心组件,包括新增的可访问性功能、性能优化和响应式支持。所有组件均符合 WCAG 2.1 AA/AAA 标准,提供完整的键盘导航和屏幕阅读器支持。
描述: 完全重构的上下文编辑器,提供消息管理、变量处理和工具配置功能。
文件位置: packages/ui/src/components/ContextEditor.vue
interface ContextEditorProps {
/** 模态框可见性 */
visible: boolean
/** 上下文状态数据 */
state: ContextState
/** 只读模式 */
readonly?: boolean
/** 自定义样式类名 */
customClass?: string
/** 尺寸大小 */
size?: 'small' | 'medium' | 'large'
/** 全局可用变量(用于变量解析和预览) */
availableVariables?: Record<string, string>
}
interface ContextState {
/** 消息列表 */
messages: ConversationMessage[]
/** 变量映射 */
variables: Record<string, string>
/** 工具配置 */
tools: ToolConfig[]
/** 显示变量预览 */
showVariablePreview: boolean
/** 显示工具管理器 */
showToolManager: boolean
/** 编辑模式 */
mode: 'edit' | 'preview'
}
interface ContextEditorEmits {
/** 保存上下文 */
save: (context: ContextState) => void
/** 取消编辑 */
cancel: () => void
/** 更新可见性 */
'update:visible': (visible: boolean) => void
/** 上下文状态更新 */
'update:state': (state: ContextState) => void
/** 上下文内容变更 */
contextChange: (context: ContextState) => void
}
<template>
<ContextEditor>
<!-- 自定义工具栏 -->
<template #toolbar>
<NButton>自定义按钮</NButton>
</template>
<!-- 自定义底部 -->
<template #footer>
<div class="custom-footer">自定义内容</div>
</template>
</ContextEditor>
</template>
变量标签页专门用于管理上下文级变量覆盖:
role、aria-label、aria-describedby 支持<template>
<div>
<NButton @click="showEditor = true">
打开编辑器
</NButton>
<ContextEditor
v-model:visible="showEditor"
:state="contextState"
:available-variables="availableVariables"
@save="handleSave"
@cancel="handleCancel"
@update:state="handleStateUpdate"
@contextChange="handleContextChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ContextEditor, type ContextState } from '@prompt-optimizer/ui'
const showEditor = ref(false)
// 上下文状态数据
const contextState = ref<ContextState>({
messages: [
{ role: 'user', content: 'Hello {{name}}' },
{ role: 'assistant', content: 'Hi there!' }
],
variables: { name: 'World' }, // 上下文覆盖变量
tools: [],
showVariablePreview: true,
showToolManager: true,
mode: 'edit'
})
// 全局可用变量(包括预定义和全局变量)
const availableVariables = ref<Record<string, string>>({
currentDate: new Date().toISOString(),
userName: 'Default User',
// 其他全局变量...
})
const handleSave = (context: ContextState) => {
console.log('Context saved:', context)
showEditor.value = false
}
const handleCancel = () => {
showEditor.value = false
}
const handleStateUpdate = (state: ContextState) => {
console.log('State updated:', state)
// 实时持久化逻辑
}
const handleContextChange = (context: ContextState) => {
console.log('Context changed:', context)
// 上下文变更处理逻辑
}
</script>
描述: 用于显示和管理工具调用结果的折叠面板组件。
文件位置: packages/ui/src/components/ToolCallDisplay.vue
interface ToolCallDisplayProps {
/** 工具调用列表 */
toolCalls?: ToolCall[]
/** 初始折叠状态 */
collapsed?: boolean
/** 组件大小 */
size?: 'small' | 'medium' | 'large'
/** 最大显示数量 */
maxItems?: number
}
interface ToolCall {
/** 调用ID */
id: string
/** 工具名称 */
name: string
/** 调用参数 */
arguments?: Record<string, any>
/** 调用结果 */
result?: any
/** 错误信息 */
error?: string
/** 调用状态 */
status: 'pending' | 'success' | 'error'
/** 时间戳 */
timestamp: number
}
<template>
<ToolCallDisplay
:tool-calls="toolCalls"
:collapsed="false"
size="medium"
:max-items="50"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ToolCallDisplay, type ToolCall } from '@prompt-optimizer/ui'
const toolCalls = ref<ToolCall[]>([
{
id: 'call_1',
name: 'get_weather',
arguments: { location: 'Beijing', unit: 'celsius' },
result: { temperature: 25, condition: 'sunny' },
status: 'success',
timestamp: Date.now()
},
{
id: 'call_2',
name: 'send_email',
arguments: { to: '[email protected]', subject: 'Test' },
error: 'Network timeout',
status: 'error',
timestamp: Date.now()
}
])
</script>
描述: 专门为屏幕阅读器用户提供增强支持的组件。
文件位置: packages/ui/src/components/ScreenReaderSupport.vue
interface ScreenReaderSupportProps {
/** 增强模式 */
enhanced?: boolean
/** 显示导航帮助 */
showNavigationHelp?: boolean
/** 显示快捷键帮助 */
showShortcutHelp?: boolean
/** 自动通知 */
autoAnnounce?: boolean
}
aria-live 区域用于状态更新通知interface ScreenReaderSupportMethods {
/** 发送通知消息 */
announce(message: string, priority: 'polite' | 'assertive'): void
/** 显示快捷键帮助 */
showShortcuts(): void
/** 显示导航帮助 */
showNavigation(): void
}
<template>
<div>
<ScreenReaderSupport
ref="screenReader"
:enhanced="accessibilityMode"
:show-navigation-help="showNav"
:show-shortcut-help="showShortcuts"
@shortcut="handleShortcut"
/>
<NButton @click="notifyUser">
发送通知
</NButton>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ScreenReaderSupport } from '@prompt-optimizer/ui'
const screenReader = ref<InstanceType<typeof ScreenReaderSupport>>()
const accessibilityMode = ref(false)
const showNav = ref(false)
const showShortcuts = ref(false)
const notifyUser = () => {
screenReader.value?.announce('操作完成', 'polite')
}
const handleShortcut = (key: string) => {
console.log('快捷键触发:', key)
}
</script>
描述: 提供全面的可访问性功能,包括键盘导航、ARIA 管理和屏幕阅读器支持。
文件位置: packages/ui/src/composables/useAccessibility.ts
function useAccessibility(componentName?: string): {
// 键盘导航
keyboard: {
handleKeyPress: (event: KeyboardEvent) => boolean
setFocusableElements: (elements: HTMLElement[]) => void
focusNext: () => void
focusPrevious: () => void
focusFirst: () => void
focusLast: () => void
}
// ARIA 标签管理
aria: {
getLabel: (key: string, fallback?: string) => string
getDescription: (key: string, fallback?: string) => string
getRole: (elementType: string) => string
getLiveRegionText: (key: string) => string
}
// 消息通知
announce: (message: string, priority?: 'polite' | 'assertive') => void
// 焦点管理
enableFocusTrap: () => void
disableFocusTrap: () => void
// 响应式状态
focusableElements: Ref<HTMLElement[]>
currentFocusIndex: Ref<number>
trapFocus: Ref<boolean>
isAccessibilityMode: Ref<boolean>
accessibilityClasses: Ref<Record<string, boolean>>
liveRegionMessage: Ref<string>
announcements: Ref<string[]>
features: Ref<AccessibilityFeatures>
}
interface AccessibilityFeatures {
reduceMotion: boolean
highContrast: boolean
screenReaderMode: boolean
keyboardOnly: boolean
}
<template>
<div :class="accessibilityClasses">
<button
v-for="(item, index) in items"
:key="item.id"
:aria-label="aria.getLabel('item', item.name)"
@keydown="keyboard.handleKeyPress"
>
{{ item.name }}
</button>
<div
role="status"
aria-live="polite"
class="sr-only"
>
{{ liveRegionMessage }}
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useAccessibility } from '@prompt-optimizer/ui'
const items = ref([
{ id: 1, name: '项目1' },
{ id: 2, name: '项目2' },
{ id: 3, name: '项目3' }
])
const {
keyboard,
aria,
announce,
enableFocusTrap,
disableFocusTrap,
accessibilityClasses,
liveRegionMessage
} = useAccessibility('MyComponent')
onMounted(() => {
const buttons = document.querySelectorAll('button')
keyboard.setFocusableElements(Array.from(buttons) as HTMLElement[])
enableFocusTrap()
announce('组件已加载', 'polite')
})
</script>
描述: 专业的焦点管理系统,支持焦点陷阱、键盘导航和自动焦点恢复。
文件位置: packages/ui/src/composables/useFocusManager.ts
function useFocusManager(options: FocusManagerOptions = {}): {
// 核心方法
trapFocus: () => Promise<void>
releaseFocus: () => void
moveFocusNext: () => boolean
moveFocusPrevious: () => boolean
focusFirstElement: () => boolean
focusLastElement: () => boolean
// 工具方法
updateFocusableElements: () => HTMLElement[]
isFocusable: (element: HTMLElement) => boolean
// 响应式状态
focusableElements: Ref<HTMLElement[]>
currentFocusIndex: Ref<number>
isTrapped: Ref<boolean>
lastFocusedElement: Ref<HTMLElement | null>
}
interface FocusManagerOptions {
container?: string | HTMLElement
autoTrap?: boolean
restoreFocus?: boolean
skipHidden?: boolean
}
<template>
<div ref="containerRef" class="focus-container">
<h2>焦点管理示例</h2>
<NButton @click="trapFocus">启用焦点陷阱</NButton>
<NButton @click="releaseFocus">释放焦点陷阱</NButton>
<NInput placeholder="输入框1" />
<NInput placeholder="输入框2" />
<NButton>确认</NButton>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useFocusManager } from '@prompt-optimizer/ui'
const containerRef = ref<HTMLElement>()
const {
trapFocus,
releaseFocus,
moveFocusNext,
moveFocusPrevious,
focusableElements,
currentFocusIndex,
isTrapped
} = useFocusManager({
container: containerRef,
restoreFocus: true
})
onMounted(() => {
// 监听键盘事件
document.addEventListener('keydown', (e) => {
if (!isTrapped.value) return
if (e.key === 'Tab') {
e.preventDefault()
if (e.shiftKey) {
moveFocusPrevious()
} else {
moveFocusNext()
}
}
})
})
</script>
描述: WCAG 合规性自动化测试工具,用于检测和验证可访问性问题。
文件位置: packages/ui/src/composables/useAccessibilityTesting.ts
function useAccessibilityTesting(): {
runTest: (options: TestOptions) => Promise<TestResult>
runSingleRule: (rule: string, scope?: Element) => TestResult
getAvailableRules: () => TestRule[]
}
interface TestOptions {
scope?: Element
wcagLevel?: 'A' | 'AA' | 'AAA'
rules?: string[]
includeWarnings?: boolean
}
interface TestResult {
score: number
issues: AccessibilityIssue[]
warnings: AccessibilityIssue[]
passedRules: string[]
timestamp: number
}
interface AccessibilityIssue {
rule: string
severity: 'critical' | 'major' | 'minor'
message: string
element?: HTMLElement
wcagLevel: 'A' | 'AA' | 'AAA'
}
<script setup lang="ts">
import { onMounted } from 'vue'
import { useAccessibilityTesting } from '@prompt-optimizer/ui'
const { runTest, runSingleRule } = useAccessibilityTesting()
onMounted(async () => {
// 运行全面测试
const result = await runTest({
scope: document.body,
wcagLevel: 'AA',
includeWarnings: true
})
console.log('可访问性测试结果:', result)
if (result.score < 80) {
console.warn('可访问性分数较低:', result.score)
result.issues.forEach(issue => {
console.error(`${issue.rule}: ${issue.message}`)
})
}
// 单独测试某个规则
const imgAltResult = runSingleRule('img-alt')
if (imgAltResult.issues.length > 0) {
console.warn('图片缺少alt属性')
}
})
</script>
所有组件遵循统一的 CSS 类命名规范:
// 基础组件类
.component-name {
// 基础样式
}
// 状态类
.component-name--state {
// 状态样式
}
// 修饰符类
.component-name__element {
// 元素样式
}
// 可访问性相关类
.sr-only {
// 仅屏幕阅读器可见
}
.keyboard-focus {
// 键盘焦点样式
}
.accessibility-mode {
// 可访问性模式样式
}
// 移动端
@media (max-width: 767px) {
.responsive-mobile { /* 样式 */ }
}
// 平板端
@media (min-width: 768px) and (max-width: 1023px) {
.responsive-tablet { /* 样式 */ }
}
// 桌面端
@media (min-width: 1024px) {
.responsive-desktop { /* 样式 */ }
}
// 组件懒加载
const ContextEditor = defineAsyncComponent(
() => import('./components/ContextEditor.vue')
)
// 路由级别代码分割
const routes = [
{
path: '/editor',
component: () => import('./pages/EditorPage.vue')
}
]
<template>
<!-- 大量数据的虚拟列表 -->
<VirtualList
:items="largeDataset"
:item-height="50"
:visible-count="10"
>
<template #item="{ item }">
<div class="virtual-item">{{ item.name }}</div>
</template>
</VirtualList>
</template>
import { useDebounceThrottle } from '@prompt-optimizer/ui'
const { debounce, throttle } = useDebounceThrottle()
// 搜索输入防抖
const handleSearch = debounce((query: string) => {
// 执行搜索逻辑
}, 300)
// 滚动事件节流
const handleScroll = throttle(() => {
// 处理滚动逻辑
}, 16)
import { createI18n } from 'vue-i18n'
import zhCN from './locales/zh-CN'
import enUS from './locales/en-US'
const i18n = createI18n({
locale: 'zh-CN',
fallbackLocale: 'en-US',
messages: {
'zh-CN': zhCN,
'en-US': enUS
}
})
// zh-CN.ts
export default {
accessibility: {
labels: {
contextEditor: '上下文编辑器',
closeButton: '关闭按钮',
saveButton: '保存按钮'
},
descriptions: {
contextEditor: '编辑消息、变量和工具配置',
navigationHelp: '使用Tab键在元素间导航'
},
announcements: {
saved: '内容已保存',
loading: '正在加载中',
error: '发生错误,请重试'
}
}
}
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import ContextEditor from '../ContextEditor.vue'
describe('ContextEditor', () => {
it('应该正确渲染基本结构', () => {
const wrapper = mount(ContextEditor, {
props: {
visible: true,
state: {
messages: [],
variables: {},
tools: []
}
}
})
expect(wrapper.find('[role="dialog"]').exists()).toBe(true)
})
})
import { useAccessibilityTesting } from '@prompt-optimizer/ui'
describe('Accessibility Tests', () => {
it('应该通过WCAG AA标准', async () => {
const { runTest } = useAccessibilityTesting()
const result = await runTest({ wcagLevel: 'AA' })
expect(result.score).toBeGreaterThan(80)
expect(result.issues.filter(i => i.severity === 'critical')).toHaveLength(0)
})
})
describe('端到端测试', () => {
it('应该支持完整的用户流程', async () => {
// 测试完整的用户交互流程
await page.goto('/')
await page.click('[data-testid="open-editor"]')
await page.fill('[aria-label="消息输入框"]', '测试内容')
await page.click('[aria-label="保存按钮"]')
expect(await page.textContent('[role="status"]')).toContain('保存成功')
})
})
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: 'src/index.ts',
name: 'PromptOptimizerUI',
formats: ['es', 'cjs']
},
rollupOptions: {
external: ['vue', 'naive-ui'],
output: {
globals: {
vue: 'Vue',
'naive-ui': 'NaiveUI'
}
}
}
}
})
{
"name": "@prompt-optimizer/ui",
"version": "1.0.0",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./style": "./dist/style.css"
}
}
// 推荐的组件结构
<template>
<div class="component-name" :class="componentClasses">
<!-- 内容 -->
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useAccessibility } from '../composables/useAccessibility'
// Props 定义
interface Props {
visible: boolean
readonly?: boolean
}
const props = withDefaults(defineProps<Props>(), {
readonly: false
})
// Emits 定义
interface Emits {
'update:visible': [visible: boolean]
}
const emit = defineEmits<Emits>()
// 可访问性支持
const { accessibility } = useAccessibility('ComponentName')
// 响应式状态
const localVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
})
// 计算属性
const componentClasses = computed(() => ({
'component-name--readonly': props.readonly,
...accessibility.classes.value
}))
</script>
<style scoped>
.component-name {
/* 基础样式 */
}
.component-name--readonly {
/* 只读状态样式 */
}
</style>
如有问题或建议,请通过以下方式联系:
最后更新时间: 2024年XX月XX日