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.
@@ -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
+ }
@@ -13,3 +13,5 @@
13
13
  @import './button.scss';
14
14
  @import './space.scss';
15
15
  @import './steps.scss';
16
+ @import './sPageTabs.scss';
17
+ @import './segmented.scss';
@@ -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
- <slot></slot>
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: () => true,
51
+ default: () => false,
50
52
  },
51
53
  showFooter: {
52
54
  type: Boolean,
@@ -1,2 +1,3 @@
1
1
  export { default as SPageButton } from './PageButton/index';
2
+ export { default as SPageTabs } from './PageTabs/index';
2
3
  export { default as SPagedetail } from './Pagedetail.vue';