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
+1 -1
View File
@@ -18,7 +18,7 @@ request.interceptors.response.use(
err => {
if (err.response?.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
window.location.href = '/admin/login'
}
return Promise.reject(err)
}
+1
View File
@@ -33,6 +33,7 @@
<el-menu-item index="/settings">AI 配置</el-menu-item>
<el-menu-item index="/links">友链管理</el-menu-item>
<el-menu-item index="/feedbacks">反馈管理</el-menu-item>
<el-menu-item index="/users">用户管理</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
+36 -2
View File
@@ -4,7 +4,23 @@
<h2>人群管理</h2>
<el-button type="primary" @click="dialogVisible = true; form = {}">添加</el-button>
</div>
<el-table :data="crowds" style="margin-top: 20px">
<div class="search-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索名称..."
clearable
style="width: 200px"
@input="handleSearch"
>
<template #prefix>
<span>🔍</span>
</template>
</el-input>
<span class="result-count"> {{ filteredCrowds.length }} </span>
</div>
<el-table :data="filteredCrowds" style="margin-top: 16px">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="sort_order" label="排序" width="100" />
@@ -34,15 +50,23 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { getCrowds, createCrowd, updateCrowd, deleteCrowd } from '@/api/category'
import { ElMessage, ElMessageBox } from 'element-plus'
const crowds = ref([])
const dialogVisible = ref(false)
const form = ref({})
const searchKeyword = ref('')
const filteredCrowds = computed(() => {
if (!searchKeyword.value) return crowds.value
const kw = searchKeyword.value.toLowerCase()
return crowds.value.filter(c => c.name.toLowerCase().includes(kw))
})
const loadData = async () => { crowds.value = await getCrowds() }
const handleSearch = () => { /* computed property handles filtering */ }
const handleEdit = (row) => { form.value = { ...row }; dialogVisible.value = true }
const handleSave = async () => {
form.value.id ? await updateCrowd(form.value.id, form.value) : await createCrowd(form.value)
@@ -60,4 +84,14 @@ onMounted(() => loadData())
<style scoped>
.header { display: flex; justify-content: space-between; align-items: center; }
.search-bar {
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
}
.result-count {
color: #909399;
font-size: 13px;
}
</style>
+39 -2
View File
@@ -4,7 +4,23 @@
<h2>类型管理</h2>
<el-button type="primary" @click="dialogVisible = true; form = {}">添加</el-button>
</div>
<el-table :data="types" style="margin-top: 20px">
<div class="search-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索名称..."
clearable
style="width: 200px"
@input="handleSearch"
>
<template #prefix>
<span>🔍</span>
</template>
</el-input>
<span class="result-count"> {{ filteredTypes.length }} </span>
</div>
<el-table :data="paginatedTypes" style="margin-top: 16px">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="sort_order" label="排序" width="100" />
@@ -34,15 +50,26 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { getTypes, createType, updateType, deleteType } from '@/api/category'
import { ElMessage, ElMessageBox } from 'element-plus'
const types = ref([])
const dialogVisible = ref(false)
const form = ref({})
const searchKeyword = ref('')
const searchResults = ref([])
const filteredTypes = computed(() => {
if (!searchKeyword.value) return types.value
const kw = searchKeyword.value.toLowerCase()
return types.value.filter(t => t.name.toLowerCase().includes(kw))
})
const paginatedTypes = computed(() => filteredTypes.value)
const loadData = async () => { types.value = await getTypes() }
const handleSearch = () => { /* computed property handles filtering */ }
const handleEdit = (row) => { form.value = { ...row }; dialogVisible.value = true }
const handleSave = async () => {
form.value.id ? await updateType(form.value.id, form.value) : await createType(form.value)
@@ -60,4 +87,14 @@ onMounted(() => loadData())
<style scoped>
.header { display: flex; justify-content: space-between; align-items: center; }
.search-bar {
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
}
.result-count {
color: #909399;
font-size: 13px;
}
</style>
+35 -2
View File
@@ -3,7 +3,20 @@
<div class="header">
<h2>反馈管理</h2>
</div>
<el-table :data="feedbacks" v-loading="loading" style="margin-top: 20px">
<div class="search-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索名称/内容..."
clearable
style="width: 240px"
>
<template #prefix><span>🔍</span></template>
</el-input>
<span class="result-count"> {{ filteredFeedbacks.length }} </span>
</div>
<el-table :data="filteredFeedbacks" v-loading="loading" style="margin-top: 16px">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="名称" width="120" />
<el-table-column prop="email" label="邮箱" width="200" />
@@ -19,12 +32,22 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { getFeedbacks, deleteFeedback } from '@/api/feedback'
import { ElMessage, ElMessageBox } from 'element-plus'
const feedbacks = ref([])
const loading = ref(false)
const searchKeyword = ref('')
const filteredFeedbacks = computed(() => {
if (!searchKeyword.value) return feedbacks.value
const kw = searchKeyword.value.toLowerCase()
return feedbacks.value.filter(f =>
(f.name && f.name.toLowerCase().includes(kw)) ||
(f.content && f.content.toLowerCase().includes(kw))
)
})
const loadFeedbacks = async () => {
loading.value = true
@@ -51,4 +74,14 @@ onMounted(() => loadFeedbacks())
justify-content: space-between;
align-items: center;
}
.search-bar {
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
}
.result-count {
color: #909399;
font-size: 13px;
}
</style>
+34 -2
View File
@@ -4,7 +4,20 @@
<h2>友链管理</h2>
<el-button type="primary" @click="$router.push('/links/edit')">添加友链</el-button>
</div>
<el-table :data="links" v-loading="loading" style="margin-top: 20px">
<div class="search-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索名称/URL..."
clearable
style="width: 240px"
>
<template #prefix><span>🔍</span></template>
</el-input>
<span class="result-count"> {{ filteredLinks.length }} </span>
</div>
<el-table :data="filteredLinks" v-loading="loading" style="margin-top: 16px">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="url" label="URL" min-width="200">
@@ -26,12 +39,21 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { getLinks, deleteLink } from '@/api/link'
import { ElMessage, ElMessageBox } from 'element-plus'
const links = ref([])
const loading = ref(false)
const searchKeyword = ref('')
const filteredLinks = computed(() => {
if (!searchKeyword.value) return links.value
const kw = searchKeyword.value.toLowerCase()
return links.value.filter(l =>
l.name.toLowerCase().includes(kw) || l.url.toLowerCase().includes(kw)
)
})
const loadLinks = async () => {
loading.value = true
@@ -58,4 +80,14 @@ onMounted(() => loadLinks())
justify-content: space-between;
align-items: center;
}
.search-bar {
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
}
.result-count {
color: #909399;
font-size: 13px;
}
</style>
+1
View File
@@ -9,6 +9,7 @@ export default defineConfig({
'@': path.resolve(__dirname, './src')
}
},
base: '/admin/',
server: {
port: 3001,
proxy: {