feat: 添加 PGP 公钥展示和复制功能

This commit is contained in:
2026-05-02 15:26:45 +08:00
parent 414259cec6
commit cb8c9c6764
4 changed files with 142 additions and 4 deletions

View File

@@ -31,7 +31,7 @@
<template v-for="link in links" :key="link.url"> <template v-for="link in links" :key="link.url">
<NuxtLink <NuxtLink
:to="link.url" :to="link.url"
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white/10 backdrop-blur-sm border border-white/10 text-text-primary text-sm font-medium transition-all duration-200 hover:bg-primary/20 hover:border-primary/40 hover:text-primary hover:-translate-y-1" class="social-link-chip inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium transition-all duration-200 hover:-translate-y-1"
> >
<span <span
v-if="iconFor(link)" v-if="iconFor(link)"
@@ -44,6 +44,41 @@
</template> </template>
</div> </div>
<div
v-if="pgpInfo.publicKey"
class="mt-4 rounded-2xl border border-white/20 bg-white/10 p-4 backdrop-blur-xl"
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="min-w-0">
<p class="m-0 text-sm font-semibold text-white">PGP 公钥</p>
<p class="m-0 mt-1 break-all text-xs text-text-muted">
指纹{{ formatFingerprint(pgpInfo.fingerprint) || "未提供指纹" }}
</p>
</div>
<div class="flex items-center gap-2">
<a
v-if="pgpInfo.keyUrl"
:href="pgpInfo.keyUrl"
target="_blank"
rel="noreferrer"
class="social-link-chip inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium"
>
查看公钥
</a>
<button
type="button"
class="social-link-chip inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium"
@click="copyPgpPublicKey"
>
复制公钥
</button>
</div>
</div>
<p v-if="copyFeedback" class="m-0 mt-2 text-xs text-text-muted">{{ copyFeedback }}</p>
</div>
<!-- <button <!-- <button
v-show="canScrollRight" v-show="canScrollRight"
aria-label="向右滚动" aria-label="向右滚动"
@@ -72,7 +107,8 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount } from "vue"; import { computed, ref, onMounted, onBeforeUnmount } from "vue";
import siteConfig from "~/config/siteConfig";
defineProps({ defineProps({
links: { links: {
@@ -83,6 +119,16 @@ defineProps({
const container = ref(null); const container = ref(null);
const showOnlyIcons = ref(false); const showOnlyIcons = ref(false);
const copyFeedback = ref("");
const pgpInfo = computed(() => {
const pgp = siteConfig.profile?.pgp || {};
return {
fingerprint: pgp.fingerprint || "",
publicKey: pgp.publicKey || "",
keyUrl: pgp.keyUrl || "",
};
});
const iconMap = { const iconMap = {
bilibili: "simple-icons:bilibili", bilibili: "simple-icons:bilibili",
@@ -114,6 +160,20 @@ const iconFor = (link) => {
return null; return null;
}; };
const formatFingerprint = (fingerprint) => {
if (!fingerprint) return "";
return fingerprint.match(/.{1,4}/g)?.join(" ") || fingerprint;
};
async function copyPgpPublicKey() {
if (!pgpInfo.value.publicKey || !navigator.clipboard) return;
await navigator.clipboard.writeText(pgpInfo.value.publicKey);
copyFeedback.value = "已复制公钥";
window.setTimeout(() => {
copyFeedback.value = "";
}, 1600);
}
function updateScrollButtons() { function updateScrollButtons() {
const el = container.value; const el = container.value;
if (!el) return; if (!el) return;
@@ -144,6 +204,26 @@ onBeforeUnmount(() => {
</script> </script>
<style scoped> <style scoped>
.social-link-chip {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), rgba(124, 193, 255, 0.12));
border: 1px solid rgba(255, 255, 255, 0.3);
color: rgba(250, 253, 255, 0.98);
text-shadow: 0 1px 2px rgba(15, 22, 41, 0.4);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.28),
0 10px 28px rgba(8, 14, 28, 0.16);
backdrop-filter: blur(12px) saturate(160%);
}
.social-link-chip:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(124, 193, 255, 0.2));
border-color: rgba(124, 193, 255, 0.58);
color: #f6fbff;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.35),
0 14px 30px rgba(124, 193, 255, 0.18);
}
.social-links-scroll { .social-links-scroll {
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */

View File

@@ -5,6 +5,33 @@ const siteConfig = {
avatar: "/avatar-1.webp", // public/avatar.webp avatar: "/avatar-1.webp", // public/avatar.webp
bio: "趁世界还未重启之前 约一次爱恋", bio: "趁世界还未重启之前 约一次爱恋",
email: "i@rhen.cloud", email: "i@rhen.cloud",
pgp: {
fingerprint: "4A0D0DE4379AEB4562ED5EC0A574A617378C4E0B",
publicKey: `-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEaaqxVRYJKwYBBAHaRw8BAQdAGfMGv3ZrFvyC3aB69rrCe7hj19VXqfn+fxQ4
R1xxsK+0GFJoZW5DbG91ZCA8aUByaGVuLmNsb3VkPoiyBBMWCgBaGxSAAAAAAAQA
Dm1hbnUyLDIuNSsxLjExLDIsMQIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIX
gBYhBEoNDeQ3mutFYu1ewKV0phc3jE4LBQJpuBlSAhkBAAoJEKV0phc3jE4L3cYB
AOVr0OASfXF7fv7hE9u82CYtCB3o70bc+hF0cvqdHn+RAQCfEgw5iQo0GA2BfhPK
U1VKL71dm/QxGJ12n9Q2SsWwDrQgUmhlbkNsb3VkIDxyaGVuY2xvdWRAc2lpd2F5
Lm9yZz6IrwQTFgoAVxYhBEoNDeQ3mutFYu1ewKV0phc3jE4LBQJpuBjzGxSAAAAA
AAQADm1hbnUyLDIuNSsxLjExLDIsMQIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe
BwIXgAAKCRCldKYXN4xOC+OtAP9UIlQwEBDKeOVvBTknykmrN2XfPH+9BBd5YUC+
l44rAQEAmjzieQWLCwz8D9Ythya+6rRE0eXa6Kd0cL8Stwe9wwq4MwRpqrFVFgkr
BgEEAdpHDwEBB0D7rYWSdRC5vUBQw1FgX83X0WZOSRPYhzi1o1PkEE0GxIiUBBgW
CgA8FiEESg0N5Dea60Vi7V7ApXSmFzeMTgsFAmmqsVUbFIAAAAAABAAObWFudTIs
Mi41KzEuMTEsMiwxAhsgAAoJEKV0phc3jE4LKp4BAIsaNWogAP0TxrRseS3zk+BE
/K5sdmIt4nJNYVC91keVAQC2PFhdfbVRIbisJ7k6atOPrjKSeUMKHhYbQWky0ptB
C7g4BGmqsVUSCisGAQQBl1UBBQEBB0CQAYihK+4Qeq0jMXhko5JFhztIcGM3muKb
tjY4KCPQYgMBCAeIlAQYFgoAPBYhBEoNDeQ3mutFYu1ewKV0phc3jE4LBQJpqrFV
GxSAAAAAAAQADm1hbnUyLDIuNSsxLjExLDIsMQIbDAAKCRCldKYXN4xOC1QLAQCx
e7ogTB50YVDIMF7A8iQMm9Kn29vjsLftSBsTDzUB4gD+NeGyST5c81RIbNf0eWUk
En5WfP0rfILKDkvm8jD0/AU=
=7KnD
-----END PGP PUBLIC KEY BLOCK-----`,
keyUrl: "/rhencloud.asc",
},
birthday: "2010-03-28", birthday: "2010-03-28",
// gender: "女", // gender: "女",
pronouns: "她", pronouns: "她",

23
public/rhencloud.asc Normal file
View File

@@ -0,0 +1,23 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEaaqxVRYJKwYBBAHaRw8BAQdAGfMGv3ZrFvyC3aB69rrCe7hj19VXqfn+fxQ4
R1xxsK+0GFJoZW5DbG91ZCA8aUByaGVuLmNsb3VkPoiyBBMWCgBaGxSAAAAAAAQA
Dm1hbnUyLDIuNSsxLjExLDIsMQIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIX
gBYhBEoNDeQ3mutFYu1ewKV0phc3jE4LBQJpuBlSAhkBAAoJEKV0phc3jE4L3cYB
AOVr0OASfXF7fv7hE9u82CYtCB3o70bc+hF0cvqdHn+RAQCfEgw5iQo0GA2BfhPK
U1VKL71dm/QxGJ12n9Q2SsWwDrQgUmhlbkNsb3VkIDxyaGVuY2xvdWRAc2lpd2F5
Lm9yZz6IrwQTFgoAVxYhBEoNDeQ3mutFYu1ewKV0phc3jE4LBQJpuBjzGxSAAAAA
AAQADm1hbnUyLDIuNSsxLjExLDIsMQIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe
BwIXgAAKCRCldKYXN4xOC+OtAP9UIlQwEBDKeOVvBTknykmrN2XfPH+9BBd5YUC+
l44rAQEAmjzieQWLCwz8D9Ythya+6rRE0eXa6Kd0cL8Stwe9wwq4MwRpqrFVFgkr
BgEEAdpHDwEBB0D7rYWSdRC5vUBQw1FgX83X0WZOSRPYhzi1o1PkEE0GxIiUBBgW
CgA8FiEESg0N5Dea60Vi7V7ApXSmFzeMTgsFAmmqsVUbFIAAAAAABAAObWFudTIs
Mi41KzEuMTEsMiwxAhsgAAoJEKV0phc3jE4LKp4BAIsaNWogAP0TxrRseS3zk+BE
/K5sdmIt4nJNYVC91keVAQC2PFhdfbVRIbisJ7k6atOPrjKSeUMKHhYbQWky0ptB
C7g4BGmqsVUSCisGAQQBl1UBBQEBB0CQAYihK+4Qeq0jMXhko5JFhztIcGM3muKb
tjY4KCPQYgMBCAeIlAQYFgoAPBYhBEoNDeQ3mutFYu1ewKV0phc3jE4LBQJpqrFV
GxSAAAAAAAQADm1hbnUyLDIuNSsxLjExLDIsMQIbDAAKCRCldKYXN4xOC1QLAQCx
e7ogTB50YVDIMF7A8iQMm9Kn29vjsLftSBsTDzUB4gD+NeGyST5c81RIbNf0eWUk
En5WfP0rfILKDkvm8jD0/AU=
=7KnD
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1,10 +1,18 @@
{ {
"extends": "./.nuxt/tsconfig.json", "extends": "./.nuxt/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"types": ["node"] "types": [
"node"
]
}, },
"vueCompilerOptions": { "vueCompilerOptions": {
"globalTypesPath": "./node_modules/.vue-global-types" "globalTypesPath": "./node_modules/.vue-global-types"
}, },
"include": ["nuxt.config.ts", "app/**/*.ts", "app/**/*.vue", "app/**/*.d.ts", "server/**/*.ts"] "include": [
"nuxt.config.ts",
"app/**/*.ts",
"app/**/*.vue",
"app/**/*.d.ts",
"server/**/*.ts"
]
} }