n20-common-lib 3.1.15 → 3.1.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n20-common-lib",
3
- "version": "3.1.15",
3
+ "version": "3.1.17",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -56,6 +56,8 @@
56
56
  @import './v3/header.scss';
57
57
  @import './v3/footer.scss';
58
58
  @import './v3/radioCard.scss';
59
+ @import './v3/collapse.scss';
60
+ @import './v3/anchor.scss';
59
61
 
60
62
  //临时引入
61
63
  @import '../../components/ChildRange/style.scss';
@@ -144,21 +144,53 @@ iframe {
144
144
 
145
145
  /* 内外边距 — 4px 基础单位,所有值均为 4 的倍数 */
146
146
  @mixin pm-fn($n) {
147
- .pa-#{$n} { padding: #{$n}px; }
148
- .pt-#{$n} { padding-top: #{$n}px; }
149
- .pr-#{$n} { padding-right: #{$n}px; }
150
- .pb-#{$n} { padding-bottom: #{$n}px; }
151
- .pl-#{$n} { padding-left: #{$n}px; }
152
- .px-#{$n} { padding-left: #{$n}px; padding-right: #{$n}px; }
153
- .py-#{$n} { padding-top: #{$n}px; padding-bottom: #{$n}px; }
154
-
155
- .ma-#{$n} { margin: #{$n}px; }
156
- .mt-#{$n} { margin-top: #{$n}px; }
157
- .mr-#{$n} { margin-right: #{$n}px; }
158
- .mb-#{$n} { margin-bottom: #{$n}px; }
159
- .ml-#{$n} { margin-left: #{$n}px; }
160
- .mx-#{$n} { margin-left: #{$n}px; margin-right: #{$n}px; }
161
- .my-#{$n} { margin-top: #{$n}px; margin-bottom: #{$n}px; }
147
+ .pa-#{$n} {
148
+ padding: #{$n}px;
149
+ }
150
+ .pt-#{$n} {
151
+ padding-top: #{$n}px;
152
+ }
153
+ .pr-#{$n} {
154
+ padding-right: #{$n}px;
155
+ }
156
+ .pb-#{$n} {
157
+ padding-bottom: #{$n}px;
158
+ }
159
+ .pl-#{$n} {
160
+ padding-left: #{$n}px;
161
+ }
162
+ .px-#{$n} {
163
+ padding-left: #{$n}px;
164
+ padding-right: #{$n}px;
165
+ }
166
+ .py-#{$n} {
167
+ padding-top: #{$n}px;
168
+ padding-bottom: #{$n}px;
169
+ }
170
+
171
+ .ma-#{$n} {
172
+ margin: #{$n}px;
173
+ }
174
+ .mt-#{$n} {
175
+ margin-top: #{$n}px;
176
+ }
177
+ .mr-#{$n} {
178
+ margin-right: #{$n}px;
179
+ }
180
+ .mb-#{$n} {
181
+ margin-bottom: #{$n}px;
182
+ }
183
+ .ml-#{$n} {
184
+ margin-left: #{$n}px;
185
+ }
186
+ .mx-#{$n} {
187
+ margin-left: #{$n}px;
188
+ margin-right: #{$n}px;
189
+ }
190
+ .my-#{$n} {
191
+ margin-top: #{$n}px;
192
+ margin-bottom: #{$n}px;
193
+ }
162
194
  }
163
195
 
164
196
  $spacing-values: 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 48, 64;
@@ -624,6 +656,7 @@ $list: 45, 60, 80, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 2
624
656
  /* height: 100%; 速龙浏览器下子元素无法继承父元素的高速度 */
625
657
  .n20-page-content {
626
658
  height: 100%;
659
+ padding-top: 4px;
627
660
  overflow-y: auto;
628
661
  overflow-x: hidden;
629
662
  }
