-
Notifications
You must be signed in to change notification settings - Fork 5
feat(accordion): 实现 Accordion 手风琴组件 #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughSkiyee UI 库新增 Accordion(手风琴)组件系统,包括两个核心组件(SkAccordion 和 SkAccordionItem)、样式定义、类型声明和8个示例页面,支持单项/多项展开、边框、禁用、自定义图标和插槽等功能。 Changes
Sequence DiagramsequenceDiagram
participant User as 用户交互
participant Parent as SkAccordion<br/>(父组件)
participant Context as InjectionKey<br/>(上下文)
participant Child as SkAccordionItem<br/>(子组件)
User->>Parent: 点击/v-model 更新
Parent->>Parent: 计算 isActive()
Parent->>Parent: 执行 toggle()
Parent->>Context: provide(SK_ACCORDION_KEY)
Context->>Child: 注入 props/isActive/toggle
Child->>Child: 读取激活状态
Child->>Child: 渲染动画效果
Child->>Parent: emit('click')
Parent->>Parent: 更新 modelValue
Parent->>User: 显示最新状态
Estimated code review effort🎯 4 (复杂) | ⏱️ ~50 分钟
Poem
Pre-merge checks and finishing touches✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ Deploy Preview for skiyee-ui ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/skiyee-uni-ui/src/components/sk-accordion.vue (1)
48-50: 建议优化默认值以适配手风琴模式。当前默认值
[]在多选模式下是合理的,但在手风琴模式(accordion: true)下,v-model 应该是单个值(string | number)而非数组。初始状态为[]在语义上不够清晰,建议:
- 在文档中明确说明手风琴模式下建议初始化为空字符串或具体值
- 或在组件内部添加类型规范化逻辑,根据
accordion属性自动调整 modelValue 的类型这不会导致功能错误,但会提升 API 的清晰度。
示例规范化逻辑:
const modelValue = defineModel<string | number | (string | number)[]>({ default: () => [], }) +// 规范化 modelValue 类型 +watchEffect(() => { + if (props.accordion && Array.isArray(modelValue.value)) { + modelValue.value = '' + } else if (!props.accordion && !Array.isArray(modelValue.value)) { + modelValue.value = [] + } +})
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
examples/uni/src/pages-navigation/accordion/accordion.vue(1 hunks)examples/uni/src/pages-navigation/accordion/base.vue(1 hunks)examples/uni/src/pages-navigation/accordion/bordered.vue(1 hunks)examples/uni/src/pages-navigation/accordion/controlled.vue(1 hunks)examples/uni/src/pages-navigation/accordion/custom.vue(1 hunks)examples/uni/src/pages-navigation/accordion/disabled.vue(1 hunks)examples/uni/src/pages-navigation/accordion/icon.vue(1 hunks)examples/uni/src/pages-navigation/accordion/multiple.vue(1 hunks)packages/skiyee-uni-ui/src/components/sk-accordion-item.vue(1 hunks)packages/skiyee-uni-ui/src/components/sk-accordion.vue(1 hunks)packages/skiyee-uni-ui/src/constants/accordion.ts(1 hunks)packages/skiyee-uni-ui/src/constants/index.ts(1 hunks)packages/skiyee-uni-ui/src/styles/index.ts(1 hunks)packages/skiyee-uni-ui/src/styles/sk-accordion-item.ts(1 hunks)packages/skiyee-uni-ui/src/styles/sk-accordion.ts(1 hunks)packages/skiyee-uni-ui/src/types/accordion.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/skiyee-uni-ui/src/styles/sk-accordion-item.ts (1)
packages/skiyee-uni-ui/src/styles/index.ts (2)
SkAccordionItemUcv(12-12)SkAccordionItemUcvProps(11-11)
packages/skiyee-uni-ui/src/styles/sk-accordion.ts (1)
packages/skiyee-uni-ui/src/styles/index.ts (2)
SkAccordionUcv(9-9)SkAccordionUcvProps(8-8)
packages/skiyee-uni-ui/src/types/accordion.ts (3)
packages/skiyee-uni-ui/src/styles/index.ts (2)
SkAccordionUcvProps(8-8)SkAccordionItemUcvProps(11-11)packages/skiyee-uni-ui/src/styles/sk-accordion.ts (1)
SkAccordionUcvProps(38-38)packages/skiyee-uni-ui/src/styles/sk-accordion-item.ts (1)
SkAccordionItemUcvProps(72-72)
packages/skiyee-uni-ui/src/constants/accordion.ts (2)
packages/skiyee-uni-ui/src/styles/index.ts (1)
SkAccordionUcvProps(8-8)packages/skiyee-uni-ui/src/styles/sk-accordion.ts (1)
SkAccordionUcvProps(38-38)
🪛 ESLint
packages/skiyee-uni-ui/src/constants/accordion.ts
[error] 11-11: 'SkAccordionItemUcvProps' is defined but never used.
(unused-imports/no-unused-imports)
🔇 Additional comments (17)
packages/skiyee-uni-ui/src/types/accordion.ts (3)
9-50: 类型定义清晰且设计合理!Accordion 的类型定义很好地支持了单选和多选两种模式,通过
modelValue的联合类型设计实现了灵活性。属性文档完整,且正确引用了样式系统的类型(SkAccordionUcvProps)实现类型复用。
55-76: 事件和插槽定义符合 Vue 规范!事件定义遵循了 Vue 的 v-model 约定,同时提供
update:modelValue和change事件,这是标准做法。插槽定义简洁明了。
81-131: AccordionItem API 设计完善!子组件的属性、事件和插槽定义完整,特别是提供了
title、icon和default三个插槽,为用户提供了充分的自定义能力。类型定义与样式系统良好集成。packages/skiyee-uni-ui/src/constants/index.ts (1)
8-8: 导出语句正确!新增的 accordion 常量导出遵循了现有的代码模式,与其他模块导出保持一致。
examples/uni/src/pages-navigation/accordion/accordion.vue (1)
1-22: 手风琴模式示例实现正确!代码正确展示了 accordion 单选模式的用法:使用
string类型的 ref 配合accordion属性,确保每次只能展开一个面板。示例内容清晰,演示了组件的核心功能。examples/uni/src/pages-navigation/accordion/disabled.vue (1)
1-22: 禁用状态示例实现正确!代码展示了多选模式下的禁用功能,使用数组类型的 ref 支持多项展开。禁用项的配置正确,能够有效演示
disabled属性的效果。examples/uni/src/pages-navigation/accordion/base.vue (1)
1-19: 基础示例简洁清晰!这是一个标准的多选模式基础示例,代码结构清晰,使用场景明确。内容介绍了组件库的基本信息,非常适合作为入门示例。
examples/uni/src/pages-navigation/accordion/multiple.vue (1)
1-22: 多选示例展示完整!代码很好地演示了多选模式的特性,通过初始化
['1', '3']展示了同时展开多个面板的效果。内容组织合理,清晰展示了组件的灵活性。packages/skiyee-uni-ui/src/styles/sk-accordion-item.ts (2)
20-70: 样式定义完整且结构合理!UCV 样式定义涵盖了所有必要的状态和变体:
- 元素样式使用了统一的设计 token(sk-unit、text-primary 等)
- active 状态通过
rotate-180和max-h-0实现了流畅的展开/收起动画- 正确处理了 disabled、border 和 iconPosition 等变体
- 默认值设置合理
使用
max-h-0实现折叠是标准做法,配合transition-all能提供良好的用户体验。
72-72: 类型导出正确!正确导出了
SkAccordionItemUcvProps类型,与类型系统良好集成。examples/uni/src/pages-navigation/accordion/bordered.vue (1)
1-38: 边框样式对比示例设计优秀!通过并列展示两个独立的 Accordion 实例,清晰地对比了有边框和无边框两种样式。每个示例都配有说明文字,描述了各自的视觉特点和使用场景,非常有助于用户理解
border属性的效果。packages/skiyee-uni-ui/src/components/sk-accordion.vue (6)
1-23: 文档完善,类型导出规范。JSDoc 注释清晰,包含了示例和文档链接,类型重导出遵循了 Vue 3 最佳实践。
26-38: 组件配置合理。使用
inheritAttrs: false和virtualHost: true配置是小程序环境下的良好实践。
40-46: 属性默认值设置合理。默认配置符合常见使用场景,图标使用标准的 iconify 格式。
61-68: 状态判断逻辑正确。正确处理了手风琴模式(单值比较)和多选模式(数组包含判断)的状态检查。
71-97: 切换逻辑实现正确。正确实现了两种模式的切换逻辑:
- 手风琴模式:在展开和折叠之间切换
- 多选模式:在数组中添加或移除项
第 80 行的防御性编程(
Array.isArray检查)能够处理边缘情况,确保代码健壮性。
99-110: 依赖注入和模板实现得当。使用
useProvide向子组件共享状态和方法,遵循 Vue provide/inject 模式。模板结构简洁明了。
| const contentRef = ref<HTMLElement>() | ||
| const contentHeight = ref<string>('auto') | ||
|
|
||
| // 当前是否处于展开状态 | ||
| const isActive = computed(() => { | ||
| if (!parent || props.name === undefined) { | ||
| return false | ||
| } | ||
| return parent.isActive(props.name) | ||
| }) | ||
|
|
||
| // 计算图标 | ||
| const iconName = computed(() => { | ||
| if (props.icon) { | ||
| return props.icon | ||
| } | ||
| if (!parent) { | ||
| return 'i-lucide:chevron-down' | ||
| } | ||
| return isActive.value ? (parent.props.expandIcon || 'i-lucide:chevron-up') : (parent.props.collapseIcon || 'i-lucide:chevron-down') | ||
| }) | ||
|
|
||
| // 计算样式类 | ||
| const classes = computed(() => { | ||
| const computedProps = { | ||
| ...props, | ||
| active: isActive.value, | ||
| border: parent?.props.border ?? true, | ||
| iconPosition: parent?.props.iconPosition ?? 'right', | ||
| } | ||
| return SkAccordionItemUcv(computedProps) | ||
| }) | ||
|
|
||
| // 点击标题栏 | ||
| function handleClick() { | ||
| if (props.disabled || !parent || props.name === undefined) { | ||
| return | ||
| } | ||
|
|
||
| parent.toggle(props.name) | ||
| emits('click') | ||
| } | ||
|
|
||
| // 计算内容区域高度(用于动画) | ||
| const contentStyle = computed(() => { | ||
| if (isActive.value) { | ||
| return { | ||
| maxHeight: contentHeight.value === 'auto' ? 'none' : contentHeight.value, | ||
| } | ||
| } | ||
| return { | ||
| maxHeight: '0', | ||
| } | ||
| }) | ||
|
|
||
| // 监听展开状态变化 | ||
| function onTransitionEnd() { | ||
| if (isActive.value) { | ||
| contentHeight.value = 'auto' | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
修复内容展开/收起动画失效
当前 contentHeight 一直保持 'auto',contentStyle 在展开时返回 maxHeight: 'none',导致 max-height 没有从具体像素值过渡到 0 —— 浏览器不会为 'none' → '0' 触发过渡,也不会触发 transitionend,因此内容会瞬间展开/收起,且动画逻辑(包括 contentRef、onTransitionEnd)完全失效。请预先读出面板的实际高度,在展开/收起时用具体像素值驱动过渡,再在动画结束后恢复 'auto'。
-import { computed, ref } from 'vue'
+import { computed, nextTick, ref, watch } from 'vue'
@@
-const contentRef = ref<HTMLElement>()
-const contentHeight = ref<string>('auto')
+const contentRef = ref<HTMLElement>()
+const contentHeight = ref<string>('0px')
+
+function scheduleFrame(cb: () => void) {
+ if (typeof requestAnimationFrame === 'function') {
+ requestAnimationFrame(cb)
+ }
+ else {
+ setTimeout(cb, 16)
+ }
+}
@@
const isActive = computed(() => {
@@
})
+watch(
+ () => isActive.value,
+ async (active) => {
+ const el = contentRef.value
+ if (!el) {
+ return
+ }
+
+ await nextTick()
+
+ const height = `${el.scrollHeight}px`
+
+ if (active) {
+ contentHeight.value = height
+ }
+ else {
+ contentHeight.value = height
+ scheduleFrame(() => {
+ contentHeight.value = '0px'
+ })
+ }
+ },
+ { immediate: true }
+)
+
@@
-const contentStyle = computed(() => {
- if (isActive.value) {
- return {
- maxHeight: contentHeight.value === 'auto' ? 'none' : contentHeight.value,
- }
- }
- return {
- maxHeight: '0',
- }
-})
+const contentStyle = computed(() => ({
+ maxHeight: contentHeight.value,
+}))
@@
function onTransitionEnd() {
if (isActive.value) {
contentHeight.value = 'auto'
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const contentRef = ref<HTMLElement>() | |
| const contentHeight = ref<string>('auto') | |
| // 当前是否处于展开状态 | |
| const isActive = computed(() => { | |
| if (!parent || props.name === undefined) { | |
| return false | |
| } | |
| return parent.isActive(props.name) | |
| }) | |
| // 计算图标 | |
| const iconName = computed(() => { | |
| if (props.icon) { | |
| return props.icon | |
| } | |
| if (!parent) { | |
| return 'i-lucide:chevron-down' | |
| } | |
| return isActive.value ? (parent.props.expandIcon || 'i-lucide:chevron-up') : (parent.props.collapseIcon || 'i-lucide:chevron-down') | |
| }) | |
| // 计算样式类 | |
| const classes = computed(() => { | |
| const computedProps = { | |
| ...props, | |
| active: isActive.value, | |
| border: parent?.props.border ?? true, | |
| iconPosition: parent?.props.iconPosition ?? 'right', | |
| } | |
| return SkAccordionItemUcv(computedProps) | |
| }) | |
| // 点击标题栏 | |
| function handleClick() { | |
| if (props.disabled || !parent || props.name === undefined) { | |
| return | |
| } | |
| parent.toggle(props.name) | |
| emits('click') | |
| } | |
| // 计算内容区域高度(用于动画) | |
| const contentStyle = computed(() => { | |
| if (isActive.value) { | |
| return { | |
| maxHeight: contentHeight.value === 'auto' ? 'none' : contentHeight.value, | |
| } | |
| } | |
| return { | |
| maxHeight: '0', | |
| } | |
| }) | |
| // 监听展开状态变化 | |
| function onTransitionEnd() { | |
| if (isActive.value) { | |
| contentHeight.value = 'auto' | |
| } | |
| } | |
| import { computed, nextTick, ref, watch } from 'vue' | |
| const contentRef = ref<HTMLElement>() | |
| const contentHeight = ref<string>('0px') | |
| function scheduleFrame(cb: () => void) { | |
| if (typeof requestAnimationFrame === 'function') { | |
| requestAnimationFrame(cb) | |
| } | |
| else { | |
| setTimeout(cb, 16) | |
| } | |
| } | |
| // 当前是否处于展开状态 | |
| const isActive = computed(() => { | |
| if (!parent || props.name === undefined) { | |
| return false | |
| } | |
| return parent.isActive(props.name) | |
| }) | |
| watch( | |
| () => isActive.value, | |
| async (active) => { | |
| const el = contentRef.value | |
| if (!el) { | |
| return | |
| } | |
| await nextTick() | |
| const height = `${el.scrollHeight}px` | |
| if (active) { | |
| contentHeight.value = height | |
| } | |
| else { | |
| contentHeight.value = height | |
| scheduleFrame(() => { | |
| contentHeight.value = '0px' | |
| }) | |
| } | |
| }, | |
| { immediate: true } | |
| ) | |
| // 计算图标 | |
| const iconName = computed(() => { | |
| if (props.icon) { | |
| return props.icon | |
| } | |
| if (!parent) { | |
| return 'i-lucide:chevron-down' | |
| } | |
| return isActive.value ? (parent.props.expandIcon || 'i-lucide:chevron-up') : (parent.props.collapseIcon || 'i-lucide:chevron-down') | |
| }) | |
| // 计算样式类 | |
| const classes = computed(() => { | |
| const computedProps = { | |
| ...props, | |
| active: isActive.value, | |
| border: parent?.props.border ?? true, | |
| iconPosition: parent?.props.iconPosition ?? 'right', | |
| } | |
| return SkAccordionItemUcv(computedProps) | |
| }) | |
| // 点击标题栏 | |
| function handleClick() { | |
| if (props.disabled || !parent || props.name === undefined) { | |
| return | |
| } | |
| parent.toggle(props.name) | |
| emits('click') | |
| } | |
| // 计算内容区域高度(用于动画) | |
| const contentStyle = computed(() => ({ | |
| maxHeight: contentHeight.value, | |
| })) | |
| // 监听展开状态变化 | |
| function onTransitionEnd() { | |
| if (isActive.value) { | |
| contentHeight.value = 'auto' | |
| } | |
| } |
|
|
||
| import type { InjectionKey } from 'vue' | ||
|
|
||
| import type { SkAccordionItemUcvProps, SkAccordionUcvProps } from '../styles' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
移除未使用的导入避免 ESLint 失败
SkAccordionItemUcvProps 没有在该文件中使用,ESLint(unused-imports/no-unused-imports)已经报错,会导致管线直接失败。请删除这个多余的导入。
-import type { SkAccordionItemUcvProps, SkAccordionUcvProps } from '../styles'
+import type { SkAccordionUcvProps } from '../styles'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import type { SkAccordionItemUcvProps, SkAccordionUcvProps } from '../styles' | |
| import type { SkAccordionUcvProps } from '../styles' |
🧰 Tools
🪛 ESLint
[error] 11-11: 'SkAccordionItemUcvProps' is defined but never used.
(unused-imports/no-unused-imports)
🤖 Prompt for AI Agents
packages/skiyee-uni-ui/src/constants/accordion.ts around line 11: the import
includes an unused symbol SkAccordionItemUcvProps which triggers ESLint
unused-imports/no-unused-imports and breaks CI; remove SkAccordionItemUcvProps
from the import list so only SkAccordionUcvProps is imported.
#37
Summary by CodeRabbit