tang-ui-x 1.3.6 → 1.3.7
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.
- package/components/TCard/index.uvue +45 -47
- package/components/TNoticeBar/type.uts +1 -1
- package/components/Tabs/index.uvue +1 -1
- package/package.json +1 -1
- package/static/tailwind.css +0 -40
- package/components/Tags/README.md +0 -297
- package/components/Tags/index.uvue +0 -354
- package/components/Tags/type.uts +0 -117
|
@@ -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
|
-
<
|
|
127
|
-
<slot name="footer"></slot>
|
|
128
|
-
</view>
|
|
126
|
+
<slot name="footer"/>
|
|
129
127
|
</view>
|
|
130
128
|
</template>
|
package/package.json
CHANGED
package/static/tailwind.css
CHANGED
|
@@ -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
|
}
|
|
@@ -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>
|
package/components/Tags/type.uts
DELETED
|
@@ -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
|
-
}
|