@@ -0,0 +1,119 @@
1
+ .n20-anchor-v3 {
2
+ box-sizing: border-box;
3
+ display: inline-flex;
4
+ align-items: flex-start;
5
+ gap: 4px;
6
+
7
+ & *,
8
+ & *::before,
9
+ & *::after {
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ &.is-static {
14
+ position: static;
15
+ }
16
+
17
+ &__indicator {
18
+ flex-shrink: 0;
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ padding: 12px 8px;
23
+ background: $--color-white;
24
+ border: 1px solid $--border-color-base;
25
+ border-radius: 15.5px;
26
+ filter: drop-shadow(0px 8px 10px rgba(0, 0, 0, 0.1));
27
+ }
28
+
29
+ &__indicator-bar {
30
+ flex-shrink: 0;
31
+ width: 2px;
32
+ height: 4px;
33
+ background: $--color-text-placeholder;
34
+ transition: height 0.2s ease, background 0.2s ease;
35
+
36
+ & + & {
37
+ margin-top: 4px;
38
+ }
39
+
40
+ &.is-active {
41
+ height: 16px;
42
+ background: $--color-primary;
43
+ }
44
+ }
45
+
46
+ &__list {
47
+ display: flex;
48
+ flex-direction: column;
49
+ align-items: flex-start;
50
+ padding: 8px;
51
+ background: $--color-white;
52
+ border: 1px solid $--border-color-base;
53
+ border-radius: 4px;
54
+ filter: drop-shadow(0px 8px 10px rgba(0, 0, 0, 0.1));
55
+ }
56
+
57
+ &__item {
58
+ flex-shrink: 0;
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 8px;
62
+ width: 104px;
63
+ padding: 4px 8px;
64
+ border-radius: 2px;
65
+ cursor: pointer;
66
+ user-select: none;
67
+ text-decoration: none;
68
+ color: inherit;
69
+ outline: none;
70
+ transition: background-color 0.15s ease;
71
+
72
+ &:hover,
73
+ &:focus,
74
+ &:active {
75
+ text-decoration: none;
76
+ color: inherit;
77
+ }
78
+
79
+ &:not(.is-active):not(.is-disabled):hover {
80
+ background: $--background-color-base;
81
+ }
82
+
83
+ &.is-active {
84
+ .n20-anchor-v3__bar {
85
+ height: 12px;
86
+ background: $--color-primary;
87
+ }
88
+ .n20-anchor-v3__label {
89
+ color: $--color-primary;
90
+ }
91
+ }
92
+
93
+ &.is-disabled {
94
+ cursor: not-allowed;
95
+ pointer-events: none;
96
+ opacity: 0.5;
97
+ }
98
+ }
99
+
100
+ &__bar {
101
+ flex-shrink: 0;
102
+ width: 2px;
103
+ height: 2px;
104
+ background: $--color-text-placeholder;
105
+ border-radius: 0 4px 4px 0;
106
+ transition: height 0.15s ease, background 0.15s ease;
107
+ }
108
+
109
+ &__label {
110
+ flex-shrink: 0;
111
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
112
+ font-size: 13px;
113
+ font-weight: 400;
114
+ line-height: 22px;
115
+ color: $--color-text-primary;
116
+ white-space: nowrap;
117
+ word-break: break-word;
118
+ }
119
+ }
@@ -0,0 +1,71 @@
1
+ .n20-page-collapse {
2
+ background: $--background-color-base;
3
+ padding: 12px 16px;
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 12px;
7
+ box-sizing: border-box;
8
+
9
+ &__header {
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: space-between;
13
+ cursor: pointer;
14
+ user-select: none;
15
+ }
16
+
17
+ &__title {
18
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
19
+ font-size: 14px;
20
+ font-weight: 500;
21
+ line-height: 22px;
22
+ color: $--color-info;
23
+ }
24
+
25
+ &__action {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 4px;
29
+ padding: 1px 4px;
30
+ border-radius: 2px;
31
+ cursor: pointer;
32
+ overflow: hidden;
33
+ }
34
+
35
+ &__action-text {
36
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
37
+ font-size: 13px;
38
+ font-weight: 400;
39
+ line-height: 22px;
40
+ color: $--color-primary;
41
+ }
42
+
43
+ &__arrow {
44
+ display: inline-flex;
45
+ align-items: center;
46
+ color: $--color-primary;
47
+ transition: transform 0.2s ease;
48
+ }
49
+
50
+ &__arrow-up {
51
+ transform: rotate(0deg);
52
+ }
53
+
54
+ &__arrow-down {
55
+ transform: rotate(180deg);
56
+ }
57
+
58
+ &__body-wrap {
59
+ display: grid;
60
+ grid-template-rows: 0fr;
61
+ transition: grid-template-rows 0.3s ease;
62
+
63
+ &.is-expanded {
64
+ grid-template-rows: 1fr;
65
+ }
66
+ }
67
+
68
+ &__body {
69
+ overflow: hidden;
70
+ }
71
+ }
@@ -0,0 +1,226 @@
1
+ <template>
2
+ <div
3
+ :class="anchorClasses"
4
+ :style="containerStyle"
5
+ @mouseenter="onMouseEnter"
6
+ @mouseleave="onMouseLeave"
7
+ >
8
+ <div v-show="menuVisible" class="n20-anchor-v3__list">
9
+ <a
10
+ v-for="item in normalizedOptions"
11
+ :key="item.value"
12
+ :href="'#' + item.value"
13
+ :class="['n20-anchor-v3__item', { 'is-active': isActive(item), 'is-disabled': item.disabled }]"
14
+ @click="handleClick(item, $event)"
15
+ >
16
+ <span class="n20-anchor-v3__bar"></span>
17
+ <span class="n20-anchor-v3__label">{{ item.label }}</span>
18
+ </a>
19
+ </div>
20
+ <div class="n20-anchor-v3__indicator">
21
+ <span
22
+ v-for="(item, idx) in normalizedOptions"
23
+ :key="`ind-${item.value}`"
24
+ :class="['n20-anchor-v3__indicator-bar', { 'is-active': idx === activeIndex }]"
25
+ ></span>
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <script>
31
+ export default {
32
+ name: 'AnchorV3',
33
+ props: {
34
+ options: {
35
+ type: Array,
36
+ required: true,
37
+ default: () => []
38
+ },
39
+ position: {
40
+ type: String,
41
+ default: 'fixed',
42
+ validator: (v) => ['fixed', 'absolute', 'static'].includes(v)
43
+ },
44
+ offsetTop: {
45
+ type: Number,
46
+ default: 80
47
+ },
48
+ offsetRight: {
49
+ type: Number,
50
+ default: 24
51
+ },
52
+ scrollSpy: {
53
+ type: Boolean,
54
+ default: true
55
+ },
56
+ scrollContainer: {
57
+ type: [String, HTMLElement],
58
+ default: null
59
+ }
60
+ },
61
+ data() {
62
+ return {
63
+ currentHash: '',
64
+ ticking: false,
65
+ sortedOptions: [],
66
+ scrollTarget: null,
67
+ menuVisible: false
68
+ }
69
+ },
70
+ computed: {
71
+ anchorClasses() {
72
+ return ['n20-anchor-v3', `is-${this.position}`]
73
+ },
74
+ normalizedOptions() {
75
+ return (this.options || []).filter((o) => o && o.value != null && o.label != null)
76
+ },
77
+ activeIndex() {
78
+ return this.normalizedOptions.findIndex((o) => this.isActive(o))
79
+ },
80
+ containerStyle() {
81
+ if (this.position === 'static') return {}
82
+ return {
83
+ position: this.position,
84
+ top: `${this.offsetTop}px`,
85
+ right: `${this.offsetRight}px`,
86
+ zIndex: 100
87
+ }
88
+ },
89
+ triggerY() {
90
+ return this.offsetTop
91
+ }
92
+ },
93
+ watch: {
94
+ normalizedOptions() {
95
+ this.sortedOptions = []
96
+ },
97
+ scrollContainer() {
98
+ if (this.scrollSpy) {
99
+ this.bindScrollTarget()
100
+ }
101
+ },
102
+ scrollSpy(val) {
103
+ if (val) {
104
+ this.bindScrollTarget()
105
+ } else {
106
+ this.unbindScrollTarget()
107
+ }
108
+ }
109
+ },
110
+ mounted() {
111
+ this.currentHash = (typeof window !== 'undefined' ? window.location.hash : '') || ''
112
+ window.addEventListener('hashchange', this.onHashChange)
113
+ if (this.scrollSpy) {
114
+ this.bindScrollTarget()
115
+ }
116
+ },
117
+ beforeDestroy() {
118
+ window.removeEventListener('hashchange', this.onHashChange)
119
+ if (this.scrollSpy) {
120
+ this.unbindScrollTarget()
121
+ }
122
+ },
123
+ methods: {
124
+ isActive(item) {
125
+ return '#' + item.value === this.currentHash
126
+ },
127
+ handleClick(item, e) {
128
+ if (item.disabled) {
129
+ e.preventDefault()
130
+ return
131
+ }
132
+ this.$emit('click', item)
133
+ },
134
+ onMouseEnter() {
135
+ this.menuVisible = true
136
+ },
137
+ onMouseLeave() {
138
+ this.menuVisible = false
139
+ },
140
+ onHashChange() {
141
+ this.currentHash = window.location.hash || ''
142
+ },
143
+ resolveScrollTarget() {
144
+ if (typeof window === 'undefined' || typeof document === 'undefined') return null
145
+ const c = this.scrollContainer
146
+ if (c === 'window') return window
147
+ if (typeof c === 'string' && c) return document.querySelector(c) || this.findScrollableParent() || window
148
+ if (c && c.nodeType === 1) return c
149
+ return this.findScrollableParent() || window
150
+ },
151
+ findScrollableParent() {
152
+ let el = this.$el && this.$el.parentElement
153
+ while (el && el !== document.documentElement) {
154
+ const style = window.getComputedStyle(el)
155
+ const overflowY = style.overflowY
156
+ const overflow = style.overflow
157
+ if (overflowY === 'auto' || overflowY === 'scroll' || overflow === 'auto' || overflow === 'scroll') {
158
+ return el
159
+ }
160
+ el = el.parentElement
161
+ }
162
+ return null
163
+ },
164
+ bindScrollTarget() {
165
+ this.unbindScrollTarget()
166
+ this.scrollTarget = this.resolveScrollTarget()
167
+ if (this.scrollTarget) {
168
+ this.scrollTarget.addEventListener('scroll', this.onScroll, { passive: true })
169
+ this.$nextTick(() => this.updateActiveSection())
170
+ }
171
+ },
172
+ unbindScrollTarget() {
173
+ if (this.scrollTarget) {
174
+ this.scrollTarget.removeEventListener('scroll', this.onScroll)
175
+ this.scrollTarget = null
176
+ }
177
+ },
178
+ onScroll() {
179
+ if (this.ticking) return
180
+ this.ticking = true
181
+ requestAnimationFrame(() => {
182
+ this.updateActiveSection()
183
+ this.ticking = false
184
+ })
185
+ },
186
+ getSortedOptions() {
187
+ if (this.sortedOptions.length === this.normalizedOptions.length && this.sortedOptions.length > 0) {
188
+ return this.sortedOptions
189
+ }
190
+ const pageY = (typeof window !== 'undefined' ? window.pageYOffset : 0) || 0
191
+ this.sortedOptions = [...this.normalizedOptions].sort((a, b) => {
192
+ const elA = typeof document !== 'undefined' ? document.getElementById(String(a.value)) : null
193
+ const elB = typeof document !== 'undefined' ? document.getElementById(String(b.value)) : null
194
+ const topA = elA ? elA.getBoundingClientRect().top + pageY : 0
195
+ const topB = elB ? elB.getBoundingClientRect().top + pageY : 0
196
+ return topA - topB
197
+ })
198
+ return this.sortedOptions
199
+ },
200
+ updateActiveSection() {
201
+ if (typeof window === 'undefined' || typeof document === 'undefined') return
202
+ const triggerY = this.triggerY
203
+ const sorted = this.getSortedOptions()
204
+ let activeValue = null
205
+ for (const option of sorted) {
206
+ const el = document.getElementById(String(option.value))
207
+ if (!el) continue
208
+ const rect = el.getBoundingClientRect()
209
+ if (rect.top <= triggerY) {
210
+ activeValue = option.value
211
+ } else {
212
+ break
213
+ }
214
+ }
215
+ if (activeValue) {
216
+ const newHash = '#' + activeValue
217
+ if (this.currentHash !== newHash) {
218
+ this.currentHash = newHash
219
+ }
220
+ } else if (this.currentHash) {
221
+ this.currentHash = ''
222
+ }
223
+ }
224
+ }
225
+ }
226
+ </script>
@@ -0,0 +1,69 @@
1
+ <template>
2
+ <div class="n20-page-collapse">
3
+ <div class="n20-page-collapse__header" @click="toggle">
4
+ <div class="n20-page-collapse__title">
5
+ <slot name="title">{{ title }}</slot>
6
+ </div>
7
+ <div class="n20-page-collapse__action">
8
+ <span class="n20-page-collapse__action-text">{{ actionText }}</span>
9
+ <i
10
+ class="n20-page-collapse__arrow"
11
+ :class="isExpanded ? 'n20-page-collapse__arrow-up' : 'n20-page-collapse__arrow-down'"
12
+ >
13
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
14
+ <path d="M3 9.5L7 5.5L11 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
15
+ </svg>
16
+ </i>
17
+ </div>
18
+ </div>
19
+ <div class="n20-page-collapse__body-wrap" :class="{ 'is-expanded': isExpanded }">
20
+ <div class="n20-page-collapse__body">
21
+ <slot></slot>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script>
28
+ import { $lc } from '../../../utils/i18n/index'
29
+
30
+ export default {
31
+ name: 'Collapse',
32
+ props: {
33
+ title: {
34
+ type: String,
35
+ default: ''
36
+ },
37
+ expanded: {
38
+ type: Boolean,
39
+ default: null
40
+ }
41
+ },
42
+ data() {
43
+ return {
44
+ localExpanded: true
45
+ }
46
+ },
47
+ computed: {
48
+ isControlled() {
49
+ return this.expanded !== null
50
+ },
51
+ isExpanded() {
52
+ return this.isControlled ? this.expanded : this.localExpanded
53
+ },
54
+ actionText() {
55
+ return this.isExpanded ? $lc('收起') : $lc('展开')
56
+ }
57
+ },
58
+ methods: {
59
+ toggle() {
60
+ const next = !this.isExpanded
61
+ if (!this.isControlled) {
62
+ this.localExpanded = next
63
+ }
64
+ this.$emit('update:expanded', next)
65
+ this.$emit('change', next)
66
+ }
67
+ }
68
+ }
69
+ </script>
@@ -3,11 +3,11 @@
3
3
  <div class="n20-page-header-left">
4
4
  <div v-if="showBack" class="n20-page-header-v3__back" @click="handleBack">
5
5
  <i :class="icon"></i>
6
- <span class="n20-page-header-v3__back-text">{{ backText }}</span>
6
+ <span class="n20-page-header-v3__back-text">{{ title }}</span>
7
7
  </div>
8
8
  <div v-if="showBack" class="n20-page-header-v3__divider"></div>
9
9
  <div class="n20-page-header-v3__title">
10
- <slot name="title">{{ title }}</slot>
10
+ <slot name="content">{{ content }}</slot>
11
11
  </div>
12
12
  </div>
13
13
  <div :class="`n20-page-header-right ${extraClass}`">
@@ -28,11 +28,11 @@ export default {
28
28
  type: Boolean,
29
29
  default: true
30
30
  },
31
- backText: {
31
+ title: {
32
32
  type: String,
33
33
  default: '返回'
34
34
  },
35
- title: {
35
+ content: {
36
36
  type: String,
37
37
  default: ''
38
38
  },
@@ -47,7 +47,7 @@ export default {
47
47
  },
48
48
  methods: {
49
49
  handleBack() {
50
- this.$emit('onBack')
50
+ this.$emit('back')
51
51
  }
52
52
  }
53
53
  }