chain-saasui 1.0.10-alpha.2 → 1.0.10-alpha.3
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/css/button.scss +9 -0
- package/components/css/index.scss +2 -0
- package/components/css/pagetable.scss +6 -0
- package/components/css/sPageTabs.scss +143 -0
- package/components/css/segmented.scss +71 -0
- package/components/src/SButton/Button/index.vue +8 -1
- package/components/src/SIcon/Icon/index.vue +20 -0
- package/components/src/SPage/PageTabs/index.vue +322 -0
- package/components/src/SPage/Pagetable.vue +5 -3
- package/components/src/SPage/index.js +1 -0
- package/components/src/SSegmented/segmented.vue +250 -0
- package/components/src/SSegmented/test-show.html +363 -0
- package/components/src/STable/STablecolumn.vue +1 -4
- package/components/src/STable/index.vue +35 -3
- package/components/src/index.js +4 -0
- package/lib/css/index.css +1 -1
- package/lib/saasui.common.js +1 -1
- package/package.json +1 -1
|
@@ -22,3 +22,12 @@
|
|
|
22
22
|
padding-right: 8px;
|
|
23
23
|
padding-left: 8px;
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
.saas-button-border__primary {
|
|
27
|
+
border-color: var(--saas-success-600);
|
|
28
|
+
color: var(--saas-success-600);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.saas-button-border__primary:hover {
|
|
32
|
+
background-color: var(--saas-success-50);
|
|
33
|
+
}
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
border-radius: 0;
|
|
44
44
|
border-top: 1px solid #dbdee7;
|
|
45
45
|
}
|
|
46
|
+
.saas-page-table--hasButton .saas-page-content__box {
|
|
47
|
+
overflow-y: auto;
|
|
48
|
+
}
|
|
46
49
|
|
|
47
50
|
.saas-page-table__sub {
|
|
48
51
|
margin-top: 8px;
|
|
@@ -89,6 +92,9 @@
|
|
|
89
92
|
flex: 1;
|
|
90
93
|
min-height: 300px;
|
|
91
94
|
}
|
|
95
|
+
.saas-page-content__box {
|
|
96
|
+
height: 100%;
|
|
97
|
+
}
|
|
92
98
|
.saas-page-table__footer {
|
|
93
99
|
flex-shrink: 0;
|
|
94
100
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
.saas-stabs {
|
|
2
|
+
width: 100%;
|
|
3
|
+
background: #fff;
|
|
4
|
+
border-bottom: 1px solid #E4E6ED;
|
|
5
|
+
position: relative; /* 为指示线提供定位上下文 */
|
|
6
|
+
padding-bottom: 8px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.saas-stabs--no-border {
|
|
10
|
+
border-bottom: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.saas-stabs-container {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: space-between;
|
|
17
|
+
width: 100%;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 滚动容器
|
|
21
|
+
.saas-stabs-scroll-container {
|
|
22
|
+
flex: 1;
|
|
23
|
+
min-width: 0;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
position: relative;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.saas-stabs-nav {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
padding: 0;
|
|
32
|
+
overflow-x: auto;
|
|
33
|
+
overflow-y: hidden;
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
position: relative;
|
|
36
|
+
min-width: 100%;
|
|
37
|
+
|
|
38
|
+
// 隐藏滚动条
|
|
39
|
+
scrollbar-width: none; /* Firefox */
|
|
40
|
+
-ms-overflow-style: none; /* IE and Edge */
|
|
41
|
+
|
|
42
|
+
&::-webkit-scrollbar {
|
|
43
|
+
display: none; /* Chrome, Safari and Opera */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 滑动按钮
|
|
48
|
+
.saas-stabs-scroll-btn {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
width: 32px;
|
|
53
|
+
height: 32px;
|
|
54
|
+
background: #fff;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
color: #666;
|
|
57
|
+
font-size: 14px;
|
|
58
|
+
transition: all 0.3s ease;
|
|
59
|
+
flex-shrink: 0;
|
|
60
|
+
z-index: 2;
|
|
61
|
+
|
|
62
|
+
&:hover {
|
|
63
|
+
color: var(--saas-success);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&:active {
|
|
67
|
+
transform: scale(0.95);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
&--left {
|
|
71
|
+
margin-right: 8px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&--right {
|
|
75
|
+
margin-left: 8px;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.saas-stabs-extra {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
margin-left: 16px;
|
|
83
|
+
flex-shrink: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.stabs-nav-item {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
color: var(--saas-gray-600);
|
|
91
|
+
font-size: 14px;
|
|
92
|
+
transition: all 0.3s ease;
|
|
93
|
+
user-select: none;
|
|
94
|
+
white-space: nowrap;
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.stabs-nav-item:hover {
|
|
99
|
+
color: var(--saas-success);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.stabs-nav-item__active {
|
|
103
|
+
color: var(--saas-success);
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.stabs-nav-item__active:hover {
|
|
108
|
+
background-color: transparent;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.stabs-nav-item__disabled {
|
|
112
|
+
color: var(--saas-gray-400);
|
|
113
|
+
cursor: not-allowed;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.stabs-nav-item__disabled:hover {
|
|
117
|
+
color: var(--saas-gray-400);
|
|
118
|
+
background-color: transparent;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.stabs-nav-item-text {
|
|
122
|
+
height: 22px;
|
|
123
|
+
}
|
|
124
|
+
/* 滑动指示线 */
|
|
125
|
+
.stabs-nav-indicator {
|
|
126
|
+
position: absolute;
|
|
127
|
+
bottom: 0;
|
|
128
|
+
left: 0;
|
|
129
|
+
height: 2px;
|
|
130
|
+
background-color: var(--saas-success);
|
|
131
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
132
|
+
border-radius: 1px;
|
|
133
|
+
z-index: 1;
|
|
134
|
+
pointer-events: none; /* 确保不影响点击事件 */
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* 响应式设计 */
|
|
138
|
+
@media (max-width: 768px) {
|
|
139
|
+
.stabs-nav-item {
|
|
140
|
+
padding: 10px 0;
|
|
141
|
+
font-size: 13px;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
.saas-segmented {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
position: relative;
|
|
4
|
+
padding: 1px;
|
|
5
|
+
border-radius: 6px 6px 6px 6px;
|
|
6
|
+
border: 1px solid var(--saas-gray-border);
|
|
7
|
+
background-color: #ffffff;
|
|
8
|
+
width: fit-content;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.saas-segmented-slider {
|
|
12
|
+
position: absolute;
|
|
13
|
+
top: 1px;
|
|
14
|
+
left: 1px;
|
|
15
|
+
height: calc(100% - 2px);
|
|
16
|
+
background-color: var(--saas-success-100);
|
|
17
|
+
border-radius: 4px;
|
|
18
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
19
|
+
z-index: 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.saas-segmented-item {
|
|
23
|
+
position: relative;
|
|
24
|
+
padding: 6px 8px;
|
|
25
|
+
line-height: 1;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
z-index: 2;
|
|
28
|
+
transition: color 0.3s ease;
|
|
29
|
+
font-size: 14px;
|
|
30
|
+
color: var(--saas-gray-500);
|
|
31
|
+
white-space: nowrap;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* 图标样式 */
|
|
35
|
+
.saas-segmented-item__icon {
|
|
36
|
+
display: inline-block;
|
|
37
|
+
line-height: 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.saas-segmented-item__text {
|
|
41
|
+
display: inline-block;
|
|
42
|
+
line-height: 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
.saas-segmented-item__active {
|
|
47
|
+
color: var(--saas-success);
|
|
48
|
+
font-weight: 500;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* 带图标的项目样式 */
|
|
52
|
+
.saas-segmented-item__with-icon {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
gap: 4px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* 只显示图标的项目样式 */
|
|
60
|
+
.saas-segmented-item__icon-only {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
min-width: 32px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* 图标和文字的垂直对齐 */
|
|
68
|
+
.saas-segmented-item__with-icon .saas-segmented-item__icon,
|
|
69
|
+
.saas-segmented-item__with-icon .saas-segmented-item__text {
|
|
70
|
+
vertical-align: middle;
|
|
71
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!-- TextElButton.vue -->
|
|
2
2
|
<template>
|
|
3
|
-
<el-button :class="['saas-button', getButtonType]" v-bind="$attrs" v-on="filterListeners" @click="handleClick" :type="buttonTypeComputed">
|
|
3
|
+
<el-button :class="['saas-button', getButtonType, themeClass]" v-bind="$attrs" v-on="filterListeners" @click="handleClick" :type="buttonTypeComputed">
|
|
4
4
|
<slot />
|
|
5
5
|
</el-button>
|
|
6
6
|
</template>
|
|
@@ -14,11 +14,18 @@ export default {
|
|
|
14
14
|
type: String,
|
|
15
15
|
default: 'default',
|
|
16
16
|
},
|
|
17
|
+
border: {
|
|
18
|
+
type: String,
|
|
19
|
+
default: '',
|
|
20
|
+
},
|
|
17
21
|
},
|
|
18
22
|
computed: {
|
|
19
23
|
buttonTypeComputed() {
|
|
20
24
|
return this.type === 'icon' ? 'default' : this.type;
|
|
21
25
|
},
|
|
26
|
+
themeClass() {
|
|
27
|
+
if (this.border) return `saas-button-border__${this.border}`;
|
|
28
|
+
},
|
|
22
29
|
getButtonType() {
|
|
23
30
|
return this.type ? `saas-button__${this.type}` : '';
|
|
24
31
|
},
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span :class="['saas-icon', icon]" :style="iconStyle"></span>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
export default {
|
|
7
|
+
name: 'SIcon',
|
|
8
|
+
props: {
|
|
9
|
+
icon: { type: String, required: true }, // 传入icon类名,比如 "el-icon-edit"
|
|
10
|
+
size: { type: [Number, String], default: 16 }, // 图标大小,默认16px
|
|
11
|
+
},
|
|
12
|
+
computed: {
|
|
13
|
+
iconStyle() {
|
|
14
|
+
return {
|
|
15
|
+
fontSize: typeof this.size === 'number' ? this.size + 'px' : this.size,
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="saas-stabs" :class="{ 'saas-stabs--no-border': !showBorder }">
|
|
3
|
+
<div class="saas-stabs-container">
|
|
4
|
+
<!-- 左滑动按钮 -->
|
|
5
|
+
<div class="saas-stabs-scroll-btn saas-stabs-scroll-btn--left" v-show="showLeftButton" @click="scrollLeft">
|
|
6
|
+
<i class="el-icon-arrow-left"></i>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<!-- 滚动容器 -->
|
|
10
|
+
<div class="saas-stabs-scroll-container" ref="scrollContainer">
|
|
11
|
+
<div class="saas-stabs-nav" ref="tabsNav">
|
|
12
|
+
<div
|
|
13
|
+
v-for="(tab, index) in tabs"
|
|
14
|
+
v-if="getTabShow(tab)"
|
|
15
|
+
:key="getTabValue(tab) || index"
|
|
16
|
+
class="stabs-nav-item"
|
|
17
|
+
:class="{
|
|
18
|
+
'stabs-nav-item__active': getTabValue(tab) === currentActiveKey,
|
|
19
|
+
'stabs-nav-item__disabled': tab.disabled,
|
|
20
|
+
}"
|
|
21
|
+
:style="{ marginRight: index === tabs.length - 1 ? '0' : typeof gap === 'number' ? gap + 'px' : gap }"
|
|
22
|
+
@click="handleTabClick(tab, index)"
|
|
23
|
+
:ref="`tab-${getTabValue(tab)}`">
|
|
24
|
+
<span class="stabs-nav-item-text">{{ getTabLabel(tab) }}</span>
|
|
25
|
+
<slot name="tab-suffix" :tab="tab" :index="index"></slot>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- 右滑动按钮 -->
|
|
31
|
+
<div class="saas-stabs-scroll-btn saas-stabs-scroll-btn--right" v-show="showRightButton" @click="scrollRight">
|
|
32
|
+
<i class="el-icon-arrow-right"></i>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- 右侧插槽 -->
|
|
36
|
+
<div class="saas-stabs-extra" v-if="$slots.extra">
|
|
37
|
+
<slot name="extra"></slot>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<!-- 滑动指示线 - 移动到容器级别 -->
|
|
41
|
+
<div class="stabs-nav-indicator" ref="indicator" :style="indicatorStyle"></div>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script>
|
|
46
|
+
/**
|
|
47
|
+
* STabs 标签页组件
|
|
48
|
+
*
|
|
49
|
+
* tabs 数组中每个对象支持的属性:
|
|
50
|
+
* - label: 标签显示文本
|
|
51
|
+
* - value: 标签的值(唯一标识)
|
|
52
|
+
* - disabled: 是否禁用(可选)
|
|
53
|
+
* - show: 控制标签是否显示(可选)
|
|
54
|
+
* - 布尔值:true显示,false隐藏
|
|
55
|
+
* - 函数:接收当前tab项作为参数,返回布尔值
|
|
56
|
+
*
|
|
57
|
+
* 示例:
|
|
58
|
+
* tabs: [
|
|
59
|
+
* { label: '标签1', value: 'tab1', show: true },
|
|
60
|
+
* { label: '标签2', value: 'tab2', show: false },
|
|
61
|
+
* { label: '标签3', value: 'tab3', show: (tab) => checkPermission(tab.value) }
|
|
62
|
+
* ]
|
|
63
|
+
*/
|
|
64
|
+
export default {
|
|
65
|
+
name: 'SPageTabs',
|
|
66
|
+
props: {
|
|
67
|
+
// tabs 数据数组
|
|
68
|
+
tabs: {
|
|
69
|
+
type: Array,
|
|
70
|
+
default: () => [],
|
|
71
|
+
},
|
|
72
|
+
// 当前激活的 tab value (支持 v-model)
|
|
73
|
+
value: {
|
|
74
|
+
type: [String, Number],
|
|
75
|
+
default: '',
|
|
76
|
+
},
|
|
77
|
+
// 字段名配置
|
|
78
|
+
fieldNames: {
|
|
79
|
+
type: Object,
|
|
80
|
+
default: () => ({
|
|
81
|
+
label: 'label',
|
|
82
|
+
value: 'value',
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
// 标签之间的间距
|
|
86
|
+
gap: {
|
|
87
|
+
type: [String, Number],
|
|
88
|
+
default: 32,
|
|
89
|
+
},
|
|
90
|
+
// 是否显示下边框
|
|
91
|
+
showBorder: {
|
|
92
|
+
type: Boolean,
|
|
93
|
+
default: true,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
data() {
|
|
97
|
+
return {
|
|
98
|
+
indicatorStyle: {
|
|
99
|
+
transform: 'translateX(0px)',
|
|
100
|
+
width: '0px',
|
|
101
|
+
},
|
|
102
|
+
// 滚动相关状态
|
|
103
|
+
showLeftButton: false,
|
|
104
|
+
showRightButton: false,
|
|
105
|
+
scrollStep: 200, // 每次滚动的距离
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
computed: {
|
|
109
|
+
// 合并默认字段名配置
|
|
110
|
+
mergedFieldNames() {
|
|
111
|
+
return {
|
|
112
|
+
label: 'label',
|
|
113
|
+
value: 'value',
|
|
114
|
+
...this.fieldNames,
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
currentActiveKey() {
|
|
118
|
+
return this.value;
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
methods: {
|
|
122
|
+
// 获取 tab 的显示文本
|
|
123
|
+
getTabLabel(tab) {
|
|
124
|
+
return tab[this.mergedFieldNames.label] || '';
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// 获取 tab 的值
|
|
128
|
+
getTabValue(tab) {
|
|
129
|
+
return tab[this.mergedFieldNames.value];
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// 获取 tab 的显示状态
|
|
133
|
+
getTabShow(tab) {
|
|
134
|
+
// 如果没有show属性,默认显示
|
|
135
|
+
if (tab.show === undefined) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
// 如果show是函数,调用函数并传入当前tab项
|
|
139
|
+
if (typeof tab.show === 'function') {
|
|
140
|
+
return tab.show(tab);
|
|
141
|
+
}
|
|
142
|
+
return Boolean(tab.show);
|
|
143
|
+
},
|
|
144
|
+
handleTabClick(tab, index) {
|
|
145
|
+
if (tab.disabled) return;
|
|
146
|
+
// 触发 tab 点击事件
|
|
147
|
+
this.$emit('tab-click', tab, index);
|
|
148
|
+
// 更新激活的 tab
|
|
149
|
+
const tabValue = this.getTabValue(tab);
|
|
150
|
+
if (tabValue !== this.currentActiveKey) {
|
|
151
|
+
// 支持 v-model (Vue 2)
|
|
152
|
+
this.$emit('input', tabValue);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
// 更新指示线位置
|
|
156
|
+
updateIndicator() {
|
|
157
|
+
this.$nextTick(() => {
|
|
158
|
+
if (!this.currentActiveKey || !this.$refs.tabsNav) return;
|
|
159
|
+
|
|
160
|
+
const activeTabRef = this.$refs[`tab-${this.currentActiveKey}`];
|
|
161
|
+
if (!activeTabRef || !activeTabRef[0]) return;
|
|
162
|
+
|
|
163
|
+
const activeTab = activeTabRef[0];
|
|
164
|
+
const scrollContainer = this.$refs.scrollContainer;
|
|
165
|
+
|
|
166
|
+
// 获取各个容器的位置信息
|
|
167
|
+
const tabRect = activeTab.getBoundingClientRect();
|
|
168
|
+
const scrollContainerRect = scrollContainer.getBoundingClientRect();
|
|
169
|
+
|
|
170
|
+
// 计算标签相对于滚动容器的位置
|
|
171
|
+
const left = tabRect.left - scrollContainerRect.left + scrollContainer.scrollLeft;
|
|
172
|
+
const width = tabRect.width;
|
|
173
|
+
|
|
174
|
+
// 计算滚动容器相对于主容器的偏移(包括左侧按钮的宽度)
|
|
175
|
+
const leftButton = document.querySelector('.saas-stabs-scroll-btn--left');
|
|
176
|
+
const leftOffset = leftButton && leftButton.offsetWidth ? leftButton.offsetWidth + 8 : 0; // 8px是margin
|
|
177
|
+
|
|
178
|
+
// 更新指示线样式
|
|
179
|
+
this.indicatorStyle = {
|
|
180
|
+
transform: `translateX(${left + leftOffset}px)`,
|
|
181
|
+
width: `${width}px`,
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// 检查是否需要显示滚动按钮
|
|
187
|
+
checkScrollButtons() {
|
|
188
|
+
this.$nextTick(() => {
|
|
189
|
+
const scrollContainer = this.$refs.scrollContainer;
|
|
190
|
+
const tabsNav = this.$refs.tabsNav;
|
|
191
|
+
|
|
192
|
+
if (!scrollContainer || !tabsNav) return;
|
|
193
|
+
const containerWidth = scrollContainer.clientWidth;
|
|
194
|
+
const navWidth = tabsNav.scrollWidth;
|
|
195
|
+
const scrollLeft = scrollContainer.scrollLeft;
|
|
196
|
+
|
|
197
|
+
// 判断是否需要显示左右按钮
|
|
198
|
+
this.showLeftButton = scrollLeft > 0;
|
|
199
|
+
this.showRightButton = scrollLeft < navWidth - containerWidth;
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// 向左滚动
|
|
204
|
+
scrollLeft() {
|
|
205
|
+
const scrollContainer = this.$refs.scrollContainer;
|
|
206
|
+
if (!scrollContainer) return;
|
|
207
|
+
|
|
208
|
+
const newScrollLeft = Math.max(0, scrollContainer.scrollLeft - this.scrollStep);
|
|
209
|
+
scrollContainer.scrollTo({
|
|
210
|
+
left: newScrollLeft,
|
|
211
|
+
behavior: 'smooth',
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// 向右滚动
|
|
216
|
+
scrollRight() {
|
|
217
|
+
const scrollContainer = this.$refs.scrollContainer;
|
|
218
|
+
const tabsNav = this.$refs.tabsNav;
|
|
219
|
+
if (!scrollContainer || !tabsNav) return;
|
|
220
|
+
|
|
221
|
+
const maxScrollLeft = tabsNav.scrollWidth - scrollContainer.clientWidth;
|
|
222
|
+
const newScrollLeft = Math.min(maxScrollLeft, scrollContainer.scrollLeft + this.scrollStep);
|
|
223
|
+
scrollContainer.scrollTo({
|
|
224
|
+
left: newScrollLeft,
|
|
225
|
+
behavior: 'smooth',
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// 滚动到激活的tab
|
|
230
|
+
scrollToActiveTab() {
|
|
231
|
+
this.$nextTick(() => {
|
|
232
|
+
if (!this.currentActiveKey || !this.$refs.scrollContainer) return;
|
|
233
|
+
|
|
234
|
+
const activeTabRef = this.$refs[`tab-${this.currentActiveKey}`];
|
|
235
|
+
if (!activeTabRef || !activeTabRef[0]) return;
|
|
236
|
+
|
|
237
|
+
const activeTab = activeTabRef[0];
|
|
238
|
+
const scrollContainer = this.$refs.scrollContainer;
|
|
239
|
+
|
|
240
|
+
// 获取tab和容器的位置信息
|
|
241
|
+
const tabRect = activeTab.getBoundingClientRect();
|
|
242
|
+
const containerRect = scrollContainer.getBoundingClientRect();
|
|
243
|
+
|
|
244
|
+
// 计算tab相对于容器的位置
|
|
245
|
+
const tabLeft = activeTab.offsetLeft;
|
|
246
|
+
const tabWidth = activeTab.offsetWidth;
|
|
247
|
+
const containerWidth = scrollContainer.clientWidth;
|
|
248
|
+
const currentScrollLeft = scrollContainer.scrollLeft;
|
|
249
|
+
|
|
250
|
+
// 如果tab在可视区域外,则滚动到合适位置
|
|
251
|
+
if (tabLeft < currentScrollLeft) {
|
|
252
|
+
// tab在左侧不可见,滚动到tab开始位置
|
|
253
|
+
scrollContainer.scrollTo({
|
|
254
|
+
left: tabLeft - 20, // 留一点边距
|
|
255
|
+
behavior: 'smooth',
|
|
256
|
+
});
|
|
257
|
+
} else if (tabLeft + tabWidth > currentScrollLeft + containerWidth) {
|
|
258
|
+
// tab在右侧不可见,滚动到tab结束位置
|
|
259
|
+
scrollContainer.scrollTo({
|
|
260
|
+
left: tabLeft + tabWidth - containerWidth + 20, // 留一点边距
|
|
261
|
+
behavior: 'smooth',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// 处理滚动事件
|
|
268
|
+
handleScroll() {
|
|
269
|
+
this.checkScrollButtons();
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
// 处理窗口大小变化
|
|
273
|
+
handleResize() {
|
|
274
|
+
this.updateIndicator();
|
|
275
|
+
this.checkScrollButtons();
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
mounted() {
|
|
280
|
+
// 初始化指示线位置
|
|
281
|
+
this.updateIndicator();
|
|
282
|
+
|
|
283
|
+
// 初始化滚动按钮状态
|
|
284
|
+
this.checkScrollButtons();
|
|
285
|
+
|
|
286
|
+
// 监听窗口大小变化
|
|
287
|
+
window.addEventListener('resize', this.handleResize);
|
|
288
|
+
|
|
289
|
+
// 监听滚动事件
|
|
290
|
+
if (this.$refs.scrollContainer) {
|
|
291
|
+
this.$refs.scrollContainer.addEventListener('scroll', this.handleScroll);
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
beforeDestroy() {
|
|
296
|
+
// 清理事件监听
|
|
297
|
+
window.removeEventListener('resize', this.handleResize);
|
|
298
|
+
|
|
299
|
+
if (this.$refs.scrollContainer) {
|
|
300
|
+
this.$refs.scrollContainer.removeEventListener('scroll', this.handleScroll);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
watch: {
|
|
305
|
+
// 监听激活标签变化
|
|
306
|
+
currentActiveKey() {
|
|
307
|
+
this.updateIndicator();
|
|
308
|
+
this.scrollToActiveTab();
|
|
309
|
+
},
|
|
310
|
+
// 监听标签数据变化
|
|
311
|
+
tabs: {
|
|
312
|
+
handler() {
|
|
313
|
+
this.$nextTick(() => {
|
|
314
|
+
this.updateIndicator();
|
|
315
|
+
this.checkScrollButtons();
|
|
316
|
+
});
|
|
317
|
+
},
|
|
318
|
+
deep: true,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
</script>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div :class="['saas-page-table', { 'saas-page-table--hasButton': showFooterButton }]">
|
|
3
3
|
<div class="saas-page-table__header">
|
|
4
|
-
<template v-if="showHeader">
|
|
4
|
+
<template v-if="showHeader || showClose">
|
|
5
5
|
<div v-if="$slots.header">
|
|
6
6
|
<slot name="header"></slot>
|
|
7
7
|
</div>
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
</div>
|
|
29
29
|
</div>
|
|
30
30
|
<div class="saas-page-table__content">
|
|
31
|
-
<
|
|
31
|
+
<div class="saas-page-content__box">
|
|
32
|
+
<slot></slot>
|
|
33
|
+
</div>
|
|
32
34
|
</div>
|
|
33
35
|
<div class="saas-page-table__footer">
|
|
34
36
|
<slot name="footer">
|
|
@@ -46,7 +48,7 @@ export default {
|
|
|
46
48
|
props: {
|
|
47
49
|
showHeader: {
|
|
48
50
|
type: Boolean,
|
|
49
|
-
default: () =>
|
|
51
|
+
default: () => false,
|
|
50
52
|
},
|
|
51
53
|
showFooter: {
|
|
52
54
|
type: Boolean,
|