Skip to content

Commit 5e6af7a

Browse files
committed
feat: 博客增加vip功能
1 parent d00df1f commit 5e6af7a

File tree

6 files changed

+386
-12
lines changed

6 files changed

+386
-12
lines changed

docs/.vitepress/config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,10 @@ let nav_config = [
315315
text: "工具",
316316
link: "/spider-tools/",
317317
},
318+
{
319+
text: "博客会员",
320+
link: "/vip/",
321+
},
318322
{
319323
text: "资源导航",
320324
link: "/nav/",
@@ -715,6 +719,8 @@ export default defineConfig({
715719
// code: '广告',
716720
// placement: '广告'
717721
// }
722+
// 会员相关(示例激活码,可在此处配置多个,支持明文或SHA-256哈希)
723+
vipActivationHashes: ["e5b1764ba2392383c2c97b5ea76f9978a590be0ef0811fdd0d7b53b0c0cd0ebc"],
718724
},
719725
define: {
720726
__API_BASE__: JSON.stringify('https://api.example.com'),
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<script setup>
2+
import { computed } from 'vue'
3+
4+
const props = defineProps({
5+
type: { type: String, default: 'red' },
6+
label: { type: String, default: '按钮' },
7+
width: { type: String, default: 'auto' },
8+
height: { type: String, default: 'auto' },
9+
color: { type: String, default: '#fff' }
10+
})
11+
12+
const buttonClass = computed(() => ({
13+
red: 'btn-1',
14+
blue: 'btn-2',
15+
purple: 'btn-3'
16+
}[props.type] || 'btn-1'))
17+
</script>
18+
19+
<template>
20+
<button class="btn" :class="buttonClass" :style="{ width, height, color }">
21+
{{ label }}
22+
</button>
23+
24+
</template>
25+
26+
<style scoped>
27+
@keyframes vipAnim {
28+
0% { background-position: right center; }
29+
100% { background-position: center left; }
30+
}
31+
.btn {
32+
padding: 5px 10px;
33+
border: none;
34+
transition: all 0.5s ease-out;
35+
font-weight: 500;
36+
border-radius: 4px;
37+
background-size: 200% auto;
38+
box-shadow: 0 0 15px 0 rgba(0,0,0,0.3);
39+
cursor: pointer;
40+
position: relative;
41+
animation: vipAnim 3s ease-in-out alternate infinite;
42+
}
43+
.btn:hover { transform: scale(1.05); }
44+
.btn-1 {
45+
background-image: linear-gradient(to right, rgb(255,220,50) 0%, rgb(170,40,120) 51%, rgb(255,220,50) 100%);
46+
}
47+
.btn-2 {
48+
background-image: linear-gradient(to right, rgb(0,200,255) 0%, rgb(0,40,255) 51%, rgb(0,200,255) 100%);
49+
}
50+
.btn-3 {
51+
background-image: linear-gradient(to right, rgb(0,40,255) 0%, rgb(230,90,230) 51%, rgb(0,40,255) 100%);
52+
}
53+
</style>
54+

docs/.vitepress/theme/index.js

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,14 @@ import { onMounted, watch, nextTick } from 'vue';
1212

1313
import ElementPlus from 'element-plus'
1414
import 'element-plus/dist/index.css'
15+
import VipBtn from './components/VipBtn.vue'
1516

1617
/** @type {import('vitepress').Theme} */
1718
export default {
1819
extends: DefaultTheme,
19-
enhanceApp({ app,router,siteData}) {
20-
// 注册自定义全局组件
21-
app.component('WebLink',WebLink).use(ElementPlus);
22-
},
23-
async enhanceApp() {
20+
async enhanceApp({ app }) {
21+
app.use(ElementPlus);
22+
app.component('VipBtn', VipBtn);
2423
if (!import.meta.env.SSR) {
2524
const { loadOml2d } = await import('oh-my-live2d');
2625
loadOml2d({
@@ -35,7 +34,7 @@ export default {
3534
// 添加 giscus 评论系统的 script 标签
3635
setup() {
3736
// Get frontmatter and route
38-
const { frontmatter } = useData();
37+
const { frontmatter, theme } = useData();
3938
const route = useRoute();
4039
// giscus配置
4140
giscusTalk({
@@ -69,9 +68,98 @@ export default {
6968
() => route.path,
7069
() => nextTick(() => initZoom())
7170
);
72-
73-
74-
75-
71+
const gateId = 'member-gate-overlay';
72+
const removeGate = () => {
73+
const ov = document.getElementById(gateId);
74+
if (ov) ov.remove();
75+
const docEl = document.querySelector('.VPDoc') || document.querySelector('.main .content') || document.querySelector('.main');
76+
if (docEl) docEl.classList.remove('member-lock-blur');
77+
};
78+
const sha256 = async (s) => {
79+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
80+
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
81+
};
82+
const applyGate = async () => {
83+
const fm = frontmatter.value || {};
84+
const need = fm.memberOnly === true || fm.memberOnly === 'true';
85+
if (!need) {
86+
removeGate();
87+
return;
88+
}
89+
const themeCfg = theme.value || {};
90+
const globalPass = themeCfg.memberPassword ?? (typeof __VIP_PASSWORD__ !== 'undefined' ? __VIP_PASSWORD__ : undefined);
91+
const globalHash = themeCfg.memberHash ?? (typeof __VIP_HASH__ !== 'undefined' ? __VIP_HASH__ : undefined);
92+
const globalActs = themeCfg.vipActivationHashes ?? (typeof __VIP_ACTIVATION_HASHES__ !== 'undefined' ? __VIP_ACTIVATION_HASHES__ : undefined);
93+
const actList = Array.isArray(globalActs) ? globalActs : (typeof globalActs === 'string' ? [globalActs] : []);
94+
const key = 'vp_member_unlock:*';
95+
const unlocked = localStorage.getItem(key) === '1';
96+
if (unlocked) {
97+
removeGate();
98+
return;
99+
}
100+
const docEl = document.querySelector('.VPDoc') || document.querySelector('.main .content') || document.querySelector('.main');
101+
if (docEl) docEl.classList.add('member-lock-blur');
102+
let overlay = document.getElementById(gateId);
103+
if (!overlay) {
104+
overlay = document.createElement('div');
105+
overlay.id = gateId;
106+
overlay.className = 'member-gate-overlay';
107+
const title = fm.memberTitle || '会员内容已锁定';
108+
overlay.innerHTML = `
109+
<div class="member-gate-dialog">
110+
<h3>${title}</h3>
111+
<div class="row">
112+
<input type="password" placeholder="输入密码" />
113+
<div class="member-gate-error"></div>
114+
<button type="button">确认</button>
115+
</div>
116+
</div>
117+
`;
118+
const input = overlay.querySelector('input');
119+
const btn = overlay.querySelector('button');
120+
const err = overlay.querySelector('.member-gate-error');
121+
btn.addEventListener('click', async () => {
122+
const v = (input?.value || '').trim();
123+
let ok = false;
124+
if (globalPass !== undefined && globalPass !== null) {
125+
const sp = String(globalPass);
126+
const isHex64 = /^[0-9a-fA-F]{64}$/.test(sp);
127+
if (isHex64) {
128+
const h = await sha256(v);
129+
ok = h.toLowerCase() === sp.toLowerCase();
130+
} else {
131+
ok = v === sp;
132+
}
133+
} else if (globalHash) {
134+
const h = await sha256(v);
135+
ok = h.toLowerCase() === String(globalHash).toLowerCase();
136+
} else if (actList.length > 0) {
137+
const items = actList.map(s => String(s));
138+
const allHex = items.every(x => /^[0-9a-fA-F]{64}$/.test(x));
139+
if (allHex) {
140+
const h = await sha256(v);
141+
ok = items.map(x => x.toLowerCase()).includes(h.toLowerCase());
142+
} else {
143+
ok = items.includes(v);
144+
}
145+
}
146+
if (ok) {
147+
localStorage.setItem(key, '1');
148+
removeGate();
149+
} else if (err) {
150+
err.textContent = '密码错误';
151+
input?.focus();
152+
}
153+
});
154+
document.body.appendChild(overlay);
155+
}
156+
};
157+
onMounted(() => {
158+
applyGate();
159+
});
160+
watch(
161+
() => route.path,
162+
() => nextTick(() => applyGate())
163+
);
76164
}
77-
}
165+
}

docs/backend/docker.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
---
2+
memberOnly: true
3+
---
14
[![](/imgs/ads/lky.png)](https://www.lcayun.com/aff/DECEDOZS)
25

36

@@ -42,4 +45,3 @@ docker ps -a
4245
#查看运行的容器:
4346
docker ps
4447
```
45-

docs/public/css/index.css

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,58 @@
1717
color: rgb(79, 125, 239);
1818
}
1919
/* alertify的弹窗样式 结束*/
20+
.member-gate-overlay {
21+
position: fixed;
22+
inset: 0;
23+
background: rgba(0, 0, 0, 0.35);
24+
backdrop-filter: blur(2px);
25+
display: flex;
26+
align-items: center;
27+
justify-content: center;
28+
z-index: 99999;
29+
}
30+
.member-gate-dialog {
31+
background: var(--vp-c-bg);
32+
border: 1px solid var(--vp-c-divider);
33+
border-radius: 12px;
34+
padding: 18px;
35+
width: 320px;
36+
max-width: 90%;
37+
box-shadow: var(--vp-shadow-2);
38+
color: var(--vp-c-text-1);
39+
}
40+
.member-gate-dialog h3 {
41+
margin: 0 0 12px;
42+
font-size: 18px;
43+
}
44+
.member-gate-dialog .row {
45+
display: grid;
46+
gap: 8px;
47+
}
48+
.member-gate-dialog input {
49+
width: 100%;
50+
height: 36px;
51+
border: 1px solid var(--vp-c-divider);
52+
border-radius: 8px;
53+
padding: 0 10px;
54+
background: var(--vp-c-bg-soft);
55+
color: var(--vp-c-text-1);
56+
outline: none;
57+
}
58+
.member-gate-dialog button {
59+
height: 36px;
60+
border: none;
61+
border-radius: 8px;
62+
background: var(--vp-c-brand-1);
63+
color: #fff;
64+
cursor: pointer;
65+
}
66+
.member-gate-error {
67+
color: var(--vp-c-danger-1);
68+
font-size: 12px;
69+
min-height: 14px;
70+
}
71+
.member-lock-blur {
72+
filter: blur(12px);
73+
pointer-events: none;
74+
}

0 commit comments

Comments
 (0)