n20-project-component 1.0.1 → 1.0.5

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,133 +1,133 @@
1
- <template>
2
- <span class="n20-copy-text" @click.stop="handleCopy">
3
- <span class="n20-copy-text__content" ref="content">
4
- <slot>{{ text }}</slot>
5
- </span>
6
- <i
7
- v-if="showIcon"
8
- :class="['n20-copy-text__icon', copied ? 'el-icon-check' : 'el-icon-document-copy']"
9
- :style="{ order: iconPosition === 'left' ? -1 : 1 }"
10
- />
11
- </span>
12
- </template>
13
-
14
- <script>
15
- /**
16
- * N20CopyText - 点击复制文本组件
17
- *
18
- * 点击后将指定文本复制到剪贴板,通过 Element Message 显示成功反馈。
19
- *
20
- * @example
21
- * <N20CopyText :text="row.billNo">{{ row.billNo }}</N20CopyText>
22
- *
23
- * @example
24
- * <!-- 自动获取 slot 内容 -->
25
- * <N20CopyText>BILL-2024-001</N20CopyText>
26
- */
27
- export default {
28
- name: 'N20CopyText',
29
- props: {
30
- /** 要复制的文本(默认取 slot 内文本) */
31
- text: {
32
- type: [String, Number],
33
- default: ''
34
- },
35
- /** 复制成功提示文字 */
36
- successTip: {
37
- type: String,
38
- default: '已复制'
39
- },
40
- /** 是否显示复制图标 */
41
- showIcon: {
42
- type: Boolean,
43
- default: true
44
- },
45
- /** 图标位置 */
46
- iconPosition: {
47
- type: String,
48
- default: 'right',
49
- validator: v => ['left', 'right'].includes(v)
50
- },
51
- /** 是否禁用 */
52
- disabled: {
53
- type: Boolean,
54
- default: false
55
- }
56
- },
57
- data() {
58
- return {
59
- copied: false,
60
- timer: null
61
- }
62
- },
63
- beforeDestroy() {
64
- clearTimeout(this.timer)
65
- },
66
- methods: {
67
- async handleCopy() {
68
- if (this.disabled || this.copied) return
69
-
70
- const copyText = this.text || (this.$refs.content && this.$refs.content.innerText) || ''
71
- if (!copyText) return
72
-
73
- try {
74
- if (navigator.clipboard && window.isSecureContext) {
75
- await navigator.clipboard.writeText(String(copyText))
76
- } else {
77
- // fallback for HTTP or older browsers
78
- const textarea = document.createElement('textarea')
79
- textarea.value = String(copyText)
80
- textarea.style.position = 'fixed'
81
- textarea.style.left = '-9999px'
82
- document.body.appendChild(textarea)
83
- textarea.select()
84
- document.execCommand('copy')
85
- document.body.removeChild(textarea)
86
- }
87
-
88
- this.copied = true
89
- this.$message.success(this.successTip)
90
- this.$emit('copy', String(copyText))
91
-
92
- this.timer = setTimeout(() => {
93
- this.copied = false
94
- }, 2000)
95
- } catch (err) {
96
- this.$message.error('复制失败')
97
- this.$emit('error', err)
98
- }
99
- }
100
- }
101
- }
102
- </script>
103
-
104
- <style lang="scss" scoped>
105
- .n20-copy-text {
106
- display: inline-flex;
107
- align-items: center;
108
- gap: 4px;
109
- cursor: pointer;
110
-
111
- &:hover {
112
- .n20-copy-text__icon {
113
- opacity: 1;
114
- }
115
- }
116
-
117
- &__content {
118
- display: inline;
119
- }
120
-
121
- &__icon {
122
- font-size: 14px;
123
- color: #909399;
124
- opacity: 0.4;
125
- transition: all 0.2s ease;
126
-
127
- &.el-icon-check {
128
- color: #67C23A;
129
- opacity: 1;
130
- }
131
- }
132
- }
133
- </style>
1
+ <template>
2
+ <span class="n20-copy-text" @click.stop="handleCopy">
3
+ <span class="n20-copy-text__content" ref="content">
4
+ <slot>{{ text }}</slot>
5
+ </span>
6
+ <i
7
+ v-if="showIcon"
8
+ :class="['n20-copy-text__icon', copied ? 'el-icon-check' : 'el-icon-document-copy']"
9
+ :style="{ order: iconPosition === 'left' ? -1 : 1 }"
10
+ />
11
+ </span>
12
+ </template>
13
+
14
+ <script>
15
+ /**
16
+ * N20CopyText - 点击复制文本组件
17
+ *
18
+ * 点击后将指定文本复制到剪贴板,通过 Element Message 显示成功反馈。
19
+ *
20
+ * @example
21
+ * <N20CopyText :text="row.billNo">{{ row.billNo }}</N20CopyText>
22
+ *
23
+ * @example
24
+ * <!-- 自动获取 slot 内容 -->
25
+ * <N20CopyText>BILL-2024-001</N20CopyText>
26
+ */
27
+ export default {
28
+ name: 'N20CopyText',
29
+ props: {
30
+ /** 要复制的文本(默认取 slot 内文本) */
31
+ text: {
32
+ type: [String, Number],
33
+ default: ''
34
+ },
35
+ /** 复制成功提示文字 */
36
+ successTip: {
37
+ type: String,
38
+ default: '已复制'
39
+ },
40
+ /** 是否显示复制图标 */
41
+ showIcon: {
42
+ type: Boolean,
43
+ default: true
44
+ },
45
+ /** 图标位置 */
46
+ iconPosition: {
47
+ type: String,
48
+ default: 'right',
49
+ validator: v => ['left', 'right'].includes(v)
50
+ },
51
+ /** 是否禁用 */
52
+ disabled: {
53
+ type: Boolean,
54
+ default: false
55
+ }
56
+ },
57
+ data() {
58
+ return {
59
+ copied: false,
60
+ timer: null
61
+ }
62
+ },
63
+ beforeDestroy() {
64
+ clearTimeout(this.timer)
65
+ },
66
+ methods: {
67
+ async handleCopy() {
68
+ if (this.disabled || this.copied) return
69
+
70
+ const copyText = this.text || (this.$refs.content && this.$refs.content.innerText) || ''
71
+ if (!copyText) return
72
+
73
+ try {
74
+ if (navigator.clipboard && window.isSecureContext) {
75
+ await navigator.clipboard.writeText(String(copyText))
76
+ } else {
77
+ // fallback for HTTP or older browsers
78
+ const textarea = document.createElement('textarea')
79
+ textarea.value = String(copyText)
80
+ textarea.style.position = 'fixed'
81
+ textarea.style.left = '-9999px'
82
+ document.body.appendChild(textarea)
83
+ textarea.select()
84
+ document.execCommand('copy')
85
+ document.body.removeChild(textarea)
86
+ }
87
+
88
+ this.copied = true
89
+ this.$message.success(this.successTip)
90
+ this.$emit('copy', String(copyText))
91
+
92
+ this.timer = setTimeout(() => {
93
+ this.copied = false
94
+ }, 2000)
95
+ } catch (err) {
96
+ this.$message.error('复制失败')
97
+ this.$emit('error', err)
98
+ }
99
+ }
100
+ }
101
+ }
102
+ </script>
103
+
104
+ <style lang="scss" scoped>
105
+ .n20-copy-text {
106
+ display: inline-flex;
107
+ align-items: center;
108
+ gap: 4px;
109
+ cursor: pointer;
110
+
111
+ &:hover {
112
+ .n20-copy-text__icon {
113
+ opacity: 1;
114
+ }
115
+ }
116
+
117
+ &__content {
118
+ display: inline;
119
+ }
120
+
121
+ &__icon {
122
+ font-size: 14px;
123
+ color: #909399;
124
+ opacity: 0.4;
125
+ transition: all 0.2s ease;
126
+
127
+ &.el-icon-check {
128
+ color: #67C23A;
129
+ opacity: 1;
130
+ }
131
+ }
132
+ }
133
+ </style>
@@ -0,0 +1,318 @@
1
+ <template>
2
+ <div
3
+ class="n20-floating-toolbar-wrapper"
4
+ ref="wrapper"
5
+ @mouseup="handleMouseUp"
6
+ >
7
+ <!-- 包裹的内容区域 -->
8
+ <slot></slot>
9
+
10
+ <!-- 浮现工具条 -->
11
+ <transition name="n20-toolbar-fade">
12
+ <div
13
+ v-if="visible"
14
+ ref="toolbar"
15
+ class="n20-floating-toolbar"
16
+ :style="toolbarStyle"
17
+ @mousedown.prevent
18
+ >
19
+ <div class="n20-floating-toolbar__arrow"></div>
20
+ <div class="n20-floating-toolbar__body">
21
+ <button
22
+ v-for="(btn, idx) in actions"
23
+ :key="idx"
24
+ class="n20-floating-toolbar__btn"
25
+ :title="btn.label"
26
+ @click.stop="handleAction(btn, idx)"
27
+ >
28
+ <i v-if="btn.icon" :class="btn.icon"></i>
29
+ <span v-if="btn.label && showLabel">{{ btn.label }}</span>
30
+ </button>
31
+ <!-- 允许通过 slot 自定义工具栏内容 -->
32
+ <slot name="toolbar" :selectedText="selectedText"></slot>
33
+ </div>
34
+ </div>
35
+ </transition>
36
+ </div>
37
+ </template>
38
+
39
+ <script>
40
+ /**
41
+ * N20FloatingToolbar — 选中浮现工具栏
42
+ *
43
+ * 包裹内容区域,用户选中文字后在选区上方浮现操作按钮条。
44
+ * 鼠标拖选文字松开后,选区上方浮现深色工具条,
45
+ * 点击按钮触发事件,点其他地方消失。
46
+ *
47
+ * @example
48
+ * <N20FloatingToolbar
49
+ * :actions="[
50
+ * { label: '复制', icon: 'el-icon-document-copy' },
51
+ * { label: '搜索', icon: 'el-icon-search' },
52
+ * { label: '高亮', icon: 'el-icon-edit' },
53
+ * ]"
54
+ * @action="handleToolbarAction"
55
+ * >
56
+ * <p>这里是一段可以被选中的文本内容...</p>
57
+ * </N20FloatingToolbar>
58
+ */
59
+ export default {
60
+ name: 'N20FloatingToolbar',
61
+ props: {
62
+ /**
63
+ * 工具栏按钮列表
64
+ * 每项 { label: string, icon?: string, command?: string }
65
+ */
66
+ actions: {
67
+ type: Array,
68
+ default: () => [
69
+ { label: '复制', icon: 'el-icon-document-copy', command: 'copy' },
70
+ { label: '搜索', icon: 'el-icon-search', command: 'search' },
71
+ { label: '高亮', icon: 'el-icon-edit', command: 'highlight' },
72
+ ]
73
+ },
74
+ /** 是否在按钮中显示文字标签 */
75
+ showLabel: {
76
+ type: Boolean,
77
+ default: true
78
+ },
79
+ /** 工具栏与选区的垂直间距(px) */
80
+ offset: {
81
+ type: Number,
82
+ default: 8
83
+ },
84
+ /** 是否禁用 */
85
+ disabled: {
86
+ type: Boolean,
87
+ default: false
88
+ },
89
+ /** 选中最少字符数才触发 */
90
+ minLength: {
91
+ type: Number,
92
+ default: 1
93
+ }
94
+ },
95
+ data() {
96
+ return {
97
+ visible: false,
98
+ selectedText: '',
99
+ toolbarStyle: {
100
+ top: '0px',
101
+ left: '0px'
102
+ }
103
+ }
104
+ },
105
+ mounted() {
106
+ // 监听全局点击 → 隐藏工具栏
107
+ this._onDocClick = (e) => {
108
+ if (this.visible && this.$refs.toolbar && !this.$refs.toolbar.contains(e.target)) {
109
+ this.hide()
110
+ }
111
+ }
112
+ // 监听滚动 → 隐藏工具栏
113
+ this._onScroll = () => {
114
+ if (this.visible) {
115
+ this.hide()
116
+ }
117
+ }
118
+ document.addEventListener('mousedown', this._onDocClick, true)
119
+ window.addEventListener('scroll', this._onScroll, true)
120
+ },
121
+ beforeDestroy() {
122
+ document.removeEventListener('mousedown', this._onDocClick, true)
123
+ window.removeEventListener('scroll', this._onScroll, true)
124
+ },
125
+ methods: {
126
+ /** 鼠标松开时判断是否有选区 */
127
+ handleMouseUp() {
128
+ if (this.disabled) return
129
+
130
+ // 延迟一帧让浏览器完成选区更新
131
+ this.$nextTick(() => {
132
+ setTimeout(() => {
133
+ this.checkSelection()
134
+ }, 0)
135
+ })
136
+ },
137
+
138
+ /** 检测当前选区 */
139
+ checkSelection() {
140
+ const selection = window.getSelection()
141
+ if (!selection || selection.isCollapsed || !selection.toString().trim()) {
142
+ this.hide()
143
+ return
144
+ }
145
+
146
+ const text = selection.toString().trim()
147
+ if (text.length < this.minLength) {
148
+ this.hide()
149
+ return
150
+ }
151
+
152
+ // 确保选区在 wrapper 内
153
+ const wrapper = this.$refs.wrapper
154
+ if (!wrapper) return
155
+
156
+ const range = selection.getRangeAt(0)
157
+ if (!wrapper.contains(range.commonAncestorContainer)) {
158
+ this.hide()
159
+ return
160
+ }
161
+
162
+ this.selectedText = text
163
+ this.positionToolbar(range)
164
+ this.visible = true
165
+ },
166
+
167
+ /** 根据选区 Range 定位工具栏 */
168
+ positionToolbar(range) {
169
+ const rect = range.getBoundingClientRect()
170
+ const wrapperRect = this.$refs.wrapper.getBoundingClientRect()
171
+
172
+ // 工具栏居中于选区上方
173
+ const left = rect.left + rect.width / 2 - wrapperRect.left
174
+ const top = rect.top - wrapperRect.top - this.offset
175
+
176
+ this.toolbarStyle = {
177
+ left: `${left}px`,
178
+ top: `${top}px`
179
+ }
180
+ },
181
+
182
+ /** 点击按钮 */
183
+ handleAction(btn, idx) {
184
+ this.$emit('action', {
185
+ command: btn.command || btn.label,
186
+ label: btn.label,
187
+ index: idx,
188
+ text: this.selectedText,
189
+ })
190
+
191
+ // 内置 copy 行为
192
+ if (btn.command === 'copy') {
193
+ this.doCopy(this.selectedText)
194
+ }
195
+
196
+ this.hide()
197
+ },
198
+
199
+ /** 复制文本到剪贴板 */
200
+ async doCopy(text) {
201
+ try {
202
+ if (navigator.clipboard && window.isSecureContext) {
203
+ await navigator.clipboard.writeText(text)
204
+ } else {
205
+ const textarea = document.createElement('textarea')
206
+ textarea.value = text
207
+ textarea.style.position = 'fixed'
208
+ textarea.style.left = '-9999px'
209
+ document.body.appendChild(textarea)
210
+ textarea.select()
211
+ document.execCommand('copy')
212
+ document.body.removeChild(textarea)
213
+ }
214
+ this.$message && this.$message.success('已复制')
215
+ } catch (err) {
216
+ this.$message && this.$message.error('复制失败')
217
+ }
218
+ },
219
+
220
+ /** 隐藏工具栏 */
221
+ hide() {
222
+ this.visible = false
223
+ this.selectedText = ''
224
+ },
225
+
226
+ /** 外部可调用:手动显示 */
227
+ show() {
228
+ this.checkSelection()
229
+ }
230
+ }
231
+ }
232
+ </script>
233
+
234
+ <style lang="scss" scoped>
235
+ .n20-floating-toolbar-wrapper {
236
+ position: relative;
237
+ display: inline-block;
238
+ width: 100%;
239
+ }
240
+
241
+ .n20-floating-toolbar {
242
+ position: absolute;
243
+ z-index: 9999;
244
+ transform: translateX(-50%) translateY(-100%);
245
+ pointer-events: auto;
246
+
247
+ // 箭头
248
+ &__arrow {
249
+ position: absolute;
250
+ left: 50%;
251
+ bottom: -5px;
252
+ transform: translateX(-50%) rotate(45deg);
253
+ width: 10px;
254
+ height: 10px;
255
+ background: #1f2937;
256
+ border-radius: 0 0 2px 0;
257
+ }
258
+
259
+ // 主体
260
+ &__body {
261
+ display: inline-flex;
262
+ align-items: center;
263
+ gap: 2px;
264
+ padding: 4px 6px;
265
+ background: #1f2937;
266
+ border-radius: 6px;
267
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.24), 0 0 0 1px rgba(255, 255, 255, 0.06) inset;
268
+ white-space: nowrap;
269
+ }
270
+
271
+ // 按钮
272
+ &__btn {
273
+ display: inline-flex;
274
+ align-items: center;
275
+ gap: 4px;
276
+ padding: 5px 10px;
277
+ border: none;
278
+ border-radius: 4px;
279
+ background: transparent;
280
+ color: #e5e7eb;
281
+ font-size: 13px;
282
+ line-height: 1;
283
+ cursor: pointer;
284
+ transition: background 0.15s ease, color 0.15s ease;
285
+ outline: none;
286
+
287
+ i {
288
+ font-size: 14px;
289
+ }
290
+
291
+ &:hover {
292
+ background: rgba(255, 255, 255, 0.12);
293
+ color: #fff;
294
+ }
295
+
296
+ &:active {
297
+ background: rgba(255, 255, 255, 0.18);
298
+ }
299
+
300
+ & + & {
301
+ margin-left: 1px;
302
+ }
303
+ }
304
+ }
305
+
306
+ // 出现动画
307
+ .n20-toolbar-fade-enter-active {
308
+ transition: opacity 0.18s ease, transform 0.18s ease;
309
+ }
310
+ .n20-toolbar-fade-leave-active {
311
+ transition: opacity 0.12s ease, transform 0.12s ease;
312
+ }
313
+ .n20-toolbar-fade-enter,
314
+ .n20-toolbar-fade-leave-to {
315
+ opacity: 0;
316
+ transform: translateX(-50%) translateY(-100%) scale(0.92);
317
+ }
318
+ </style>
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <el-checkbox-group v-model="checkArrayCalc" :max="maxLength" class="popover-check-box" @change="checkChange">
3
+ <vue-draggable
4
+ :list="filterList"
5
+ :animation="200"
6
+ group="dragBox"
7
+ :handle="'.n20-icon-tuodong'"
8
+ @change="dragChange"
9
+ >
10
+ <div v-for="(item, index) in filterList" :key="index" class="n20-drag-list-item">
11
+ <el-checkbox :disabled="item.static" :label="item.id" :checked="item.checked">{{ item.label }}</el-checkbox
12
+ ><i class="n20-drag-icon n20-icon-tuodong"></i>
13
+ </div>
14
+ </vue-draggable>
15
+ </el-checkbox-group>
16
+ </template>
17
+
18
+ <script>
19
+ import importGlobal from 'n20-common-lib/src/utils/importGlobal.js'
20
+ export default {
21
+ name: 'FilterItem',
22
+ components: {
23
+ vueDraggable: () => importGlobal('vuedraggable', () => import(/*webpackChunkName: "vuedraggable"*/ 'vuedraggable'))
24
+ },
25
+ props: {
26
+ filterList: {
27
+ type: Array,
28
+ default: () => []
29
+ },
30
+ checkArray: {
31
+ type: Array,
32
+ default: () => []
33
+ },
34
+ maxLength: {
35
+ type: Number
36
+ }
37
+ },
38
+ computed: {
39
+ checkArrayCalc: {
40
+ get() {
41
+ return this.checkArray
42
+ },
43
+ set(value) {
44
+ // 处理初始化时候的报错
45
+ if (typeof value !== 'boolean') {
46
+ this.$emit('update:checkArray', value)
47
+ }
48
+ return value
49
+ }
50
+ }
51
+ },
52
+ methods: {
53
+ checkChange(keys) {
54
+ if (keys.length > this.maxLength) {
55
+ return this.$message.warning(`${$lc('最多只能添加')}${this.maxLength}${$lc('个')}${$lc('筛选条件!')}`)
56
+ }
57
+ this.$emit('checked', keys)
58
+ },
59
+ dragChange() {
60
+ this.$emit('checked', this.checkArrayCalc)
61
+ }
62
+ }
63
+ }
64
+ </script>