@jx3box/jx3box-ui 2.1.8 → 2.1.10

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.
@@ -152,6 +152,7 @@ export default {
152
152
  },
153
153
  common: {
154
154
  copySuccess: "Copied",
155
+ copySuccessToast: "Copied",
155
156
  copyFailed: "Copy failed",
156
157
  cannotCopyClipboard: "Cannot access clipboard",
157
158
  browserCopyFailed: "Browser copy failed",
@@ -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",
@@ -152,6 +152,7 @@ export default {
152
152
  },
153
153
  common: {
154
154
  copySuccess: "复制成功",
155
+ copySuccessToast: "复制成功",
155
156
  copyFailed: "复制失败",
156
157
  cannotCopyClipboard: "无法复制到剪贴板",
157
158
  browserCopyFailed: "浏览器复制失败",
@@ -152,6 +152,7 @@ export default {
152
152
  },
153
153
  common: {
154
154
  copySuccess: "複製成功",
155
+ copySuccessToast: "複製成功",
155
156
  copyFailed: "複製失敗",
156
157
  cannotCopyClipboard: "無法複製到剪貼簿",
157
158
  browserCopyFailed: "瀏覽器複製失敗",
package/index.js CHANGED
@@ -32,6 +32,7 @@ import Thx from "./src/single/Thx.vue";
32
32
  import UserPop from "./src/author/UserPop.vue";
33
33
  import SuspendCommon from "./src/SuspendCommon.vue";
34
34
  import AdminDrop from "./src/bread/AdminDrop.vue";
35
+ import Copy from "./src/interact/Copy.vue";
35
36
 
36
37
  import {
37
38
  Jx3boxUiI18n,
@@ -72,6 +73,7 @@ const components = {
72
73
  UserPop,
73
74
  SuspendCommon,
74
75
  AdminDrop,
76
+ Copy,
75
77
  };
76
78
 
77
79
  const install = function (app) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jx3box/jx3box-ui",
3
- "version": "2.1.8",
3
+ "version": "2.1.10",
4
4
  "description": "JX3BOX Vue3 UI",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -32,12 +32,12 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@element-plus/icons-vue": "^2.3.2",
35
- "@jx3box/jx3box-common": "^9.2.0",
35
+ "@jx3box/jx3box-common": "^9.2.1",
36
36
  "@jx3box/jx3box-data": "^3.9.4",
37
- "@jx3box/jx3box-editor": "^3.1.1",
37
+ "@jx3box/jx3box-editor": "^3.1.3",
38
38
  "@jx3box/jx3box-emotion": "^1.3.0",
39
39
  "@jx3box/jx3box-macro": "^1.0.3",
40
- "@jx3box/jx3box-talent": "^1.3.12",
40
+ "@jx3box/jx3box-talent": "^1.3.13",
41
41
  "@tinymce/tinymce-vue": "^5.1.1",
42
42
  "@vueuse/core": "^9.13.0",
43
43
  "@vueuse/head": "^0.7.6",
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
+ <Copy value="xfbsyxj">xfbsyxj</Copy>
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 Copy 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
+ Copy,
207
213
  Author,
208
214
  // PostHeader,
209
215
  PostTopic,
@@ -123,6 +123,7 @@ export default {
123
123
  user-select: none;
124
124
  padding: 5px 0;
125
125
  padding-right: 20px;
126
+ .w(150px);
126
127
 
127
128
  &.on {
128
129
  .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>