Skip to content

Conversation

@skiyee
Copy link
Owner

@skiyee skiyee commented Nov 4, 2025

#37

Summary by CodeRabbit

  • 新功能
    • 新增手风琴组件,支持单选或多选展开模式
    • 支持自定义图标、边框、禁用状态等配置选项
    • 支持插槽自定义标题、内容和图标
    • 包含8个使用示例,演示基础用法、受控状态、自定义样式等场景

@coderabbitai
Copy link

coderabbitai bot commented Nov 4, 2025

Walkthrough

Skiyee UI 库新增 Accordion(手风琴)组件系统,包括两个核心组件(SkAccordion 和 SkAccordionItem)、样式定义、类型声明和8个示例页面,支持单项/多项展开、边框、禁用、自定义图标和插槽等功能。

Changes

Cohort / File(s) 摘要
核心 Accordion 组件
packages/skiyee-uni-ui/src/components/sk-accordion.vue, packages/skiyee-uni-ui/src/components/sk-accordion-item.vue
添加主组件和子项组件。父组件通过 provide/inject 提供上下文给子组件,支持 v-model 双向绑定,并提供 isActive 和 toggle 方法。子组件使用上下文管理状态、处理禁用、动画过渡和自定义图标。
类型定义
packages/skiyee-uni-ui/src/types/accordion.ts
新增 6 个接口定义 Accordion 和 AccordionItem 的 props、emits 和 slots。
样式与常量
packages/skiyee-uni-ui/src/styles/sk-accordion.ts, packages/skiyee-uni-ui/src/styles/sk-accordion-item.ts, packages/skiyee-uni-ui/src/constants/accordion.ts
新增样式变体定义(SkAccordionUcv、SkAccordionItemUcv)、注入键常量(SK_ACCORDION_KEY)及其对应的 props 类型导出。
模块导出
packages/skiyee-uni-ui/src/constants/index.ts, packages/skiyee-uni-ui/src/styles/index.ts
更新导出语句以包含新的 accordion 模块和样式相关的常量与类型。
示例页面
examples/uni/src/pages-navigation/accordion/base.vue, examples/uni/src/pages-navigation/accordion/accordion.vue, examples/uni/src/pages-navigation/accordion/bordered.vue, examples/uni/src/pages-navigation/accordion/controlled.vue, examples/uni/src/pages-navigation/accordion/custom.vue, examples/uni/src/pages-navigation/accordion/disabled.vue, examples/uni/src/pages-navigation/accordion/icon.vue, examples/uni/src/pages-navigation/accordion/multiple.vue
添加 8 个示例组件,分别演示基础用法、单项激活、有无边框、受控展开/收起、自定义插槽、禁用项、图标配置和多项展开等场景。

Sequence Diagram

sequenceDiagram
    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: 显示最新状态
Loading

Estimated code review effort

🎯 4 (复杂) | ⏱️ ~50 分钟

  • 需要重点关注的区域:
    • packages/skiyee-uni-ui/src/components/sk-accordion.vue 中 toggle 逻辑对单项/多项模式的处理正确性
    • packages/skiyee-uni-ui/src/components/sk-accordion-item.vue 中动画过渡的 maxHeight 计算和过渡结束时的高度重置逻辑
    • packages/skiyee-uni-ui/src/constants/accordion.ts 中注入键的类型定义完整性
    • 8 个示例文件中是否正确演示了各种使用场景和配置组合
    • 父子组件间 provide/inject 的类型安全性和上下文传递的正确性

Poem

🐰 崭新的手风琴展开来,
一按一合多惬意,
禁用样式和图标换,
插槽自定义任意排,
八个例子齐齐来,
折叠展开皆精彩!✨

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 标题 'feat(accordion): 实现 Accordion 手风琴组件' 准确描述了本次变更的主要内容:实现了完整的 Accordion(手风琴)组件功能,包括组件实现、类型定义、样式系统和多个示例。
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/accordion

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@netlify
Copy link

netlify bot commented Nov 4, 2025

Deploy Preview for skiyee-ui ready!

Name Link
🔨 Latest commit 4fefe3d
🔍 Latest deploy log https://app.netlify.com/projects/skiyee-ui/deploys/690a0a5ffcc08c000864c8b4
😎 Deploy Preview https://deploy-preview-38--skiyee-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link

@coderabbitai coderabbitai bot left a 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)而非数组。初始状态为 [] 在语义上不够清晰,建议:

  1. 在文档中明确说明手风琴模式下建议初始化为空字符串或具体值
  2. 或在组件内部添加类型规范化逻辑,根据 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

📥 Commits

Reviewing files that changed from the base of the PR and between a1d5cd3 and 4fefe3d.

📒 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:modelValuechange 事件,这是标准做法。插槽定义简洁明了。


81-131: AccordionItem API 设计完善!

子组件的属性、事件和插槽定义完整,特别是提供了 titleicondefault 三个插槽,为用户提供了充分的自定义能力。类型定义与样式系统良好集成。

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-180max-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: falsevirtualHost: true 配置是小程序环境下的良好实践。


40-46: 属性默认值设置合理。

默认配置符合常见使用场景,图标使用标准的 iconify 格式。


61-68: 状态判断逻辑正确。

正确处理了手风琴模式(单值比较)和多选模式(数组包含判断)的状态检查。


71-97: 切换逻辑实现正确。

正确实现了两种模式的切换逻辑:

  • 手风琴模式:在展开和折叠之间切换
  • 多选模式:在数组中添加或移除项

第 80 行的防御性编程(Array.isArray 检查)能够处理边缘情况,确保代码健壮性。


99-110: 依赖注入和模板实现得当。

使用 useProvide 向子组件共享状态和方法,遵循 Vue provide/inject 模式。模板结构简洁明了。

Comment on lines +50 to +110
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'
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

修复内容展开/收起动画失效

当前 contentHeight 一直保持 'auto'contentStyle 在展开时返回 maxHeight: 'none',导致 max-height 没有从具体像素值过渡到 0 —— 浏览器不会为 'none' → '0' 触发过渡,也不会触发 transitionend,因此内容会瞬间展开/收起,且动画逻辑(包括 contentRefonTransitionEnd)完全失效。请预先读出面板的实际高度,在展开/收起时用具体像素值驱动过渡,再在动画结束后恢复 '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.

Suggested change
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'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

移除未使用的导入避免 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants