@jx3box/jx3box-ui 2.1.7 → 2.1.9
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/i18n/messages/en-US.js +1 -0
- package/i18n/messages/vi.js +1 -0
- package/i18n/messages/zh-CN.js +1 -0
- package/i18n/messages/zh-TW.js +1 -0
- package/package.json +1 -1
- package/src/App.vue +7 -1
- package/src/filters/orderBy.vue +2 -6
- package/src/filters/tagBy.vue +2 -6
- package/src/interact/Copy.vue +243 -0
package/i18n/messages/en-US.js
CHANGED
package/i18n/messages/vi.js
CHANGED
|
@@ -152,6 +152,7 @@ export default {
|
|
|
152
152
|
},
|
|
153
153
|
common: {
|
|
154
154
|
copySuccess: "Đã sao chép",
|
|
155
|
+
copySuccessToast: "Đã sao chép",
|
|
155
156
|
copyFailed: "Sao chép thất bại",
|
|
156
157
|
cannotCopyClipboard: "Không thể truy cập clipboard",
|
|
157
158
|
browserCopyFailed: "Trình duyệt không sao chép được",
|
package/i18n/messages/zh-CN.js
CHANGED
package/i18n/messages/zh-TW.js
CHANGED
package/package.json
CHANGED
package/src/App.vue
CHANGED
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
<div class="m-block">
|
|
35
35
|
<Mark label="渊穷砂" BGL="#24292e" BGR="#ffadcb" value="按键启动"></Mark>
|
|
36
36
|
</div>
|
|
37
|
+
<h1 class="m-title">复制组件</h1>
|
|
38
|
+
<div class="m-block">
|
|
39
|
+
<CopyComp value="xfbsyxj">xfbsyxj</CopyComp>
|
|
40
|
+
</div>
|
|
37
41
|
|
|
38
42
|
<h1 class="m-title">切换侧边栏</h1>
|
|
39
43
|
<div class="m-block">
|
|
@@ -194,7 +198,8 @@ import PostVersion from "./single/PostVersion.vue";
|
|
|
194
198
|
import PostCollection from "./single/PostCollection.vue";
|
|
195
199
|
import QRcode from "./interact/QRcode.vue";
|
|
196
200
|
import UploadBanner from "./upload/UploadBanner.vue";
|
|
197
|
-
import Mark from "./interact/Mark.vue"
|
|
201
|
+
import Mark from "./interact/Mark.vue";
|
|
202
|
+
import CopyComp from "./interact/Copy.vue";
|
|
198
203
|
|
|
199
204
|
import GamePrice from "./wiki/GamePrice.vue";
|
|
200
205
|
import WikiComments from "./wiki/WikiComments.vue";
|
|
@@ -204,6 +209,7 @@ export default {
|
|
|
204
209
|
name: "App",
|
|
205
210
|
components: {
|
|
206
211
|
Mark,
|
|
212
|
+
CopyComp,
|
|
207
213
|
Author,
|
|
208
214
|
// PostHeader,
|
|
209
215
|
PostTopic,
|
package/src/filters/orderBy.vue
CHANGED
|
@@ -7,12 +7,8 @@
|
|
|
7
7
|
})
|
|
8
8
|
}}</span>
|
|
9
9
|
<span class="u-toggle">
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
</el-icon>
|
|
13
|
-
<el-icon class="el-icon-arrow-up">
|
|
14
|
-
<arrow-up />
|
|
15
|
-
</el-icon>
|
|
10
|
+
<i class="el-icon-arrow-down"> </i>
|
|
11
|
+
<i class="el-icon-arrow-up"> </i>
|
|
16
12
|
</span>
|
|
17
13
|
</span>
|
|
18
14
|
<span class="u-options">
|
package/src/filters/tagBy.vue
CHANGED
|
@@ -8,12 +8,8 @@
|
|
|
8
8
|
{{ current || $jx3boxT("jx3boxUi.tagBy.all", "全部") }}</span
|
|
9
9
|
>
|
|
10
10
|
<span class="u-toggle">
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
</el-icon>
|
|
14
|
-
<el-icon class="el-icon-arrow-up">
|
|
15
|
-
<arrow-up />
|
|
16
|
-
</el-icon>
|
|
11
|
+
<i class="el-icon-arrow-down"> </i>
|
|
12
|
+
<i class="el-icon-arrow-up"> </i>
|
|
17
13
|
</span>
|
|
18
14
|
</span>
|
|
19
15
|
<span class="u-options">
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- 增加交互反馈样式和键盘聚焦样式 -->
|
|
3
|
+
<div
|
|
4
|
+
ref="trigger"
|
|
5
|
+
class="w-copy-container"
|
|
6
|
+
role="button"
|
|
7
|
+
:aria-label="defaultSuccessText"
|
|
8
|
+
tabindex="0"
|
|
9
|
+
@click="handleCopy"
|
|
10
|
+
@keydown.enter.prevent="handleCopy"
|
|
11
|
+
@keydown.space.prevent="handleCopy"
|
|
12
|
+
>
|
|
13
|
+
<slot></slot>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- 传送门到 Body 确保不受父级 overflow 限制 -->
|
|
17
|
+
<teleport to="body">
|
|
18
|
+
<transition name="copy-toast-fade">
|
|
19
|
+
<div v-if="toastVisible" class="w-copy-toast" :style="{ left: `${toastX}px`, top: `${toastY}px` }">
|
|
20
|
+
<div class="w-copy-toast__content">
|
|
21
|
+
<template v-if="showDefaultTip">
|
|
22
|
+
<svg class="w-copy-toast__svg" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
23
|
+
<path
|
|
24
|
+
fill="currentColor"
|
|
25
|
+
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
|
|
26
|
+
></path>
|
|
27
|
+
</svg>
|
|
28
|
+
<span class="w-copy-toast__text">{{ defaultSuccessText }}</span>
|
|
29
|
+
</template>
|
|
30
|
+
<template v-else>
|
|
31
|
+
<span class="w-copy-toast__text">{{ tipText }}</span>
|
|
32
|
+
</template>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</transition>
|
|
36
|
+
</teleport>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script>
|
|
40
|
+
import throttleFn from "lodash/throttle";
|
|
41
|
+
import i18nMixin from "../../i18n/mixin";
|
|
42
|
+
|
|
43
|
+
export default {
|
|
44
|
+
name: "CopyComp",
|
|
45
|
+
mixins: [i18nMixin],
|
|
46
|
+
props: {
|
|
47
|
+
// 支持字符串、数字,或返回这些类型的函数(用于实时获取最新数据)
|
|
48
|
+
value: {
|
|
49
|
+
type: [String, Number, Function],
|
|
50
|
+
default: "",
|
|
51
|
+
},
|
|
52
|
+
tip: {
|
|
53
|
+
type: String,
|
|
54
|
+
default: "",
|
|
55
|
+
},
|
|
56
|
+
throttle: {
|
|
57
|
+
type: Number,
|
|
58
|
+
default: 1500, // 默认防抖时间
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
emits: ["success", "error"],
|
|
62
|
+
data() {
|
|
63
|
+
return {
|
|
64
|
+
toastVisible: false,
|
|
65
|
+
toastX: 0,
|
|
66
|
+
toastY: 0,
|
|
67
|
+
hideTimer: null,
|
|
68
|
+
throttledCopy: null,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
computed: {
|
|
72
|
+
tipText() {
|
|
73
|
+
return this.tip;
|
|
74
|
+
},
|
|
75
|
+
showDefaultTip() {
|
|
76
|
+
return !this.tipText;
|
|
77
|
+
},
|
|
78
|
+
defaultSuccessText() {
|
|
79
|
+
return this.$jx3boxT ? this.$jx3boxT("jx3boxUi.common.copySuccessToast", "复制成功") : "复制成功";
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
created() {
|
|
83
|
+
this.throttledCopy = throttleFn(
|
|
84
|
+
(payload) => {
|
|
85
|
+
this.executeCopy(payload);
|
|
86
|
+
},
|
|
87
|
+
this.throttle,
|
|
88
|
+
{ leading: true, trailing: false }
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
methods: {
|
|
92
|
+
handleCopy(event) {
|
|
93
|
+
const payload = {
|
|
94
|
+
target: event.currentTarget,
|
|
95
|
+
};
|
|
96
|
+
this.throttledCopy(payload);
|
|
97
|
+
},
|
|
98
|
+
async executeCopy(payload) {
|
|
99
|
+
try {
|
|
100
|
+
// 获取待复制内容(支持函数调用)
|
|
101
|
+
const rawValue = typeof this.value === "function" ? this.value() : this.value;
|
|
102
|
+
const content = `${rawValue ?? ""}`;
|
|
103
|
+
|
|
104
|
+
if (!content) return;
|
|
105
|
+
|
|
106
|
+
const isSuccess = await this.copyToClipboard(content);
|
|
107
|
+
|
|
108
|
+
if (isSuccess) {
|
|
109
|
+
this.calculatePosition(payload);
|
|
110
|
+
this.showToast();
|
|
111
|
+
this.$emit("success", content);
|
|
112
|
+
} else {
|
|
113
|
+
this.$emit("error", "ExecCommand failed");
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
this.$emit("error", err);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
// 核心复制逻辑:优先 Clipboard API,降级使用 textarea
|
|
120
|
+
async copyToClipboard(text) {
|
|
121
|
+
if (navigator?.clipboard?.writeText) {
|
|
122
|
+
try {
|
|
123
|
+
await navigator.clipboard.writeText(text);
|
|
124
|
+
return true;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.warn("Clipboard API failed, falling back...");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const textArea = document.createElement("textarea");
|
|
131
|
+
textArea.value = text;
|
|
132
|
+
textArea.style.position = "fixed";
|
|
133
|
+
textArea.style.left = "-9999px";
|
|
134
|
+
textArea.style.top = "-9999px";
|
|
135
|
+
document.body.appendChild(textArea);
|
|
136
|
+
textArea.focus();
|
|
137
|
+
textArea.select();
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const successful = document.execCommand("copy");
|
|
141
|
+
document.body.removeChild(textArea);
|
|
142
|
+
return successful;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
document.body.removeChild(textArea);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
calculatePosition(payload) {
|
|
149
|
+
const { target } = payload;
|
|
150
|
+
if (!target) return;
|
|
151
|
+
|
|
152
|
+
// 固定在触发文字上方约 30px,不再跟随鼠标坐标
|
|
153
|
+
const rect = target.getBoundingClientRect();
|
|
154
|
+
this.toastX = rect.left + rect.width / 2;
|
|
155
|
+
this.toastY = rect.top - 30;
|
|
156
|
+
},
|
|
157
|
+
showToast() {
|
|
158
|
+
clearTimeout(this.hideTimer);
|
|
159
|
+
this.toastVisible = true;
|
|
160
|
+
this.hideTimer = setTimeout(() => {
|
|
161
|
+
this.toastVisible = false;
|
|
162
|
+
}, 1800);
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
beforeUnmount() {
|
|
166
|
+
clearTimeout(this.hideTimer);
|
|
167
|
+
this.throttledCopy?.cancel?.();
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
</script>
|
|
171
|
+
|
|
172
|
+
<style lang="less">
|
|
173
|
+
.w-copy-container {
|
|
174
|
+
display: inline-flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
justify-content: center;
|
|
177
|
+
cursor: pointer;
|
|
178
|
+
user-select: none;
|
|
179
|
+
transition: transform 0.1s ease-out, opacity 0.2s ease;
|
|
180
|
+
border-radius: 4px;
|
|
181
|
+
vertical-align: middle;
|
|
182
|
+
will-change: transform; // 优化渲染,防止抖动
|
|
183
|
+
|
|
184
|
+
// 点击时的缩放反馈:缩小比例幅度,防止视觉抖动过大
|
|
185
|
+
&:active {
|
|
186
|
+
transform: scale(0.97);
|
|
187
|
+
opacity: 0.85;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 键盘聚焦样式
|
|
191
|
+
&:focus-visible {
|
|
192
|
+
outline: 2px solid #22c55e;
|
|
193
|
+
outline-offset: 2px;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.w-copy-toast {
|
|
198
|
+
position: fixed;
|
|
199
|
+
z-index: 9999;
|
|
200
|
+
transform: translate(-50%, -100%);
|
|
201
|
+
pointer-events: none;
|
|
202
|
+
will-change: transform, opacity;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.w-copy-toast__content {
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
gap: 8px;
|
|
209
|
+
padding: 6px 14px;
|
|
210
|
+
background: rgba(31, 45, 61, 0.9); // 深色背景,对比度更高
|
|
211
|
+
backdrop-filter: blur(8px); // 磨砂玻璃效果
|
|
212
|
+
border-radius: 8px;
|
|
213
|
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
|
|
214
|
+
color: #ffffff;
|
|
215
|
+
font-size: 13px;
|
|
216
|
+
white-space: nowrap;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.w-copy-toast__svg {
|
|
220
|
+
width: 14px;
|
|
221
|
+
height: 14px;
|
|
222
|
+
color: #22c55e; // 成功绿色
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* 优化后的过渡动画:带有一点向上漂浮的感觉 */
|
|
226
|
+
.copy-toast-fade-enter-active {
|
|
227
|
+
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.copy-toast-fade-leave-active {
|
|
231
|
+
transition: all 0.4s cubic-bezier(0.755, 0.05, 0.855, 0.06);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.copy-toast-fade-enter-from {
|
|
235
|
+
opacity: 0;
|
|
236
|
+
transform: translate(-50%, -60%) scale(0.9);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.copy-toast-fade-leave-to {
|
|
240
|
+
opacity: 0;
|
|
241
|
+
transform: translate(-50%, -140%) scale(0.95);
|
|
242
|
+
}
|
|
243
|
+
</style>
|