feat: 前后台静态资源路径冲突修复 + 用户管理功能

- 修改 admin/vite.config.js 添加 base: '/admin/' 解决静态资源路径问题
- 修复后台 index.html 资源引用从 /assets/ → /admin/assets/
- 更新优化器默认 API 地址为服务器地址
- 添加友链管理相关代码
- 修复多处分类管理页面
This commit is contained in:
bwstudio
2026-06-10 07:30:49 +08:00
parent aecf1f1f15
commit 595394fcb7
22 changed files with 740 additions and 100 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
import request from '@/api/request'
export const getLinks = () => request.get('/links')
export const getLinks = () => request.get('/links')
export const applyLink = (data) => request.post('/links/apply', data)
+89 -5
View File
@@ -7,11 +7,28 @@
<span class="tag tag-crowd" v-for="(name, i) in (joke.crowd_names||[])" :key="'c'+i">{{ name }}</span>
</div>
<!-- 标题 -->
<h1 class="detail-title">{{ joke.title }}</h1>
<!-- 标题 + AI评价 -->
<div class="title-row">
<h1 class="detail-title">{{ joke.title }}</h1>
<span v-if="joke.ai_level" class="ai-badge" :class="'ai-' + joke.ai_level">
{{ getLevelText(joke.ai_level) }}
</span>
</div>
<!-- 内容 -->
<div class="detail-content">{{ joke.content }}</div>
<!-- 内容优先显示润色内容 -->
<div class="detail-content">
<template v-if="joke.polished_content">
<div class="polished-label"> AI 润色版</div>
{{ joke.polished_content }}
</template>
<template v-else>{{ joke.content }}</template>
</div>
<!-- 原始内容如果有润色 -->
<details v-if="joke.polished_content" class="original-content">
<summary>查看原文</summary>
{{ joke.content }}
</details>
<!-- 操作栏 -->
<div class="detail-actions">
@@ -33,6 +50,8 @@
</button>
<span class="sep">|</span>
<span>{{ formatDate(joke.created_at) }}</span>
<span v-if="joke.ai_score" class="sep">|</span>
<span v-if="joke.ai_score" class="ai-score">评分: {{ joke.ai_score.toFixed(1) }}</span>
</div>
</div>
</div>
@@ -106,6 +125,16 @@ const formatDate = (dateStr) => {
return new Date(dateStr).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })
}
function getLevelText(level) {
const map = {
'excellent': '⭐⭐⭐ 精品',
'good': '⭐⭐ 良好',
'ordinary': '⭐ 普通',
'poor': '⚠️ 待优化'
}
return map[level] || ''
}
onMounted(() => loadJoke())
watch(() => route.params.id, () => loadJoke())
@@ -131,7 +160,60 @@ watch(() => route.params.id, () => loadJoke())
transition: background 0.3s, box-shadow 0.3s;
}
.detail-tags { display: flex; gap: 10px; }
.detail-tags { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
/* 标题行:标题 + AI评价右对齐 */
.title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
/* AI 评价徽章 */
.ai-badge {
font-size: 13px;
padding: 4px 14px;
border-radius: 14px;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
}
.ai-excellent { background: linear-gradient(135deg, #fef3c7, #fde68a); color: #92400e; }
.ai-good { background: linear-gradient(135deg, #d1fae5, #a7f3d0); color: #065f46; }
.ai-ordinary { background: #f3f4f6; color: #4b5563; }
.ai-poor { background: #fee2e2; color: #991b1b; }
/* AI 润色标签 */
.polished-label {
font-size: 12px;
color: #9333ea;
font-weight: 600;
margin-bottom: 8px;
display: inline-block;
}
/* 原文折叠 */
.original-content {
margin-top: 16px;
padding: 12px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 13px;
color: var(--text-muted);
}
.original-content summary {
cursor: pointer;
font-weight: 600;
color: var(--text-secondary);
}
/* AI 评分 */
.ai-score {
color: #9333ea;
font-weight: 600;
}
.detail-title {
font-size: 26px;
@@ -140,6 +222,8 @@ watch(() => route.params.id, () => loadJoke())
line-height: 1.4;
letter-spacing: 0.5px;
transition: color 0.3s;
margin: 0;
flex: 1;
}
.detail-content {
+44 -5
View File
@@ -6,11 +6,16 @@
<span class="tag tag-crowd" v-for="(name, i) in (joke.crowd_names||[])" :key="'c'+i">{{ name }}</span>
</div>
<!-- 标题 -->
<h3 class="card-title">{{ joke.title }}</h3>
<!-- 标题 + AI评价等级 -->
<div class="title-row">
<h3 class="card-title">{{ joke.title }}</h3>
<span v-if="joke.ai_level" class="ai-badge" :class="'ai-' + joke.ai_level">
{{ getLevelText(joke.ai_level) }}
</span>
</div>
<!-- 内容 -->
<p class="card-content">{{ joke.content }}</p>
<!-- 内容优先显示润色内容 -->
<p class="card-content">{{ joke.polished_content || joke.content }}</p>
<!-- 底部信息 -->
<div class="card-footer">
@@ -33,6 +38,16 @@
defineProps({
joke: { type: Object, required: true }
})
function getLevelText(level) {
const map = {
'excellent': '⭐⭐⭐ 精品',
'good': '⭐⭐ 良好',
'ordinary': '⭐ 普通',
'poor': '⚠️ 待优化'
}
return map[level] || ''
}
</script>
<style scoped>
@@ -68,7 +83,15 @@ defineProps({
.joke-card:hover::before { transform: scaleY(1); }
/* 标签 */
.card-header { display: flex; gap: 8px; flex-wrap: wrap; }
.card-header { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* 标题行:标题 + AI评价右对齐 */
.title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
/* 标题 */
.card-title {
@@ -80,8 +103,24 @@ defineProps({
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0;
flex: 1;
}
/* AI 评价徽章 */
.ai-badge {
font-size: 11px;
padding: 3px 10px;
border-radius: 12px;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
}
.ai-excellent { background: linear-gradient(135deg, #fef3c7, #fde68a); color: #92400e; }
.ai-good { background: linear-gradient(135deg, #d1fae5, #a7f3d0); color: #065f46; }
.ai-ordinary { background: #f3f4f6; color: #4b5563; }
.ai-poor { background: #fee2e2; color: #991b1b; }
/* 内容 */
.card-content {
font-size: 14px;
+205 -1
View File
@@ -26,15 +26,62 @@
<div class="link-desc" v-if="link.description">{{ link.description }}</div>
</a>
</div>
<!-- 自助申请 -->
<div class="apply-section">
<button class="apply-btn" @click="showDialog = true">
<span class="apply-icon">+</span>
自助申请友链
</button>
</div>
<!-- 申请弹窗 -->
<div v-if="showDialog" class="dialog-overlay" @click.self="showDialog = false">
<div class="dialog">
<h2 class="dialog-title">申请友链</h2>
<form @submit.prevent="handleApply" class="apply-form">
<div class="form-item">
<label>网站名称 <span class="required">*</span></label>
<input v-model="form.name" type="text" placeholder="请输入网站名称" required />
</div>
<div class="form-item">
<label>网站地址 <span class="required">*</span></label>
<input v-model="form.url" type="url" placeholder="https://example.com" required />
</div>
<div class="form-item">
<label>网站描述</label>
<input v-model="form.description" type="text" placeholder="简短描述您的网站" />
</div>
<div class="form-item">
<label>联系方式</label>
<input v-model="form.contact" type="text" placeholder="邮箱或社交账号(选填)" />
</div>
<div class="dialog-footer">
<button type="button" class="btn-cancel" @click="showDialog = false">取消</button>
<button type="submit" class="btn-submit" :disabled="submitting">
{{ submitting ? '提交中...' : '提交申请' }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getLinks } from '@/api/link'
import { getLinks, applyLink } from '@/api/link'
const links = ref([])
const loading = ref(true)
const showDialog = ref(false)
const submitting = ref(false)
const form = ref({
name: '',
url: '',
description: '',
contact: '',
})
onMounted(async () => {
try {
@@ -45,6 +92,21 @@ onMounted(async () => {
loading.value = false
}
})
const handleApply = async () => {
submitting.value = true
try {
await applyLink(form.value)
alert('申请已提交,等待审核!')
showDialog.value = false
form.value = { name: '', url: '', description: '', contact: '' }
} catch (e) {
alert('提交失败,请重试')
console.error(e)
} finally {
submitting.value = false
}
}
</script>
<style scoped>
@@ -101,4 +163,146 @@ onMounted(async () => {
color: var(--text-secondary);
line-height: 1.5;
}
/* 自助申请 */
.apply-section {
margin-top: 32px;
text-align: center;
}
.apply-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: var(--bg-card);
border: 2px dashed var(--border);
border-radius: var(--radius);
color: var(--text-secondary);
font-size: 15px;
cursor: pointer;
transition: all 0.2s;
}
.apply-btn:hover {
border-color: var(--primary);
color: var(--primary);
}
.apply-icon {
font-size: 18px;
font-weight: 700;
}
/* 弹窗 */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog {
background: var(--bg-card);
border-radius: var(--radius);
padding: 24px;
width: 90%;
max-width: 420px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.dialog-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 20px;
color: var(--text-primary);
}
.apply-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-item label {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
.required {
color: #e53935;
}
.form-item input {
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 14px;
background: var(--bg-primary);
color: var(--text-primary);
transition: border-color 0.2s;
}
.form-item input:focus {
outline: none;
border-color: var(--primary);
}
.form-item input::placeholder {
color: var(--text-muted);
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 8px;
}
.btn-cancel,
.btn-submit {
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.btn-cancel {
background: transparent;
border: 1px solid var(--border);
color: var(--text-secondary);
}
.btn-cancel:hover {
background: var(--bg-hover);
}
.btn-submit {
background: var(--primary);
border: none;
color: #fff;
}
.btn-submit:hover:not(:disabled) {
opacity: 0.9;
}
.btn-submit:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>