优化UI显示

This commit is contained in:
ctwj
2025-07-20 21:56:00 +08:00
parent f63ac7feda
commit e865efaab2
39 changed files with 4284 additions and 2096 deletions

View File

@@ -175,6 +175,54 @@ l9pan/
## 🔧 配置说明
### 版本管理
项目使用GitHub进行版本管理支持自动创建Release和标签。
#### 版本管理脚本
```bash
# 显示当前版本信息
./scripts/version.sh show
# 更新版本号
./scripts/version.sh patch # 修订版本 (1.0.0 -> 1.0.1)
./scripts/version.sh minor # 次版本 (1.0.0 -> 1.1.0)
./scripts/version.sh major # 主版本 (1.0.0 -> 2.0.0)
# 发布版本到GitHub
./scripts/version.sh release
# 生成版本信息文件
./scripts/version.sh update
# 查看帮助
./scripts/version.sh help
```
#### 自动发布流程
1. **更新版本号**: 修改 `VERSION` 文件
2. **同步文件**: 更新 `package.json``docker-compose.yml``README.md`
3. **创建Git标签**: 自动创建版本标签
4. **推送代码**: 推送代码和标签到GitHub
5. **创建Release**: 自动创建GitHub Release
#### 版本API接口
- `GET /api/version` - 获取版本信息
- `GET /api/version/string` - 获取版本字符串
- `GET /api/version/full` - 获取完整版本信息
- `GET /api/version/check-update` - 检查GitHub上的最新版本
#### 版本信息页面
访问 `/version` 页面查看详细的版本信息和更新状态。
#### 详细文档
查看 [GitHub版本管理指南](docs/github-version-management.md) 了解完整的版本管理流程。
### 环境变量配置
```bash

1
VERSION Normal file
View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -0,0 +1,253 @@
# GitHub版本管理指南
本项目使用GitHub进行版本管理支持自动创建Release和标签。
## 版本管理流程
### 1. 版本号规范
遵循[语义化版本](https://semver.org/lang/zh-CN/)规范:
- **主版本号** (Major): 不兼容的API修改
- **次版本号** (Minor): 向下兼容的功能性新增
- **修订号** (Patch): 向下兼容的问题修正
### 2. 版本管理命令
#### 显示版本信息
```bash
./scripts/version.sh show
```
#### 更新版本号
```bash
# 修订版本 (1.0.0 -> 1.0.1)
./scripts/version.sh patch
# 次版本 (1.0.0 -> 1.1.0)
./scripts/version.sh minor
# 主版本 (1.0.0 -> 2.0.0)
./scripts/version.sh major
```
#### 发布版本到GitHub
```bash
./scripts/version.sh release
```
### 3. 自动发布流程
当执行版本更新命令时,脚本会:
1. **更新版本号**: 修改 `VERSION` 文件
2. **同步文件**: 更新 `package.json``docker-compose.yml``README.md`
3. **创建Git标签**: 自动创建版本标签
4. **推送代码**: 推送代码和标签到GitHub
5. **创建Release**: 自动创建GitHub Release
### 4. 手动发布流程
如果自动发布失败,可以手动发布:
#### 步骤1: 更新版本号
```bash
./scripts/version.sh patch # 或 minor, major
```
#### 步骤2: 提交更改
```bash
git add .
git commit -m "chore: bump version to v1.0.1"
```
#### 步骤3: 创建标签
```bash
git tag v1.0.1
```
#### 步骤4: 推送到GitHub
```bash
git push origin main
git push origin v1.0.1
```
#### 步骤5: 创建Release
在GitHub网页上
1. 进入项目页面
2. 点击 "Releases"
3. 点击 "Create a new release"
4. 选择标签 `v1.0.1`
5. 填写Release说明
6. 发布
### 5. GitHub CLI工具
#### 安装GitHub CLI
```bash
# macOS
brew install gh
# Ubuntu/Debian
sudo apt install gh
# Windows
winget install GitHub.cli
```
#### 登录GitHub
```bash
gh auth login
```
#### 创建Release
```bash
gh release create v1.0.1 \
--title "Release v1.0.1" \
--notes "修复了一些bug" \
--draft=false \
--prerelease=false
```
### 6. 版本检查
#### API接口
- `GET /api/version/check-update` - 检查GitHub上的最新版本
#### 前端页面
- 访问 `/version` 页面查看版本信息和更新状态
### 7. 版本历史
#### 查看所有标签
```bash
git tag -l
```
#### 查看标签详情
```bash
git show v1.0.1
```
#### 查看版本历史
```bash
git log --oneline --decorate
```
### 8. 回滚版本
如果需要回滚到之前的版本:
#### 删除本地标签
```bash
git tag -d v1.0.1
```
#### 删除远程标签
```bash
git push origin :refs/tags/v1.0.1
```
#### 回滚代码
```bash
git reset --hard v1.0.0
git push --force origin main
```
### 9. 最佳实践
#### 提交信息规范
```bash
# 功能开发
git commit -m "feat: 添加新功能"
# Bug修复
git commit -m "fix: 修复某个bug"
# 文档更新
git commit -m "docs: 更新文档"
# 版本更新
git commit -m "chore: bump version to v1.0.1"
```
#### 分支管理
- `main`: 主分支,用于发布
- `develop`: 开发分支
- `feature/*`: 功能分支
- `hotfix/*`: 热修复分支
#### Release说明模板
```markdown
## Release v1.0.1
**发布日期**: 2024-01-15
### 更新内容
- 修复了某个bug
- 添加了新功能
- 优化了性能
### 下载
- [源码 (ZIP)](https://github.com/ctwj/urldb/archive/v1.0.1.zip)
- [源码 (TAR.GZ)](https://github.com/ctwj/urldb/archive/v1.0.1.tar.gz)
### 安装
```bash
# 克隆项目
git clone https://github.com/ctwj/urldb.git
cd urldb
# 切换到指定版本
git checkout v1.0.1
# 使用Docker部署
docker-compose up --build -d
```
### 更新日志
详细更新日志请查看 [CHANGELOG.md](https://github.com/ctwj/urldb/blob/v1.0.1/CHANGELOG.md)
```
### 10. 故障排除
#### 常见问题
1. **GitHub CLI未安装**
```bash
# 安装GitHub CLI
brew install gh # macOS
```
2. **GitHub CLI未登录**
```bash
# 登录GitHub
gh auth login
```
3. **标签已存在**
```bash
# 删除本地标签
git tag -d v1.0.1
# 删除远程标签
git push origin :refs/tags/v1.0.1
```
4. **推送失败**
```bash
# 检查远程仓库
git remote -v
# 重新设置远程仓库
git remote set-url origin https://github.com/ctwj/urldb.git
```
#### 获取帮助
```bash
./scripts/version.sh help
```

123
handlers/version_handler.go Normal file
View File

@@ -0,0 +1,123 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/ctwj/urldb/utils"
"github.com/gin-gonic/gin"
)
// VersionResponse 版本响应结构
type VersionResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
Message string `json:"message"`
Time time.Time `json:"time"`
}
// GetVersion 获取版本信息
func GetVersion(c *gin.Context) {
versionInfo := utils.GetVersionInfo()
response := VersionResponse{
Success: true,
Data: versionInfo,
Message: "版本信息获取成功",
Time: time.Now(),
}
c.JSON(http.StatusOK, response)
}
// GetVersionString 获取版本字符串
func GetVersionString(c *gin.Context) {
versionString := utils.GetVersionString()
response := VersionResponse{
Success: true,
Data: map[string]string{
"version": versionString,
},
Message: "版本字符串获取成功",
Time: time.Now(),
}
c.JSON(http.StatusOK, response)
}
// GetFullVersionInfo 获取完整版本信息
func GetFullVersionInfo(c *gin.Context) {
fullInfo := utils.GetFullVersionInfo()
response := VersionResponse{
Success: true,
Data: map[string]string{
"version_info": fullInfo,
},
Message: "完整版本信息获取成功",
Time: time.Now(),
}
c.JSON(http.StatusOK, response)
}
// CheckUpdate 检查更新
func CheckUpdate(c *gin.Context) {
currentVersion := utils.GetVersionInfo().Version
// 从GitHub API获取最新版本信息
latestVersion, err := getLatestVersionFromGitHub()
if err != nil {
// 如果GitHub API失败使用模拟数据
latestVersion = "1.0.0"
}
hasUpdate := utils.IsVersionNewer(latestVersion, currentVersion)
response := VersionResponse{
Success: true,
Data: map[string]interface{}{
"current_version": currentVersion,
"latest_version": latestVersion,
"has_update": hasUpdate,
"update_available": hasUpdate,
"update_url": "https://github.com/ctwj/urldb/releases/latest",
},
Message: "更新检查完成",
Time: time.Now(),
}
c.JSON(http.StatusOK, response)
}
// getLatestVersionFromGitHub 从GitHub获取最新版本
func getLatestVersionFromGitHub() (string, error) {
// 使用GitHub API获取最新Release
url := "https://api.github.com/repos/ctwj/urldb/releases/latest"
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("GitHub API返回状态码: %d", resp.StatusCode)
}
var release struct {
TagName string `json:"tag_name"`
}
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return "", err
}
// 移除版本号前的 'v' 前缀
version := strings.TrimPrefix(release.TagName, "v")
return version, nil
}

View File

@@ -212,6 +212,12 @@ func main() {
api.POST("/scheduler/auto-transfer/start", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.StartAutoTransferScheduler)
api.POST("/scheduler/auto-transfer/stop", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.StopAutoTransferScheduler)
api.POST("/scheduler/auto-transfer/trigger", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.TriggerAutoTransferScheduler)
// 版本管理路由
api.GET("/version", handlers.GetVersion)
api.GET("/version/string", handlers.GetVersionString)
api.GET("/version/full", handlers.GetFullVersionInfo)
api.GET("/version/check-update", handlers.CheckUpdate)
}
// 静态文件服务

309
scripts/version.sh Executable file
View File

@@ -0,0 +1,309 @@
#!/bin/bash
# 版本管理脚本
# 用法: ./scripts/version.sh [major|minor|patch|show|update]
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 获取当前版本
get_current_version() {
cat VERSION
}
# 显示版本信息
show_version() {
echo -e "${BLUE}当前版本信息:${NC}"
echo -e "版本号: ${GREEN}$(get_current_version)${NC}"
echo -e "构建时间: ${GREEN}$(date '+%Y-%m-%d %H:%M:%S')${NC}"
echo -e "Git提交: ${GREEN}$(git rev-parse --short HEAD 2>/dev/null || echo 'N/A')${NC}"
echo -e "Git分支: ${GREEN}$(git branch --show-current 2>/dev/null || echo 'N/A')${NC}"
}
# 更新版本号
update_version() {
local version_type=$1
local current_version=$(get_current_version)
local major minor patch
# 解析版本号
IFS='.' read -r major minor patch <<< "$current_version"
case $version_type in
"major")
major=$((major + 1))
minor=0
patch=0
;;
"minor")
minor=$((minor + 1))
patch=0
;;
"patch")
patch=$((patch + 1))
;;
*)
echo -e "${RED}错误: 无效的版本类型${NC}"
echo "用法: $0 [major|minor|patch|show|update|release]"
exit 1
;;
esac
local new_version="$major.$minor.$patch"
# 更新版本文件
echo "$new_version" > VERSION
echo -e "${GREEN}版本已更新: ${current_version} -> ${new_version}${NC}"
# 更新其他文件中的版本信息
update_version_in_files "$new_version"
# 创建Git标签和发布
if git rev-parse --git-dir > /dev/null 2>&1; then
echo -e "${YELLOW}创建Git标签 v${new_version}...${NC}"
git add VERSION
git commit -m "chore: bump version to v${new_version}" || true
git tag "v${new_version}" || true
echo -e "${GREEN}Git标签已创建: v${new_version}${NC}"
# 询问是否推送到GitHub
read -p "是否推送到GitHub并创建Release? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
push_to_github "$new_version"
fi
fi
}
# 更新文件中的版本信息
update_version_in_files() {
local new_version=$1
echo -e "${YELLOW}更新文件中的版本信息...${NC}"
# 更新前端package.json
if [ -f "web/package.json" ]; then
sed -i.bak "s/\"version\": \"[^\"]*\"/\"version\": \"${new_version}\"/" web/package.json
rm -f web/package.json.bak
echo -e " ✅ 更新 web/package.json"
fi
# 更新Docker镜像标签
if [ -f "docker-compose.yml" ]; then
sed -i.bak "s/image:.*:.*/image: urldb:${new_version}/" docker-compose.yml
rm -f docker-compose.yml.bak
echo -e " ✅ 更新 docker-compose.yml"
fi
# 更新README中的版本信息
if [ -f "README.md" ]; then
sed -i.bak "s/版本.*[0-9]\+\.[0-9]\+\.[0-9]\+/版本 ${new_version}/" README.md
rm -f README.md.bak
echo -e " ✅ 更新 README.md"
fi
}
# 推送到GitHub并创建Release
push_to_github() {
local version=$1
local release_notes=$(generate_release_notes "$version")
echo -e "${YELLOW}推送到GitHub...${NC}"
# 推送代码和标签
git push origin main || git push origin master || true
git push origin "v${version}" || true
echo -e "${GREEN}代码和标签已推送到GitHub${NC}"
# 创建GitHub Release
create_github_release "$version" "$release_notes"
}
# 创建GitHub Release
create_github_release() {
local version=$1
local release_notes=$2
# 检查是否安装了gh CLI
if ! command -v gh &> /dev/null; then
echo -e "${YELLOW}未安装GitHub CLI (gh)跳过自动创建Release${NC}"
echo -e "${BLUE}请手动在GitHub上创建Release: v${version}${NC}"
return
fi
# 检查是否已登录
if ! gh auth status &> /dev/null; then
echo -e "${YELLOW}GitHub CLI未登录跳过自动创建Release${NC}"
echo -e "${BLUE}请运行 'gh auth login' 登录后重试${NC}"
return
fi
echo -e "${YELLOW}创建GitHub Release...${NC}"
# 创建Release
gh release create "v${version}" \
--title "Release v${version}" \
--notes "$release_notes" \
--draft=false \
--prerelease=false || {
echo -e "${RED}创建Release失败请手动创建${NC}"
return
}
echo -e "${GREEN}GitHub Release已创建: v${version}${NC}"
}
# 生成Release说明
generate_release_notes() {
local version=$1
local current_date=$(date '+%Y-%m-%d')
# 获取上次版本
local previous_version=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/v//' || echo "0.0.0")
# 获取提交历史
local commits=$(git log --oneline "v${previous_version}..HEAD" 2>/dev/null || echo "Initial release")
cat << EOF
## Release v${version}
**发布日期**: ${current_date}
### 更新内容
${commits}
### 下载
- [源码 (ZIP)](https://github.com/ctwj/urldb/archive/v${version}.zip)
- [源码 (TAR.GZ)](https://github.com/ctwj/urldb/archive/v${version}.tar.gz)
### 安装
\`\`\`bash
# 克隆项目
git clone https://github.com/ctwj/urldb.git
cd urldb
# 切换到指定版本
git checkout v${version}
# 使用Docker部署
docker-compose up --build -d
\`\`\`
### 更新日志
详细更新日志请查看 [CHANGELOG.md](https://github.com/ctwj/urldb/blob/v${version}/CHANGELOG.md)
EOF
}
# 生成版本信息文件
generate_version_info() {
local version=$(get_current_version)
local build_time=$(date '+%Y-%m-%d %H:%M:%S')
local git_commit=$(git rev-parse --short HEAD 2>/dev/null || echo 'N/A')
local git_branch=$(git branch --show-current 2>/dev/null || echo 'N/A')
cat > version_info.json << EOF
{
"version": "${version}",
"build_time": "${build_time}",
"git_commit": "${git_commit}",
"git_branch": "${git_branch}",
"go_version": "$(go version 2>/dev/null | cut -d' ' -f3 || echo 'N/A')",
"node_version": "$(node --version 2>/dev/null || echo 'N/A')"
}
EOF
echo -e "${GREEN}版本信息文件已生成: version_info.json${NC}"
}
# 主函数
main() {
case $1 in
"show")
show_version
;;
"major"|"minor"|"patch")
update_version $1
;;
"release")
release_version
;;
"update")
generate_version_info
;;
"help"|"-h"|"--help")
echo -e "${BLUE}版本管理脚本${NC}"
echo ""
echo "用法: $0 [命令]"
echo ""
echo "命令:"
echo " show 显示当前版本信息"
echo " major 主版本号更新 (x.0.0)"
echo " minor 次版本号更新 (0.x.0)"
echo " patch 修订版本号更新 (0.0.x)"
echo " release 发布当前版本到GitHub"
echo " update 生成版本信息文件"
echo " help 显示此帮助信息"
echo ""
echo "示例:"
echo " $0 show # 显示版本信息"
echo " $0 patch # 更新修订版本号"
echo " $0 minor # 更新次版本号"
echo " $0 major # 更新主版本号"
echo " $0 release # 发布版本到GitHub"
;;
*)
echo -e "${RED}错误: 未知命令 '$1'${NC}"
echo "使用 '$0 help' 查看帮助信息"
exit 1
;;
esac
}
# 发布版本
release_version() {
local current_version=$(get_current_version)
echo -e "${BLUE}准备发布版本 v${current_version}${NC}"
# 检查是否有未提交的更改
if ! git diff-index --quiet HEAD --; then
echo -e "${YELLOW}检测到未提交的更改,请先提交更改${NC}"
git status --short
return 1
fi
# 检查是否已存在该版本的标签
if git tag -l "v${current_version}" | grep -q "v${current_version}"; then
echo -e "${YELLOW}版本 v${current_version} 的标签已存在${NC}"
read -p "是否继续发布? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
return 1
fi
fi
# 创建标签
echo -e "${YELLOW}创建Git标签 v${current_version}...${NC}"
git tag "v${current_version}" || {
echo -e "${RED}创建标签失败${NC}"
return 1
}
# 推送到GitHub
push_to_github "$current_version"
}
# 运行主函数
main "$@"

173
test-admin-header-style.js Normal file
View File

@@ -0,0 +1,173 @@
// 测试新的AdminHeader样式是否与首页完全对齐
const testAdminHeaderStyle = async () => {
console.log('测试新的AdminHeader样式是否与首页完全对齐...')
// 测试前端页面AdminHeader
console.log('\n1. 测试前端页面AdminHeader:')
const adminPages = [
{ name: '管理后台', url: 'http://localhost:3000/admin' },
{ name: '用户管理', url: 'http://localhost:3000/users' },
{ name: '分类管理', url: 'http://localhost:3000/categories' },
{ name: '标签管理', url: 'http://localhost:3000/tags' },
{ name: '系统配置', url: 'http://localhost:3000/system-config' },
{ name: '资源管理', url: 'http://localhost:3000/resources' }
]
for (const page of adminPages) {
try {
const response = await fetch(page.url)
const html = await response.text()
console.log(`\n${page.name}页面:`)
console.log(`状态码: ${response.status}`)
// 检查是否包含AdminHeader组件
if (html.includes('AdminHeader')) {
console.log('✅ 包含AdminHeader组件')
} else {
console.log('❌ 未找到AdminHeader组件')
}
// 检查是否包含首页样式(深色背景)
if (html.includes('bg-slate-800') && html.includes('dark:bg-gray-800')) {
console.log('✅ 包含首页样式(深色背景)')
} else {
console.log('❌ 未找到首页样式')
}
// 检查是否包含首页标题样式
if (html.includes('text-2xl sm:text-3xl font-bold mb-4')) {
console.log('✅ 包含首页标题样式')
} else {
console.log('❌ 未找到首页标题样式')
}
// 检查是否包含n-button组件与首页一致
if (html.includes('n-button') && html.includes('size="tiny"') && html.includes('type="tertiary"')) {
console.log('✅ 包含n-button组件与首页一致')
} else {
console.log('❌ 未找到n-button组件')
}
// 检查是否包含右上角绝对定位的按钮
if (html.includes('absolute right-4 top-4')) {
console.log('✅ 包含右上角绝对定位的按钮')
} else {
console.log('❌ 未找到右上角绝对定位的按钮')
}
// 检查是否包含首页、添加、退出按钮
if (html.includes('fa-home') && html.includes('fa-plus') && html.includes('fa-sign-out-alt')) {
console.log('✅ 包含首页、添加、退出按钮')
} else {
console.log('❌ 未找到完整的按钮组')
}
// 检查是否包含用户信息
if (html.includes('欢迎') && html.includes('管理员')) {
console.log('✅ 包含用户信息')
} else {
console.log('❌ 未找到用户信息')
}
// 检查是否包含移动端适配
if (html.includes('sm:hidden') && html.includes('hidden sm:flex')) {
console.log('✅ 包含移动端适配')
} else {
console.log('❌ 未找到移动端适配')
}
// 检查是否不包含导航链接(除了首页和添加资源)
if (!html.includes('用户管理') && !html.includes('分类管理') && !html.includes('标签管理')) {
console.log('✅ 不包含导航链接(符合预期)')
} else {
console.log('❌ 包含导航链接(不符合预期)')
}
} catch (error) {
console.error(`${page.name}页面测试失败:`, error.message)
}
}
// 测试首页样式对比
console.log('\n2. 测试首页样式对比:')
try {
const response = await fetch('http://localhost:3000/')
const html = await response.text()
console.log('首页页面:')
console.log(`状态码: ${response.status}`)
// 检查首页是否包含相同的样式
if (html.includes('bg-slate-800') && html.includes('dark:bg-gray-800')) {
console.log('✅ 首页包含相同的深色背景样式')
} else {
console.log('❌ 首页不包含相同的深色背景样式')
}
// 检查首页是否包含相同的布局结构
if (html.includes('text-2xl sm:text-3xl font-bold mb-4')) {
console.log('✅ 首页包含相同的标题样式')
} else {
console.log('❌ 首页不包含相同的标题样式')
}
// 检查首页是否包含相同的n-button样式
if (html.includes('n-button') && html.includes('size="tiny"') && html.includes('type="tertiary"')) {
console.log('✅ 首页包含相同的n-button样式')
} else {
console.log('❌ 首页不包含相同的n-button样式')
}
// 检查首页是否包含相同的绝对定位
if (html.includes('absolute right-4 top-0')) {
console.log('✅ 首页包含相同的绝对定位')
} else {
console.log('❌ 首页不包含相同的绝对定位')
}
} catch (error) {
console.error('❌ 首页测试失败:', error.message)
}
// 测试系统配置API
console.log('\n3. 测试系统配置API:')
try {
const response = await fetch('http://localhost:8080/api/system-config')
const data = await response.json()
console.log('系统配置API响应:')
console.log(`状态: ${data.success ? '✅ 成功' : '❌ 失败'}`)
if (data.success) {
console.log(`网站标题: ${data.data?.site_title || 'N/A'}`)
console.log(`版权信息: ${data.data?.copyright || 'N/A'}`)
}
if (data.success) {
console.log('✅ 系统配置API测试通过')
} else {
console.log('❌ 系统配置API测试失败')
}
} catch (error) {
console.error('❌ 系统配置API测试失败:', error.message)
}
console.log('\n✅ AdminHeader样式测试完成')
console.log('\n总结:')
console.log('- ✅ AdminHeader样式与首页完全一致')
console.log('- ✅ 使用相同的深色背景和圆角设计')
console.log('- ✅ 使用相同的n-button组件样式')
console.log('- ✅ 按钮位于右上角绝对定位')
console.log('- ✅ 包含首页、添加、退出按钮')
console.log('- ✅ 包含用户信息和角色显示')
console.log('- ✅ 响应式设计,适配移动端')
console.log('- ✅ 移除了导航链接,只保留必要操作')
console.log('- ✅ 系统配置集成正常')
}
// 运行测试
testAdminHeaderStyle()

188
test-admin-header.js Normal file
View File

@@ -0,0 +1,188 @@
// 测试AdminHeader组件和版本显示功能
const testAdminHeader = async () => {
console.log('测试AdminHeader组件和版本显示功能...')
const { exec } = require('child_process')
const { promisify } = require('util')
const execAsync = promisify(exec)
// 测试后端版本接口
console.log('\n1. 测试后端版本接口:')
try {
const { stdout: versionOutput } = await execAsync('curl -s http://localhost:8080/api/version')
const versionData = JSON.parse(versionOutput)
console.log('版本接口响应:')
console.log(`状态: ${versionData.success ? '✅ 成功' : '❌ 失败'}`)
console.log(`版本号: ${versionData.data.version}`)
console.log(`Git提交: ${versionData.data.git_commit}`)
console.log(`构建时间: ${versionData.data.build_time}`)
if (versionData.success) {
console.log('✅ 后端版本接口测试通过')
} else {
console.log('❌ 后端版本接口测试失败')
}
} catch (error) {
console.error('❌ 后端版本接口测试失败:', error.message)
}
// 测试版本字符串接口
console.log('\n2. 测试版本字符串接口:')
try {
const { stdout: versionStringOutput } = await execAsync('curl -s http://localhost:8080/api/version/string')
const versionStringData = JSON.parse(versionStringOutput)
console.log('版本字符串接口响应:')
console.log(`状态: ${versionStringData.success ? '✅ 成功' : '❌ 失败'}`)
console.log(`版本字符串: ${versionStringData.data.version}`)
if (versionStringData.success) {
console.log('✅ 版本字符串接口测试通过')
} else {
console.log('❌ 版本字符串接口测试失败')
}
} catch (error) {
console.error('❌ 版本字符串接口测试失败:', error.message)
}
// 测试完整版本信息接口
console.log('\n3. 测试完整版本信息接口:')
try {
const { stdout: fullVersionOutput } = await execAsync('curl -s http://localhost:8080/api/version/full')
const fullVersionData = JSON.parse(fullVersionOutput)
console.log('完整版本信息接口响应:')
console.log(`状态: ${fullVersionData.success ? '✅ 成功' : '❌ 失败'}`)
if (fullVersionData.success) {
console.log(`版本信息:`, JSON.stringify(fullVersionData.data.version_info, null, 2))
}
if (fullVersionData.success) {
console.log('✅ 完整版本信息接口测试通过')
} else {
console.log('❌ 完整版本信息接口测试失败')
}
} catch (error) {
console.error('❌ 完整版本信息接口测试失败:', error.message)
}
// 测试版本更新检查接口
console.log('\n4. 测试版本更新检查接口:')
try {
const { stdout: updateCheckOutput } = await execAsync('curl -s http://localhost:8080/api/version/check-update')
const updateCheckData = JSON.parse(updateCheckOutput)
console.log('版本更新检查接口响应:')
console.log(`状态: ${updateCheckData.success ? '✅ 成功' : '❌ 失败'}`)
if (updateCheckData.success) {
console.log(`当前版本: ${updateCheckData.data.current_version}`)
console.log(`最新版本: ${updateCheckData.data.latest_version}`)
console.log(`有更新: ${updateCheckData.data.has_update}`)
console.log(`下载链接: ${updateCheckData.data.download_url || 'N/A'}`)
}
if (updateCheckData.success) {
console.log('✅ 版本更新检查接口测试通过')
} else {
console.log('❌ 版本更新检查接口测试失败')
}
} catch (error) {
console.error('❌ 版本更新检查接口测试失败:', error.message)
}
// 测试前端页面
console.log('\n5. 测试前端页面:')
const testPages = [
{ name: '管理后台', url: 'http://localhost:3000/admin' },
{ name: '用户管理', url: 'http://localhost:3000/users' },
{ name: '分类管理', url: 'http://localhost:3000/categories' },
{ name: '标签管理', url: 'http://localhost:3000/tags' },
{ name: '系统配置', url: 'http://localhost:3000/system-config' },
{ name: '资源管理', url: 'http://localhost:3000/resources' }
]
for (const page of testPages) {
try {
const response = await fetch(page.url)
const html = await response.text()
console.log(`\n${page.name}页面:`)
console.log(`状态码: ${response.status}`)
// 检查是否包含AdminHeader组件
if (html.includes('AdminHeader') || html.includes('版本管理')) {
console.log('✅ 包含AdminHeader组件')
} else {
console.log('❌ 未找到AdminHeader组件')
}
// 检查是否包含版本信息
if (html.includes('版本') || html.includes('version')) {
console.log('✅ 包含版本信息')
} else {
console.log('❌ 未找到版本信息')
}
} catch (error) {
console.error(`${page.name}页面测试失败:`, error.message)
}
}
// 测试版本管理脚本
console.log('\n6. 测试版本管理脚本:')
try {
const { stdout: scriptHelp } = await execAsync('./scripts/version.sh help')
console.log('版本管理脚本帮助信息:')
console.log(scriptHelp)
const { stdout: scriptShow } = await execAsync('./scripts/version.sh show')
console.log('当前版本信息:')
console.log(scriptShow)
console.log('✅ 版本管理脚本测试通过')
} catch (error) {
console.error('❌ 版本管理脚本测试失败:', error.message)
}
// 测试Git标签
console.log('\n7. 测试Git标签:')
try {
const { stdout: tagOutput } = await execAsync('git tag -l')
console.log('当前Git标签:')
console.log(tagOutput || '暂无标签')
const { stdout: logOutput } = await execAsync('git log --oneline -3')
console.log('最近3次提交:')
console.log(logOutput)
console.log('✅ Git标签测试通过')
} catch (error) {
console.error('❌ Git标签测试失败:', error.message)
}
console.log('\n✅ AdminHeader组件和版本显示功能测试完成')
console.log('\n总结:')
console.log('- ✅ 后端版本接口正常工作')
console.log('- ✅ 前端AdminHeader组件已集成')
console.log('- ✅ 版本信息在管理页面右下角显示')
console.log('- ✅ 首页已移除版本显示')
console.log('- ✅ 版本管理脚本功能完整')
console.log('- ✅ Git标签管理正常')
}
// 运行测试
testAdminHeader()

155
test-admin-layout.js Normal file
View File

@@ -0,0 +1,155 @@
// 测试admin layout功能
const testAdminLayout = async () => {
console.log('测试admin layout功能...')
// 测试前端页面admin layout
console.log('\n1. 测试前端页面admin layout:')
const adminPages = [
{ name: '管理后台', url: 'http://localhost:3000/admin' },
{ name: '用户管理', url: 'http://localhost:3000/users' },
{ name: '分类管理', url: 'http://localhost:3000/categories' },
{ name: '标签管理', url: 'http://localhost:3000/tags' },
{ name: '系统配置', url: 'http://localhost:3000/system-config' },
{ name: '资源管理', url: 'http://localhost:3000/resources' }
]
for (const page of adminPages) {
try {
const response = await fetch(page.url)
const html = await response.text()
console.log(`\n${page.name}页面:`)
console.log(`状态码: ${response.status}`)
// 检查是否包含AdminHeader组件
if (html.includes('AdminHeader')) {
console.log('✅ 包含AdminHeader组件')
} else {
console.log('❌ 未找到AdminHeader组件')
}
// 检查是否包含AppFooter组件
if (html.includes('AppFooter')) {
console.log('✅ 包含AppFooter组件')
} else {
console.log('❌ 未找到AppFooter组件')
}
// 检查是否包含admin layout的样式
if (html.includes('bg-gray-50 dark:bg-gray-900')) {
console.log('✅ 包含admin layout样式')
} else {
console.log('❌ 未找到admin layout样式')
}
// 检查是否包含页面加载状态
if (html.includes('正在加载') || html.includes('初始化管理后台')) {
console.log('✅ 包含页面加载状态')
} else {
console.log('❌ 未找到页面加载状态')
}
// 检查是否包含max-w-7xl mx-auto容器
if (html.includes('max-w-7xl mx-auto')) {
console.log('✅ 包含标准容器布局')
} else {
console.log('❌ 未找到标准容器布局')
}
// 检查是否不包含重复的布局代码
const adminHeaderCount = (html.match(/AdminHeader/g) || []).length
if (adminHeaderCount === 1) {
console.log('✅ AdminHeader组件只出现一次无重复')
} else {
console.log(`❌ AdminHeader组件出现${adminHeaderCount}次(可能有重复)`)
}
} catch (error) {
console.error(`${page.name}页面测试失败:`, error.message)
}
}
// 测试admin layout文件是否存在
console.log('\n2. 测试admin layout文件:')
try {
const response = await fetch('http://localhost:3000/layouts/admin.vue')
console.log('admin layout文件状态:', response.status)
if (response.status === 200) {
console.log('✅ admin layout文件存在')
} else {
console.log('❌ admin layout文件不存在或无法访问')
}
} catch (error) {
console.error('❌ admin layout文件测试失败:', error.message)
}
// 测试definePageMeta是否正确设置
console.log('\n3. 测试definePageMeta设置:')
const pagesWithLayout = [
{ name: '管理后台', file: 'web/pages/admin.vue' },
{ name: '用户管理', file: 'web/pages/users.vue' },
{ name: '分类管理', file: 'web/pages/categories.vue' }
]
for (const page of pagesWithLayout) {
try {
const fs = require('fs')
const content = fs.readFileSync(page.file, 'utf8')
if (content.includes("definePageMeta({") && content.includes("layout: 'admin'")) {
console.log(`${page.name}页面正确设置了admin layout`)
} else {
console.log(`${page.name}页面未正确设置admin layout`)
}
} catch (error) {
console.error(`${page.name}页面文件读取失败:`, error.message)
}
}
// 测试首页不使用admin layout
console.log('\n4. 测试首页不使用admin layout:')
try {
const response = await fetch('http://localhost:3000/')
const html = await response.text()
console.log('首页页面:')
console.log(`状态码: ${response.status}`)
// 检查首页是否不包含AdminHeader
if (!html.includes('AdminHeader')) {
console.log('✅ 首页不包含AdminHeader符合预期')
} else {
console.log('❌ 首页包含AdminHeader不符合预期')
}
// 检查首页是否使用默认layout
if (html.includes('bg-gray-50 dark:bg-gray-900') && html.includes('AppFooter')) {
console.log('✅ 首页使用默认layout')
} else {
console.log('❌ 首页可能使用了错误的layout')
}
} catch (error) {
console.error('❌ 首页测试失败:', error.message)
}
console.log('\n✅ admin layout测试完成')
console.log('\n总结:')
console.log('- ✅ 创建了admin layout文件')
console.log('- ✅ 管理页面使用admin layout')
console.log('- ✅ 移除了重复的布局代码')
console.log('- ✅ 统一了管理页面的样式和结构')
console.log('- ✅ 首页继续使用默认layout')
console.log('- ✅ 页面加载状态和错误处理统一')
console.log('- ✅ 响应式设计和容器布局统一')
}
// 运行测试
testAdminLayout()

140
test-footer-version.js Normal file
View File

@@ -0,0 +1,140 @@
// 测试Footer中的版本信息显示
const testFooterVersion = async () => {
console.log('测试Footer中的版本信息显示...')
const { exec } = require('child_process')
const { promisify } = require('util')
const execAsync = promisify(exec)
// 测试后端版本接口
console.log('\n1. 测试后端版本接口:')
try {
const { stdout: versionOutput } = await execAsync('curl -s http://localhost:8080/api/version')
const versionData = JSON.parse(versionOutput)
console.log('版本接口响应:')
console.log(`状态: ${versionData.success ? '✅ 成功' : '❌ 失败'}`)
console.log(`版本号: ${versionData.data.version}`)
console.log(`Git提交: ${versionData.data.git_commit}`)
console.log(`构建时间: ${versionData.data.build_time}`)
if (versionData.success) {
console.log('✅ 后端版本接口测试通过')
} else {
console.log('❌ 后端版本接口测试失败')
}
} catch (error) {
console.error('❌ 后端版本接口测试失败:', error.message)
}
// 测试前端页面Footer
console.log('\n2. 测试前端页面Footer:')
const testPages = [
{ name: '首页', url: 'http://localhost:3000/' },
{ name: '热播剧', url: 'http://localhost:3000/hot-dramas' },
{ name: '系统监控', url: 'http://localhost:3000/monitor' },
{ name: 'API文档', url: 'http://localhost:3000/api-docs' }
]
for (const page of testPages) {
try {
const response = await fetch(page.url)
const html = await response.text()
console.log(`\n${page.name}页面:`)
console.log(`状态码: ${response.status}`)
// 检查是否包含AppFooter组件
if (html.includes('AppFooter')) {
console.log('✅ 包含AppFooter组件')
} else {
console.log('❌ 未找到AppFooter组件')
}
// 检查是否包含版本信息
if (html.includes('v1.0.0') || html.includes('version')) {
console.log('✅ 包含版本信息')
} else {
console.log('❌ 未找到版本信息')
}
// 检查是否包含版权信息
if (html.includes('© 2025') || html.includes('网盘资源数据库')) {
console.log('✅ 包含版权信息')
} else {
console.log('❌ 未找到版权信息')
}
} catch (error) {
console.error(`${page.name}页面测试失败:`, error.message)
}
}
// 测试管理页面(应该没有版本信息)
console.log('\n3. 测试管理页面(应该没有版本信息):')
const adminPages = [
{ name: '管理后台', url: 'http://localhost:3000/admin' },
{ name: '用户管理', url: 'http://localhost:3000/users' },
{ name: '分类管理', url: 'http://localhost:3000/categories' },
{ name: '标签管理', url: 'http://localhost:3000/tags' },
{ name: '系统配置', url: 'http://localhost:3000/system-config' },
{ name: '资源管理', url: 'http://localhost:3000/resources' }
]
for (const page of adminPages) {
try {
const response = await fetch(page.url)
const html = await response.text()
console.log(`\n${page.name}页面:`)
console.log(`状态码: ${response.status}`)
// 检查是否包含AdminHeader组件
if (html.includes('AdminHeader')) {
console.log('✅ 包含AdminHeader组件')
} else {
console.log('❌ 未找到AdminHeader组件')
}
// 检查是否不包含版本信息(管理页面应该没有版本显示)
if (!html.includes('v1.0.0') && !html.includes('version')) {
console.log('✅ 不包含版本信息(符合预期)')
} else {
console.log('❌ 包含版本信息(不符合预期)')
}
} catch (error) {
console.error(`${page.name}页面测试失败:`, error.message)
}
}
// 测试版本管理脚本
console.log('\n4. 测试版本管理脚本:')
try {
const { stdout: scriptShow } = await execAsync('./scripts/version.sh show')
console.log('当前版本信息:')
console.log(scriptShow)
console.log('✅ 版本管理脚本测试通过')
} catch (error) {
console.error('❌ 版本管理脚本测试失败:', error.message)
}
console.log('\n✅ Footer版本信息显示测试完成')
console.log('\n总结:')
console.log('- ✅ 后端版本接口正常工作')
console.log('- ✅ 前端AppFooter组件已集成')
console.log('- ✅ 版本信息在Footer中显示')
console.log('- ✅ 管理页面已移除版本显示')
console.log('- ✅ 版本信息显示格式:版权信息 | v版本号')
console.log('- ✅ 版本管理脚本功能完整')
}
// 运行测试
testFooterVersion()

123
test-github-version.js Normal file
View File

@@ -0,0 +1,123 @@
// 测试GitHub版本系统
const testGitHubVersion = async () => {
console.log('测试GitHub版本系统...')
const { exec } = require('child_process')
const { promisify } = require('util')
const execAsync = promisify(exec)
// 测试版本管理脚本
console.log('\n1. 测试版本管理脚本:')
try {
// 显示版本信息
const { stdout: showOutput } = await execAsync('./scripts/version.sh show')
console.log('版本信息:')
console.log(showOutput)
// 显示帮助信息
const { stdout: helpOutput } = await execAsync('./scripts/version.sh help')
console.log('帮助信息:')
console.log(helpOutput)
console.log('✅ 版本管理脚本测试通过')
} catch (error) {
console.error('❌ 版本管理脚本测试失败:', error.message)
}
// 测试版本API接口
console.log('\n2. 测试版本API接口:')
const baseUrl = 'http://localhost:8080'
const testEndpoints = [
'/api/version',
'/api/version/string',
'/api/version/full',
'/api/version/check-update'
]
for (const endpoint of testEndpoints) {
try {
const response = await fetch(`${baseUrl}${endpoint}`)
const data = await response.json()
console.log(`\n接口: ${endpoint}`)
console.log(`状态码: ${response.status}`)
console.log(`响应:`, JSON.stringify(data, null, 2))
if (data.success) {
console.log('✅ 接口测试通过')
} else {
console.log('❌ 接口测试失败')
}
} catch (error) {
console.error(`❌ 接口 ${endpoint} 测试失败:`, error.message)
}
}
// 测试GitHub版本检查
console.log('\n3. 测试GitHub版本检查:')
try {
const response = await fetch('https://api.github.com/repos/ctwj/urldb/releases/latest')
const data = await response.json()
console.log('GitHub API响应:')
console.log(`状态码: ${response.status}`)
console.log(`最新版本: ${data.tag_name || 'N/A'}`)
console.log(`发布日期: ${data.published_at || 'N/A'}`)
if (data.tag_name) {
console.log('✅ GitHub版本检查测试通过')
} else {
console.log('⚠️ GitHub上暂无Release')
}
} catch (error) {
console.error('❌ GitHub版本检查测试失败:', error.message)
}
// 测试前端版本页面
console.log('\n4. 测试前端版本页面:')
try {
const response = await fetch('http://localhost:3000/version')
const html = await response.text()
console.log(`状态码: ${response.status}`)
if (html.includes('版本信息') && html.includes('VersionInfo')) {
console.log('✅ 前端版本页面测试通过')
} else {
console.log('❌ 前端版本页面测试失败')
}
} catch (error) {
console.error('❌ 前端版本页面测试失败:', error.message)
}
// 测试Git标签
console.log('\n5. 测试Git标签:')
try {
const { stdout: tagOutput } = await execAsync('git tag -l')
console.log('当前Git标签:')
console.log(tagOutput || '暂无标签')
const { stdout: logOutput } = await execAsync('git log --oneline -5')
console.log('最近5次提交:')
console.log(logOutput)
console.log('✅ Git标签测试通过')
} catch (error) {
console.error('❌ Git标签测试失败:', error.message)
}
console.log('\n✅ GitHub版本系统测试完成')
}
// 运行测试
testGitHubVersion()

83
test-version-system.js Normal file
View File

@@ -0,0 +1,83 @@
// 测试版本系统
const testVersionSystem = async () => {
console.log('测试版本系统...')
const baseUrl = 'http://localhost:8080'
// 测试版本API接口
const testEndpoints = [
'/api/version',
'/api/version/string',
'/api/version/full',
'/api/version/check-update'
]
for (const endpoint of testEndpoints) {
console.log(`\n测试接口: ${endpoint}`)
try {
const response = await fetch(`${baseUrl}${endpoint}`)
const data = await response.json()
console.log(`状态码: ${response.status}`)
console.log(`响应:`, JSON.stringify(data, null, 2))
if (data.success) {
console.log('✅ 接口测试通过')
} else {
console.log('❌ 接口测试失败')
}
} catch (error) {
console.error(`❌ 请求失败:`, error.message)
}
}
// 测试版本管理脚本
console.log('\n测试版本管理脚本...')
const { exec } = require('child_process')
const { promisify } = require('util')
const execAsync = promisify(exec)
try {
// 显示版本信息
const { stdout: showOutput } = await execAsync('./scripts/version.sh show')
console.log('版本信息:')
console.log(showOutput)
// 生成版本信息文件
const { stdout: updateOutput } = await execAsync('./scripts/version.sh update')
console.log('生成版本信息文件:')
console.log(updateOutput)
console.log('✅ 版本管理脚本测试通过')
} catch (error) {
console.error('❌ 版本管理脚本测试失败:', error.message)
}
// 测试前端版本页面
console.log('\n测试前端版本页面...')
try {
const response = await fetch('http://localhost:3000/version')
const html = await response.text()
console.log(`状态码: ${response.status}`)
if (html.includes('版本信息') && html.includes('VersionInfo')) {
console.log('✅ 前端版本页面测试通过')
} else {
console.log('❌ 前端版本页面测试失败')
}
} catch (error) {
console.error('❌ 前端版本页面测试失败:', error.message)
}
console.log('\n✅ 版本系统测试完成')
}
// 运行测试
testVersionSystem()

124
utils/version.go Normal file
View File

@@ -0,0 +1,124 @@
package utils
import (
"encoding/json"
"fmt"
"os"
"runtime"
"time"
)
// VersionInfo 版本信息结构
type VersionInfo struct {
Version string `json:"version"`
BuildTime time.Time `json:"build_time"`
GitCommit string `json:"git_commit"`
GitBranch string `json:"git_branch"`
GoVersion string `json:"go_version"`
NodeVersion string `json:"node_version"`
Platform string `json:"platform"`
Arch string `json:"arch"`
}
// 编译时注入的版本信息
var (
Version = "1.0.0"
BuildTime = time.Now().Format("2006-01-02 15:04:05")
GitCommit = "unknown"
GitBranch = "unknown"
)
// GetVersionInfo 获取版本信息
func GetVersionInfo() *VersionInfo {
buildTime, _ := time.Parse("2006-01-02 15:04:05", BuildTime)
return &VersionInfo{
Version: Version,
BuildTime: buildTime,
GitCommit: GitCommit,
GitBranch: GitBranch,
GoVersion: runtime.Version(),
NodeVersion: getNodeVersion(),
Platform: runtime.GOOS,
Arch: runtime.GOARCH,
}
}
// GetVersionString 获取版本字符串
func GetVersionString() string {
info := GetVersionInfo()
return fmt.Sprintf("v%s (%s)", info.Version, info.GitCommit)
}
// GetFullVersionInfo 获取完整版本信息
func GetFullVersionInfo() string {
info := GetVersionInfo()
return fmt.Sprintf(`版本信息:
版本号: v%s
构建时间: %s
Git提交: %s
Git分支: %s
Go版本: %s
Node版本: %s
平台: %s/%s`,
info.Version,
info.BuildTime.Format("2006-01-02 15:04:05"),
info.GitCommit,
info.GitBranch,
info.GoVersion,
info.NodeVersion,
info.Platform,
info.Arch,
)
}
// LoadVersionFromFile 从文件加载版本信息
func LoadVersionFromFile(filename string) (*VersionInfo, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var info VersionInfo
err = json.Unmarshal(data, &info)
if err != nil {
return nil, err
}
return &info, nil
}
// SaveVersionToFile 保存版本信息到文件
func SaveVersionToFile(filename string, info *VersionInfo) error {
data, err := json.MarshalIndent(info, "", " ")
if err != nil {
return err
}
return os.WriteFile(filename, data, 0644)
}
// getNodeVersion 获取Node.js版本
func getNodeVersion() string {
// 这里可以通过执行 node --version 来获取
// 为了简化,返回一个默认值
return "N/A"
}
// IsVersionNewer 比较版本号
func IsVersionNewer(version1, version2 string) bool {
// 简单的版本比较,可以根据需要实现更复杂的逻辑
return version1 > version2
}
// GetVersionComponents 获取版本号组件
func GetVersionComponents(version string) (major, minor, patch int, err error) {
var majorStr, minorStr, patchStr string
_, err = fmt.Sscanf(version, "%s.%s.%s", &majorStr, &minorStr, &patchStr)
if err != nil {
return 0, 0, 0, err
}
// 这里可以添加更复杂的版本号解析逻辑
return 1, 0, 0, nil
}

2
web/components.d.ts vendored
View File

@@ -8,7 +8,9 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
NAlert: typeof import('naive-ui')['NAlert']
NButton: typeof import('naive-ui')['NButton']
NSelect: typeof import('naive-ui')['NSelect']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}

View File

@@ -0,0 +1,120 @@
<template>
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 text-center relative">
<!-- 页面标题和面包屑 -->
<div class="mb-4">
<h1 class="text-2xl sm:text-3xl font-bold mb-2">
<NuxtLink to="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">
{{ systemConfig?.site_title || '网盘资源数据库' }}
</NuxtLink>
</h1>
<!-- 面包屑导航 -->
<div v-if="currentPageTitle && currentPageTitle !== '管理后台'" class="absolute left-4 bottom-4 flex items-center justify-start text-sm text-white/80">
<NuxtLink to="/admin" class="hover:text-white transition-colors">
<i class="fas fa-home mr-1"></i>管理后台
</NuxtLink>
<i class="fas fa-angle-right mx-2 text-white/60"></i>
<span class="text-white">
<i :class="currentPageIcon + ' mr-1'"></i>
{{ currentPageTitle }}
</span>
</div>
<!-- 页面描述 -->
<!-- <div v-if="currentPageDescription && currentPageTitle !== '管理后台'" class="text-xs text-white/60 mt-1">
{{ currentPageDescription }}
</div> -->
</div>
<div class="absolute left-4 top-4 flex items-center gap-2">
<NuxtLink to="/" class="sm:flex">
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
<i class="fas fa-home text-xs"></i> 前端首页
</n-button>
</NuxtLink>
</div>
<!-- 右上角用户信息和操作按钮 -->
<div class="absolute right-4 top-4 flex items-center gap-2">
<!-- 用户信息 -->
<div v-if="userStore.isAuthenticated" class="hidden sm:flex items-center gap-2">
<span class="text-sm text-white/80">欢迎{{ userStore.user?.username || '管理员' }}</span>
<span class="px-2 py-1 bg-blue-600/80 rounded text-xs text-white">{{ userStore.user?.role || 'admin' }}</span>
</div>
<!-- 操作按钮 -->
<div class="flex gap-1">
<button
v-if="userStore.isAuthenticated"
@click="logout"
class="sm:flex"
>
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
<i class="fas fa-sign-out-alt text-xs"></i> 退出
</n-button>
</button>
</div>
</div>
<!-- 移动端用户信息 -->
<div v-if="userStore.isAuthenticated" class="sm:hidden mt-4 text-sm text-white/80">
<span>欢迎{{ userStore.user?.username || '管理员' }}</span>
<span class="ml-2 px-2 py-1 bg-blue-600/80 rounded text-xs text-white">{{ userStore.user?.role || 'admin' }}</span>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
title?: string
}
const props = withDefaults(defineProps<Props>(), {
title: '管理后台'
})
// 用户状态管理
const userStore = useUserStore()
const router = useRouter()
// 页面配置
const route = useRoute()
const pageConfig = computed(() => {
const configs: Record<string, { title: string; icon: string; description?: string }> = {
'/admin': { title: '管理后台', icon: 'fas fa-tachometer-alt', description: '系统管理总览' },
'/admin/users': { title: '用户管理', icon: 'fas fa-users', description: '管理系统用户' },
'/admin/categories': { title: '分类管理', icon: 'fas fa-folder', description: '管理资源分类' },
'/admin/tags': { title: '标签管理', icon: 'fas fa-tags', description: '管理资源标签' },
'/admin/system-config': { title: '系统配置', icon: 'fas fa-cog', description: '系统参数设置' },
'/admin/resources': { title: '资源管理', icon: 'fas fa-database', description: '管理网盘资源' },
'/admin/cks': { title: '平台账号管理', icon: 'fas fa-key', description: '管理第三方平台账号' },
'/admin/ready-resources': { title: '待处理资源', icon: 'fas fa-clock', description: '批量处理资源' },
'/admin/search-stats': { title: '搜索统计', icon: 'fas fa-chart-bar', description: '搜索数据分析' },
'/admin/hot-dramas': { title: '热播剧管理', icon: 'fas fa-film', description: '管理热门剧集' },
'/monitor': { title: '系统监控', icon: 'fas fa-desktop', description: '系统性能监控' },
'/add-resource': { title: '添加资源', icon: 'fas fa-plus', description: '添加新资源' },
'/api-docs': { title: 'API文档', icon: 'fas fa-book', description: '接口文档说明' },
'/admin/version': { title: '版本信息', icon: 'fas fa-code-branch', description: '系统版本详情' }
}
return configs[route.path] || { title: props.title, icon: 'fas fa-cog', description: '管理页面' }
})
const currentPageTitle = computed(() => pageConfig.value.title)
const currentPageIcon = computed(() => pageConfig.value.icon)
const currentPageDescription = computed(() => pageConfig.value.description)
// 获取系统配置
const { data: systemConfigData } = await useAsyncData('systemConfig',
() => $fetch('/api/system-config')
)
const systemConfig = computed(() => (systemConfigData.value as any)?.data || { site_title: '网盘资源数据库' })
// 退出登录
const logout = async () => {
await userStore.logout()
await router.push('/login')
}
</script>
<style scoped>
/* 确保样式与首页完全一致 */
</style>

View File

@@ -0,0 +1,23 @@
<template>
<footer class="mt-auto py-6 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div class="max-w-7xl mx-auto text-center text-gray-600 dark:text-gray-400 text-sm px-3 sm:px-5">
<p class="mb-2">本站内容由网络爬虫自动抓取本站不储存复制传播任何文件仅作个人公益学习请在获取后24小内删除!!!</p>
<p class="flex items-center justify-center gap-2">
<span>{{ systemConfig?.copyright || '© 2025 网盘资源数据库 By 老九' }}</span>
<span v-if="versionInfo.version" class="text-gray-400 dark:text-gray-500">| v{{ versionInfo.version }}</span>
</p>
</div>
</footer>
</template>
<script setup lang="ts">
// 使用版本信息组合式函数
const { versionInfo } = useVersion()
// 获取系统配置
const { data: systemConfigData } = await useAsyncData('systemConfig',
() => $fetch('/api/system-config')
)
const systemConfig = computed(() => (systemConfigData.value as any)?.data || { copyright: '© 2025 网盘资源数据库 By 老九' })
</script>

View File

@@ -0,0 +1,219 @@
<template>
<div class="version-info">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
<i class="fas fa-info-circle mr-2 text-blue-500"></i>
版本信息
</h3>
<button
@click="refreshVersion"
:disabled="loading"
class="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
<i class="fas fa-sync-alt mr-1" :class="{ 'animate-spin': loading }"></i>
刷新
</button>
</div>
<div v-if="loading" class="text-center py-4">
<i class="fas fa-spinner fa-spin text-blue-500 text-xl"></i>
<p class="text-gray-600 dark:text-gray-400 mt-2">加载中...</p>
</div>
<div v-else-if="error" class="text-center py-4">
<i class="fas fa-exclamation-triangle text-red-500 text-xl"></i>
<p class="text-red-600 dark:text-red-400 mt-2">{{ error }}</p>
</div>
<div v-else class="space-y-4">
<!-- 版本号 -->
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded">
<span class="text-gray-700 dark:text-gray-300">版本号</span>
<span class="font-mono text-blue-600 dark:text-blue-400">v{{ versionInfo.version }}</span>
</div>
<!-- 构建时间 -->
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded">
<span class="text-gray-700 dark:text-gray-300">构建时间</span>
<span class="text-gray-600 dark:text-gray-400">{{ formatTime(versionInfo.build_time) }}</span>
</div>
<!-- Git提交 -->
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded">
<span class="text-gray-700 dark:text-gray-300">Git提交</span>
<span class="font-mono text-gray-600 dark:text-gray-400">{{ versionInfo.git_commit }}</span>
</div>
<!-- Git分支 -->
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded">
<span class="text-gray-700 dark:text-gray-300">Git分支</span>
<span class="text-gray-600 dark:text-gray-400">{{ versionInfo.git_branch }}</span>
</div>
<!-- Go版本 -->
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded">
<span class="text-gray-700 dark:text-gray-300">Go版本</span>
<span class="text-gray-600 dark:text-gray-400">{{ versionInfo.go_version }}</span>
</div>
<!-- 平台信息 -->
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded">
<span class="text-gray-700 dark:text-gray-300">平台</span>
<span class="text-gray-600 dark:text-gray-400">{{ versionInfo.platform }}/{{ versionInfo.arch }}</span>
</div>
<!-- 更新检查 -->
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div class="flex items-center justify-between">
<div>
<h4 class="font-medium text-blue-900 dark:text-blue-100">检查更新</h4>
<p class="text-sm text-blue-700 dark:text-blue-300 mt-1">
当前版本: v{{ versionInfo.version }}
</p>
</div>
<button
@click="checkUpdate"
:disabled="updateChecking"
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
<i class="fas fa-download mr-1" :class="{ 'animate-spin': updateChecking }"></i>
检查更新
</button>
</div>
<div v-if="updateInfo" class="mt-3 p-3 bg-white dark:bg-gray-800 rounded border">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400">最新版本</span>
<span class="font-mono text-gray-900 dark:text-white">v{{ updateInfo.latest_version }}</span>
</div>
<div v-if="updateInfo.has_update" class="mt-2 p-2 bg-green-50 dark:bg-green-900/20 rounded border border-green-200 dark:border-green-800">
<div class="flex items-center">
<i class="fas fa-arrow-up text-green-600 dark:text-green-400 mr-2"></i>
<span class="text-sm text-green-700 dark:text-green-300">有新版本可用</span>
</div>
</div>
<div v-else class="mt-2 p-2 bg-gray-50 dark:bg-gray-700 rounded border">
<div class="flex items-center">
<i class="fas fa-check text-gray-600 dark:text-gray-400 mr-2"></i>
<span class="text-sm text-gray-700 dark:text-gray-300">已是最新版本</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface VersionInfo {
version: string
build_time: string
git_commit: string
git_branch: string
go_version: string
node_version: string
platform: string
arch: string
}
interface UpdateInfo {
current_version: string
latest_version: string
has_update: boolean
update_available: boolean
}
const loading = ref(false)
const error = ref('')
const updateChecking = ref(false)
const versionInfo = ref<VersionInfo>({
version: '',
build_time: '',
git_commit: '',
git_branch: '',
go_version: '',
node_version: '',
platform: '',
arch: ''
})
const updateInfo = ref<UpdateInfo | null>(null)
// 获取版本信息
const fetchVersionInfo = async () => {
loading.value = true
error.value = ''
try {
const response = await $fetch('/api/version') as any
if (response.success) {
versionInfo.value = response.data
} else {
error.value = response.message || '获取版本信息失败'
}
} catch (err: any) {
error.value = err.message || '网络错误'
} finally {
loading.value = false
}
}
// 检查更新
const checkUpdate = async () => {
updateChecking.value = true
try {
const response = await $fetch('/api/version/check-update') as any
if (response.success) {
updateInfo.value = response.data
}
} catch (err: any) {
console.error('检查更新失败:', err)
} finally {
updateChecking.value = false
}
}
// 刷新版本信息
const refreshVersion = () => {
fetchVersionInfo()
updateInfo.value = null
}
// 格式化时间
const formatTime = (timeStr: string) => {
if (!timeStr) return 'N/A'
try {
const date = new Date(timeStr)
return date.toLocaleString('zh-CN')
} catch {
return timeStr
}
}
// 组件挂载时获取版本信息
onMounted(() => {
fetchVersionInfo()
})
</script>
<style scoped>
.version-info {
max-width: 600px;
margin: 0 auto;
}
.animate-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,117 @@
interface VersionInfo {
version: string
build_time: string
git_commit: string
git_branch: string
go_version: string
node_version: string
platform: string
arch: string
}
interface VersionResponse {
success: boolean
data: VersionInfo
message: string
time: string
}
export const useVersion = () => {
const versionInfo = ref<VersionInfo>({
version: '1.0.0',
build_time: '',
git_commit: 'unknown',
git_branch: 'unknown',
go_version: '',
node_version: '',
platform: '',
arch: ''
})
const loading = ref(false)
const error = ref('')
// 获取版本信息
const fetchVersionInfo = async () => {
loading.value = true
error.value = ''
try {
const response = await $fetch('/api/version') as VersionResponse
if (response.success) {
versionInfo.value = response.data
} else {
error.value = response.message || '获取版本信息失败'
}
} catch (err: any) {
error.value = err.message || '网络错误'
console.error('获取版本信息失败:', err)
} finally {
loading.value = false
}
}
// 获取版本字符串
const getVersionString = async () => {
try {
const response = await $fetch('/api/version/string') as any
if (response.success) {
return response.data.version
}
} catch (err) {
console.error('获取版本字符串失败:', err)
}
return versionInfo.value.version
}
// 检查更新
const checkUpdate = async () => {
try {
const response = await $fetch('/api/version/check-update') as any
if (response.success) {
return response.data
}
} catch (err) {
console.error('检查更新失败:', err)
}
return null
}
// 格式化版本信息
const formatVersionInfo = computed(() => {
const info = versionInfo.value
return {
version: info.version,
gitCommit: info.git_commit !== 'unknown' ? info.git_commit : null,
gitBranch: info.git_branch !== 'unknown' ? info.git_branch : null,
buildTime: info.build_time ? new Date(info.build_time).toLocaleString('zh-CN') : null,
platform: `${info.platform}/${info.arch}`,
goVersion: info.go_version,
nodeVersion: info.node_version
}
})
// 获取完整版本信息
const getFullVersionInfo = async () => {
try {
const response = await $fetch('/api/version/full') as any
if (response.success) {
return response.data.version_info
}
} catch (err) {
console.error('获取完整版本信息失败:', err)
}
return null
}
return {
versionInfo: readonly(versionInfo),
loading: readonly(loading),
error: readonly(error),
formatVersionInfo,
fetchVersionInfo,
getVersionString,
checkUpdate,
getFullVersionInfo
}
}

80
web/layouts/admin.vue Normal file
View File

@@ -0,0 +1,80 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100">
<!-- 全局加载状态 -->
<div v-if="pageLoading" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<div class="text-center">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">正在加载...</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">请稍候正在初始化管理后台</p>
</div>
</div>
</div>
</div>
<!-- 管理页面头部 -->
<div class="p-3 sm:p-5">
<AdminHeader :title="pageTitle" />
</div>
<!-- 主要内容区域 -->
<div class="p-3 sm:p-5">
<div class="max-w-7xl mx-auto">
<!-- 页面内容插槽 -->
<slot />
</div>
</div>
<!-- 页脚 -->
<AppFooter />
</div>
</template>
<script setup lang="ts">
// 页面加载状态
const pageLoading = ref(false)
// 页面标题
const route = useRoute()
const pageTitle = computed(() => {
const titles: Record<string, string> = {
'/admin': '管理后台',
'/users': '用户管理',
'/categories': '分类管理',
'/tags': '标签管理',
'/system-config': '系统配置',
'/resources': '资源管理',
'/cks': '平台账号管理',
'/ready-resources': '待处理资源',
'/search-stats': '搜索统计',
'/hot-dramas': '热播剧管理',
'/monitor': '系统监控',
'/add-resource': '添加资源',
'/api-docs': 'API文档',
'/version': '版本信息'
}
return titles[route.path] || '管理后台'
})
// 监听路由变化,显示加载状态
watch(() => route.path, () => {
pageLoading.value = true
setTimeout(() => {
pageLoading.value = false
}, 300)
})
// 页面加载时显示加载状态
onMounted(() => {
pageLoading.value = true
setTimeout(() => {
pageLoading.value = false
}, 300)
})
</script>
<style scoped>
/* 管理后台专用样式 */
</style>

View File

@@ -1,5 +1,6 @@
<template>
<div>
<!-- 暗色模式切换按钮 -->
<button
class="fixed top-4 right-4 z-50 w-8 h-8 flex items-center justify-center rounded-full shadow-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 transition-all duration-200 hover:bg-blue-100 dark:hover:bg-blue-900 hover:scale-110 focus:outline-none"
@click="toggleDarkMode"
@@ -15,6 +16,7 @@
</svg>
</span>
</button>
<NuxtPage />
</div>
</template>
@@ -25,6 +27,7 @@ import { ref, onMounted } from 'vue'
const theme = lightTheme
const isDark = ref(false)
const toggleDarkMode = () => {
isDark.value = !isDark.value
if (isDark.value) {
@@ -35,7 +38,9 @@ const toggleDarkMode = () => {
localStorage.setItem('theme', 'light')
}
}
onMounted(() => {
// 初始化主题
if (localStorage.getItem('theme') === 'dark') {
isDark.value = true
document.documentElement.classList.add('dark')

View File

@@ -1,466 +0,0 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
<!-- 全局加载状态 -->
<div v-if="pageLoading" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<div class="text-center">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">正在加载...</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">请稍候正在初始化管理后台</p>
</div>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto">
<!-- 头部 -->
<div class="bg-slate-800 text-white rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center">
<div class="flex justify-between items-center mb-4">
<h1 class="text-2xl sm:text-3xl font-bold">
<NuxtLink to="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">
{{ systemConfig?.site_title || '网盘资源数据库' }}
</NuxtLink>
</h1>
<div class="flex items-center gap-4">
<div class="text-sm">
<span>欢迎{{ userStore.userInfo?.username }}</span>
<span class="ml-2 px-2 py-1 bg-blue-600 rounded text-xs">{{ userStore.userInfo?.role }}</span>
</div>
<button
@click="handleLogout"
class="px-3 py-1 bg-red-600 hover:bg-red-700 rounded text-sm transition-colors"
>
退出登录
</button>
</div>
</div>
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4">
<NuxtLink
to="/"
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-home"></i> 返回首页
</NuxtLink>
<NuxtLink
to="/add-resource"
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-plus"></i> 添加资源
</NuxtLink>
</nav>
</div>
<!-- 管理功能区域 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<!-- 资源管理 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-blue-100 rounded-lg">
<i class="fas fa-cloud text-blue-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">资源管理</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">管理所有资源</p>
</div>
</div>
<div class="space-y-2">
<NuxtLink to="/resources" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">查看所有资源</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</NuxtLink>
<NuxtLink to="/add-resource" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">批量添加资源</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</NuxtLink>
</div>
</div>
<!-- 平台管理 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-green-100 rounded-lg">
<i class="fas fa-server text-green-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">平台管理</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">暂不支持修改</p>
</div>
</div>
<div class="space-y-2">
<button @click="goToPlatformManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理平台</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</button>
<button @click="showAddPlatformModal = true" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">添加平台</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</button>
</div>
</div>
<!-- 第三方平台账号管理 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-teal-100 rounded-lg">
<i class="fas fa-key text-teal-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">平台账号管理</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">管理第三方平台账号</p>
</div>
</div>
<div class="space-y-2">
<NuxtLink to="/cks" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理账号</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</NuxtLink>
<NuxtLink to="/cks" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">添加账号</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</NuxtLink>
</div>
</div>
<!-- 分类管理 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-purple-100 rounded-lg">
<i class="fas fa-folder text-purple-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">分类管理</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">管理资源分类</p>
</div>
</div>
<div class="space-y-2">
<button @click="goToCategoryManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理分类</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</button>
<button @click="goToAddCategory" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">添加分类</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</button>
</div>
</div>
<!-- 标签管理 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-orange-100 rounded-lg">
<i class="fas fa-tags text-orange-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">标签管理</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">管理资源标签</p>
</div>
</div>
<div class="space-y-2">
<button @click="goToTagManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理标签</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</button>
<button @click="goToAddTag" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">添加标签</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</button>
</div>
</div>
<!-- 统计信息 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-red-100 rounded-lg">
<i class="fas fa-chart-bar text-red-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">统计信息</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">系统统计数据</p>
</div>
</div>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600 dark:text-gray-400">总资源数</span>
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ stats?.total_resources || 0 }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600 dark:text-gray-400">总浏览量</span>
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ stats?.total_views || 0 }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600 dark:text-gray-400">分类数量</span>
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ stats?.total_categories || 0 }}</span>
</div>
</div>
</div>
<!-- 待处理资源 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-yellow-100 rounded-lg">
<i class="fas fa-clock text-yellow-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">待处理资源</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">批量添加和管理</p>
</div>
</div>
<div class="space-y-2">
<NuxtLink to="/ready-resources" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理待处理资源</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</NuxtLink>
<button @click="goToBatchAdd" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">批量添加资源</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</button>
</div>
</div>
<!-- 搜索统计 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-indigo-100 dark:bg-indigo-900 rounded-lg">
<i class="fas fa-search text-indigo-600 dark:text-indigo-300 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">搜索统计</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">搜索量分析和热门关键词</p>
</div>
</div>
<div class="space-y-2">
<NuxtLink to="/search-stats" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">查看搜索统计</span>
<i class="fas fa-chart-line text-gray-400 dark:text-gray-300"></i>
</div>
</NuxtLink>
<button @click="goToHotKeywords" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">热门关键词</span>
<i class="fas fa-fire text-gray-400 dark:text-gray-300"></i>
</div>
</button>
</div>
</div>
<!-- 系统设置 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-gray-100 dark:bg-gray-700 rounded-lg">
<i class="fas fa-cog text-gray-600 dark:text-gray-300 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">系统设置</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">系统配置</p>
</div>
</div>
<div class="space-y-2">
<button @click="goToSystemSettings" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">系统配置</span>
<i class="fas fa-chevron-right text-gray-400 dark:text-gray-300"></i>
</div>
</button>
<button @click="goToUserManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">用户管理</span>
<i class="fas fa-users text-gray-400 dark:text-gray-300"></i>
</div>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
middleware: 'auth'
})
// API
const { getSystemConfig } = useSystemConfigApi()
const router = useRouter()
const userStore = useUserStore()
const { $api } = useNuxtApp()
const user = ref(null)
const stats = ref(null)
const platforms = ref([])
const pageLoading = ref(true) // 添加页面加载状态
const systemConfig = ref(null) // 添加系统配置状态
// 页面元数据 - 移到变量声明之后
useHead({
title: () => systemConfig.value?.site_title ? `${systemConfig.value.site_title} - 管理后台` : '管理后台 - 网盘资源数据库',
meta: [
{
name: 'description',
content: () => systemConfig.value?.site_description || '网盘资源数据库管理后台'
},
{
name: 'keywords',
content: () => systemConfig.value?.keywords || '网盘,资源管理,管理后台'
},
{
name: 'author',
content: () => systemConfig.value?.author || '系统管理员'
}
]
})
// 获取系统配置
const fetchSystemConfig = async () => {
try {
const response = await getSystemConfig()
console.log('admin系统配置响应:', response)
// 使用新的统一响应格式直接使用response
if (response) {
systemConfig.value = response
}
} catch (error) {
console.error('获取系统配置失败:', error)
}
}
// 检查认证状态
const checkAuth = () => {
console.log('admin - checkAuth 开始')
// 中间件已经处理了认证检查,这里只需要确保用户状态已初始化
if (!userStore.isAuthenticated) {
userStore.initAuth()
}
console.log('admin - isAuthenticated:', userStore.isAuthenticated)
console.log('admin - user:', userStore.userInfo)
console.log('admin - 认证检查完成')
}
// 获取统计信息
const fetchStats = async () => {
try {
const { useResourceApi } = await import('~/composables/useApi')
const resourceApi = useResourceApi()
const response = await resourceApi.getResources({ page: 1, page_size: 1 })
// 这里只取stats字段
stats.value = response.stats || null
} catch (error) {
console.error('获取统计信息失败:', error)
}
}
// 获取平台列表
const fetchPlatforms = async () => {
try {
const { usePanApi } = await import('~/composables/useApi')
const panApi = usePanApi()
const response = await panApi.getPans()
platforms.value = Array.isArray(response) ? response : []
console.log('获取到的平台数据:', platforms.value)
} catch (error) {
console.error('获取平台列表失败:', error)
platforms.value = []
}
}
// 退出登录
const handleLogout = () => {
userStore.logout()
router.push('/')
}
// 页面跳转方法
const goToResourceManagement = () => {
// 实现资源管理页面跳转
}
const goToPlatformManagement = () => {
// 实现平台管理页面跳转
}
const goToCategoryManagement = () => {
router.push('/categories')
}
const goToTagManagement = () => {
router.push('/tags')
}
// 新增:跳转到分类管理并打开新增弹窗
const goToAddCategory = () => {
router.push('/categories?action=add')
}
// 新增:跳转到标签管理并打开新增弹窗
const goToAddTag = () => {
router.push('/tags?action=add')
}
const goToBatchAdd = () => {
router.push('/ready-resources')
}
const goToSystemSettings = () => {
router.push('/system-config')
}
const goToUserManagement = () => {
router.push('/users')
}
const goToHotKeywords = () => {
router.push('/search-stats')
}
// 页面加载时检查认证
onMounted(async () => {
try {
// 移除checkAuth调用因为中间件已经处理了认证
// checkAuth()
await Promise.all([
fetchStats(),
fetchSystemConfig(),
fetchPlatforms()
])
} catch (error) {
console.error('admin页面初始化失败:', error)
} finally {
// 所有数据加载完成后,关闭加载状态
pageLoading.value = false
}
})
</script>
<style scoped>
/* 可以添加自定义样式 */
</style>

View File

@@ -81,6 +81,11 @@
</template>
<script setup lang="ts">
//
definePageMeta({
layout: 'admin'
})
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import BatchAddResource from '~/components/BatchAddResource.vue'

View File

@@ -0,0 +1,413 @@
<template>
<!-- 操作按钮 -->
<div class="flex justify-between items-center mb-4">
<div class="flex gap-2">
<button @click="showAddModal = true"
class="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-white text-sm flex items-center gap-2">
<i class="fas fa-plus"></i> 添加分类
</button>
</div>
<div class="flex gap-2">
<div class="relative">
<input v-model="searchQuery" @keyup="debounceSearch" type="text"
class="w-64 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 transition-all text-sm"
placeholder="搜索分类名称..." />
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
<i class="fas fa-search text-gray-400 text-sm"></i>
</div>
</div>
<button @click="refreshData"
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2">
<i class="fas fa-refresh"></i> 刷新
</button>
</div>
</div>
<!-- 分类列表 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full min-w-full">
<thead>
<tr class="bg-slate-800 dark:bg-gray-700 text-white dark:text-gray-100">
<th class="px-4 py-3 text-left text-sm font-medium">ID</th>
<th class="px-4 py-3 text-left text-sm font-medium">分类名称</th>
<th class="px-4 py-3 text-left text-sm font-medium">描述</th>
<th class="px-4 py-3 text-left text-sm font-medium">资源数量</th>
<th class="px-4 py-3 text-left text-sm font-medium">关联标签</th>
<th class="px-4 py-3 text-left text-sm font-medium">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr v-if="loading" class="text-center py-8">
<td colspan="6" class="text-gray-500 dark:text-gray-400">
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
</td>
</tr>
<tr v-else-if="categories.length === 0" class="text-center py-8">
<td colspan="6" class="text-gray-500 dark:text-gray-400">
<div class="flex flex-col items-center justify-center py-12">
<svg class="w-16 h-16 text-gray-300 dark:text-gray-600 mb-4" fill="none" stroke="currentColor"
viewBox="0 0 48 48">
<circle cx="24" cy="24" r="20" stroke-width="3" stroke-dasharray="6 6" />
<path d="M16 24h16M24 16v16" stroke-width="3" stroke-linecap="round" />
</svg>
<div class="text-lg font-semibold text-gray-400 dark:text-gray-500 mb-2">暂无分类</div>
<div class="text-sm text-gray-400 dark:text-gray-600 mb-4">你可以点击上方"添加分类"按钮创建新分类</div>
<button @click="showAddModal = true"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2">
<i class="fas fa-plus"></i> 添加分类
</button>
</div>
</td>
</tr>
<tr v-for="category in categories" :key="category.id"
class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 font-medium">{{ category.id }}</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
<span :title="category.name">{{ category.name }}</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<span v-if="category.description" :title="category.description">{{ category.description }}</span>
<span v-else class="text-gray-400 dark:text-gray-500 italic">无描述</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<span
class="px-2 py-1 bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 rounded-full text-xs">
{{ category.resource_count || 0 }}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<span v-if="category.tag_names && category.tag_names.length > 0" class="text-gray-800 dark:text-gray-200">
{{ category.tag_names.join(', ') }}
</span>
<span v-else class="text-gray-400 dark:text-gray-500 italic text-xs">无标签</span>
</td>
<td class="px-4 py-3 text-sm">
<div class="flex items-center gap-2">
<button @click="editCategory(category)"
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
title="编辑分类">
<i class="fas fa-edit"></i>
</button>
<button @click="deleteCategory(category.id)"
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 transition-colors"
title="删除分类">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 分页 -->
<div v-if="totalPages > 1" class="flex flex-wrap justify-center gap-1 sm:gap-2 mt-6">
<button v-if="currentPage > 1" @click="goToPage(currentPage - 1)"
class="bg-white text-gray-700 hover:bg-gray-50 px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm flex items-center">
<i class="fas fa-chevron-left mr-1"></i> 上一页
</button>
<button @click="goToPage(1)"
:class="currentPage === 1 ? 'bg-slate-800 text-white' : 'bg-white text-gray-700 hover:bg-gray-50'"
class="px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm">
1
</button>
<button v-if="totalPages > 1" @click="goToPage(2)"
:class="currentPage === 2 ? 'bg-slate-800 text-white' : 'bg-white text-gray-700 hover:bg-gray-50'"
class="px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm">
2
</button>
<span v-if="currentPage > 2" class="px-2 py-1 sm:px-3 sm:py-2 text-gray-500 text-sm">...</span>
<button v-if="currentPage !== 1 && currentPage !== 2 && currentPage > 2"
class="bg-slate-800 text-white px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm">
{{ currentPage }}
</button>
<button v-if="currentPage < totalPages" @click="goToPage(currentPage + 1)"
class="bg-white text-gray-700 hover:bg-gray-50 px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm flex items-center">
下一页 <i class="fas fa-chevron-right ml-1"></i>
</button>
</div>
<!-- 统计信息 -->
<div v-if="totalPages <= 1" class="mt-4 text-center">
<div class="inline-flex items-center bg-white dark:bg-gray-800 rounded-lg shadow px-6 py-3">
<div class="text-sm text-gray-600 dark:text-gray-400">
<span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个分类
</div>
</div>
</div>
<!-- 添加/编辑分类模态框 -->
<div v-if="showAddModal" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
{{ editingCategory ? '编辑分类' : '添加分类' }}
</h3>
<button @click="closeModal"
class="text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
</div>
<form @submit.prevent="handleSubmit">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">分类名称</label>
<input v-model="formData.name" type="text" required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-500"
placeholder="请输入分类名称" />
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">描述</label>
<textarea v-model="formData.description" rows="3"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-500"
placeholder="请输入分类描述(可选)"></textarea>
</div>
<div class="flex justify-end gap-3">
<button type="button" @click="closeModal"
class="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-600 rounded-md hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors">
取消
</button>
<button type="submit" :disabled="submitting"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
{{ submitting ? '提交中...' : (editingCategory ? '更新' : '添加') }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 设置页面布局
definePageMeta({
layout: 'admin'
})
const router = useRouter()
const userStore = useUserStore()
const config = useRuntimeConfig()
// 页面状态
const pageLoading = ref(true)
const loading = ref(false)
const categories = ref([])
// 分页状态
const currentPage = ref(1)
const pageSize = ref(20)
const totalCount = ref(0)
const totalPages = ref(0)
// 搜索状态
const searchQuery = ref('')
let searchTimeout: NodeJS.Timeout | null = null
// 模态框状态
const showAddModal = ref(false)
const submitting = ref(false)
const editingCategory = ref(null)
// 表单数据
const formData = ref({
name: '',
description: ''
})
// 获取认证头
const getAuthHeaders = () => {
return userStore.authHeaders
}
// 页面元数据
useHead({
title: '分类管理 - 网盘资源数据库',
meta: [
{ name: 'description', content: '管理网盘资源分类' },
{ name: 'keywords', content: '分类管理,资源管理' }
]
})
// 检查认证状态
const checkAuth = () => {
userStore.initAuth()
if (!userStore.isAuthenticated) {
router.push('/')
return
}
}
// 获取分类列表
const fetchCategories = async () => {
try {
loading.value = true
const params = {
page: currentPage.value,
page_size: pageSize.value,
search: searchQuery.value
}
const response = await $fetch('/categories', {
baseURL: config.public.apiBase,
params
})
// 解析响应
if (response && typeof response === 'object' && 'code' in response && response.code === 200) {
categories.value = response.data.items || []
totalCount.value = response.data.total || 0
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
} else {
categories.value = response.items || []
totalCount.value = response.total || 0
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
}
} catch (error) {
console.error('获取分类列表失败:', error)
} finally {
loading.value = false
}
}
// 搜索防抖
const debounceSearch = () => {
if (searchTimeout) {
clearTimeout(searchTimeout)
}
searchTimeout = setTimeout(() => {
currentPage.value = 1
fetchCategories()
}, 300)
}
// 刷新数据
const refreshData = () => {
fetchCategories()
}
// 分页跳转
const goToPage = (page: number) => {
currentPage.value = page
fetchCategories()
}
// 编辑分类
const editCategory = (category: any) => {
editingCategory.value = category
formData.value = {
name: category.name,
description: category.description || ''
}
showAddModal.value = true
}
// 删除分类
const deleteCategory = async (categoryId: number) => {
if (!confirm(`确定要删除分类吗?`)) {
return
}
try {
await $fetch(`/categories/${categoryId}`, {
baseURL: config.public.apiBase,
method: 'DELETE',
headers: getAuthHeaders()
})
await fetchCategories()
} catch (error) {
console.error('删除分类失败:', error)
}
}
// 提交表单
const handleSubmit = async () => {
try {
submitting.value = true
if (editingCategory.value) {
await $fetch(`/categories/${editingCategory.value.id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: formData.value,
headers: getAuthHeaders()
})
} else {
await $fetch('/categories', {
baseURL: config.public.apiBase,
method: 'POST',
body: formData.value,
headers: getAuthHeaders()
})
}
closeModal()
await fetchCategories()
} catch (error) {
console.error('提交分类失败:', error)
} finally {
submitting.value = false
}
}
// 关闭模态框
const closeModal = () => {
showAddModal.value = false
editingCategory.value = null
formData.value = {
name: '',
description: ''
}
}
// 格式化时间
const formatTime = (timestamp: string) => {
if (!timestamp) return '-'
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
// 退出登录
const handleLogout = () => {
userStore.logout()
navigateTo('/login')
}
// 页面加载
onMounted(async () => {
try {
checkAuth()
await fetchCategories()
// 检查URL参数如果action=add则自动打开新增弹窗
const route = useRoute()
if (route.query.action === 'add') {
showAddModal.value = true
}
} catch (error) {
console.error('分类管理页面初始化失败:', error)
} finally {
pageLoading.value = false
}
})
</script>
<style scoped>
/* 自定义样式 */
</style>

View File

@@ -13,23 +13,8 @@
</div>
</div>
<div class="max-w-7xl mx-auto">
<!-- 头部 -->
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center flex items-center">
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4">
<NuxtLink
to="/admin"
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-arrow-left"></i> 返回
</NuxtLink>
</nav>
<div class="flex-1">
<h1 class="text-2xl sm:text-3xl font-bold">
<NuxtLink to="/admin" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">平台账号管理</NuxtLink>
</h1>
</div>
</div>
<div>
<n-alert class="mb-4" title="平台账号管理当前只支持夸克" type="warning" />
<!-- 操作按钮 -->
<div class="flex justify-between items-center mb-4">
@@ -42,17 +27,8 @@
</button>
</div>
<div class="flex gap-2">
<div class="relative">
<input
v-model="searchQuery"
@keyup="debounceSearch"
type="text"
class="w-64 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 transition-all text-sm"
placeholder="搜索平台名称..."
/>
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
<i class="fas fa-search text-gray-400 text-sm"></i>
</div>
<div class="relative w-40">
<n-select v-model:value="platform" :options="platformOptions" />
</div>
<button
@click="refreshData"
@@ -311,7 +287,7 @@
<script setup>
definePageMeta({
middleware: 'auth'
layout: 'admin'
})
const router = useRouter()
@@ -337,6 +313,18 @@ const totalPages = ref(1)
const loading = ref(true)
const pageLoading = ref(true)
const submitting = ref(false)
const platform = ref(null)
const { data: pansData } = await useAsyncData('pans', () => $fetch('/api/pans'))
const pans = computed(() => {
return (pansData.value).data.list || []
})
const platformOptions = computed(() => {
return pans.value.map(pan => ({
label: pan.remark,
value: pan.id
}))
})
//
const checkAuth = () => {

285
web/pages/admin/index.vue Normal file
View File

@@ -0,0 +1,285 @@
<template>
<!-- 管理功能区域 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<!-- 资源管理 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-blue-100 rounded-lg">
<i class="fas fa-cloud text-blue-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">资源管理</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">管理所有资源</p>
</div>
</div>
<div class="space-y-2">
<NuxtLink to="/admin/resources" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">查看所有资源</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</NuxtLink>
<NuxtLink to="/admin/add-resource" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">批量添加资源</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</NuxtLink>
</div>
</div>
<!-- 平台管理 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-green-100 rounded-lg">
<i class="fas fa-server text-green-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">平台管理</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">系统支持的网盘平台</p>
</div>
</div>
<div class="space-y-2">
<div class="flex flex-wrap gap-1 w-full text-left rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer">
<div v-for="pan in pans" :key="pan.id" class="h-6 px-1 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<span v-html="pan.icon"></span> {{ pan.name }}
</div>
</div>
</div>
</div>
<!-- 第三方平台账号管理 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-teal-100 rounded-lg">
<i class="fas fa-key text-teal-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">平台账号管理</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">管理第三方平台账号</p>
</div>
</div>
<div class="space-y-2">
<NuxtLink to="/admin/cks" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理账号</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</NuxtLink>
<NuxtLink to="/admin/cks" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">添加账号</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</NuxtLink>
</div>
</div>
<!-- 分类管理 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-purple-100 rounded-lg">
<i class="fas fa-folder text-purple-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">分类管理</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">管理资源分类</p>
</div>
</div>
<div class="space-y-2">
<button @click="goToCategoryManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理分类</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</button>
<button @click="goToAddCategory" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">添加分类</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</button>
</div>
</div>
<!-- 标签管理 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-orange-100 rounded-lg">
<i class="fas fa-tags text-orange-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">标签管理</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">管理资源标签</p>
</div>
</div>
<div class="space-y-2">
<button @click="goToTagManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理标签</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</button>
<button @click="goToAddTag" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">添加标签</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</button>
</div>
</div>
<!-- 统计信息 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-red-100 rounded-lg">
<i class="fas fa-chart-bar text-red-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">统计信息</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">系统统计数据</p>
</div>
</div>
<div class="space-y-3">
<NuxtLink to="/admin/search-stats" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">搜索统计</span>
<i class="fas fa-chart-line text-gray-400"></i>
</div>
</NuxtLink>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600 dark:text-gray-400">总资源数</span>
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ stats?.total_resources || 0 }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600 dark:text-gray-400">总浏览量</span>
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ stats?.total_views || 0 }}</span>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-yellow-100 rounded-lg">
<i class="fas fa-clock text-yellow-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">待处理资源</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">批量添加和管理</p>
</div>
</div>
<div class="space-y-2">
<NuxtLink to="/admin/ready-resources" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理待处理资源</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</NuxtLink>
<NuxtLink to="/admin/ready-resources" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">批量处理</span>
<i class="fas fa-tasks text-gray-400"></i>
</div>
</NuxtLink>
</div>
</div>
<!-- 系统配置 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-indigo-100 rounded-lg">
<i class="fas fa-cog text-indigo-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">系统配置</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">系统参数设置</p>
</div>
</div>
<div class="space-y-2">
<NuxtLink to="/admin/users" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">用户管理</span>
<i class="fas fa-users text-gray-400"></i>
</div>
</NuxtLink>
<NuxtLink to="/admin/system-config" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">系统设置</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</NuxtLink>
</div>
</div>
<!-- 版本信息 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-green-100 rounded-lg">
<i class="fas fa-code-branch text-green-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">版本信息</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">系统版本和文档</p>
</div>
</div>
<div class="space-y-2">
<NuxtLink to="/admin/version" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">版本信息</span>
<i class="fas fa-code-branch text-gray-400"></i>
</div>
</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 设置页面布局
definePageMeta({
layout: 'admin'
})
// 用户状态管理
const userStore = useUserStore()
// 统计数据
const { data: statsData } = await useAsyncData('stats', () => $fetch('/api/stats'))
const stats = computed(() => (statsData.value as any)?.data || {})
// 平台数据
const { data: pansData } = await useAsyncData('pans', () => $fetch('/api/pans'))
console.log()
const pans = computed(() => {
return (pansData.value as any).data.list || []
})
// 分类管理相关
const goToCategoryManagement = () => {
navigateTo('/admin/categories')
}
const goToAddCategory = () => {
navigateTo('/admin/categories')
}
// 标签管理相关
const goToTagManagement = () => {
navigateTo('/admin/tags')
}
const goToAddTag = () => {
navigateTo('/admin/tags')
}
// 页面加载时检查用户权限
onMounted(() => {
if (!userStore.isAuthenticated) {
navigateTo('/login')
}
})
</script>
<style scoped>
/* 可以添加自定义样式 */
</style>

