feat(doc): 拓展文档功能并优化主题配置

- 新增 SecureMedia 组件以支持安全媒体播放
- 更新导航栏和侧边栏结构,将"processed"重命名为"enhance"
- 在主题配置中添加 PDF.js 相关设置
- 更新 .gitignore 文件以排除 pdfjs 相关目录
This commit is contained in:
高手 2025-02-18 10:01:50 +08:00
parent cb12614fc3
commit 7fa137e24e
15 changed files with 248 additions and 7 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ gateway/static/*
gateway/main gateway/main
gateway/.env gateway/.env
gateway/config/urls.json gateway/config/urls.json
doc/src/.vuepress/public/pdfjs/

View File

@ -1,9 +1,11 @@
import { defineClientConfig } from "@vuepress/client"; import { defineClientConfig } from "@vuepress/client";
import UserInfo from "./components/UserInfo.vue"; import UserInfo from "./components/UserInfo.vue";
import SecureMedia from "./components/SecureMedia.vue";
export default defineClientConfig({ export default defineClientConfig({
enhance({ app, router }) { enhance({ app, router }) {
app.component("UserInfo", UserInfo); app.component("UserInfo", UserInfo);
app.component("SecureMedia", SecureMedia);
// 仅在客户端执行 // 仅在客户端执行
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {

View File

@ -0,0 +1,139 @@
<template>
<div class="secure-media-container">
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
加载中...
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-state">
{{ error.message }}
</div>
<!-- 内容展示 -->
<template v-else-if="secureUrl">
<!-- PDF 文件 -->
<PDF v-if="isPDF" :url="secureUrl" />
<!-- 视频文件 -->
<video v-else-if="isVideo" :src="secureUrl" controls class="secure-video" preload="metadata" />
<!-- 图片文件 -->
<img v-else-if="isImage" :src="secureUrl" :alt="alt" class="secure-image" />
<!-- 音频文件 -->
<audio v-else-if="isAudio" :src="secureUrl" controls class="secure-audio" />
<!-- 不支持的文件类型 -->
<div v-else class="unsupported-media">
不支持的文件类型
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
const props = defineProps({
src: {
type: String,
required: true
},
alt: {
type: String,
default: ''
}
})
const secureUrl = ref('')
const error = ref(null)
const loading = ref(true)
//
const isPDF = computed(() => props.src.toLowerCase().endsWith('.pdf'))
const isVideo = computed(() => /\.(mp4|webm|ogg|mov)$/i.test(props.src))
const isImage = computed(() => /\.(jpg|jpeg|png|gif|webp)$/i.test(props.src))
const isAudio = computed(() => /\.(mp3|wav|ogg)$/i.test(props.src))
//
const getSecureUrl = async () => {
try {
loading.value = true
error.value = null
const response = await fetch('/api/secure-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: props.src
})
})
if (!response.ok) {
throw new Error('获取安全链接失败')
}
const data = await response.json()
secureUrl.value = data.secureUrl
} catch (err) {
error.value = err
console.error('获取安全链接失败:', err)
} finally {
loading.value = false
}
}
onMounted(() => {
getSecureUrl()
})
</script>
<style scoped>
.secure-media-container {
width: 100%;
margin: 1rem 0;
}
.secure-image {
max-width: 100%;
height: auto;
}
.secure-audio {
width: 100%;
}
.secure-video {
width: 100%;
max-height: 80vh;
border-radius: 4px;
}
.unsupported-media {
padding: 1rem;
background: #f8f9fa;
border-radius: 4px;
color: #666;
text-align: center;
}
.loading-state {
padding: 1rem;
text-align: center;
color: #666;
background: #f8f9fa;
border-radius: 4px;
}
.error-state {
padding: 1rem;
text-align: center;
color: #dc3545;
background: #fff3f3;
border-radius: 4px;
border: 1px solid #ffcdd2;
}
</style>

View File

@ -2,5 +2,5 @@ import { navbar } from "vuepress-theme-hope";
export default navbar([ export default navbar([
"/", "/",
"/processed/", "/enhance/",
]); ]);

View File

@ -4,10 +4,10 @@ export default sidebar({
"/": [ "/": [
"", "",
{ {
text: "译文", text: "拓展",
icon: "laptop-code", icon: "laptop-code",
prefix: "processed/", prefix: "enhance/",
link: "processed/", link: "enhance/",
children: "structure", children: "structure",
}, },
{ {

View File

@ -1,5 +1,4 @@
import { hopeTheme } from "vuepress-theme-hope"; import { hopeTheme } from "vuepress-theme-hope";
import UserInfo from "./components/UserInfo.vue";
import navbar from "./navbar.js"; import navbar from "./navbar.js";
import sidebar from "./sidebar.js"; import sidebar from "./sidebar.js";
@ -24,7 +23,7 @@ export default hopeTheme({
navbarLayout: { navbarLayout: {
start: ["Brand"], start: ["Brand"],
center: ["Links"], center: ["Links"],
end: [ "Outlook", "UserInfo"], end: ["Outlook", "UserInfo"],
}, },
// 侧边栏 // 侧边栏
@ -135,6 +134,11 @@ export default hopeTheme({
components: { components: {
components: ["Badge", "VPCard", "PDF"], components: ["Badge", "VPCard", "PDF"],
componentOptions: {
pdf: {
pdfjs: '/pdfjs/', // 假设 PDF.js 放在 public/pdfjs/web/
},
},
}, },
icon: { icon: {

View File

@ -1,5 +1,5 @@
--- ---
title: 译文 title: 拓展
index: false index: false
icon: laptop-code icon: laptop-code
--- ---

View File

@ -2,6 +2,8 @@
title: 人物关系 title: 人物关系
--- ---
<SecureMedia src="https://files.jdysya.top/beishan/beishan-relation-graph.pdf" />
## 十一世 ## 十一世
- **可观**:有一子,光友。生卒不详。 - **可观**:有一子,光友。生卒不详。

13
doc/src/enhance/show.md Normal file
View File

@ -0,0 +1,13 @@
---
title: 媒体展示
---
善琪公之墓碑
<SecureMedia src="https://files.jdysya.top/beishan/xiang/IMG_1868.jpeg" />
善琪公与德艳公墓地环绕
<SecureMedia src="https://files.jdysya.top/beishan/xiang/IMG_1870.MOV" />

79
gateway/handlers/media.go Normal file
View File

@ -0,0 +1,79 @@
package handlers
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
)
type SecureUrlRequest struct {
URL string `json:"url"`
}
// urlEncode 对URL路径进行编码保持斜杠不变
func urlEncode(s string) string {
return strings.ReplaceAll(url.QueryEscape(s), "%2F", "/")
}
// toHex16 将时间戳转换为16进制小写形式
func toHex16(t int64) string {
return fmt.Sprintf("%x", t)
}
func GenerateSecureURL(c *gin.Context) {
var req SecureUrlRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求"})
return
}
// 从环境变量获取防盗链密钥
secretKey := os.Getenv("QINIU_ANTILEECH_KEY")
if secretKey == "" {
c.JSON(http.StatusInternalServerError, gin.H{"error": "未配置防盗链密钥"})
return
}
// 设置链接有效期为5分钟
deadline := time.Now().Add(5 * time.Minute).Unix()
deadlineHex := toHex16(deadline)
// 解析URL
parsedURL, err := url.Parse(req.URL)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的URL"})
return
}
// 获取并编码路径部分
urlPath := parsedURL.Path
encodedPath := urlEncode(urlPath)
// 构建签名字符串
signStr := secretKey + encodedPath + deadlineHex
// 计算MD5签名
hash := md5.New()
hash.Write([]byte(signStr))
sign := hex.EncodeToString(hash.Sum(nil))
// 构建查询参数
query := parsedURL.Query()
query.Set("sign", sign)
query.Set("t", deadlineHex)
// 重建URL
parsedURL.RawQuery = query.Encode()
secureUrl := parsedURL.String()
c.JSON(http.StatusOK, gin.H{
"secureUrl": secureUrl,
})
}

View File

@ -75,6 +75,7 @@ func main() {
// 添加路由认证中间件 // 添加路由认证中间件
r.Use(middleware.RouteAuthMiddleware()) r.Use(middleware.RouteAuthMiddleware())
r.POST("/api/secure-url", handlers.GenerateSecureURL)
// 在权限校验中间件后添加退出路由 // 在权限校验中间件后添加退出路由
r.GET("/logout", handlers.Logout) r.GET("/logout", handlers.Logout)