tang-ui-x 1.3.6 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,31 +1,31 @@
1
- <script setup lang="uts" >
2
- import { computed } from 'vue'
3
- import type { TCardProps } from './type.uts'
4
-
5
- /**
6
- * TCard 卡片组件
7
- * @description 通用卡片容器组件,用于展示内容块
8
- */
9
-
10
- // 定义 props
11
- const props = withDefaults(defineProps<TCardProps>(), {
12
- title: '',
13
- subtitle: '',
14
- shadow: true,
15
- border: false,
16
- padding: 'medium',
17
- radius: 'medium',
18
- customClass: ''
19
- })
20
-
21
- // 定义 emits
22
- const emit = defineEmits<{
23
- click: []
24
- }>()
25
-
26
- /**
27
- * 计算卡片样式类
28
- */
1
+ <script setup lang="uts" >
2
+ import { computed } from 'vue'
3
+ import type { TCardProps } from './type.uts'
4
+
5
+ /**
6
+ * TCard 卡片组件
7
+ * @description 通用卡片容器组件,用于展示内容块
8
+ */
9
+
10
+ // 定义 props
11
+ const props = withDefaults(defineProps<TCardProps>(), {
12
+ title: '',
13
+ subtitle: '',
14
+ shadow: true,
15
+ border: false,
16
+ padding: 'medium',
17
+ radius: 'medium',
18
+ customClass: ''
19
+ })
20
+
21
+ // 定义 emits
22
+ const emit = defineEmits<{
23
+ click: []
24
+ }>()
25
+
26
+ /**
27
+ * 计算卡片样式类
28
+ */
29
29
  const cardClass = computed(() => {
30
30
  const classes: string[] = [
31
31
  't-card',
@@ -66,11 +66,11 @@ const cardClass = computed(() => {
66
66
  classes.push('rounded-lg')
67
67
  break
68
68
  }
69
-
70
- if (props.customClass !== '') {
71
- classes.push(props.customClass)
72
- }
73
-
69
+
70
+ if (props.customClass !== '') {
71
+ classes.push(props.customClass)
72
+ }
73
+
74
74
  return classes.join(' ')
75
75
  })
76
76
 
@@ -92,18 +92,18 @@ const cardStyle = computed((): string => {
92
92
 
93
93
  return styles.join('; ')
94
94
  })
95
-
96
- /**
97
- * 处理卡片点击
98
- */
99
- const handleClick = (): void => {
100
- emit('click')
101
- }
102
- </script>
103
-
104
- <template>
95
+
96
+ /**
97
+ * 处理卡片点击
98
+ */
99
+ const handleClick = (): void => {
100
+ emit('click')
101
+ }
102
+ </script>
103
+
104
+ <template>
105
105
  <view :class="cardClass" :style="cardStyle" @click="handleClick">
106
- <!-- 卡片头部 -->
106
+ <!-- 卡片头部 -->
107
107
  <view
108
108
  v-if="title !== '' || subtitle !== ''"
109
109
  class="t-card__header mb-4 flex flex-row items-center justify-between border-b border-border-soft pb-4"
@@ -123,8 +123,6 @@ const handleClick = (): void => {
123
123
  </view>
124
124
 
125
125
  <!-- 卡片底部 -->
126
- <view class="t-card__footer mt-4 border-t border-border-soft pt-4">
127
- <slot name="footer"></slot>
128
- </view>
126
+ <slot name="footer"/>
129
127
  </view>
130
128
  </template>
@@ -6,7 +6,7 @@ export type NoticeBarMode = 'closeable' | 'link'
6
6
 
7
7
  export type TNoticeBarProps = {
8
8
  text?: string
9
- mode?: NoticeBarMode | null
9
+ mode?: NoticeBarMode | undefined
10
10
  color?: string
11
11
  background?: string
12
12
  leftIcon?: string
@@ -38,13 +38,17 @@ const hasItemIcon = (item: TabItem): boolean => {
38
38
  }
39
39
 
40
40
  const hasItemBadge = (item: TabItem): boolean => {
41
- return item.badge !== null && item.badge !== ''
41
+ return item.badge != null && item.badge !== ''
42
+ }
43
+
44
+ const isEmptyKey = (value: string | number | null): boolean => {
45
+ return value == null || (typeof value === 'string' && value === '')
42
46
  }
43
47
 
44
48
  /** 获取默认激活项的 key */
45
49
  const getDefaultKey = (): string | number => {
46
50
  const defaultActiveKey = props.defaultActiveKey
47
- if (defaultActiveKey !== null) {
51
+ if (defaultActiveKey != null) {
48
52
  return defaultActiveKey
49
53
  }
50
54
  if (props.items.length > 0) {
@@ -61,17 +65,17 @@ const getDefaultKey = (): string | number => {
61
65
 
62
66
  /** 初始化默认激活项 */
63
67
  onMounted(() => {
64
- if (model.value == null || model.value == '') {
68
+ if (isEmptyKey(model.value)) {
65
69
  model.value = getDefaultKey()
66
70
  }
67
71
  })
68
72
 
69
73
  const isItemActive = (item: TabItem): boolean => {
70
74
  const currentKey = model.value
71
- if (currentKey == null || currentKey == '') {
75
+ if (isEmptyKey(currentKey)) {
72
76
  return false
73
77
  }
74
- if (item.key == null || item.key == '') {
78
+ if (isEmptyKey(item.key)) {
75
79
  return false
76
80
  }
77
81
  return item.key == currentKey
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tang-ui-x",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "description": "UniApp X UI 组件库 - 基于 uni-app x 的移动端 UI 组件库",
5
5
  "main": "index.uts",
6
6
  "module": "index.uts",
@@ -61,9 +61,6 @@
61
61
  .top-1\/2 {
62
62
  top: 50%
63
63
  }
64
- .top-2 {
65
- top: 8px
66
- }
67
64
  .top-3 {
68
65
  top: 12px
69
66
  }
@@ -93,10 +90,6 @@
93
90
  margin-left: auto;
94
91
  margin-right: auto
95
92
  }
96
- .mx-sm {
97
- margin-left: 8px;
98
- margin-right: 8px
99
- }
100
93
  .my-0 {
101
94
  margin-top: 0px;
102
95
  margin-bottom: 0px
@@ -170,9 +163,6 @@
170
163
  .mt-3 {
171
164
  margin-top: 12px
172
165
  }
173
- .mt-4 {
174
- margin-top: 16px
175
- }
176
166
  .mt-5 {
177
167
  margin-top: 20px
178
168
  }
@@ -332,9 +322,6 @@
332
322
  .min-w-10 {
333
323
  min-width: 40px
334
324
  }
335
- .min-w-7 {
336
- min-width: 28px
337
- }
338
325
  .min-w-theme {
339
326
  min-width: 74px
340
327
  }
@@ -797,10 +784,6 @@
797
784
  padding-left: 4px;
798
785
  padding-right: 4px
799
786
  }
800
- .px-10 {
801
- padding-left: 40px;
802
- padding-right: 40px
803
- }
804
787
  .px-2 {
805
788
  padding-left: 8px;
806
789
  padding-right: 8px
@@ -809,10 +792,6 @@
809
792
  padding-left: 10px;
810
793
  padding-right: 10px
811
794
  }
812
- .px-2xl {
813
- padding-left: 32px;
814
- padding-right: 32px
815
- }
816
795
  .px-3 {
817
796
  padding-left: 12px;
818
797
  padding-right: 12px
@@ -825,10 +804,6 @@
825
804
  padding-left: 20px;
826
805
  padding-right: 20px
827
806
  }
828
- .px-7 {
829
- padding-left: 28px;
830
- padding-right: 28px
831
- }
832
807
  .px-8 {
833
808
  padding-left: 32px;
834
809
  padding-right: 32px
@@ -837,10 +812,6 @@
837
812
  padding-left: 16px;
838
813
  padding-right: 16px
839
814
  }
840
- .px-xl {
841
- padding-left: 24px;
842
- padding-right: 24px
843
- }
844
815
  .py-0\.5 {
845
816
  padding-top: 2px;
846
817
  padding-bottom: 2px
@@ -881,10 +852,6 @@
881
852
  padding-top: 16px;
882
853
  padding-bottom: 16px
883
854
  }
884
- .py-5 {
885
- padding-top: 20px;
886
- padding-bottom: 20px
887
- }
888
855
  .py-8 {
889
856
  padding-top: 32px;
890
857
  padding-bottom: 32px
@@ -893,10 +860,6 @@
893
860
  padding-top: 16px;
894
861
  padding-bottom: 16px
895
862
  }
896
- .py-md {
897
- padding-top: 12px;
898
- padding-bottom: 12px
899
- }
900
863
  .py-sm {
901
864
  padding-top: 8px;
902
865
  padding-bottom: 8px
@@ -1007,9 +970,6 @@
1007
970
  .leading-6 {
1008
971
  line-height: 24px
1009
972
  }
1010
- .leading-7 {
1011
- line-height: 28px
1012
- }
1013
973
  .leading-\[1\.5\] {
1014
974
  line-height: 1.5
1015
975
  }
@@ -158,7 +158,7 @@ export function generateStorageKey(prefix: string, key: string): string {
158
158
  }
159
159
 
160
160
  // 检查 存储支持
161
- export function checkStorageSupport(storageType: 'sync'): boolean {
161
+ export function checkStorageSupport(_storageType: 'sync'): boolean {
162
162
  try {
163
163
  // uni-app x 天然支持同步存储
164
164
  const testKey = '__storage_test__'
@@ -1,297 +0,0 @@
1
- # Tabs 标签页组件
2
-
3
- 类似 Ant Design 的 Tabs 标签页组件,支持多种样式和功能。
4
-
5
- ## 基本用法
6
-
7
- ```vue
8
- <script setup>
9
- import Tabs from "@/components/Tabs";
10
- import type { TabItem } from "@/components/Tabs";
11
-
12
- const tabItems: TabItem[] = [
13
- { key: "1", label: "选项卡 1" },
14
- { key: "2", label: "选项卡 2" },
15
- { key: "3", label: "选项卡 3" },
16
- ];
17
-
18
- const activeKey = ref("1");
19
- </script>
20
-
21
- <template>
22
- <Tabs :items="tabItems" v-model="activeKey">
23
- <view v-if="activeKey === '1'">内容 1</view>
24
- <view v-if="activeKey === '2'">内容 2</view>
25
- <view v-if="activeKey === '3'">内容 3</view>
26
- </Tabs>
27
- </template>
28
- ```
29
-
30
- ## 卡片式标签页
31
-
32
- ```vue
33
- <template>
34
- <Tabs :items="tabItems" type="card" v-model="activeKey">
35
- <view v-if="activeKey === '1'">卡片内容 1</view>
36
- <view v-if="activeKey === '2'">卡片内容 2</view>
37
- </Tabs>
38
- </template>
39
- ```
40
-
41
- ## 带徽标和图标
42
-
43
- ```vue
44
- <script setup>
45
- const tabItems: TabItem[] = [
46
- { key: "1", label: "消息", icon: "💬", badge: 5 },
47
- { key: "2", label: "通知", icon: "🔔", badge: 99 },
48
- { key: "3", label: "设置", icon: "⚙️" },
49
- ];
50
- </script>
51
-
52
- <template>
53
- <Tabs :items="tabItems" />
54
- </template>
55
- ```
56
-
57
- ## 禁用选项卡
58
-
59
- ```vue
60
- <script setup>
61
- const tabItems: TabItem[] = [
62
- { key: "1", label: "可用选项" },
63
- { key: "2", label: "禁用选项", disabled: true },
64
- { key: "3", label: "可用选项" },
65
- ];
66
- </script>
67
- ```
68
-
69
- ## 居中显示
70
-
71
- ```vue
72
- <template>
73
- <Tabs :items="tabItems" centered />
74
- </template>
75
- ```
76
-
77
- ## 自定义颜色
78
-
79
- ```vue
80
- <template>
81
- <Tabs :items="tabItems" active-color="#52c41a" inactive-color="#999999" />
82
- </template>
83
- ```
84
-
85
- ### 颜色变量机制
86
-
87
- 组件内部使用 CSS 变量统一管理颜色,支持以下变量:
88
-
89
- - `--active-color`:激活标签的主颜色(控制文字、边框、滑块颜色)
90
- - `--active-color-rgb`:激活颜色的 RGB 值(用于生成透明背景)
91
-
92
- 组件会自动将 `activeColor` prop 转换为对应的 CSS 变量,无需手动设置。
93
-
94
- **card 类型的增强效果:**
95
-
96
- card 类型的激活标签会自动应用:
97
-
98
- - 透明背景色:`rgba(var(--active-color-rgb), 0.1)`
99
- - 轻微上移效果:`translateY(-2px)`
100
- - 动态阴影:`0 4px 12px rgba(var(--active-color-rgb), 0.2)`
101
-
102
- ## 不同尺寸
103
-
104
- ```vue
105
- <template>
106
- <!-- 小号 -->
107
- <Tabs :items="tabItems" size="small" />
108
-
109
- <!-- 中号(默认) -->
110
- <Tabs :items="tabItems" size="medium" />
111
-
112
- <!-- 大号 -->
113
- <Tabs :items="tabItems" size="large" />
114
- </template>
115
- ```
116
-
117
- ## 使用插槽自定义内容
118
-
119
- ```vue
120
- <template>
121
- <Tabs :items="tabItems" v-model="activeKey">
122
- <template #default="{ activeKey }">
123
- <view class="p-4"> 当前激活: {{ activeKey }} </view>
124
- </template>
125
- </Tabs>
126
- </template>
127
- ```
128
-
129
- ## API
130
-
131
- ### TabItem 类型
132
-
133
- | 属性 | 说明 | 类型 | 默认值 |
134
- | -------- | ---------------- | ------------------ | ------- |
135
- | key | 唯一标识(必填) | `string` | - |
136
- | label | 标签标题(必填) | `string` | - |
137
- | disabled | 是否禁用 | `boolean` | `false` |
138
- | badge | 徽标数字 | `number \| string` | - |
139
- | icon | 自定义图标 | `string` | - |
140
-
141
- ### Props
142
-
143
- | 属性 | 说明 | 类型 | 默认值 |
144
- | ----------------------------- | ------------------ | -------------------------------- | ------------ |
145
- | items | 选项卡数据 | `TabItem[]` | `[]` |
146
- | activeKey / v-model:activeKey | 当前激活的 tab key | `string` | - |
147
- | defaultActiveKey | 默认激活的 tab key | `string` | 第一项的 key |
148
- | type | 选项卡类型 | `'line' \| 'card'` | `'line'` |
149
- | tabPosition | 选项卡位置 | `'top' \| 'bottom'` | `'top'` |
150
- | centered | 是否居中显示 | `boolean` | `false` |
151
- | scrollable | 是否可滑动 | `boolean` | `true` |
152
- | activeColor | 激活标签的颜色 | `string` | `'#1677ff'` |
153
- | inactiveColor | 未激活标签的颜色 | `string` | `'#666666'` |
154
- | size | 标签大小 | `'small' \| 'medium' \| 'large'` | `'medium'` |
155
- | animated | 是否显示动画 | `boolean` | `true` |
156
-
157
- ### Events
158
-
159
- | 事件名 | 说明 | 回调参数 |
160
- | ---------------- | ------------------------- | ------------------------------ |
161
- | change | 切换标签时触发 | `(key: string)` |
162
- | update:activeKey | 当前激活的 tab 改变时触发 | `(key: string)` |
163
- | tabClick | 点击标签时触发 | `(key: string, item: TabItem)` |
164
-
165
- ### Slots
166
-
167
- | 插槽名 | 说明 | 参数 |
168
- | ------- | -------- | ----------------------- |
169
- | default | 内容区域 | `{ activeKey: string }` |
170
-
171
- ## 完整示例
172
-
173
- ```vue
174
- <script setup>
175
- import Tabs from "@/components/Tabs";
176
- import type { TabItem } from "@/components/Tabs";
177
-
178
- const tabItems: TabItem[] = [
179
- { key: "tab1", label: "首页", icon: "🏠" },
180
- { key: "tab2", label: "消息", icon: "💬", badge: 5 },
181
- { key: "tab3", label: "禁用", disabled: true },
182
- { key: "tab4", label: "设置", icon: "⚙️" },
183
- ];
184
-
185
- const activeKey = ref("tab1");
186
-
187
- const handleChange = (key: string) => {
188
- console.log("切换到:", key);
189
- };
190
-
191
- const handleTabClick = (key: string, item: TabItem) => {
192
- console.log("点击标签:", key, item);
193
- };
194
- </script>
195
-
196
- <template>
197
- <view class="page">
198
- <Tabs
199
- :items="tabItems"
200
- v-model="activeKey"
201
- type="line"
202
- size="medium"
203
- active-color="#1677ff"
204
- @change="handleChange"
205
- @tab-click="handleTabClick"
206
- >
207
- <view class="tab-content">
208
- <view v-if="activeKey === 'tab1'" class="p-4">
209
- <text>首页内容</text>
210
- </view>
211
- <view v-if="activeKey === 'tab2'" class="p-4">
212
- <text>消息内容</text>
213
- </view>
214
- <view v-if="activeKey === 'tab4'" class="p-4">
215
- <text>设置内容</text>
216
- </view>
217
- </view>
218
- </Tabs>
219
- </view>
220
- </template>
221
-
222
- <style lang="scss" scoped>
223
- .page {
224
- padding: 32px;
225
- }
226
-
227
- .tab-content {
228
- background-color: #ffffff;
229
- min-height: 400px;
230
- }
231
- </style>
232
- ```
233
-
234
- ## 注意事项
235
-
236
- 1. 每个 `TabItem` 的 `key` 必须唯一
237
- 2. `v-model:activeKey` 支持双向绑定
238
- 3. 滑动指示器动画仅在 `type="line"` 且 `animated=true` 时生效
239
- 4. 使用 `scrollable` 时,标签超出屏幕宽度会自动滚动
240
- 5. `tabPosition="bottom"` 可将标签放置在底部(适合底部导航场景)
241
-
242
- ```ts
243
- // 选项项
244
-
245
- /**
246
-
247
- * TabItem 类型定义
248
-
249
- */
250
-
251
- export type TabItem = {
252
- /** 唯一标识 */
253
-
254
- key: string;
255
-
256
- /** 标签标题 */
257
-
258
- label: string;
259
-
260
- /** 是否禁用 */
261
-
262
- disabled?: boolean;
263
-
264
- /** 徽标数字 */
265
-
266
- badge?: number | string;
267
-
268
- /** 自定义图标 */
269
-
270
- icon?: string;
271
- };
272
-
273
- type TabsProps = {
274
- /** 选项卡数据 */
275
- items: TabItem[];
276
- /** 当前激活的 tab key */
277
- activeKey?: string;
278
- /** 默认激活的 tab key */
279
- defaultActiveKey?: string;
280
- /** 选项卡类型 */
281
- type?: "line" | "card";
282
- /** 选项卡位置 */
283
- tabPosition?: "top" | "bottom";
284
- /** 是否居中显示 */
285
- centered?: boolean;
286
- /** 是否可滑动 */
287
- scrollable?: boolean;
288
- /** 激活标签的颜色 */
289
- activeColor?: string;
290
- /** 未激活标签的颜色 */
291
- inactiveColor?: string;
292
- /** 标签大小 */
293
- size?: "small" | "medium" | "large";
294
- /** 是否显示动画 */
295
- animated?: boolean;
296
- };
297
- ```
@@ -1,354 +0,0 @@
1
- <script setup lang="uts">
2
- import { computed, nextTick, onMounted, ref } from 'vue'
3
- import type { Ref } from 'vue'
4
- import type { TagItem, TTagsProps } from './type.uts'
5
-
6
- type TagsModelValue = string | number | null
7
-
8
- // v-model 绑定当前激活 key
9
- const model = defineModel({ default: null }) as Ref<TagsModelValue>
10
-
11
- const props = withDefaults(defineProps<TTagsProps>(), {
12
- items: () => [],
13
- type: 'line',
14
- tabPosition: 'top',
15
- centered: false,
16
- scrollable: true,
17
- activeColor: '#00bba7',
18
- inactiveColor: '#666666',
19
- size: 'medium',
20
- animated: true,
21
- })
22
-
23
- const emit = defineEmits<{
24
- /** 切换标签时触发 */
25
- change: [key: string | number]
26
- /** 当前激活的 tag 改变时触发 */
27
- 'update:activeKey': [key: string | number]
28
- /** 点击标签时触发 */
29
- tagClick: [key: string | number, item: TagItem]
30
- }>()
31
-
32
- const hasItemIcon = (item: TagItem): boolean => {
33
- return item.icon != null && item.icon !== ''
34
- }
35
-
36
- const hasItemBadge = (item: TagItem): boolean => {
37
- return item.badge != null && item.badge !== ''
38
- }
39
-
40
- /** 获取默认激活项的 key */
41
- const getDefaultKey = (): string | number => {
42
- const defaultActiveKey = props.defaultActiveKey
43
- if (defaultActiveKey != null && defaultActiveKey !== '') {
44
- return defaultActiveKey
45
- }
46
- if (props.items.length > 0) {
47
- const firstEnabled = props.items.find((item: TagItem): boolean => item.disabled !== true)
48
- if (firstEnabled != null) {
49
- return firstEnabled.key
50
- }
51
- return props.items[0].key
52
- }
53
- return ''
54
- }
55
-
56
- /** 初始化默认激活项 */
57
- onMounted(() => {
58
- if (model.value === null || model.value === '') {
59
- model.value = getDefaultKey()
60
- }
61
- })
62
-
63
- const isItemActive = (item: TagItem): boolean => {
64
- const currentKey = model.value
65
- if (currentKey === null || currentKey === '') {
66
- return false
67
- }
68
- if (item.key === null || item.key === '') {
69
- return false
70
- }
71
- return item.key === currentKey
72
- }
73
-
74
- /** 计算当前激活项的索引 */
75
- const activeIndex = computed((): number => {
76
- return props.items.findIndex(item => isItemActive(item))
77
- })
78
-
79
- const showNavLine = computed((): boolean => {
80
- return props.type === 'line' && props.animated === true && activeIndex.value >= 0
81
- })
82
-
83
- /** 获取当前激活项的 DOM 元素 */
84
- const navListRef = ref<UniElement | null>(null)
85
-
86
- /** 将十六进制颜色转换为 RGB */
87
- const hexToRgb = (hex: string): string => {
88
- // 移除 # 号
89
- hex = hex.replace('#', '')
90
-
91
- // 处理缩写形式 (#fff)
92
- if (hex.length === 3) {
93
- const r = hex.substring(0, 1)
94
- const g = hex.substring(1, 2)
95
- const b = hex.substring(2, 3)
96
- hex = r + r + g + g + b + b
97
- }
98
-
99
- // 转换为 RGB
100
- const r = parseInt(hex.substring(0, 2), 16)
101
- const g = parseInt(hex.substring(2, 4), 16)
102
- const b = parseInt(hex.substring(4, 6), 16)
103
-
104
- return `${r}, ${g}, ${b}`
105
- }
106
-
107
- /** 计算激活颜色的 RGB 值 */
108
- const activeColorRgb = computed(() => {
109
- // 如果是 rgba 格式,直接提取 RGB 部分
110
- if (props.activeColor.startsWith('rgb')) {
111
- const match = props.activeColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
112
- if (match != null) {
113
- return `${match[1]}, ${match[2]}, ${match[3]}`
114
- }
115
- }
116
- // 如果是十六进制颜色,转换为 RGB
117
- return hexToRgb(props.activeColor)
118
- })
119
-
120
- const containerClass = computed((): string => {
121
- return 'w-full overflow-hidden bg-white'
122
- })
123
-
124
- const getNavListClass = (isBottom: boolean): string => {
125
- const classes: string[] = [
126
- 'relative',
127
- 'flex',
128
- 'flex-row',
129
- 'items-center',
130
- 'overflow-x-auto',
131
- 'overflow-y-hidden',
132
- 'whitespace-nowrap'
133
- ]
134
-
135
- if (props.type === 'card') {
136
- classes.push('rounded-t-xl', 'bg-action-divider', 'px-xl', 'py-lg')
137
- } else {
138
- classes.push('py-5')
139
- }
140
-
141
- if (props.centered) {
142
- classes.push('justify-center')
143
- }
144
-
145
- if (isBottom) {
146
- classes.push('border-t', 'border-divider')
147
- }
148
-
149
- return classes.join(' ')
150
- }
151
-
152
- const getItemPaddingClass = (): string => {
153
- if (props.size === 'small') {
154
- return 'px-5 py-md'
155
- }
156
-
157
- if (props.size === 'large') {
158
- return 'px-10 py-5'
159
- }
160
-
161
- return props.type === 'card' ? 'px-2xl py-lg' : 'px-7 py-lg'
162
- }
163
-
164
- const navLabelClass = computed((): string => {
165
- const classes: string[] = ['transition-all', 'duration-300']
166
-
167
- if (props.size === 'small') {
168
- classes.push('text-2xl')
169
- } else if (props.size === 'large') {
170
- classes.push('text-hero')
171
- } else {
172
- classes.push('text-7')
173
- }
174
-
175
- return classes.join(' ')
176
- })
177
-
178
- const navLineClass = computed((): string => {
179
- const classes: string[] = ['absolute', 'h-1', 'rounded-2', 'transition-all', 'duration-300']
180
-
181
- if (props.tabPosition === 'bottom') {
182
- classes.push('top-0')
183
- } else {
184
- classes.push('bottom-0')
185
- }
186
-
187
- return classes.join(' ')
188
- })
189
-
190
- const navLineStyle = computed((): string => {
191
- return `background-color: ${props.activeColor}; opacity: ${props.animated ? 1 : 0};`
192
- })
193
-
194
- const getNavItemClass = (item: TagItem): string => {
195
- const classes: string[] = [
196
- 'relative',
197
- 'inline-flex',
198
- 'items-center',
199
- 'justify-center',
200
- 'origin-center',
201
- 'transition-all',
202
- 'duration-300',
203
- getItemPaddingClass()
204
- ]
205
-
206
- if (props.type === 'card') {
207
- classes.push('mx-sm', 'rounded-lg', 'border', 'border-border-soft', 'bg-white')
208
- }
209
-
210
- if (isItemActive(item) && props.type !== 'card') {
211
- classes.push('scale-105')
212
- }
213
-
214
- if (item.disabled === true) {
215
- classes.push('cursor-not-allowed', 'opacity-50')
216
- } else {
217
- classes.push('cursor-pointer')
218
- }
219
-
220
- return classes.join(' ')
221
- }
222
-
223
- const getNavItemStyle = (item: TagItem): string => {
224
- const styles: string[] = []
225
-
226
- if (props.type === 'card' && isItemActive(item)) {
227
- styles.push(`background-color: rgba(${activeColorRgb.value}, 0.1)`)
228
- styles.push(`border-color: ${props.activeColor}`)
229
- styles.push(`box-shadow: 0 4px 12px rgba(${activeColorRgb.value}, 0.2)`)
230
- styles.push('transform: translateY(-2px)')
231
- }
232
-
233
- return styles.join('; ')
234
- }
235
-
236
- /** 点击切换 tab */
237
- const handleClick = (item: TagItem, _index: number) => {
238
- if (item.disabled === true) return
239
-
240
- model.value = item.key
241
- emit('update:activeKey', item.key)
242
- emit('change', item.key)
243
- emit('tagClick', item.key, item)
244
-
245
- // 滚动到激活项
246
- if (props.scrollable) {
247
- nextTick(() => {
248
- })
249
- }
250
- }
251
-
252
- const getNavLabelStyle = (item: TagItem): string => {
253
- return `color: ${isItemActive(item) ? props.activeColor : props.inactiveColor}`
254
- }
255
-
256
- const badgeClass = computed((): string => {
257
- return 'absolute right-3 top-2 min-w-7 rounded-14 bg-danger px-2 text-center text-5 leading-7 text-white'
258
- })
259
-
260
- const badgeStyle = computed((): string => {
261
- return 'height: 28px; transform: translateX(8px);'
262
- })
263
-
264
- </script>
265
-
266
- <template>
267
- <view
268
- :class="containerClass"
269
- >
270
- <scroll-view
271
- v-if="tabPosition === 'top'"
272
- ref="navListRef"
273
- class="w-full whitespace-nowrap"
274
- :scroll-x="true"
275
- :show-scrollbar="false"
276
- >
277
- <view :class="getNavListClass(false)">
278
- <view
279
- v-for="(item, index) in items"
280
- :key="item.key"
281
- :class="getNavItemClass(item)"
282
- :style="getNavItemStyle(item)"
283
- :ref="'navItem' + index"
284
- @click="handleClick(item, index)"
285
- >
286
- <image v-if="hasItemIcon(item)" class="mr-sm h-9 w-9 transition-all duration-300" :src="item.icon" mode="aspectFit" />
287
-
288
- <text
289
- :class="navLabelClass"
290
- :style="getNavLabelStyle(item)"
291
- >
292
- {{ item.label }}
293
- </text>
294
-
295
- <view
296
- v-if="hasItemBadge(item)"
297
- :class="badgeClass"
298
- :style="badgeStyle"
299
- >
300
- {{ item.badge }}
301
- </view>
302
- </view>
303
- </view>
304
- </scroll-view>
305
-
306
- <!-- 内容插槽 -->
307
- <view class="bg-white">
308
- <slot :active-key="model"></slot>
309
- </view>
310
-
311
- <!-- 底部标签 -->
312
- <scroll-view
313
- v-if="tabPosition === 'bottom'"
314
- ref="navListRef"
315
- class="w-full whitespace-nowrap"
316
- :scroll-x="true"
317
- :show-scrollbar="false"
318
- >
319
- <view :class="getNavListClass(true)">
320
- <view
321
- v-for="(item, index) in items"
322
- :key="item.key"
323
- :class="getNavItemClass(item)"
324
- :style="getNavItemStyle(item)"
325
- @click="handleClick(item, index)"
326
- >
327
- <image v-if="hasItemIcon(item)" class="mr-sm h-9 w-9 transition-all duration-300" :src="item.icon" mode="aspectFit" />
328
-
329
- <text
330
- :class="navLabelClass"
331
- :style="getNavLabelStyle(item)"
332
- >
333
- {{ item.label }}
334
- </text>
335
-
336
- <view
337
- v-if="hasItemBadge(item)"
338
- :class="badgeClass"
339
- :style="badgeStyle"
340
- >
341
- {{ item.badge }}
342
- </view>
343
- </view>
344
-
345
- <!-- 滑动指示器 -->
346
- <view
347
- v-if="showNavLine"
348
- :class="navLineClass"
349
- :style="navLineStyle"
350
- ></view>
351
- </view>
352
- </scroll-view>
353
- </view>
354
- </template>
@@ -1,117 +0,0 @@
1
- /**
2
- * Tags 组件类型定义
3
- */
4
-
5
- /**
6
- * Tag 选项项
7
- */
8
- export type TagItem = {
9
- key: string | number
10
- label: string
11
- disabled?: boolean
12
- badge?: number | string
13
- icon?: string
14
- }
15
-
16
- /**
17
- * 标签类型
18
- */
19
- export type TagType = 'line' | 'card'
20
-
21
- /**
22
- * 标签位置
23
- */
24
- export type TagPosition = 'top' | 'bottom'
25
-
26
- /**
27
- * 标签大小
28
- */
29
- export type TagSize = 'small' | 'medium' | 'large'
30
-
31
- /**
32
- * Tags Props 接口
33
- */
34
- export type TTagsProps = {
35
- /**
36
- * 选项卡数据
37
- */
38
- items: TagItem[]
39
-
40
- /**
41
- * 当前激活的 tag key
42
- */
43
- activeKey?: string | number
44
-
45
- /**
46
- * 默认激活的 tag key
47
- */
48
- defaultActiveKey?: string | number
49
-
50
- /**
51
- * 选项卡类型
52
- * @default 'line'
53
- */
54
- type?: TagType
55
-
56
- /**
57
- * 选项卡位置
58
- * @default 'top'
59
- */
60
- tabPosition?: TagPosition
61
-
62
- /**
63
- * 是否居中显示
64
- * @default false
65
- */
66
- centered?: boolean
67
-
68
- /**
69
- * 是否可滑动
70
- * @default true
71
- */
72
- scrollable?: boolean
73
-
74
- /**
75
- * 激活标签的颜色
76
- * @default '#00bba7'
77
- */
78
- activeColor?: string
79
-
80
- /**
81
- * 未激活标签的颜色
82
- * @default '#666666'
83
- */
84
- inactiveColor?: string
85
-
86
- /**
87
- * 标签大小
88
- * @default 'medium'
89
- */
90
- size?: TagSize
91
-
92
- /**
93
- * 是否显示动画
94
- * @default true
95
- */
96
- animated?: boolean
97
- }
98
-
99
- /**
100
- * Tags 事件接口
101
- */
102
- export interface TTagsEmits {
103
- /**
104
- * 切换标签时触发
105
- */
106
- change: (key: string | number) => void
107
-
108
- /**
109
- * 当前激活的 tag 改变时触发
110
- */
111
- 'update:activeKey': (key: string | number) => void
112
-
113
- /**
114
- * 点击标签时触发
115
- */
116
- tagClick: (key: string | number, item: TagItem) => void
117
- }