Skip to content

Commit ce60a98

Browse files
committed
Them thong bao copy cho trang scan
1 parent b0d6cc1 commit ce60a98

2 files changed

Lines changed: 136 additions & 8 deletions

File tree

scan/src/App.vue

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,23 @@
254254
</template>
255255
</main>
256256

257+
<Transition name="copy-toast">
258+
<div
259+
v-if="copyToast"
260+
:class="['copy-toast', copyToast.tone]"
261+
:role="copyToast.tone === 'error' ? 'alert' : 'status'"
262+
:aria-label="`${copyToast.title}: ${copyToast.body}`"
263+
aria-live="polite"
264+
>
265+
<CheckCircle2 v-if="copyToast.tone === 'success'" :size="18" aria-hidden="true" />
266+
<AlertTriangle v-else :size="18" aria-hidden="true" />
267+
<span>
268+
<strong>{{ copyToast.title }}</strong>
269+
<small>{{ copyToast.body }}</small>
270+
</span>
271+
</div>
272+
</Transition>
273+
257274
<footer class="footer">
258275
<span>scan.mergeos.shop</span>
259276
<span>Last sync {{ lastSyncLabel }}</span>
@@ -269,6 +286,7 @@ import {
269286
ArrowDownLeft,
270287
ArrowUpRight,
271288
Braces,
289+
CheckCircle2,
272290
Coins,
273291
Copy,
274292
ExternalLink,
@@ -325,6 +343,8 @@ const queryFilter = ref('');
325343
const typeFilter = ref('all');
326344
const showAllTransactions = ref(false);
327345
const route = ref(parseRoute());
346+
const copyToast = ref(null);
347+
let copyToastTimer = 0;
328348
329349
const tokenSymbol = computed(() => config.value?.token_symbol || marketplace.value?.stats?.token_symbol || 'MRG');
330350
const paymentMode = computed(() => paymentModeLabel(config.value?.payment_mode));
@@ -391,6 +411,7 @@ onMounted(() => {
391411
392412
onBeforeUnmount(() => {
393413
window.removeEventListener('popstate', syncRoute);
414+
window.clearTimeout(copyToastTimer);
394415
});
395416
396417
async function loadExplorerData() {
@@ -703,18 +724,46 @@ function safeReturnPath(path = '/') {
703724
}
704725
705726
async function copyValue(value) {
727+
const text = String(value || '');
706728
try {
707-
await navigator.clipboard.writeText(String(value || ''));
708-
} catch {
709-
const input = document.createElement('textarea');
710-
input.value = String(value || '');
711-
document.body.appendChild(input);
712-
input.select();
713-
document.execCommand('copy');
714-
input.remove();
729+
if (navigator.clipboard?.writeText) {
730+
await navigator.clipboard.writeText(text);
731+
} else {
732+
fallbackCopyText(text);
733+
}
734+
showCopyToast('success', 'Copied to clipboard', copiedValueLabel(text));
735+
} catch (error) {
736+
showCopyToast('error', 'Copy failed', 'Please copy it manually.');
715737
}
716738
}
717739
740+
function fallbackCopyText(text) {
741+
const input = document.createElement('textarea');
742+
input.value = text;
743+
input.setAttribute('readonly', '');
744+
input.style.position = 'fixed';
745+
input.style.left = '-9999px';
746+
document.body.appendChild(input);
747+
input.select();
748+
const copied = document.execCommand('copy');
749+
input.remove();
750+
if (!copied) throw new Error('Copy command failed');
751+
}
752+
753+
function showCopyToast(tone, title, body) {
754+
window.clearTimeout(copyToastTimer);
755+
copyToast.value = { tone, title, body };
756+
copyToastTimer = window.setTimeout(() => {
757+
copyToast.value = null;
758+
}, 2600);
759+
}
760+
761+
function copiedValueLabel(value) {
762+
const text = String(value || '').trim();
763+
if (!text) return 'Value copied.';
764+
return text.length > 34 ? shortHash(text, 12, 8) : text;
765+
}
766+
718767
function typeLabel(type) {
719768
return ledgerTypeMeta(type).label;
720769
}

scan/src/styles.css

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,6 +1592,79 @@ main {
15921592
border-radius: 4px;
15931593
}
15941594

1595+
.copy-toast {
1596+
position: fixed;
1597+
right: clamp(18px, 3vw, 42px);
1598+
bottom: clamp(18px, 3vw, 42px);
1599+
z-index: 20;
1600+
display: grid;
1601+
grid-template-columns: auto minmax(0, 1fr);
1602+
gap: 12px;
1603+
align-items: center;
1604+
width: min(360px, calc(100vw - 36px));
1605+
padding: 14px 16px;
1606+
border: 1px solid rgba(77, 54, 27, 0.82);
1607+
border-radius: 5px;
1608+
background:
1609+
linear-gradient(180deg, rgba(255, 249, 225, 0.92), rgba(232, 210, 166, 0.86)),
1610+
var(--paper-grid),
1611+
rgba(246, 233, 201, 0.95);
1612+
box-shadow: 0 18px 34px rgba(51, 35, 14, 0.22), var(--engrave);
1613+
color: var(--ink);
1614+
pointer-events: none;
1615+
}
1616+
1617+
.copy-toast::before {
1618+
content: "";
1619+
position: absolute;
1620+
inset: 5px;
1621+
border: 1px solid rgba(112, 82, 44, 0.2);
1622+
border-radius: 3px;
1623+
pointer-events: none;
1624+
}
1625+
1626+
.copy-toast svg {
1627+
color: var(--forest);
1628+
stroke-width: 2.4;
1629+
}
1630+
1631+
.copy-toast.error svg {
1632+
color: var(--red);
1633+
}
1634+
1635+
.copy-toast span {
1636+
display: grid;
1637+
gap: 3px;
1638+
min-width: 0;
1639+
}
1640+
1641+
.copy-toast strong,
1642+
.copy-toast small {
1643+
overflow: hidden;
1644+
text-overflow: ellipsis;
1645+
white-space: nowrap;
1646+
}
1647+
1648+
.copy-toast strong {
1649+
font-size: 0.92rem;
1650+
}
1651+
1652+
.copy-toast small {
1653+
color: var(--muted);
1654+
font-size: 0.78rem;
1655+
}
1656+
1657+
.copy-toast-enter-active,
1658+
.copy-toast-leave-active {
1659+
transition: opacity 160ms ease, transform 160ms ease;
1660+
}
1661+
1662+
.copy-toast-enter-from,
1663+
.copy-toast-leave-to {
1664+
opacity: 0;
1665+
transform: translateY(10px);
1666+
}
1667+
15951668
.footer {
15961669
border-top-color: rgba(91, 67, 34, 0.56);
15971670
background: rgba(230, 205, 157, 0.2);
@@ -1805,6 +1878,12 @@ main {
18051878
max-width: 100%;
18061879
}
18071880

1881+
.copy-toast {
1882+
right: 18px;
1883+
bottom: 18px;
1884+
width: calc(100vw - 36px);
1885+
}
1886+
18081887
.panel-head,
18091888
.detail-head {
18101889
display: grid;

0 commit comments

Comments
 (0)