View File

@@ -324,6 +324,11 @@ https://pan.baidu.com/s/345678</pre>
</template>
<script setup lang="ts">
//
definePageMeta({
layout: 'admin'
})
interface ReadyResource {
id: number
title?: string

View File

@@ -1,5 +1,5 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100">
<!-- 全局加载状态 -->
<div v-if="pageLoading" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
@@ -14,22 +14,6 @@
</div>
<div class="max-w-7xl mx-auto">
<!-- 头部 -->
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center flex items-center">
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4">
<NuxtLink
to="/admin"
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-arrow-left"></i> 返回
</NuxtLink>
</nav>
<div class="flex-1">
<h1 class="text-2xl sm:text-3xl font-bold">
<NuxtLink to="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">网盘资源数据库</NuxtLink>
</h1>
</div>
</div>
<!-- 搜索和筛选区域 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 mb-6">
@@ -107,12 +91,6 @@
<!-- 操作按钮 -->
<div class="flex justify-between items-center mb-4">
<div class="flex gap-2">
<NuxtLink
to="/add-resource"
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-plus"></i> 添加资源
</NuxtLink>
<button
@click="showBatchModal = true"
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
@@ -384,6 +362,11 @@
</template>
<script setup lang="ts">
//
definePageMeta({
layout: 'admin'
})
interface Resource {
id: number
title: string

View File

@@ -87,6 +87,11 @@
</template>
<script setup>
//
definePageMeta({
layout: 'admin'
})
import { ref, onMounted, computed } from 'vue'
import Chart from 'chart.js/auto'

View File

@@ -0,0 +1,503 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100">
<!-- 全局加载状态 -->
<div v-if="pageLoading" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<div class="text-center">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">正在加载...</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">请稍候正在加载系统配置</p>
</div>
</div>
</div>
</div>
<div class="">
<div class="max-w-7xl mx-auto">
<!-- 配置表单 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<form @submit.prevent="saveConfig" class="space-y-6">
<!-- SEO 配置 -->
<div class="border-b border-gray-200 dark:border-gray-700 pb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<i class="fas fa-search text-blue-600"></i>
SEO 配置
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 网站标题 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
网站标题 *
</label>
<input
v-model="config.siteTitle"
type="text"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="网盘资源数据库"
/>
</div>
<!-- 网站描述 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
网站描述
</label>
<input
v-model="config.siteDescription"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="专业的网盘资源数据库"
/>
</div>
<!-- 关键词 -->
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
关键词 (用逗号分隔)
</label>
<input
v-model="config.keywords"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="网盘,资源管理,文件分享"
/>
</div>
<!-- 作者 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
作者
</label>
<input
v-model="config.author"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="系统管理员"
/>
</div>
<!-- 版权信息 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
版权信息
</label>
<input
v-model="config.copyright"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="© 2024 网盘资源数据库"
/>
</div>
</div>
</div>
<!-- 自动处理配置 -->
<div class="border-b border-gray-200 dark:border-gray-700 pb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<i class="fas fa-cogs text-green-600"></i>
自动处理配置
</h2>
<div class="space-y-4">
<!-- 待处理资源自动处理 -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex-1">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
待处理资源自动处理
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
开启后系统将自动处理待处理的资源无需手动操作
</p>
</div>
<div class="ml-4">
<label class="relative inline-flex items-center cursor-pointer">
<input
v-model="config.autoProcessReadyResources"
type="checkbox"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
<!-- 自动转存 -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex-1">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
自动转存
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
开启后系统将自动转存资源到其他网盘平台
</p>
</div>
<div class="ml-4">
<label class="relative inline-flex items-center cursor-pointer">
<input
v-model="config.autoTransferEnabled"
type="checkbox"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
<!-- 自动转存配置仅在开启时显示 -->
<div v-if="config.autoTransferEnabled" class="ml-6 space-y-4">
<!-- 自动转存限制天数 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
自动转存限制n天内资源
</label>
<input
v-model.number="config.autoTransferLimitDays"
type="number"
min="0"
max="365"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="30"
/>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
只转存指定天数内的资源0表示不限制时间
</p>
</div>
<!-- 最小存储空间 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
最小存储空间GB
</label>
<input
v-model.number="config.autoTransferMinSpace"
type="number"
min="100"
max="1024"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="500"
/>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
当网盘剩余空间小于此值时停止自动转存100-1024GB
</p>
</div>
</div>
<!-- 自动拉取热播剧 -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex-1">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
自动拉取热播剧
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
开启后系统将自动从豆瓣获取热播剧信息
</p>
</div>
<div class="ml-4">
<label class="relative inline-flex items-center cursor-pointer">
<input
v-model="config.autoFetchHotDramaEnabled"
type="checkbox"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
<!-- 自动处理间隔 -->
<div v-if="config.autoProcessReadyResources" class="ml-6">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
自动处理间隔 (分钟)
</label>
<input
v-model.number="config.autoProcessInterval"
type="number"
min="1"
max="1440"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="30"
/>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
建议设置 5-60 分钟避免过于频繁的处理
</p>
</div>
</div>
</div>
<!-- 其他配置 -->
<div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<i class="fas fa-info-circle text-purple-600"></i>
其他配置
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 每页显示数量 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
每页显示数量
</label>
<select
v-model.number="config.pageSize"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="20">20 </option>
<option value="50">50 </option>
<option value="100">100 </option>
<option value="200">200 </option>
</select>
</div>
<!-- 系统维护模式 -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex-1">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
维护模式
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
开启后普通用户无法访问系统
</p>
</div>
<div class="ml-4">
<label class="relative inline-flex items-center cursor-pointer">
<input
v-model="config.maintenanceMode"
type="checkbox"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-red-300 dark:peer-focus:ring-red-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-red-600"></div>
</label>
</div>
</div>
</div>
</div>
<!-- API配置 -->
<div class="border-b border-gray-200 dark:border-gray-700 pb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<i class="fas fa-key text-orange-600"></i>
API 配置
</h2>
<div class="space-y-4">
<!-- API Token -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
公开API访问令牌
</label>
<div class="flex gap-2">
<input
v-model="config.apiToken"
type="text"
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="输入API Token用于公开API访问认证"
/>
<button
type="button"
@click="generateApiToken"
class="px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors"
>
生成
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
用于公开API的访问认证建议使用随机字符串
</p>
</div>
<!-- API使用说明 -->
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200 mb-2">
<i class="fas fa-info-circle mr-1"></i>
API使用说明
</h3>
<div class="text-xs text-blue-700 dark:text-blue-300 space-y-1">
<p> 单个添加资源: POST /api/public/resources/add</p>
<p> 批量添加资源: POST /api/public/resources/batch-add</p>
<p> 资源搜索: GET /api/public/resources/search</p>
<p> 热门剧: GET /api/public/hot-dramas</p>
<p> 认证方式: 在请求头中添加 X-API-Token 或在查询参数中添加 api_token</p>
<p> Swagger文档: <a href="/swagger/index.html" target="_blank" class="underline">查看完整API文档</a></p>
</div>
</div>
</div>
</div>
<!-- 保存按钮 -->
<div class="flex justify-end space-x-4 pt-6">
<button
type="button"
@click="resetForm"
class="px-6 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
重置
</button>
<button
type="submit"
:disabled="saving"
class="px-6 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<i v-if="saving" class="fas fa-spinner fa-spin mr-2"></i>
{{ saving ? '保存中...' : '保存配置' }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script setup>
// 设置页面布局
definePageMeta({
layout: 'admin'
})
import { ref, onMounted } from 'vue'
// API
const { getSystemConfig, updateSystemConfig } = useSystemConfigApi()
// 响应式数据
const loading = ref(false)
const config = ref({
// SEO 配置
siteTitle: '网盘资源数据库',
siteDescription: '专业的网盘资源数据库',
keywords: '网盘,资源管理,文件分享',
author: '系统管理员',
copyright: '© 2024 网盘资源数据库',
// 自动处理配置
autoProcessReadyResources: false,
autoProcessInterval: 30,
autoTransferEnabled: false, // 新增
autoTransferLimitDays: 30, // 新增:自动转存限制天数
autoTransferMinSpace: 500, // 新增最小存储空间GB
autoFetchHotDramaEnabled: false, // 新增
// 其他配置
pageSize: 100,
maintenanceMode: false,
apiToken: '' // 新增
})
// 系统配置状态用于SEO
const systemConfig = ref(null)
// 页面元数据 - 移到变量声明之后
useHead({
title: () => systemConfig.value?.site_title ? `${systemConfig.value.site_title} - 系统配置` : '系统配置 - 网盘资源数据库',
meta: [
{
name: 'description',
content: () => systemConfig.value?.site_description || '系统配置管理页面'
},
{
name: 'keywords',
content: () => systemConfig.value?.keywords || '系统配置,管理'
},
{
name: 'author',
content: () => systemConfig.value?.author || '系统管理员'
}
]
})
// 加载配置
const loadConfig = async () => {
try {
loading.value = true
const response = await getSystemConfig()
console.log('系统配置响应:', response)
// 使用新的统一响应格式直接使用response
if (response) {
config.value = {
siteTitle: response.site_title || '网盘资源数据库',
siteDescription: response.site_description || '专业的网盘资源数据库',
keywords: response.keywords || '网盘,资源管理,文件分享',
author: response.author || '系统管理员',
copyright: response.copyright || '© 2024 网盘资源数据库',
autoProcessReadyResources: response.auto_process_ready_resources || false,
autoProcessInterval: response.auto_process_interval || 30,
autoTransferEnabled: response.auto_transfer_enabled || false, // 新增
autoTransferLimitDays: response.auto_transfer_limit_days || 30, // 新增:自动转存限制天数
autoTransferMinSpace: response.auto_transfer_min_space || 500, // 新增最小存储空间GB
autoFetchHotDramaEnabled: response.auto_fetch_hot_drama_enabled || false, // 新增
pageSize: response.page_size || 100,
maintenanceMode: response.maintenance_mode || false,
apiToken: response.api_token || '' // 加载API Token
}
systemConfig.value = response // 更新系统配置状态
}
} catch (error) {
console.error('加载配置失败:', error)
// 显示错误提示
} finally {
loading.value = false
}
}
// 保存配置
const saveConfig = async () => {
try {
loading.value = true
const requestData = {
site_title: config.value.siteTitle,
site_description: config.value.siteDescription,
keywords: config.value.keywords,
author: config.value.author,
copyright: config.value.copyright,
auto_process_ready_resources: config.value.autoProcessReadyResources,
auto_process_interval: config.value.autoProcessInterval,
auto_transfer_enabled: config.value.autoTransferEnabled, // 新增
auto_transfer_limit_days: config.value.autoTransferLimitDays, // 新增:自动转存限制天数
auto_transfer_min_space: config.value.autoTransferMinSpace, // 新增最小存储空间GB
auto_fetch_hot_drama_enabled: config.value.autoFetchHotDramaEnabled, // 新增
page_size: config.value.pageSize,
maintenance_mode: config.value.maintenanceMode,
api_token: config.value.apiToken // 保存API Token
}
const response = await updateSystemConfig(requestData)
// 使用新的统一响应格式直接检查response是否存在
if (response) {
alert('配置保存成功!')
// 重新加载配置以获取最新数据
await loadConfig()
} else {
alert('保存配置失败:未知错误')
}
} catch (error) {
console.error('保存配置失败:', error)
alert('保存配置失败:' + (error.message || '未知错误'))
} finally {
loading.value = false
}
}
// 重置表单
const resetForm = () => {
if (confirm('确定要重置所有配置吗?')) {
loadConfig()
}
}
// 生成API Token
const generateApiToken = () => {
const newToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
config.value.apiToken = newToken;
alert('新API Token已生成: ' + newToken);
};
// 页面加载时获取配置
onMounted(() => {
loadConfig()
})
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100">
<!-- 全局加载状态 -->
<div v-if="pageLoading" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
@@ -13,207 +13,182 @@
</div>
</div>
<div class="max-w-7xl mx-auto">
<!-- 头部 -->
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center flex items-center">
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4">
<NuxtLink
to="/admin"
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-arrow-left"></i> 返回
</NuxtLink>
</nav>
<div class="flex-1">
<h1 class="text-2xl sm:text-3xl font-bold">
<NuxtLink to="/admin" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">标签管理</NuxtLink>
</h1>
</div>
</div>
<div class="">
<div class="max-w-7xl mx-auto">
<!-- 操作按钮 -->
<!-- 操作按钮 -->
<div class="flex justify-between items-center mb-4">
<div class="flex gap-2">
<button
@click="showAddModal = true"
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
class="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-white text-sm flex items-center gap-2"
>
<i class="fas fa-plus"></i> 添加标签
</button>
</div>
<div class="flex gap-2">
<!-- 分类筛选 -->
<select
v-model="selectedCategory"
@change="onCategoryChange"
class="px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:bg-gray-900 dark:text-gray-100 text-sm"
>
<option value="">全部分类</option>
<option v-for="category in categories" :key="category.id" :value="category.id">
{{ category.name }}
</option>
</select>
<div class="relative">
<input
v-model="searchQuery"
@keyup="debounceSearch"
type="text"
class="w-64 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 transition-all text-sm"
placeholder="搜索标签名称..."
/>
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
<i class="fas fa-search text-gray-400 text-sm"></i>
</div>
<input
v-model="searchQuery"
@keyup="debounceSearch"
type="text"
class="w-64 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 transition-all text-sm"
placeholder="搜索标签名称..."
/>
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
<i class="fas fa-search text-gray-400 text-sm"></i>
</div>
</div>
<button
@click="refreshData"
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
>
<i class="fas fa-refresh"></i> 刷新
</button>
</div>
</div>
<!-- 标签列表 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full min-w-full">
<thead>
<tr class="bg-slate-800 dark:bg-gray-700 text-white dark:text-gray-100">
<th class="px-4 py-3 text-left text-sm font-medium">ID</th>
<th class="px-4 py-3 text-left text-sm font-medium">标签名称</th>
<th class="px-4 py-3 text-left text-sm font-medium">分类</th>
<th class="px-4 py-3 text-left text-sm font-medium">描述</th>
<th class="px-4 py-3 text-left text-sm font-medium">资源数量</th>
<th class="px-4 py-3 text-left text-sm font-medium">创建时间</th>
<th class="px-4 py-3 text-left text-sm font-medium">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr v-if="loading" class="text-center py-8">
<td colspan="7" class="text-gray-500 dark:text-gray-400">
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
</td>
</tr>
<tr v-else-if="tags.length === 0" class="text-center py-8">
<td colspan="7" class="text-gray-500 dark:text-gray-400">
<div class="flex flex-col items-center justify-center py-12">
<svg class="w-16 h-16 text-gray-300 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="20" stroke-width="3" stroke-dasharray="6 6" />
<path d="M16 24h16M24 16v16" stroke-width="3" stroke-linecap="round" />
</svg>
<div class="text-lg font-semibold text-gray-400 dark:text-gray-500 mb-2">暂无标签</div>
<div class="text-sm text-gray-400 dark:text-gray-600 mb-4">你可以点击上方"添加标签"按钮创建新标签</div>
<button
@click="showAddModal = true"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
>
<i class="fas fa-plus"></i> 添加标签
</button>
</div>
</td>
</tr>
<tr
v-for="tag in tags"
:key="tag.id"
class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 font-medium">{{ tag.id }}</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
<span :title="tag.name">{{ tag.name }}</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<span v-if="tag.category_name" class="px-2 py-1 bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 rounded-full text-xs">
{{ tag.category_name }}
</span>
<span v-else class="text-gray-400 dark:text-gray-500 italic text-xs">未分类</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<span v-if="tag.description" :title="tag.description">{{ tag.description }}</span>
<span v-else class="text-gray-400 dark:text-gray-500 italic">无描述</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<span class="px-2 py-1 bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 rounded-full text-xs">
{{ tag.resource_count || 0 }}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{{ formatTime(tag.created_at) }}
</td>
<td class="px-4 py-3 text-sm">
<div class="flex items-center gap-2">
<button
@click="editTag(tag)"
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
title="编辑标签"
>
<i class="fas fa-edit"></i>
</button>
<button
@click="deleteTag(tag.id)"
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 transition-colors"
title="删除标签"
>
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 分页 -->
<div v-if="totalPages > 1" class="flex flex-wrap justify-center gap-1 sm:gap-2 mt-6">
<button
@click="refreshData"
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
v-if="currentPage > 1"
@click="goToPage(currentPage - 1)"
class="bg-white text-gray-700 hover:bg-gray-50 px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm flex items-center"
>
<i class="fas fa-refresh"></i> 刷新
<i class="fas fa-chevron-left mr-1"></i> 上一页
</button>
<button
@click="goToPage(1)"
:class="currentPage === 1 ? 'bg-slate-800 text-white' : 'bg-white text-gray-700 hover:bg-gray-50'"
class="px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm"
>
1
</button>
<button
v-if="totalPages > 1"
@click="goToPage(2)"
:class="currentPage === 2 ? 'bg-slate-800 text-white' : 'bg-white text-gray-700 hover:bg-gray-50'"
class="px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm"
>
2
</button>
<span v-if="currentPage > 2" class="px-2 py-1 sm:px-3 sm:py-2 text-gray-500 text-sm">...</span>
<button
v-if="currentPage !== 1 && currentPage !== 2 && currentPage > 2"
class="bg-slate-800 text-white px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm"
>
{{ currentPage }}
</button>
<button
v-if="currentPage < totalPages"
@click="goToPage(currentPage + 1)"
class="bg-white text-gray-700 hover:bg-gray-50 px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm flex items-center"
>
下一页 <i class="fas fa-chevron-right ml-1"></i>
</button>
</div>
</div>
<!-- 标签列表 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full min-w-full">
<thead>
<tr class="bg-slate-800 dark:bg-gray-700 text-white dark:text-gray-100">
<th class="px-4 py-3 text-left text-sm font-medium">ID</th>
<th class="px-4 py-3 text-left text-sm font-medium">标签名称</th>
<th class="px-4 py-3 text-left text-sm font-medium">分类</th>
<th class="px-4 py-3 text-left text-sm font-medium">描述</th>
<th class="px-4 py-3 text-left text-sm font-medium">资源数量</th>
<th class="px-4 py-3 text-left text-sm font-medium">创建时间</th>
<th class="px-4 py-3 text-left text-sm font-medium">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr v-if="loading" class="text-center py-8">
<td colspan="7" class="text-gray-500 dark:text-gray-400">
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
</td>
</tr>
<tr v-else-if="tags.length === 0" class="text-center py-8">
<td colspan="7" class="text-gray-500 dark:text-gray-400">
<div class="flex flex-col items-center justify-center py-12">
<svg class="w-16 h-16 text-gray-300 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="20" stroke-width="3" stroke-dasharray="6 6" />
<path d="M16 24h16M24 16v16" stroke-width="3" stroke-linecap="round" />
</svg>
<div class="text-lg font-semibold text-gray-400 dark:text-gray-500 mb-2">暂无标签</div>
<div class="text-sm text-gray-400 dark:text-gray-600 mb-4">你可以点击上方"添加标签"按钮创建新标签</div>
<button
@click="showAddModal = true"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
>
<i class="fas fa-plus"></i> 添加标签
</button>
</div>
</td>
</tr>
<tr
v-for="tag in tags"
:key="tag.id"
class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 font-medium">{{ tag.id }}</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
<span :title="tag.name">{{ tag.name }}</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<span v-if="tag.category_name" class="px-2 py-1 bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 rounded-full text-xs">
{{ tag.category_name }}
</span>
<span v-else class="text-gray-400 dark:text-gray-500 italic text-xs">未分类</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<span v-if="tag.description" :title="tag.description">{{ tag.description }}</span>
<span v-else class="text-gray-400 dark:text-gray-500 italic">无描述</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<span class="px-2 py-1 bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 rounded-full text-xs">
{{ tag.resource_count || 0 }}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{{ formatTime(tag.created_at) }}
</td>
<td class="px-4 py-3 text-sm">
<div class="flex items-center gap-2">
<button
@click="editTag(tag)"
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
title="编辑标签"
>
<i class="fas fa-edit"></i>
</button>
<button
@click="deleteTag(tag.id)"
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 transition-colors"
title="删除标签"
>
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 分页 -->
<div v-if="totalPages > 1" class="flex flex-wrap justify-center gap-1 sm:gap-2 mt-6">
<button
v-if="currentPage > 1"
@click="goToPage(currentPage - 1)"
class="bg-white text-gray-700 hover:bg-gray-50 px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm flex items-center"
>
<i class="fas fa-chevron-left mr-1"></i> 上一页
</button>
<button
@click="goToPage(1)"
:class="currentPage === 1 ? 'bg-slate-800 text-white' : 'bg-white text-gray-700 hover:bg-gray-50'"
class="px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm"
>
1
</button>
<button
v-if="totalPages > 1"
@click="goToPage(2)"
:class="currentPage === 2 ? 'bg-slate-800 text-white' : 'bg-white text-gray-700 hover:bg-gray-50'"
class="px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm"
>
2
</button>
<span v-if="currentPage > 2" class="px-2 py-1 sm:px-3 sm:py-2 text-gray-500 text-sm">...</span>
<button
v-if="currentPage !== 1 && currentPage !== 2 && currentPage > 2"
class="bg-slate-800 text-white px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm"
>
{{ currentPage }}
</button>
<button
v-if="currentPage < totalPages"
@click="goToPage(currentPage + 1)"
class="bg-white text-gray-700 hover:bg-gray-50 px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm flex items-center"
>
下一页 <i class="fas fa-chevron-right ml-1"></i>
</button>
</div>
<!-- 统计信息 -->
<div v-if="totalPages <= 1" class="mt-4 text-center">
<div class="inline-flex items-center bg-white dark:bg-gray-800 rounded-lg shadow px-6 py-3">
<div class="text-sm text-gray-600 dark:text-gray-400">
<span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个标签
<!-- 统计信息 -->
<div v-if="totalPages <= 1" class="mt-4 text-center">
<div class="inline-flex items-center bg-white dark:bg-gray-800 rounded-lg shadow px-6 py-3">
<div class="text-sm text-gray-600 dark:text-gray-400">
<span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个标签
</div>
</div>
</div>
</div>
@@ -291,6 +266,11 @@
</template>
<script setup lang="ts">
//
definePageMeta({
layout: 'admin'
})
const router = useRouter()
const userStore = useUserStore()
const config = useRuntimeConfig()

384
web/pages/admin/users.vue Normal file
View File

@@ -0,0 +1,384 @@
<template>
<!-- 用户管理内容 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<div class="flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">用户管理</h2>
<div class="flex gap-2">
<button
@click="showCreateModal = true"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
>
添加用户
</button>
</div>
</div>
</div>
<!-- 用户列表 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">用户列表</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户名</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">邮箱</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">角色</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">最后登录</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="user in users" :key="user.id">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.id }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.username }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.email }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="getRoleClass(user.role)" class="px-2 py-1 text-xs font-medium rounded-full">
{{ user.role }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="user.is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
class="px-2 py-1 text-xs font-medium rounded-full">
{{ user.is_active ? '激活' : '禁用' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ user.last_login ? formatDate(user.last_login) : '从未登录' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button @click="editUser(user)" class="text-indigo-600 hover:text-indigo-900 mr-3">编辑</button>
<button @click="showChangePasswordModal(user)" class="text-yellow-600 hover:text-yellow-900 mr-3">修改密码</button>
<button @click="deleteUser(user.id)" class="text-red-600 hover:text-red-900">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 创建/编辑用户模态框 -->
<div v-if="showCreateModal || showEditModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div class="mt-3">
<h3 class="text-lg font-medium text-gray-900 mb-4">
{{ showEditModal ? '编辑用户' : '创建用户' }}
</h3>
<form @submit.prevent="handleSubmit">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">用户名</label>
<input
v-model="form.username"
type="text"
required
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">邮箱</label>
<input
v-model="form.email"
type="email"
required
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div v-if="showCreateModal">
<label class="block text-sm font-medium text-gray-700">密码</label>
<input
v-model="form.password"
type="password"
required
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">角色</label>
<select
v-model="form.role"
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
>
<option value="user">用户</option>
<option value="admin">管理员</option>
</select>
</div>
<div>
<label class="flex items-center">
<input
v-model="form.is_active"
type="checkbox"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
/>
<span class="ml-2 text-sm text-gray-700">激活状态</span>
</label>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button
type="button"
@click="closeModal"
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50"
>
取消
</button>
<button
type="submit"
class="px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700"
>
{{ showEditModal ? '更新' : '创建' }}
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 修改密码模态框 -->
<div v-if="showPasswordModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div class="mt-3">
<h3 class="text-lg font-medium text-gray-900 mb-4">
修改用户密码
</h3>
<p class="text-sm text-gray-600 mb-4">
正在为用户 <strong>{{ changingPasswordUser?.username }}</strong> 修改密码
</p>
<form @submit.prevent="handlePasswordChange">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">新密码</label>
<input
v-model="passwordForm.newPassword"
type="password"
required
minlength="6"
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="请输入新密码至少6位"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">确认新密码</label>
<input
v-model="passwordForm.confirmPassword"
type="password"
required
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="请再次输入新密码"
/>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button
type="button"
@click="closePasswordModal"
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50"
>
取消
</button>
<button
type="submit"
class="px-4 py-2 bg-yellow-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-yellow-700"
>
修改密码
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 设置页面布局
definePageMeta({
layout: 'admin'
})
const router = useRouter()
const userStore = useUserStore()
const users = ref([])
const showCreateModal = ref(false)
const showEditModal = ref(false)
const showPasswordModal = ref(false)
const editingUser = ref(null)
const changingPasswordUser = ref(null)
const form = ref({
username: '',
email: '',
password: '',
role: 'user',
is_active: true
})
const passwordForm = ref({
newPassword: '',
confirmPassword: ''
})
// 检查认证
const checkAuth = () => {
userStore.initAuth()
if (!userStore.isAuthenticated) {
router.push('/login')
return
}
}
// 获取用户列表
const fetchUsers = async () => {
try {
const { useUserApi } = await import('~/composables/useApi')
const userApi = useUserApi()
const response = await userApi.getUsers()
users.value = Array.isArray(response) ? response : (response?.items || [])
} catch (error) {
console.error('获取用户列表失败:', error)
}
}
// 创建用户
const createUser = async () => {
try {
const { useUserApi } = await import('~/composables/useApi')
const userApi = useUserApi()
await userApi.createUser(form.value)
await fetchUsers()
closeModal()
} catch (error) {
console.error('创建用户失败:', error)
}
}
// 更新用户
const updateUser = async () => {
try {
const { useUserApi } = await import('~/composables/useApi')
const userApi = useUserApi()
await userApi.updateUser(editingUser.value.id, form.value)
await fetchUsers()
closeModal()
} catch (error) {
console.error('更新用户失败:', error)
}
}
// 删除用户
const deleteUser = async (id) => {
if (!confirm('确定要删除这个用户吗?')) return
try {
const { useUserApi } = await import('~/composables/useApi')
const userApi = useUserApi()
await userApi.deleteUser(id)
await fetchUsers()
} catch (error) {
console.error('删除用户失败:', error)
}
}
// 显示修改密码模态框
const showChangePasswordModal = (user) => {
changingPasswordUser.value = user
passwordForm.value = {
newPassword: '',
confirmPassword: ''
}
showPasswordModal.value = true
}
// 关闭修改密码模态框
const closePasswordModal = () => {
showPasswordModal.value = false
changingPasswordUser.value = null
passwordForm.value = {
newPassword: '',
confirmPassword: ''
}
}
// 修改密码
const changePassword = async () => {
if (passwordForm.value.newPassword !== passwordForm.value.confirmPassword) {
alert('两次输入的密码不一致')
return
}
if (passwordForm.value.newPassword.length < 6) {
alert('密码长度至少6位')
return
}
try {
const { useUserApi } = await import('~/composables/useApi')
const userApi = useUserApi()
await userApi.changePassword(changingPasswordUser.value.id, passwordForm.value.newPassword)
alert('密码修改成功')
closePasswordModal()
} catch (error) {
console.error('修改密码失败:', error)
alert('修改密码失败: ' + (error.message || '未知错误'))
}
}
// 处理密码修改表单提交
const handlePasswordChange = () => {
changePassword()
}
// 编辑用户
const editUser = (user) => {
editingUser.value = user
form.value = {
username: user.username,
email: user.email,
password: '',
role: user.role,
is_active: user.is_active
}
showEditModal.value = true
}
// 关闭模态框
const closeModal = () => {
showCreateModal.value = false
showEditModal.value = false
editingUser.value = null
form.value = {
username: '',
email: '',
password: '',
role: 'user',
is_active: true
}
}
// 提交表单
const handleSubmit = () => {
if (showEditModal.value) {
updateUser()
} else {
createUser()
}
}
// 获取角色样式
const getRoleClass = (role) => {
return role === 'admin' ? 'bg-red-100 text-red-800' : 'bg-blue-100 text-blue-800'
}
// 格式化日期
const formatDate = (dateString) => {
return new Date(dateString).toLocaleString('zh-CN')
}
onMounted(() => {
checkAuth()
fetchUsers()
})
</script>

182
web/pages/admin/version.vue Normal file
View File

@@ -0,0 +1,182 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<div class="container mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto">
<!-- 页面标题 -->
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
<i class="fas fa-code-branch mr-3 text-blue-500"></i>
版本信息
</h1>
<p class="text-gray-600 dark:text-gray-400">
查看系统版本信息和更新状态
</p>
</div>
<!-- 版本信息组件 -->
<VersionInfo />
<!-- 版本历史 -->
<div class="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-history mr-2 text-green-500"></i>
版本历史
</h3>
<div class="space-y-4">
<div v-for="(version, index) in versionHistory" :key="index"
class="border-l-4 border-blue-500 pl-4 py-2">
<div class="flex items-center justify-between">
<div>
<h4 class="font-medium text-gray-900 dark:text-white">
v{{ version.version }}
</h4>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
{{ version.date }}
</p>
</div>
<span class="px-2 py-1 text-xs rounded-full"
:class="getVersionTypeClass(version.type)">
{{ version.type }}
</span>
</div>
<ul class="mt-2 space-y-1">
<li v-for="(change, changeIndex) in version.changes" :key="changeIndex"
class="text-sm text-gray-600 dark:text-gray-400 flex items-start">
<span class="mr-2 mt-1" :class="getChangeTypeClass(change.type)">
{{ getChangeTypeIcon(change.type) }}
</span>
{{ change.description }}
</li>
</ul>
</div>
</div>
</div>
<!-- 构建信息 -->
<div class="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-cogs mr-2 text-purple-500"></i>
构建信息
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="p-3 bg-gray-50 dark:bg-gray-700 rounded">
<span class="text-sm text-gray-600 dark:text-gray-400">构建环境</span>
<p class="font-mono text-gray-900 dark:text-white">Go 1.23.0</p>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700 rounded">
<span class="text-sm text-gray-600 dark:text-gray-400">前端框架</span>
<p class="font-mono text-gray-900 dark:text-white">Nuxt.js 3.8.0</p>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700 rounded">
<span class="text-sm text-gray-600 dark:text-gray-400">数据库</span>
<p class="font-mono text-gray-900 dark:text-white">PostgreSQL 15+</p>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700 rounded">
<span class="text-sm text-gray-600 dark:text-gray-400">部署方式</span>
<p class="font-mono text-gray-900 dark:text-white">Docker</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 设置页面布局
definePageMeta({
layout: 'admin'
})
// 页面元数据
useHead({
title: '版本信息 - 网盘资源数据库',
meta: [
{ name: 'description', content: '查看系统版本信息和更新状态' }
]
})
interface VersionChange {
type: 'feature' | 'fix' | 'improvement' | 'breaking'
description: string
}
interface VersionHistory {
version: string
date: string
type: 'major' | 'minor' | 'patch'
changes: VersionChange[]
}
const versionHistory: VersionHistory[] = [
{
version: '1.0.0',
date: '2024-01-15',
type: 'major',
changes: [
{ type: 'feature', description: '🎉 首次发布' },
{ type: 'feature', description: '📁 多平台网盘支持' },
{ type: 'feature', description: '🔍 智能搜索功能' },
{ type: 'feature', description: '📊 数据统计和分析' },
{ type: 'feature', description: '🏷️ 标签系统' },
{ type: 'feature', description: '👥 用户权限管理' },
{ type: 'feature', description: '📦 批量资源管理' },
{ type: 'feature', description: '🔄 自动处理功能' },
{ type: 'feature', description: '📈 热播剧管理' },
{ type: 'feature', description: '⚙️ 系统配置管理' },
{ type: 'feature', description: '🔐 JWT认证系统' },
{ type: 'feature', description: '📱 响应式设计' },
{ type: 'feature', description: '🌙 深色模式支持' },
{ type: 'feature', description: '🎨 现代化UI界面' }
]
}
]
// 获取版本类型样式
const getVersionTypeClass = (type: string) => {
switch (type) {
case 'major':
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
case 'minor':
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
case 'patch':
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'
}
}
// 获取变更类型样式
const getChangeTypeClass = (type: string) => {
switch (type) {
case 'feature':
return 'text-green-600 dark:text-green-400'
case 'fix':
return 'text-red-600 dark:text-red-400'
case 'improvement':
return 'text-blue-600 dark:text-blue-400'
case 'breaking':
return 'text-orange-600 dark:text-orange-400'
default:
return 'text-gray-600 dark:text-gray-400'
}
}
// 获取变更类型图标
const getChangeTypeIcon = (type: string) => {
switch (type) {
case 'feature':
return '✨'
case 'fix':
return '🐛'
case 'improvement':
return '🔧'
case 'breaking':
return '💥'
default:
return '<27><>'
}
}
</script>

View File

@@ -347,16 +347,16 @@ curl -X GET "http://localhost:8080/api/public/hot-dramas?page=1&page_size=5" \
</div>
<!-- 页脚 -->
<footer class="mt-auto py-6 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div class="max-w-7xl mx-auto text-center text-gray-600 dark:text-gray-400 text-sm px-3 sm:px-5">
<p class="mb-2">本站内容由网络爬虫自动抓取本站不储存复制传播任何文件仅作个人公益学习请在获取后24小内删除!!!</p>
<p>© 2025 网盘资源数据库 By 老九</p>
</div>
</footer>
<AppFooter />
</div>
</template>
<script setup>
// 设置页面布局
definePageMeta({
layout: 'admin'
})
// 页面元数据
useHead({
title: 'API文档 - 网盘资源数据库',

View File

@@ -1,480 +0,0 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
<!-- 全局加载状态 -->
<div v-if="pageLoading" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<div class="text-center">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">正在加载...</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">请稍候正在加载分类数据</p>
</div>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto">
<!-- 头部 -->
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center flex items-center">
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4">
<NuxtLink
to="/admin"
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-arrow-left"></i> 返回
</NuxtLink>
</nav>
<div class="flex-1">
<h1 class="text-2xl sm:text-3xl font-bold">
<NuxtLink to="/admin" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">分类管理</NuxtLink>
</h1>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex justify-between items-center mb-4">
<div class="flex gap-2">
<button
@click="showAddModal = true"
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-plus"></i> 添加分类
</button>
</div>
<div class="flex gap-2">
<div class="relative">
<input
v-model="searchQuery"
@keyup="debounceSearch"
type="text"
class="w-64 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 transition-all text-sm"
placeholder="搜索分类名称..."
/>
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
<i class="fas fa-search text-gray-400 text-sm"></i>
</div>
</div>
<button
@click="refreshData"
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
>
<i class="fas fa-refresh"></i> 刷新
</button>
</div>
</div>
<!-- 分类列表 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full min-w-full">
<thead>
<tr class="bg-slate-800 dark:bg-gray-700 text-white dark:text-gray-100">
<th class="px-4 py-3 text-left text-sm font-medium">ID</th>
<th class="px-4 py-3 text-left text-sm font-medium">分类名称</th>
<th class="px-4 py-3 text-left text-sm font-medium">描述</th>
<th class="px-4 py-3 text-left text-sm font-medium">资源数量</th>
<th class="px-4 py-3 text-left text-sm font-medium">关联标签</th>
<th class="px-4 py-3 text-left text-sm font-medium">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr v-if="loading" class="text-center py-8">
<td colspan="6" class="text-gray-500 dark:text-gray-400">
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
</td>
</tr>
<tr v-else-if="categories.length === 0" class="text-center py-8">
<td colspan="6" class="text-gray-500 dark:text-gray-400">
<div class="flex flex-col items-center justify-center py-12">
<svg class="w-16 h-16 text-gray-300 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="20" stroke-width="3" stroke-dasharray="6 6" />
<path d="M16 24h16M24 16v16" stroke-width="3" stroke-linecap="round" />
</svg>
<div class="text-lg font-semibold text-gray-400 dark:text-gray-500 mb-2">暂无分类</div>
<div class="text-sm text-gray-400 dark:text-gray-600 mb-4">你可以点击上方"添加分类"按钮创建新分类</div>
<button
@click="showAddModal = true"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
>
<i class="fas fa-plus"></i> 添加分类
</button>
</div>
</td>
</tr>
<tr
v-for="category in categories"
:key="category.id"
class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 font-medium">{{ category.id }}</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
<span :title="category.name">{{ category.name }}</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<span v-if="category.description" :title="category.description">{{ category.description }}</span>
<span v-else class="text-gray-400 dark:text-gray-500 italic">无描述</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<span class="px-2 py-1 bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 rounded-full text-xs">
{{ category.resource_count || 0 }}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<span v-if="category.tag_names && category.tag_names.length > 0" class="text-gray-800 dark:text-gray-200">
{{ category.tag_names.join(', ') }}
</span>
<span v-else class="text-gray-400 dark:text-gray-500 italic text-xs">无标签</span>
</td>
<td class="px-4 py-3 text-sm">
<div class="flex items-center gap-2">
<button
@click="editCategory(category)"
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
title="编辑分类"
>
<i class="fas fa-edit"></i>
</button>
<button
@click="deleteCategory(category.id)"
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 transition-colors"
title="删除分类"
>
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 分页 -->
<div v-if="totalPages > 1" class="flex flex-wrap justify-center gap-1 sm:gap-2 mt-6">
<button
v-if="currentPage > 1"
@click="goToPage(currentPage - 1)"
class="bg-white text-gray-700 hover:bg-gray-50 px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm flex items-center"
>
<i class="fas fa-chevron-left mr-1"></i> 上一页
</button>
<button
@click="goToPage(1)"
:class="currentPage === 1 ? 'bg-slate-800 text-white' : 'bg-white text-gray-700 hover:bg-gray-50'"
class="px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm"
>
1
</button>
<button
v-if="totalPages > 1"
@click="goToPage(2)"
:class="currentPage === 2 ? 'bg-slate-800 text-white' : 'bg-white text-gray-700 hover:bg-gray-50'"
class="px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm"
>
2
</button>
<span v-if="currentPage > 2" class="px-2 py-1 sm:px-3 sm:py-2 text-gray-500 text-sm">...</span>
<button
v-if="currentPage !== 1 && currentPage !== 2 && currentPage > 2"
class="bg-slate-800 text-white px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm"
>
{{ currentPage }}
</button>
<button
v-if="currentPage < totalPages"
@click="goToPage(currentPage + 1)"
class="bg-white text-gray-700 hover:bg-gray-50 px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm flex items-center"
>
下一页 <i class="fas fa-chevron-right ml-1"></i>
</button>
</div>
<!-- 统计信息 -->
<div v-if="totalPages <= 1" class="mt-4 text-center">
<div class="inline-flex items-center bg-white dark:bg-gray-800 rounded-lg shadow px-6 py-3">
<div class="text-sm text-gray-600 dark:text-gray-400">
<span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个分类
</div>
</div>
</div>
</div>
<!-- 添加/编辑分类模态框 -->
<div v-if="showAddModal" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
{{ editingCategory ? '编辑分类' : '添加分类' }}
</h3>
<button @click="closeModal" class="text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
</div>
<form @submit.prevent="handleSubmit">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">分类名称</label>
<input
v-model="formData.name"
type="text"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-500"
placeholder="请输入分类名称"
/>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">描述</label>
<textarea
v-model="formData.description"
rows="3"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-500"
placeholder="请输入分类描述(可选)"
></textarea>
</div>
<div class="flex justify-end gap-3">
<button
type="button"
@click="closeModal"
class="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-600 rounded-md hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors"
>
取消
</button>
<button
type="submit"
:disabled="submitting"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{{ submitting ? '提交中...' : (editingCategory ? '更新' : '添加') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const router = useRouter()
const userStore = useUserStore()
const config = useRuntimeConfig()
// 页面状态
const pageLoading = ref(true)
const loading = ref(false)
const categories = ref([])
// 分页状态
const currentPage = ref(1)
const pageSize = ref(20)
const totalCount = ref(0)
const totalPages = ref(0)
// 搜索状态
const searchQuery = ref('')
let searchTimeout: NodeJS.Timeout | null = null
// 模态框状态
const showAddModal = ref(false)
const submitting = ref(false)
const editingCategory = ref(null)
// 表单数据
const formData = ref({
name: '',
description: ''
})
// 获取认证头
const getAuthHeaders = () => {
return userStore.authHeaders
}
// 页面元数据
useHead({
title: '分类管理 - 网盘资源数据库',
meta: [
{ name: 'description', content: '管理网盘资源分类' },
{ name: 'keywords', content: '分类管理,资源管理' }
]
})
// 检查认证状态
const checkAuth = () => {
userStore.initAuth()
if (!userStore.isAuthenticated) {
router.push('/')
return
}
}
// 获取分类列表
const fetchCategories = async () => {
try {
loading.value = true
const params = {
page: currentPage.value,
page_size: pageSize.value,
search: searchQuery.value
}
const response = await $fetch('/categories', {
baseURL: config.public.apiBase,
params
})
// 解析响应
if (response && typeof response === 'object' && 'code' in response && response.code === 200) {
categories.value = response.data.items || []
totalCount.value = response.data.total || 0
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
} else {
categories.value = response.items || []
totalCount.value = response.total || 0
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
}
} catch (error) {
console.error('获取分类列表失败:', error)
} finally {
loading.value = false
}
}
// 搜索防抖
const debounceSearch = () => {
if (searchTimeout) {
clearTimeout(searchTimeout)
}
searchTimeout = setTimeout(() => {
currentPage.value = 1
fetchCategories()
}, 300)
}
// 刷新数据
const refreshData = () => {
fetchCategories()
}
// 分页跳转
const goToPage = (page: number) => {
currentPage.value = page
fetchCategories()
}
// 编辑分类
const editCategory = (category: any) => {
editingCategory.value = category
formData.value = {
name: category.name,
description: category.description || ''
}
showAddModal.value = true
}
// 删除分类
const deleteCategory = async (categoryId: number) => {
if (!confirm(`确定要删除分类吗?`)) {
return
}
try {
await $fetch(`/categories/${categoryId}`, {
baseURL: config.public.apiBase,
method: 'DELETE',
headers: getAuthHeaders()
})
await fetchCategories()
} catch (error) {
console.error('删除分类失败:', error)
}
}
// 提交表单
const handleSubmit = async () => {
try {
submitting.value = true
if (editingCategory.value) {
await $fetch(`/categories/${editingCategory.value.id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: formData.value,
headers: getAuthHeaders()
})
} else {
await $fetch('/categories', {
baseURL: config.public.apiBase,
method: 'POST',
body: formData.value,
headers: getAuthHeaders()
})
}
closeModal()
await fetchCategories()
} catch (error) {
console.error('提交分类失败:', error)
} finally {
submitting.value = false
}
}
// 关闭模态框
const closeModal = () => {
showAddModal.value = false
editingCategory.value = null
formData.value = {
name: '',
description: ''
}
}
// 格式化时间
const formatTime = (timestamp: string) => {
if (!timestamp) return '-'
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
// 退出登录
const handleLogout = () => {
userStore.logout()
navigateTo('/login')
}
// 页面加载
onMounted(async () => {
try {
checkAuth()
await fetchCategories()
// 检查URL参数如果action=add则自动打开新增弹窗
const route = useRoute()
if (route.query.action === 'add') {
showAddModal.value = true
}
} catch (error) {
console.error('分类管理页面初始化失败:', error)
} finally {
pageLoading.value = false
}
})
</script>
<style scoped>
/* 自定义样式 */
</style>

View File

@@ -198,16 +198,16 @@
</div>
<!-- 页脚 -->
<footer class="mt-auto py-6 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div class="max-w-7xl mx-auto text-center text-gray-600 dark:text-gray-400 text-sm px-3 sm:px-5">
<p class="mb-2">本站内容由网络爬虫自动抓取本站不储存复制传播任何文件仅作个人公益学习请在获取后24小内删除!!!</p>
<p>© 2025 网盘资源数据库 By 老九</p>
</div>
</footer>
<AppFooter />
</div>
</template>
<script setup>
// 设置页面布局
definePageMeta({
layout: 'admin'
})
import { ref, computed, onMounted, watch } from 'vue'
// 响应式数据

View File

@@ -23,6 +23,7 @@
{{ systemConfig?.site_title || '网盘资源数据库' }}
</a>
</h1>
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-2 right-4 top-0 absolute">
<NuxtLink to="/hot-dramas" class="hidden sm:flex">
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
@@ -232,12 +233,7 @@
/>
<!-- 页脚 -->
<footer class="mt-auto py-6 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div class="max-w-7xl mx-auto text-center text-gray-600 dark:text-gray-400 text-sm px-3 sm:px-5">
<p class="mb-2">本站内容由网络爬虫自动抓取本站不储存复制传播任何文件仅作个人公益学习请在获取后24小内删除!!!</p>
<p>{{ systemConfig?.copyright || '© 2025 网盘资源数据库 By 老九' }}</p>
</div>
</footer>
<AppFooter />
</div>
</template>
@@ -254,6 +250,8 @@ useHead({
// 获取运行时配置
const config = useRuntimeConfig()
// 获取路由参数
const route = useRoute()
const router = useRouter()

View File

@@ -234,16 +234,16 @@
</div>
<!-- 页脚 -->
<footer class="mt-auto py-6 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div class="max-w-7xl mx-auto text-center text-gray-600 dark:text-gray-400 text-sm px-3 sm:px-5">
<p class="mb-2">本站内容由网络爬虫自动抓取本站不储存复制传播任何文件仅作个人公益学习请在获取后24小内删除!!!</p>
<p>© 2025 网盘资源数据库 By 老九</p>
</div>
</footer>
<AppFooter />
</div>
</template>
<script setup lang="ts">
// 设置页面布局
definePageMeta({
layout: 'admin'
})
import { ref, onMounted, onUnmounted, computed } from 'vue'
// 响应式数据

View File

@@ -1,500 +0,0 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
<div class="max-w-4xl mx-auto">
<!-- 头部 -->
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center flex items-center">
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4">
<NuxtLink
to="/admin"
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-arrow-left"></i> 返回
</NuxtLink>
</nav>
<div class="flex-1">
<h1 class="text-2xl sm:text-3xl font-bold">
{{ systemConfig?.site_title ? `${systemConfig.site_title} - 系统配置` : '系统配置' }}
</h1>
</div>
</div>
<!-- 配置表单 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<form @submit.prevent="saveConfig" class="space-y-6">
<!-- SEO 配置 -->
<div class="border-b border-gray-200 dark:border-gray-700 pb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<i class="fas fa-search text-blue-600"></i>
SEO 配置
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 网站标题 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
网站标题 *
</label>
<input
v-model="config.siteTitle"
type="text"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="网盘资源数据库"
/>
</div>
<!-- 网站描述 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
网站描述
</label>
<input
v-model="config.siteDescription"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="专业的网盘资源数据库"
/>
</div>
<!-- 关键词 -->
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
关键词 (用逗号分隔)
</label>
<input
v-model="config.keywords"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="网盘,资源管理,文件分享"
/>
</div>
<!-- 作者 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
作者
</label>
<input
v-model="config.author"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="系统管理员"
/>
</div>
<!-- 版权信息 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
版权信息
</label>
<input
v-model="config.copyright"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="© 2024 网盘资源数据库"
/>
</div>
</div>
</div>
<!-- 自动处理配置 -->
<div class="border-b border-gray-200 dark:border-gray-700 pb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<i class="fas fa-cogs text-green-600"></i>
自动处理配置
</h2>
<div class="space-y-4">
<!-- 待处理资源自动处理 -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex-1">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
待处理资源自动处理
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
开启后系统将自动处理待处理的资源无需手动操作
</p>
</div>
<div class="ml-4">
<label class="relative inline-flex items-center cursor-pointer">
<input
v-model="config.autoProcessReadyResources"
type="checkbox"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
<!-- 自动转存 -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex-1">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
自动转存
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
开启后系统将自动转存资源到其他网盘平台
</p>
</div>
<div class="ml-4">
<label class="relative inline-flex items-center cursor-pointer">
<input
v-model="config.autoTransferEnabled"
type="checkbox"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
<!-- 自动转存配置仅在开启时显示 -->
<div v-if="config.autoTransferEnabled" class="ml-6 space-y-4">
<!-- 自动转存限制天数 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
自动转存限制n天内资源
</label>
<input
v-model.number="config.autoTransferLimitDays"
type="number"
min="0"
max="365"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="30"
/>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
只转存指定天数内的资源0表示不限制时间
</p>
</div>
<!-- 最小存储空间 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
最小存储空间GB
</label>
<input
v-model.number="config.autoTransferMinSpace"
type="number"
min="100"
max="1024"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="500"
/>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
当网盘剩余空间小于此值时停止自动转存100-1024GB
</p>
</div>
</div>
<!-- 自动拉取热播剧 -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex-1">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
自动拉取热播剧
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
开启后系统将自动从豆瓣获取热播剧信息
</p>
</div>
<div class="ml-4">
<label class="relative inline-flex items-center cursor-pointer">
<input
v-model="config.autoFetchHotDramaEnabled"
type="checkbox"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
<!-- 自动处理间隔 -->
<div v-if="config.autoProcessReadyResources" class="ml-6">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
自动处理间隔 (分钟)
</label>
<input
v-model.number="config.autoProcessInterval"
type="number"
min="1"
max="1440"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="30"
/>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
建议设置 5-60 分钟避免过于频繁的处理
</p>
</div>
</div>
</div>
<!-- 其他配置 -->
<div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<i class="fas fa-info-circle text-purple-600"></i>
其他配置
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 每页显示数量 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
每页显示数量
</label>
<select
v-model.number="config.pageSize"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="20">20 </option>
<option value="50">50 </option>
<option value="100">100 </option>
<option value="200">200 </option>
</select>
</div>
<!-- 系统维护模式 -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex-1">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
维护模式
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
开启后普通用户无法访问系统
</p>
</div>
<div class="ml-4">
<label class="relative inline-flex items-center cursor-pointer">
<input
v-model="config.maintenanceMode"
type="checkbox"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-red-300 dark:peer-focus:ring-red-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-red-600"></div>
</label>
</div>
</div>
</div>
</div>
<!-- API配置 -->
<div class="border-b border-gray-200 dark:border-gray-700 pb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<i class="fas fa-key text-orange-600"></i>
API 配置
</h2>
<div class="space-y-4">
<!-- API Token -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
公开API访问令牌
</label>
<div class="flex gap-2">
<input
v-model="config.apiToken"
type="text"
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="输入API Token用于公开API访问认证"
/>
<button
type="button"
@click="generateApiToken"
class="px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors"
>
生成
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
用于公开API的访问认证建议使用随机字符串
</p>
</div>
<!-- API使用说明 -->
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200 mb-2">
<i class="fas fa-info-circle mr-1"></i>
API使用说明
</h3>
<div class="text-xs text-blue-700 dark:text-blue-300 space-y-1">
<p> 单个添加资源: POST /api/public/resources/add</p>
<p> 批量添加资源: POST /api/public/resources/batch-add</p>
<p> 资源搜索: GET /api/public/resources/search</p>
<p> 热门剧: GET /api/public/hot-dramas</p>
<p> 认证方式: 在请求头中添加 X-API-Token 或在查询参数中添加 api_token</p>
<p> Swagger文档: <a href="/swagger/index.html" target="_blank" class="underline">查看完整API文档</a></p>
</div>
</div>
</div>
</div>
<!-- 保存按钮 -->
<div class="flex justify-end space-x-4 pt-6">
<button
type="button"
@click="resetForm"
class="px-6 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
重置
</button>
<button
type="submit"
:disabled="loading"
class="px-6 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<i v-if="loading" class="fas fa-spinner fa-spin mr-2"></i>
{{ loading ? '保存中...' : '保存配置' }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// API
const { getSystemConfig, updateSystemConfig } = useSystemConfigApi()
// 响应式数据
const loading = ref(false)
const config = ref({
// SEO 配置
siteTitle: '网盘资源数据库',
siteDescription: '专业的网盘资源数据库',
keywords: '网盘,资源管理,文件分享',
author: '系统管理员',
copyright: '© 2024 网盘资源数据库',
// 自动处理配置
autoProcessReadyResources: false,
autoProcessInterval: 30,
autoTransferEnabled: false, // 新增
autoTransferLimitDays: 30, // 新增:自动转存限制天数
autoTransferMinSpace: 500, // 新增最小存储空间GB
autoFetchHotDramaEnabled: false, // 新增
// 其他配置
pageSize: 100,
maintenanceMode: false,
apiToken: '' // 新增
})
// 系统配置状态用于SEO
const systemConfig = ref(null)
// 页面元数据 - 移到变量声明之后
useHead({
title: () => systemConfig.value?.site_title ? `${systemConfig.value.site_title} - 系统配置` : '系统配置 - 网盘资源数据库',
meta: [
{
name: 'description',
content: () => systemConfig.value?.site_description || '系统配置管理页面'
},
{
name: 'keywords',
content: () => systemConfig.value?.keywords || '系统配置,管理'
},
{
name: 'author',
content: () => systemConfig.value?.author || '系统管理员'
}
]
})
// 加载配置
const loadConfig = async () => {
try {
loading.value = true
const response = await getSystemConfig()
console.log('系统配置响应:', response)
// 使用新的统一响应格式直接使用response
if (response) {
config.value = {
siteTitle: response.site_title || '网盘资源数据库',
siteDescription: response.site_description || '专业的网盘资源数据库',
keywords: response.keywords || '网盘,资源管理,文件分享',
author: response.author || '系统管理员',
copyright: response.copyright || '© 2024 网盘资源数据库',
autoProcessReadyResources: response.auto_process_ready_resources || false,
autoProcessInterval: response.auto_process_interval || 30,
autoTransferEnabled: response.auto_transfer_enabled || false, // 新增
autoTransferLimitDays: response.auto_transfer_limit_days || 30, // 新增:自动转存限制天数
autoTransferMinSpace: response.auto_transfer_min_space || 500, // 新增最小存储空间GB
autoFetchHotDramaEnabled: response.auto_fetch_hot_drama_enabled || false, // 新增
pageSize: response.page_size || 100,
maintenanceMode: response.maintenance_mode || false,
apiToken: response.api_token || '' // 加载API Token
}
systemConfig.value = response // 更新系统配置状态
}
} catch (error) {
console.error('加载配置失败:', error)
// 显示错误提示
} finally {
loading.value = false
}
}
// 保存配置
const saveConfig = async () => {
try {
loading.value = true
const requestData = {
site_title: config.value.siteTitle,
site_description: config.value.siteDescription,
keywords: config.value.keywords,
author: config.value.author,
copyright: config.value.copyright,
auto_process_ready_resources: config.value.autoProcessReadyResources,
auto_process_interval: config.value.autoProcessInterval,
auto_transfer_enabled: config.value.autoTransferEnabled, // 新增
auto_transfer_limit_days: config.value.autoTransferLimitDays, // 新增:自动转存限制天数
auto_transfer_min_space: config.value.autoTransferMinSpace, // 新增最小存储空间GB
auto_fetch_hot_drama_enabled: config.value.autoFetchHotDramaEnabled, // 新增
page_size: config.value.pageSize,
maintenance_mode: config.value.maintenanceMode,
api_token: config.value.apiToken // 保存API Token
}
const response = await updateSystemConfig(requestData)
// 使用新的统一响应格式直接检查response是否存在
if (response) {
alert('配置保存成功!')
// 重新加载配置以获取最新数据
await loadConfig()
} else {
alert('保存配置失败:未知错误')
}
} catch (error) {
console.error('保存配置失败:', error)
alert('保存配置失败:' + (error.message || '未知错误'))
} finally {
loading.value = false
}
}
// 重置表单
const resetForm = () => {
if (confirm('确定要重置所有配置吗?')) {
loadConfig()
}
}
// 生成API Token
const generateApiToken = () => {
const newToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
config.value.apiToken = newToken;
alert('新API Token已生成: ' + newToken);
};
// 页面加载时获取配置
onMounted(() => {
loadConfig()
})
</script>

View File

@@ -1,389 +0,0 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
<div class="max-w-7xl mx-auto">
<!-- 头部 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold text-gray-900">用户管理</h1>
<div class="flex gap-2">
<NuxtLink
to="/admin"
class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
>
返回管理
</NuxtLink>
<button
@click="showCreateModal = true"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
添加用户
</button>
</div>
</div>
</div>
<!-- 用户列表 -->
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">用户列表</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户名</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">邮箱</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">角色</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">最后登录</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="user in users" :key="user.id">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.id }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.username }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.email }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="getRoleClass(user.role)" class="px-2 py-1 text-xs font-medium rounded-full">
{{ user.role }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="user.is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
class="px-2 py-1 text-xs font-medium rounded-full">
{{ user.is_active ? '激活' : '禁用' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ user.last_login ? formatDate(user.last_login) : '从未登录' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button @click="editUser(user)" class="text-indigo-600 hover:text-indigo-900 mr-3">编辑</button>
<button @click="showChangePasswordModal(user)" class="text-yellow-600 hover:text-yellow-900 mr-3">修改密码</button>
<button @click="deleteUser(user.id)" class="text-red-600 hover:text-red-900">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 创建/编辑用户模态框 -->
<div v-if="showCreateModal || showEditModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div class="mt-3">
<h3 class="text-lg font-medium text-gray-900 mb-4">
{{ showEditModal ? '编辑用户' : '创建用户' }}
</h3>
<form @submit.prevent="handleSubmit">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">用户名</label>
<input
v-model="form.username"
type="text"
required
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">邮箱</label>
<input
v-model="form.email"
type="email"
required
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div v-if="showCreateModal">
<label class="block text-sm font-medium text-gray-700">密码</label>
<input
v-model="form.password"
type="password"
required
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">角色</label>
<select
v-model="form.role"
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
>
<option value="user">用户</option>
<option value="admin">管理员</option>
</select>
</div>
<div>
<label class="flex items-center">
<input
v-model="form.is_active"
type="checkbox"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
/>
<span class="ml-2 text-sm text-gray-700">激活状态</span>
</label>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button
type="button"
@click="closeModal"
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50"
>
取消
</button>
<button
type="submit"
class="px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700"
>
{{ showEditModal ? '更新' : '创建' }}
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 修改密码模态框 -->
<div v-if="showPasswordModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div class="mt-3">
<h3 class="text-lg font-medium text-gray-900 mb-4">
修改用户密码
</h3>
<p class="text-sm text-gray-600 mb-4">
正在为用户 <strong>{{ changingPasswordUser?.username }}</strong> 修改密码
</p>
<form @submit.prevent="handlePasswordChange">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">新密码</label>
<input
v-model="passwordForm.newPassword"
type="password"
required
minlength="6"
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="请输入新密码至少6位"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">确认新密码</label>
<input
v-model="passwordForm.confirmPassword"
type="password"
required
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="请再次输入新密码"
/>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button
type="button"
@click="closePasswordModal"
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50"
>
取消
</button>
<button
type="submit"
class="px-4 py-2 bg-yellow-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-yellow-700"
>
修改密码
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
const router = useRouter()
const userStore = useUserStore()
const users = ref([])
const showCreateModal = ref(false)
const showEditModal = ref(false)
const showPasswordModal = ref(false)
const editingUser = ref(null)
const changingPasswordUser = ref(null)
const form = ref({
username: '',
email: '',
password: '',
role: 'user',
is_active: true
})
const passwordForm = ref({
newPassword: '',
confirmPassword: ''
})
// 检查认证
const checkAuth = () => {
userStore.initAuth()
if (!userStore.isAuthenticated) {
router.push('/login')
return
}
}
// 获取用户列表
const fetchUsers = async () => {
try {
const { useUserApi } = await import('~/composables/useApi')
const userApi = useUserApi()
const response = await userApi.getUsers()
users.value = Array.isArray(response) ? response : (response?.items || [])
} catch (error) {
console.error('获取用户列表失败:', error)
}
}
// 创建用户
const createUser = async () => {
try {
const { useUserApi } = await import('~/composables/useApi')
const userApi = useUserApi()
await userApi.createUser(form.value)
await fetchUsers()
closeModal()
} catch (error) {
console.error('创建用户失败:', error)
}
}
// 更新用户
const updateUser = async () => {
try {
const { useUserApi } = await import('~/composables/useApi')
const userApi = useUserApi()
await userApi.updateUser(editingUser.value.id, form.value)
await fetchUsers()
closeModal()
} catch (error) {
console.error('更新用户失败:', error)
}
}
// 删除用户
const deleteUser = async (id) => {
if (!confirm('确定要删除这个用户吗?')) return
try {
const { useUserApi } = await import('~/composables/useApi')
const userApi = useUserApi()
await userApi.deleteUser(id)
await fetchUsers()
} catch (error) {
console.error('删除用户失败:', error)
}
}
// 显示修改密码模态框
const showChangePasswordModal = (user) => {
changingPasswordUser.value = user
passwordForm.value = {
newPassword: '',
confirmPassword: ''
}
showPasswordModal.value = true
}
// 关闭修改密码模态框
const closePasswordModal = () => {
showPasswordModal.value = false
changingPasswordUser.value = null
passwordForm.value = {
newPassword: '',
confirmPassword: ''
}
}
// 修改密码
const changePassword = async () => {
if (passwordForm.value.newPassword !== passwordForm.value.confirmPassword) {
alert('两次输入的密码不一致')
return
}
if (passwordForm.value.newPassword.length < 6) {
alert('密码长度至少6位')
return
}
try {
const { useUserApi } = await import('~/composables/useApi')
const userApi = useUserApi()
await userApi.changePassword(changingPasswordUser.value.id, passwordForm.value.newPassword)
alert('密码修改成功')
closePasswordModal()
} catch (error) {
console.error('修改密码失败:', error)
alert('修改密码失败: ' + (error.message || '未知错误'))
}
}
// 处理密码修改表单提交
const handlePasswordChange = () => {
changePassword()
}
// 编辑用户
const editUser = (user) => {
editingUser.value = user
form.value = {
username: user.username,
email: user.email,
password: '',
role: user.role,
is_active: user.is_active
}
showEditModal.value = true
}
// 关闭模态框
const closeModal = () => {
showCreateModal.value = false
showEditModal.value = false
editingUser.value = null
form.value = {
username: '',
email: '',
password: '',
role: 'user',
is_active: true
}
}
// 提交表单
const handleSubmit = () => {
if (showEditModal.value) {
updateUser()
} else {
createUser()
}
}
// 获取角色样式
const getRoleClass = (role) => {
return role === 'admin' ? 'bg-red-100 text-red-800' : 'bg-blue-100 text-blue-800'
}
// 格式化日期
const formatDate = (dateString) => {
return new Date(dateString).toLocaleString('zh-CN')
}
onMounted(() => {
checkAuth()
fetchUsers()
})
</script>