数据结构优化
This commit is contained in:
@@ -7,21 +7,31 @@ import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
// 配置参数
|
||||
// --- 配置参数 ---
|
||||
const config = {
|
||||
starCount: 150, // 星星数量
|
||||
meteorChance: 0.005, // 流星出现概率 (每帧)
|
||||
clickWaveColor: 'rgba(100, 200, 255, 0.4)', // 点击波纹颜色
|
||||
starCount: 600, // 大幅增加星星数量
|
||||
rotationSpeed: 0.0002, // 减慢旋转速度,更具呼吸感
|
||||
clickMeteorChance: 1.0 // 点击触发流星概率
|
||||
}
|
||||
|
||||
// 类型定义
|
||||
interface Star {
|
||||
x: number
|
||||
y: number
|
||||
radius: number
|
||||
alpha: number
|
||||
fadingOut: boolean
|
||||
speed: number
|
||||
|
||||
// 轨道参数
|
||||
orbitRadius: number
|
||||
angle: number
|
||||
angularSpeed: number
|
||||
|
||||
// 属性参数
|
||||
radius: number // 显示半径
|
||||
|
||||
// 闪烁参数
|
||||
alpha: number // 当前透明度
|
||||
baseAlpha: number // 基础透明度
|
||||
twinkleSpeed: number
|
||||
twinklePhase: number
|
||||
}
|
||||
|
||||
interface Meteor {
|
||||
@@ -30,6 +40,8 @@ interface Meteor {
|
||||
length: number
|
||||
speed: number
|
||||
angle: number
|
||||
alpha: number
|
||||
width: number
|
||||
}
|
||||
|
||||
interface Wave {
|
||||
@@ -37,6 +49,8 @@ interface Wave {
|
||||
y: number
|
||||
radius: number
|
||||
alpha: number
|
||||
lineWidth: number
|
||||
hue: number
|
||||
}
|
||||
|
||||
interface Particle {
|
||||
@@ -46,213 +60,257 @@ interface Particle {
|
||||
vy: number
|
||||
life: number
|
||||
color: string
|
||||
size: number
|
||||
}
|
||||
|
||||
// 状态
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
let animationFrameId: number
|
||||
let width = 0
|
||||
let height = 0
|
||||
let centerX = 0
|
||||
let centerY = 0
|
||||
|
||||
// 状态存储
|
||||
// 对象池
|
||||
const stars: Star[] = []
|
||||
const meteors: Meteor[] = []
|
||||
const waves: Wave[] = []
|
||||
const particles: Particle[] = []
|
||||
|
||||
// 初始化星星
|
||||
// 初始化星星 (星系分布 + 大小差异化)
|
||||
const initStars = () => {
|
||||
stars.length = 0
|
||||
const maxRadius = Math.sqrt(width * width + height * height) * 0.8
|
||||
|
||||
for (let i = 0; i < config.starCount; i++) {
|
||||
stars.push({
|
||||
x: Math.random() * width,
|
||||
y: Math.random() * height,
|
||||
radius: Math.random() * 1.5,
|
||||
alpha: Math.random(),
|
||||
fadingOut: Math.random() > 0.5,
|
||||
speed: Math.random() * 0.02 + 0.005
|
||||
})
|
||||
}
|
||||
}
|
||||
// 随机轨道半径
|
||||
const orbitRadius = Math.random() * maxRadius
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
|
||||
// 绘制并更新星星
|
||||
const drawStars = () => {
|
||||
if (!ctx) return
|
||||
// 模拟不同大小的星星分布
|
||||
// 80% 是微小的背景星,15% 是中等星星,5% 是亮眼的大星
|
||||
const sizeRandom = Math.random()
|
||||
let radius, baseAlpha
|
||||
|
||||
ctx.fillStyle = '#FFF'
|
||||
stars.forEach(star => {
|
||||
// 闪烁逻辑
|
||||
if (star.fadingOut) {
|
||||
star.alpha -= star.speed
|
||||
if (star.alpha < 0) {
|
||||
star.alpha = 0
|
||||
star.fadingOut = false
|
||||
}
|
||||
if (sizeRandom > 0.95) {
|
||||
// 大星 (Hero Stars)
|
||||
radius = Math.random() * 1.5 + 1.2
|
||||
baseAlpha = Math.random() * 0.4 + 0.6 // 比较亮
|
||||
} else if (sizeRandom > 0.8) {
|
||||
// 中星
|
||||
radius = Math.random() * 0.8 + 0.6
|
||||
baseAlpha = Math.random() * 0.3 + 0.3
|
||||
} else {
|
||||
star.alpha += star.speed
|
||||
if (star.alpha > 1) {
|
||||
star.alpha = 1
|
||||
star.fadingOut = true
|
||||
}
|
||||
// 微星 (Background Dust)
|
||||
radius = Math.random() * 0.4 + 0.2
|
||||
baseAlpha = Math.random() * 0.3 + 0.1
|
||||
}
|
||||
|
||||
if (ctx) {
|
||||
ctx.globalAlpha = star.alpha
|
||||
ctx.beginPath()
|
||||
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
})
|
||||
if (ctx) {
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
}
|
||||
// 旋转速度微调:外圈通常比内圈慢(或快,取决于想要的视觉效果),这里给一点随机性
|
||||
const speedVariation = (Math.random() - 0.5) * 0.0001
|
||||
const angularSpeed = config.rotationSpeed + speedVariation
|
||||
|
||||
// 生成流星
|
||||
const spawnMeteor = () => {
|
||||
if (Math.random() < config.meteorChance) {
|
||||
const startX = Math.random() * width
|
||||
// 稍微倾斜的角度
|
||||
meteors.push({
|
||||
x: startX,
|
||||
y: 0,
|
||||
length: Math.random() * 80 + 20,
|
||||
speed: Math.random() * 10 + 5,
|
||||
angle: Math.PI / 4 // 45度角
|
||||
stars.push({
|
||||
x: 0, y: 0,
|
||||
orbitRadius,
|
||||
angle,
|
||||
angularSpeed,
|
||||
radius,
|
||||
alpha: baseAlpha,
|
||||
baseAlpha,
|
||||
twinkleSpeed: Math.random() * 0.02 + 0.005,
|
||||
twinklePhase: Math.random() * Math.PI * 2
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制并更新流星
|
||||
const drawMeteors = () => {
|
||||
if (!ctx) return
|
||||
// 渲染循环
|
||||
const render = () => {
|
||||
if (!ctx || !canvasRef.value) return
|
||||
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'
|
||||
ctx.lineWidth = 1.5
|
||||
// 1. 清空画布
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// 2. 绘制星星
|
||||
stars.forEach(star => {
|
||||
// A. 更新角度 (纯自转,不再跟随鼠标)
|
||||
star.angle += star.angularSpeed
|
||||
|
||||
// B. 计算位置
|
||||
star.x = centerX + Math.cos(star.angle) * star.orbitRadius
|
||||
star.y = centerY + Math.sin(star.angle) * star.orbitRadius
|
||||
|
||||
// C. 闪烁计算 (呼吸感)
|
||||
star.twinklePhase += star.twinkleSpeed
|
||||
const twinkle = Math.sin(star.twinklePhase) * 0.2
|
||||
star.alpha = star.baseAlpha + twinkle
|
||||
|
||||
// 限制 alpha 范围
|
||||
if (star.alpha < 0) star.alpha = 0
|
||||
if (star.alpha > 1) star.alpha = 1
|
||||
|
||||
// 绘制
|
||||
ctx!.beginPath()
|
||||
ctx!.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
|
||||
|
||||
// 只给大星星加一点辉光,增强设计感
|
||||
if (star.radius > 1.2) {
|
||||
ctx!.shadowBlur = 6
|
||||
ctx!.shadowColor = `rgba(255, 255, 255, ${star.alpha * 0.5})`
|
||||
} else {
|
||||
ctx!.shadowBlur = 0
|
||||
}
|
||||
|
||||
ctx!.fillStyle = `rgba(255, 255, 255, ${star.alpha})`
|
||||
ctx!.fill()
|
||||
})
|
||||
|
||||
ctx.shadowBlur = 0 // 重置辉光
|
||||
|
||||
// 3. 绘制流星
|
||||
updateAndDrawMeteors()
|
||||
|
||||
// 4. 绘制波纹 (Shockwave)
|
||||
updateAndDrawWaves()
|
||||
|
||||
// 5. 绘制粒子 (Sparks)
|
||||
updateAndDrawParticles()
|
||||
|
||||
animationFrameId = requestAnimationFrame(render)
|
||||
}
|
||||
|
||||
// --- 辅助绘制函数 ---
|
||||
|
||||
const updateAndDrawMeteors = () => {
|
||||
if (Math.random() < 0.002) createMeteor() // 稍微降低自然流星频率,减少干扰
|
||||
|
||||
for (let i = meteors.length - 1; i >= 0; i--) {
|
||||
const m = meteors[i]
|
||||
if (!ctx) break
|
||||
|
||||
// 计算尾巴终点
|
||||
const endX = m.x - m.length * Math.cos(m.angle)
|
||||
const endY = m.y - m.length * Math.sin(m.angle)
|
||||
|
||||
// 绘制流星
|
||||
const gradient = ctx.createLinearGradient(m.x, m.y, endX, endY)
|
||||
gradient.addColorStop(0, 'rgba(255,255,255,1)')
|
||||
gradient.addColorStop(1, 'rgba(255,255,255,0)')
|
||||
ctx.strokeStyle = gradient
|
||||
gradient.addColorStop(0, `rgba(255, 255, 255, ${m.alpha})`)
|
||||
gradient.addColorStop(0.2, `rgba(200, 230, 255, ${m.alpha * 0.8})`)
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(m.x, m.y)
|
||||
ctx.lineTo(endX, endY)
|
||||
ctx.lineWidth = m.width
|
||||
ctx.strokeStyle = gradient
|
||||
ctx.lineCap = 'round'
|
||||
ctx.stroke()
|
||||
|
||||
// 移动
|
||||
m.x += m.speed * Math.cos(m.angle)
|
||||
m.y += m.speed * Math.sin(m.angle)
|
||||
|
||||
// 移除出屏幕的流星
|
||||
if (m.x > width || m.y > height) {
|
||||
meteors.splice(i, 1)
|
||||
if (m.x > width + 100 || m.x < -100 || m.y > height + 100 || m.y < -100) {
|
||||
m.alpha -= 0.02
|
||||
if (m.alpha <= 0) meteors.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制并更新波纹(点击效果)
|
||||
const drawWaves = () => {
|
||||
if (!ctx) return
|
||||
|
||||
const updateAndDrawWaves = () => {
|
||||
for (let i = waves.length - 1; i >= 0; i--) {
|
||||
const w = waves[i]
|
||||
if (!ctx) break
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(w.x, w.y, w.radius, 0, Math.PI * 2)
|
||||
ctx.strokeStyle = config.clickWaveColor
|
||||
ctx.globalAlpha = w.alpha
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeStyle = `hsla(${w.hue}, 80%, 70%, ${w.alpha})`
|
||||
ctx.lineWidth = w.lineWidth
|
||||
ctx.stroke()
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
w.radius += 2
|
||||
w.alpha -= 0.02
|
||||
w.radius += 2.5
|
||||
w.alpha *= 0.95
|
||||
w.lineWidth *= 0.95
|
||||
|
||||
if (w.alpha <= 0) {
|
||||
waves.splice(i, 1)
|
||||
}
|
||||
if (w.alpha < 0.01) waves.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制并更新粒子(点击爆炸效果)
|
||||
const drawParticles = () => {
|
||||
if (!ctx) return
|
||||
|
||||
const updateAndDrawParticles = () => {
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const p = particles[i]
|
||||
if (!ctx) break
|
||||
|
||||
ctx.fillStyle = p.color
|
||||
ctx.beginPath()
|
||||
ctx.arc(p.x, p.y, Math.random() * 2, 0, Math.PI * 2)
|
||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
p.x += p.vx
|
||||
p.y += p.vy
|
||||
p.life -= 0.02
|
||||
p.vy += 0.08
|
||||
p.vx *= 0.96
|
||||
p.vy *= 0.96
|
||||
|
||||
if (p.life <= 0) {
|
||||
particles.splice(i, 1)
|
||||
}
|
||||
p.life -= 0.02
|
||||
p.size *= 0.95
|
||||
|
||||
if (p.life <= 0) particles.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 动画主循环
|
||||
const animate = () => {
|
||||
if (!ctx || !canvasRef.value) return
|
||||
|
||||
// 清空画布 (使用带有透明度的黑色可以制造拖尾效果,这里我们直接清空保持清晰)
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// 绘制层级
|
||||
drawStars()
|
||||
spawnMeteor()
|
||||
drawMeteors()
|
||||
drawWaves()
|
||||
drawParticles()
|
||||
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
const createMeteor = (force = false) => {
|
||||
const startX = Math.random() * width
|
||||
const angle = Math.PI / 4 + (Math.random() - 0.5) * 0.3
|
||||
meteors.push({
|
||||
x: startX,
|
||||
y: -50,
|
||||
length: Math.random() * 100 + 80,
|
||||
speed: Math.random() * 15 + 12,
|
||||
angle: angle,
|
||||
alpha: 1,
|
||||
width: Math.random() * 2 + 0.5
|
||||
})
|
||||
}
|
||||
|
||||
// 点击事件处理
|
||||
// 交互处理
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
// 添加扩散波纹
|
||||
const hue = Math.floor(Math.random() * 360)
|
||||
|
||||
waves.push({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
radius: 0,
|
||||
alpha: 1
|
||||
radius: 10,
|
||||
alpha: 1,
|
||||
lineWidth: 4,
|
||||
hue: hue
|
||||
})
|
||||
|
||||
// 添加爆炸粒子
|
||||
const particleCount = 8
|
||||
const particleCount = 16
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
const angle = (Math.PI * 2 / particleCount) * i
|
||||
const speed = Math.random() * 3 + 1
|
||||
const angle = (Math.PI * 2 / particleCount) * i + Math.random() * 0.5
|
||||
const speed = Math.random() * 5 + 2
|
||||
particles.push({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
life: 1.0,
|
||||
color: `rgba(200, 230, 255, ${Math.random()})`
|
||||
color: `hsla(${hue}, ${80 + Math.random() * 20}%, 70%, 1)`,
|
||||
size: Math.random() * 3 + 1
|
||||
})
|
||||
}
|
||||
|
||||
createMeteor(true)
|
||||
}
|
||||
|
||||
// 窗口大小调整
|
||||
const handleResize = () => {
|
||||
if (canvasRef.value) {
|
||||
width = window.innerWidth
|
||||
height = window.innerHeight
|
||||
canvasRef.value.width = width
|
||||
canvasRef.value.height = height
|
||||
// 重新分布星星,或者只在必要时调整
|
||||
|
||||
centerX = width / 2
|
||||
centerY = height / 2
|
||||
|
||||
initStars()
|
||||
}
|
||||
}
|
||||
@@ -262,13 +320,11 @@ onMounted(() => {
|
||||
ctx = canvasRef.value.getContext('2d')
|
||||
handleResize()
|
||||
|
||||
// 初始化
|
||||
initStars()
|
||||
animate()
|
||||
|
||||
// 监听全局点击和resize
|
||||
window.addEventListener('resize', handleResize)
|
||||
// Removed mousemove listener for pure atmospheric effect
|
||||
window.addEventListener('click', handleClick)
|
||||
|
||||
render()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -280,7 +336,6 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 确保画布在最底层但又不影响其他背景色(如果需要混合模式可以调整) */
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" @click.self="close">
|
||||
<div class="bg-[#1a1a1a] border border-white/10 rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden animate-reveal flex flex-col">
|
||||
<Transition name="modal">
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" @click.self="close">
|
||||
<div class="bg-[#1a1a1a] border border-white/10 rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div class="p-4 border-b border-white/10 flex justify-between items-center shrink-0">
|
||||
<h3 class="text-lg font-serif italic text-white">访问记录 - {{ postTitle }}</h3>
|
||||
<button @click="close" class="text-white/50 hover:text-white">
|
||||
@@ -28,25 +29,20 @@
|
||||
<table class="w-full border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b border-white/10">
|
||||
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">用户ID</th>
|
||||
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">IP地址</th>
|
||||
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">归属地</th>
|
||||
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">访问路径</th>
|
||||
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">状态码</th>
|
||||
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">响应时间</th>
|
||||
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">访问时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in logs.list" :key="log.id" class="border-b border-white/5 hover:bg-white/5 transition-colors">
|
||||
<td class="py-2 px-3 text-sm text-white/90">
|
||||
<span v-if="log.userId && log.userId > 0" class="text-art-accent">用户 #{{ log.userId }}</span>
|
||||
<span v-else class="text-art-muted">游客</span>
|
||||
</td>
|
||||
<td class="py-2 px-3 text-sm text-white/90 font-mono">{{ log.ip }}</td>
|
||||
<td class="py-2 px-3 text-sm text-white/80">{{ log.region || 'Unknown' }}</td>
|
||||
<td class="py-2 px-3 text-sm text-white/80 font-mono">{{ log.path }}</td>
|
||||
<td class="py-2 px-3 text-sm">
|
||||
<span :class="getStatusClass(log.statusCode)">
|
||||
{{ log.statusCode }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2 px-3 text-sm text-white/80">{{ log.responseTime }}ms</td>
|
||||
<td class="py-2 px-3 text-sm text-white/80">{{ log.createdAt }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -77,12 +73,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { getPostAccessLogs, AccessLog, PaginationResponse } from '../../services/api'
|
||||
import { getPostAccessLogs, PostAccessLog, PaginationResponse } from '../../services/api'
|
||||
import { useToast } from '../../composables/useToast'
|
||||
|
||||
interface Props {
|
||||
@@ -101,7 +98,7 @@ const emit = defineEmits<{
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const logs = ref<PaginationResponse<AccessLog>>({
|
||||
const logs = ref<PaginationResponse<PostAccessLog>>({
|
||||
list: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
@@ -138,16 +135,6 @@ const changePage = (page: number) => {
|
||||
fetchLogs()
|
||||
}
|
||||
|
||||
const getStatusClass = (status: number) => {
|
||||
if (status >= 200 && status < 300) {
|
||||
return 'text-green-400'
|
||||
} else if (status >= 400 && status < 500) {
|
||||
return 'text-yellow-400'
|
||||
} else if (status >= 500) {
|
||||
return 'text-red-400'
|
||||
}
|
||||
return 'text-white/60'
|
||||
}
|
||||
|
||||
watch(() => props.isOpen, (newVal) => {
|
||||
if (newVal && props.postId) {
|
||||
@@ -165,18 +152,24 @@ watch(() => props.postId, () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animate-reveal {
|
||||
animation: reveal 0.3s ease-out;
|
||||
/* Modal transition */
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes reveal {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.modal-enter-active .bg-\[#1a1a1a\] {
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from,
|
||||
.modal-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter-from .bg-\[#1a1a1a\],
|
||||
.modal-leave-to .bg-\[#1a1a1a\] {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>操作人</th>
|
||||
<th>操作描述</th>
|
||||
<th>IP地址</th>
|
||||
<th>归属地</th>
|
||||
<th>路径</th>
|
||||
@@ -35,6 +36,7 @@
|
||||
<tr v-for="log in logs.list" :key="log.id">
|
||||
<td>{{ log.id }}</td>
|
||||
<td>{{ log.username }}</td>
|
||||
<td class="action-description">{{ log.action || `${log.method} ${log.path}` }}</td>
|
||||
<td class="font-mono text-xs">{{ log.ip }}</td>
|
||||
<td>{{ log.region || 'Unknown' }}</td>
|
||||
<td class="log-path">{{ log.path }}</td>
|
||||
@@ -238,6 +240,15 @@ onMounted(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-description {
|
||||
color: #d4b383;
|
||||
font-weight: 500;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.method-badge {
|
||||
display: inline-block;
|
||||
padding: 0.125rem 0.375rem;
|
||||
|
||||
@@ -17,6 +17,15 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
v-if="isEditing"
|
||||
type="button"
|
||||
class="admin-btn-secondary"
|
||||
@click="openAccessLogModal"
|
||||
title="查看访问记录"
|
||||
>
|
||||
📊 访问记录
|
||||
</button>
|
||||
<button type="button" class="admin-btn-secondary" @click="handleCancel">
|
||||
取消
|
||||
</button>
|
||||
@@ -159,6 +168,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Access Log Modal -->
|
||||
<AccessLogModal
|
||||
v-if="isEditing"
|
||||
v-model:isOpen="isAccessLogModalOpen"
|
||||
:postId="route.params.id ? parseInt(route.params.id as string) : null"
|
||||
:postTitle="form.title || '文章'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -172,6 +189,7 @@ import 'md-editor-v3/lib/style.css'
|
||||
import { useToast } from '../../composables/useToast'
|
||||
import { createPost, updatePost, fetchPost, fetchCategories, fetchColumns, fetchTags, Tag } from '../../services/api'
|
||||
import CustomSelect from '../../components/CustomSelect.vue'
|
||||
import AccessLogModal from '../../components/admin/AccessLogModal.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -188,6 +206,12 @@ const isSubmitting = ref(false)
|
||||
const isEditing = computed(() => !!route.params.id)
|
||||
const errors = reactive<Record<string, string>>({})
|
||||
|
||||
// Access log modal state
|
||||
const isAccessLogModalOpen = ref(false)
|
||||
const openAccessLogModal = () => {
|
||||
isAccessLogModalOpen.value = true
|
||||
}
|
||||
|
||||
// Data sources
|
||||
const categories = ref<{value: number, label: string}[]>([])
|
||||
const columns = ref<{value: number, label: string}[]>([])
|
||||
|
||||
@@ -307,33 +307,50 @@ const allProvinces = [
|
||||
'云南', '西藏', '陕西', '甘肃', '青海', '宁夏', '新疆', '台湾', '香港', '澳门'
|
||||
]
|
||||
|
||||
// 所有省份完整名称列表(用于匹配地图数据)
|
||||
const allProvincesFullNames = [
|
||||
'北京市', '天津市', '河北省', '山西省', '内蒙古自治区', '辽宁省', '吉林省', '黑龙江省',
|
||||
'上海市', '江苏省', '浙江省', '安徽省', '福建省', '江西省', '山东省', '河南省',
|
||||
'湖北省', '湖南省', '广东省', '广西壮族自治区', '海南省', '重庆市', '四川省', '贵州省',
|
||||
'云南省', '西藏自治区', '陕西省', '甘肃省', '青海省', '宁夏回族自治区', '新疆维吾尔自治区', '台湾省', '香港特别行政区', '澳门特别行政区'
|
||||
]
|
||||
|
||||
// 将数据转换为地图需要的格式,没有值的省份默认为0
|
||||
const getMapData = () => {
|
||||
// 创建省份数据映射
|
||||
const dataMap = new Map<string, number>()
|
||||
|
||||
// 如果有数据,先填充实际数据
|
||||
// 后端现在直接返回完整的省份名称(如"浙江省"、"北京市"等)
|
||||
if (stats.value.userRegions && stats.value.userRegions.length > 0) {
|
||||
stats.value.userRegions.forEach((item: any) => {
|
||||
let provinceName = item.Region
|
||||
// 标准化省份名称(移除"省"、"市"、"自治区"等后缀)
|
||||
provinceName = provinceName.replace(/省|市|自治区|特别行政区|壮族自治区|维吾尔自治区|回族自治区/g, '')
|
||||
// 后端返回的字段名是 Region 和 Count(首字母大写)
|
||||
const provinceName = item.Region || item.region
|
||||
const count = Number(item.Count || item.count || 0)
|
||||
|
||||
// 特殊处理
|
||||
if (provinceName === '内蒙古') provinceName = '内蒙古'
|
||||
if (provinceName === '广西') provinceName = '广西'
|
||||
if (provinceName === '西藏') provinceName = '西藏'
|
||||
if (provinceName === '宁夏') provinceName = '宁夏'
|
||||
if (provinceName === '新疆') provinceName = '新疆'
|
||||
if (!provinceName) {
|
||||
console.warn('Missing province name in item:', item)
|
||||
return
|
||||
}
|
||||
|
||||
dataMap.set(provinceName, item.Count || 0)
|
||||
// 直接使用后端返回的完整省份名称,不需要任何处理
|
||||
dataMap.set(provinceName, count)
|
||||
console.log(`Set province data: ${provinceName} = ${count}`)
|
||||
})
|
||||
}
|
||||
|
||||
// 为所有省份生成数据,没有值的默认为0
|
||||
return allProvinces.map(province => {
|
||||
return [province, dataMap.get(province) || 0]
|
||||
// ECharts map 类型需要对象数组格式: [{name: '浙江省', value: 1}]
|
||||
const result = allProvincesFullNames.map(province => {
|
||||
const count = dataMap.get(province) ?? 0
|
||||
return {
|
||||
name: province,
|
||||
value: count
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Map data result:', result)
|
||||
return result
|
||||
}
|
||||
|
||||
const regionOption = computed(() => {
|
||||
@@ -352,7 +369,15 @@ const regionOption = computed(() => {
|
||||
}
|
||||
|
||||
const mapData = getMapData()
|
||||
const maxValue = mapData.length > 0 ? Math.max(...mapData.map((d: any) => d[1] || 0)) : 1
|
||||
// 计算最大值,支持对象格式 {name, value} 和数组格式 [name, value]
|
||||
const maxValue = mapData.length > 0 ? Math.max(...mapData.map((d: any) => {
|
||||
if (typeof d === 'object' && 'value' in d) {
|
||||
return d.value || 0
|
||||
} else if (Array.isArray(d)) {
|
||||
return d[1] || 0
|
||||
}
|
||||
return 0
|
||||
})) : 1
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
|
||||
@@ -147,6 +147,7 @@ export interface OperationLog {
|
||||
region?: string
|
||||
path: string
|
||||
method: string
|
||||
action?: string
|
||||
params: string
|
||||
status: number
|
||||
duration: number
|
||||
@@ -1157,8 +1158,18 @@ export const getAccessLogs = async (page: number = 1, pageSize: number = 10): Pr
|
||||
}
|
||||
}
|
||||
|
||||
// 文章访问记录类型(来自 user_access_logs 表)
|
||||
export interface PostAccessLog {
|
||||
id: number
|
||||
userId: number
|
||||
ip: string
|
||||
region: string
|
||||
articleId: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// 获取指定文章的访问记录
|
||||
export const getPostAccessLogs = async (postId: number, page: number = 1, pageSize: number = 20): Promise<PaginationResponse<AccessLog>> => {
|
||||
export const getPostAccessLogs = async (postId: number, page: number = 1, pageSize: number = 20): Promise<PaginationResponse<PostAccessLog>> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/admin/posts/${postId}/access-logs?page=${page}&pageSize=${pageSize}`, {
|
||||
headers: getAuthHeaders()
|
||||
|
||||
@@ -40,8 +40,8 @@ func GetDashboardStats(c *gin.Context) {
|
||||
uvTrend = []repositories.UVTrendData{}
|
||||
}
|
||||
|
||||
// 3. 用户画像-地域分布
|
||||
userRegions, err := repositories.GetUserRegions(startDate, endDate)
|
||||
// 3. 用户画像-地域分布(使用 user_access_logs 表)
|
||||
userRegions, err := repositories.GetUserRegionsFromUserAccessLogs(startDate, endDate)
|
||||
if err != nil {
|
||||
// 即使出错,也返回所有34个省份的默认数据(Count为0)
|
||||
userRegions = repositories.GetDefaultUserRegions()
|
||||
|
||||
@@ -208,8 +208,8 @@ func AdminGetPostAccessLogs(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文章的访问记录
|
||||
logs, total, err := repositories.GetPostAccessLogs(postID, page, pageSize)
|
||||
// 获取文章的访问记录(从 user_access_logs 表)
|
||||
logs, total, err := repositories.GetPostAccessLogsFromUserAccessLogs(postID, page, pageSize)
|
||||
if err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
@@ -219,15 +219,12 @@ func AdminGetPostAccessLogs(c *gin.Context) {
|
||||
var logList []gin.H
|
||||
for _, log := range logs {
|
||||
logList = append(logList, gin.H{
|
||||
"id": log.ID,
|
||||
"ip": log.IP,
|
||||
"userAgent": log.UserAgent,
|
||||
"path": log.Path,
|
||||
"method": log.Method,
|
||||
"statusCode": log.StatusCode,
|
||||
"responseTime": log.ResponseTime,
|
||||
"region": log.Region,
|
||||
"createdAt": time.Unix(log.CreatedAt, 0).Format("2006-01-02 15:04:05"),
|
||||
"id": log.ID,
|
||||
"userId": log.UserID,
|
||||
"ip": log.UserIP,
|
||||
"region": log.UserLocation,
|
||||
"articleId": log.ArticleID,
|
||||
"createdAt": time.Unix(log.AccessTime, 0).Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ type OperationLogResponse struct {
|
||||
Region string `json:"region"` // IP归属地
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
Action string `json:"action"` // 操作描述
|
||||
Params string `json:"params"`
|
||||
Status int `json:"status"`
|
||||
Duration int `json:"duration"`
|
||||
|
||||
@@ -150,9 +150,9 @@ func DeleteColumn(id uint) error {
|
||||
func GetPostsByColumnID(columnID uint) ([]models.Post, error) {
|
||||
var posts []models.Post
|
||||
err := config.DB.Model(&models.Post{}).
|
||||
Select("posts.*, categories.name as category_name").
|
||||
Preload("Category").
|
||||
Preload("Tags").
|
||||
Joins("JOIN column_posts cp ON posts.id = cp.post_id").
|
||||
Joins("LEFT JOIN categories ON posts.category_id = categories.id").
|
||||
Where("cp.column_id = ? AND posts.deleted_at = ? AND posts.is_published = ?", columnID, 0, 1).
|
||||
Order("cp.sort_order ASC, posts.created_at DESC").
|
||||
Find(&posts).Error
|
||||
|
||||
@@ -135,9 +135,37 @@ func GetAccessLogs(page, pageSize int, postID *int) ([]models.AccessLog, int64,
|
||||
|
||||
// 如果指定了文章ID,筛选该文章的访问记录
|
||||
if postID != nil {
|
||||
// 路径格式可能是 /api/posts/{id} 或 /blog/{id} 等
|
||||
// 使用 LIKE 匹配包含文章ID的路径
|
||||
query = query.Where("path LIKE ?", fmt.Sprintf("%%/posts/%d%%", *postID))
|
||||
// 路径格式可能是:
|
||||
// - /api/posts/{id} (API调用)
|
||||
// - /blog/{id} (前端页面访问)
|
||||
// - /posts/{id} (其他可能的格式)
|
||||
// 使用精确的匹配,避免匹配到其他ID(如123匹配到1234)
|
||||
// 使用LIKE模式,确保ID后面是 / 或 ? 或 字符串结束
|
||||
postIDStr := fmt.Sprintf("%d", *postID)
|
||||
// 匹配精确的路径格式,避免部分匹配
|
||||
// 例如:/api/posts/123 或 /api/posts/123/ 或 /api/posts/123?xxx
|
||||
// 但不匹配 /api/posts/1234
|
||||
// 使用精确的 LIKE 匹配,确保只匹配正确的文章ID
|
||||
// 匹配格式:/api/posts/{id}、/api/posts/{id}/、/api/posts/{id}?xxx
|
||||
// 匹配格式:/blog/{id}、/blog/{id}/、/blog/{id}?xxx
|
||||
// 匹配格式:/posts/{id}、/posts/{id}/、/posts/{id}?xxx
|
||||
query = query.Where(
|
||||
"(path = ? OR path LIKE ? OR path LIKE ? OR "+
|
||||
"path = ? OR path LIKE ? OR path LIKE ? OR "+
|
||||
"path = ? OR path LIKE ? OR path LIKE ?)",
|
||||
// /api/posts/{id} 格式(3个参数)
|
||||
fmt.Sprintf("/api/posts/%s", postIDStr),
|
||||
fmt.Sprintf("/api/posts/%s/%%", postIDStr),
|
||||
fmt.Sprintf("/api/posts/%s?%%", postIDStr),
|
||||
// /blog/{id} 格式(3个参数)
|
||||
fmt.Sprintf("/blog/%s", postIDStr),
|
||||
fmt.Sprintf("/blog/%s/%%", postIDStr),
|
||||
fmt.Sprintf("/blog/%s?%%", postIDStr),
|
||||
// /posts/{id} 格式(3个参数)
|
||||
fmt.Sprintf("/posts/%s", postIDStr),
|
||||
fmt.Sprintf("/posts/%s/%%", postIDStr),
|
||||
fmt.Sprintf("/posts/%s?%%", postIDStr),
|
||||
)
|
||||
}
|
||||
|
||||
var total int64
|
||||
@@ -155,8 +183,9 @@ func GetAccessLogs(page, pageSize int, postID *int) ([]models.AccessLog, int64,
|
||||
return logs, total, err
|
||||
}
|
||||
|
||||
// GetPostAccessLogs 获取指定文章的访问记录
|
||||
func GetPostAccessLogs(postID int, page, pageSize int) ([]models.AccessLog, int64, error) {
|
||||
// GetPostAccessLogsFromAccessLogs 从 access_logs 表获取指定文章的访问记录(已废弃,应使用 user_access_logs)
|
||||
// 保留此函数以保持向后兼容,但建议使用 user_access_log_repository.GetPostAccessLogs
|
||||
func GetPostAccessLogsFromAccessLogs(postID int, page, pageSize int) ([]models.AccessLog, int64, error) {
|
||||
return GetAccessLogs(page, pageSize, &postID)
|
||||
}
|
||||
|
||||
@@ -192,9 +221,11 @@ var allProvinces = []string{
|
||||
"云南", "西藏", "陕西", "甘肃", "青海", "宁夏", "新疆", "台湾", "香港", "澳门",
|
||||
}
|
||||
|
||||
// GetUserRegions 获取用户地域分布(使用 access_logs 表的 region 字段)
|
||||
// GetUserRegionsFromAccessLogs 获取用户地域分布(使用 access_logs 表的 region 字段)
|
||||
// 返回所有34个省份的数据,没有数据的省份 Count 设为 0
|
||||
func GetUserRegions(startDate, endDate string) ([]struct {
|
||||
// 注意:此函数查询的是所有访问记录,包括静态资源、API调用等
|
||||
// 建议使用 GetUserRegionsFromUserAccessLogs 获取更准确的用户访问统计
|
||||
func GetUserRegionsFromAccessLogs(startDate, endDate string) ([]struct {
|
||||
Region string
|
||||
Count int
|
||||
}, error) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/niangaodev/art-code/config"
|
||||
"github.com/niangaodev/art-code/models"
|
||||
"github.com/niangaodev/art-code/utils"
|
||||
)
|
||||
|
||||
// CreateOperationLog 创建操作日志
|
||||
@@ -51,6 +52,9 @@ func GetOperationLogs(page, pageSize int) ([]models.OperationLog, int64, error)
|
||||
|
||||
// BuildOperationLogResponse 构建操作日志响应
|
||||
func BuildOperationLogResponse(log *models.OperationLog) *models.OperationLogResponse {
|
||||
// 生成操作描述
|
||||
action := utils.GetOperationAction(log.Path, log.Method)
|
||||
|
||||
return &models.OperationLogResponse{
|
||||
ID: log.ID,
|
||||
UserID: log.UserID,
|
||||
@@ -59,6 +63,7 @@ func BuildOperationLogResponse(log *models.OperationLog) *models.OperationLogRes
|
||||
Region: log.Region,
|
||||
Path: log.Path,
|
||||
Method: log.Method,
|
||||
Action: action,
|
||||
Params: log.Params,
|
||||
Status: log.Status,
|
||||
Duration: log.Duration,
|
||||
|
||||
@@ -2,6 +2,8 @@ package repositories
|
||||
|
||||
import (
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/niangaodev/art-code/config"
|
||||
@@ -70,3 +72,140 @@ func GetTopArticlesByAccess(limit int) ([]struct {
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
// GetPostAccessLogsFromUserAccessLogs 获取指定文章的访问记录(从 user_access_logs 表)
|
||||
func GetPostAccessLogsFromUserAccessLogs(postID int, page, pageSize int) ([]models.UserAccessLog, int64, error) {
|
||||
query := config.DB.Model(&models.UserAccessLog{}).
|
||||
Where("deleted_at = ?", 0).
|
||||
Where("article_id = ?", postID)
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var logs []models.UserAccessLog
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Order("access_time DESC").
|
||||
Limit(pageSize).
|
||||
Offset(offset).
|
||||
Find(&logs).Error
|
||||
|
||||
return logs, total, err
|
||||
}
|
||||
|
||||
// GetUserRegionsFromUserAccessLogs 获取用户地域分布(使用 user_access_logs 表的 user_location 字段)
|
||||
// 返回所有34个省份的数据,没有数据的省份 Count 设为 0
|
||||
func GetUserRegionsFromUserAccessLogs(startDate, endDate string) ([]struct {
|
||||
Region string
|
||||
Count int
|
||||
}, error) {
|
||||
query := config.DB.Model(&models.UserAccessLog{}).
|
||||
Select("COALESCE(NULLIF(user_location, ''), 'Unknown') as region, COUNT(DISTINCT user_ip) as count").
|
||||
Where("deleted_at = ?", 0)
|
||||
|
||||
if startDate != "" {
|
||||
startUnix := parseDateToUnix(startDate, false)
|
||||
query = query.Where("access_time >= ?", startUnix)
|
||||
}
|
||||
|
||||
if endDate != "" {
|
||||
endUnix := parseDateToUnix(endDate, true)
|
||||
query = query.Where("access_time <= ?", endUnix)
|
||||
}
|
||||
|
||||
var results []struct {
|
||||
Region string
|
||||
Count int
|
||||
}
|
||||
err := query.Group("region").
|
||||
Order("count DESC").
|
||||
Limit(50).
|
||||
Scan(&results).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析归属地,提取省份信息并聚合
|
||||
// 注意:现在返回完整的省份名称(如"浙江省"、"北京市"等),不进行任何处理
|
||||
provinceMap := make(map[string]int)
|
||||
for _, r := range results {
|
||||
province := extractProvinceFromRegion(r.Region)
|
||||
if province != "未知" {
|
||||
provinceMap[province] += r.Count
|
||||
log.Printf("Extracted province: %s from region: %s, count: %d", province, r.Region, r.Count)
|
||||
} else {
|
||||
log.Printf("Failed to extract province from region: %s", r.Region)
|
||||
}
|
||||
}
|
||||
|
||||
// 定义完整的省份名称列表(包含"省"、"市"、"自治区"等后缀)
|
||||
allProvincesFullNames := []string{
|
||||
"北京市", "天津市", "河北省", "山西省", "内蒙古自治区", "辽宁省", "吉林省", "黑龙江省",
|
||||
"上海市", "江苏省", "浙江省", "安徽省", "福建省", "江西省", "山东省", "河南省",
|
||||
"湖北省", "湖南省", "广东省", "广西壮族自治区", "海南省", "重庆市", "四川省", "贵州省",
|
||||
"云南省", "西藏自治区", "陕西省", "甘肃省", "青海省", "宁夏回族自治区", "新疆维吾尔自治区", "台湾省", "香港特别行政区", "澳门特别行政区",
|
||||
}
|
||||
|
||||
// 为所有省份生成数据,没有数据的设为0
|
||||
var finalResults []struct {
|
||||
Region string
|
||||
Count int
|
||||
}
|
||||
for _, province := range allProvincesFullNames {
|
||||
count := provinceMap[province]
|
||||
finalResults = append(finalResults, struct {
|
||||
Region string
|
||||
Count int
|
||||
}{
|
||||
Region: province,
|
||||
Count: count,
|
||||
})
|
||||
}
|
||||
|
||||
return finalResults, nil
|
||||
}
|
||||
|
||||
// 所有省份列表(34个)- 用于用户访问日志统计
|
||||
var allProvincesForUserAccess = []string{
|
||||
"北京", "天津", "河北", "山西", "内蒙古", "辽宁", "吉林", "黑龙江",
|
||||
"上海", "江苏", "浙江", "安徽", "福建", "江西", "山东", "河南",
|
||||
"湖北", "湖南", "广东", "广西", "海南", "重庆", "四川", "贵州",
|
||||
"云南", "西藏", "陕西", "甘肃", "青海", "宁夏", "新疆", "台湾", "香港", "澳门",
|
||||
}
|
||||
|
||||
// extractProvinceFromRegion 从归属地字符串中提取省份信息
|
||||
// 格式: 国家|省份|城市|ISP
|
||||
// 例如: 中国|浙江省|杭州市|电信 -> 浙江省
|
||||
// 注意:返回完整的省份名称,包括"省"、"市"、"自治区"等后缀,以匹配地图数据
|
||||
func extractProvinceFromRegion(region string) string {
|
||||
if region == "" || region == "Unknown" || region == "Internal" {
|
||||
return "未知"
|
||||
}
|
||||
|
||||
// 使用正则表达式验证格式: 国家|省份|城市|ISP
|
||||
// 匹配格式: 至少包含"国家|省份"两部分
|
||||
regionPattern := regexp.MustCompile(`^[^|]+\|[^|]+`)
|
||||
if !regionPattern.MatchString(region) {
|
||||
log.Printf("Invalid region format (regex validation failed): %s", region)
|
||||
return "未知"
|
||||
}
|
||||
|
||||
parts := strings.Split(region, "|")
|
||||
// 格式应该是: 国家|省份|城市|ISP (至少需要2个部分)
|
||||
if len(parts) < 2 {
|
||||
log.Printf("Invalid region format (insufficient parts): %s, parts: %v", region, parts)
|
||||
return "未知"
|
||||
}
|
||||
|
||||
province := strings.TrimSpace(parts[1]) // 省份在索引1
|
||||
if province == "" || province == "0" {
|
||||
log.Printf("Invalid province in region: %s, province part: %s", region, province)
|
||||
return "未知"
|
||||
}
|
||||
|
||||
// 直接返回完整的省份名称,不移除"省"、"市"、"自治区"等后缀
|
||||
// 这样可以直接匹配地图数据中的省份名称
|
||||
return province
|
||||
}
|
||||
|
||||
345
server/utils/operation_mapper.go
Normal file
345
server/utils/operation_mapper.go
Normal file
@@ -0,0 +1,345 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetOperationAction 根据路径和方法生成友好的中文操作描述
|
||||
func GetOperationAction(path, method string) string {
|
||||
// 统一转换为小写以便匹配
|
||||
pathLower := strings.ToLower(path)
|
||||
methodUpper := strings.ToUpper(method)
|
||||
|
||||
// 用户管理
|
||||
if strings.Contains(pathLower, "/api/admin/users") {
|
||||
if methodUpper == "GET" {
|
||||
if strings.Contains(pathLower, "/users/") && !strings.HasSuffix(pathLower, "/users") {
|
||||
return "查看用户详情"
|
||||
}
|
||||
return "查看用户列表"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
return "创建用户"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新用户"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除用户"
|
||||
}
|
||||
}
|
||||
|
||||
// 角色管理
|
||||
if strings.Contains(pathLower, "/api/admin/roles") {
|
||||
if methodUpper == "GET" {
|
||||
if strings.Contains(pathLower, "/permissions") {
|
||||
return "查看权限列表"
|
||||
}
|
||||
return "查看角色列表"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
return "创建角色"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
if strings.Contains(pathLower, "/permissions") {
|
||||
return "更新角色权限"
|
||||
}
|
||||
return "更新角色"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除角色"
|
||||
}
|
||||
}
|
||||
|
||||
// 文章管理
|
||||
if strings.Contains(pathLower, "/api/admin/posts") {
|
||||
if methodUpper == "GET" {
|
||||
if strings.Contains(pathLower, "/history") {
|
||||
return "查看文章历史"
|
||||
}
|
||||
if strings.Contains(pathLower, "/access-logs") {
|
||||
return "查看文章访问日志"
|
||||
}
|
||||
return "查看文章列表"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
return "创建文章"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新文章"
|
||||
}
|
||||
if methodUpper == "PATCH" {
|
||||
if strings.Contains(pathLower, "/status") {
|
||||
return "切换文章状态"
|
||||
}
|
||||
return "更新文章"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除文章"
|
||||
}
|
||||
}
|
||||
|
||||
// 分类管理
|
||||
if strings.Contains(pathLower, "/api/admin/categories") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看分类列表"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
return "创建分类"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新分类"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除分类"
|
||||
}
|
||||
}
|
||||
|
||||
// 专栏管理
|
||||
if strings.Contains(pathLower, "/api/admin/columns") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看专栏列表"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
if strings.Contains(pathLower, "/posts") {
|
||||
return "添加文章到专栏"
|
||||
}
|
||||
return "创建专栏"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新专栏"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
if strings.Contains(pathLower, "/posts/") {
|
||||
return "从专栏移除文章"
|
||||
}
|
||||
return "删除专栏"
|
||||
}
|
||||
}
|
||||
|
||||
// 标签管理
|
||||
if strings.Contains(pathLower, "/api/admin/tags") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看标签列表"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
return "创建标签"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新标签"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除标签"
|
||||
}
|
||||
}
|
||||
|
||||
// 代码片段管理
|
||||
if strings.Contains(pathLower, "/api/admin/snippets") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看代码片段列表"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
return "创建代码片段"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新代码片段"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除代码片段"
|
||||
}
|
||||
}
|
||||
|
||||
// 作品管理
|
||||
if strings.Contains(pathLower, "/api/admin/works") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看作品列表"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
return "创建作品"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新作品"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除作品"
|
||||
}
|
||||
}
|
||||
|
||||
// 系统设置管理
|
||||
if strings.Contains(pathLower, "/api/admin/settings") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看系统设置"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
return "创建系统设置"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新系统设置"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除系统设置"
|
||||
}
|
||||
}
|
||||
|
||||
// 关于页面管理
|
||||
if strings.Contains(pathLower, "/api/admin/about") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看关于页面"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
return "创建关于页面"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新关于页面"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除关于页面"
|
||||
}
|
||||
}
|
||||
|
||||
// 客户评价管理
|
||||
if strings.Contains(pathLower, "/api/admin/testimonials") {
|
||||
if methodUpper == "POST" {
|
||||
return "创建客户评价"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新客户评价"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除客户评价"
|
||||
}
|
||||
}
|
||||
|
||||
// 合作伙伴管理
|
||||
if strings.Contains(pathLower, "/api/admin/partners") {
|
||||
if methodUpper == "POST" {
|
||||
return "创建合作伙伴"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新合作伙伴"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除合作伙伴"
|
||||
}
|
||||
}
|
||||
|
||||
// 咨询管理
|
||||
if strings.Contains(pathLower, "/api/admin/inquiries") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看咨询列表"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
if strings.Contains(pathLower, "/status") {
|
||||
return "更新咨询状态"
|
||||
}
|
||||
return "更新咨询"
|
||||
}
|
||||
}
|
||||
|
||||
// 邮箱后缀配置
|
||||
if strings.Contains(pathLower, "/api/admin/email-suffixes") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看邮箱后缀列表"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
return "创建邮箱后缀"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新邮箱后缀"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除邮箱后缀"
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索记录管理
|
||||
if strings.Contains(pathLower, "/api/admin/search-logs") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看搜索记录列表"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除搜索记录"
|
||||
}
|
||||
}
|
||||
|
||||
// 附件管理
|
||||
if strings.Contains(pathLower, "/api/admin/attachments") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看附件列表"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
if strings.Contains(pathLower, "/upload") {
|
||||
return "上传附件"
|
||||
}
|
||||
return "创建附件"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新附件"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除附件"
|
||||
}
|
||||
}
|
||||
|
||||
// 附件分类管理
|
||||
if strings.Contains(pathLower, "/api/admin/attachment-categories") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看附件分类列表"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
return "创建附件分类"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新附件分类"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除附件分类"
|
||||
}
|
||||
}
|
||||
|
||||
// OSS配置管理
|
||||
if strings.Contains(pathLower, "/api/admin/oss-configs") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看OSS配置列表"
|
||||
}
|
||||
if methodUpper == "POST" {
|
||||
return "创建OSS配置"
|
||||
}
|
||||
if methodUpper == "PUT" {
|
||||
return "更新OSS配置"
|
||||
}
|
||||
if methodUpper == "DELETE" {
|
||||
return "删除OSS配置"
|
||||
}
|
||||
}
|
||||
|
||||
// 操作日志管理
|
||||
if strings.Contains(pathLower, "/api/admin/operation-logs") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看操作日志"
|
||||
}
|
||||
}
|
||||
|
||||
// 访问日志管理
|
||||
if strings.Contains(pathLower, "/api/admin/access-logs") {
|
||||
if methodUpper == "GET" {
|
||||
return "查看访问日志"
|
||||
}
|
||||
}
|
||||
|
||||
// 仪表盘
|
||||
if strings.Contains(pathLower, "/api/admin/dashboard") {
|
||||
if methodUpper == "GET" {
|
||||
if strings.Contains(pathLower, "/stats") {
|
||||
return "查看仪表盘统计"
|
||||
}
|
||||
if strings.Contains(pathLower, "/activities") {
|
||||
return "查看最近活动"
|
||||
}
|
||||
return "查看仪表盘"
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法匹配,返回原始路径和方法作为后备
|
||||
return methodUpper + " " + path
|
||||
}
|
||||
Reference in New Issue
Block a user