update ui

This commit is contained in:
Kerwin
2025-07-10 13:58:28 +08:00
parent 9728f8fb7a
commit ab1074a2c6
22 changed files with 12304 additions and 708 deletions

211
doc/UI_FRAMEWORKS.md Normal file
View File

@@ -0,0 +1,211 @@
# Vue 3 + Nuxt.js UI框架选择指南
## 🎨 推荐的UI框架
### 1. **Naive UI** ⭐⭐⭐⭐⭐ (强烈推荐)
**特点**: 完整的Vue 3组件库TypeScript支持主题定制
**优势**:
- ✅ 完整的Vue 3支持
- ✅ TypeScript原生支持
- ✅ 组件丰富80+组件)
- ✅ 主题系统强大
- ✅ 文档完善
- ✅ 性能优秀
- ✅ 活跃维护
**适用场景**: 企业级应用,复杂界面,需要高度定制
**安装**:
```bash
npm install naive-ui vfonts @vicons/ionicons5
```
### 2. **Element Plus** ⭐⭐⭐⭐
**特点**: Vue 3版本的Element UI成熟稳定
**优势**:
- ✅ 社区活跃
- ✅ 组件齐全
- ✅ 文档详细
- ✅ 成熟稳定
- ✅ 中文文档
**适用场景**: 后台管理系统,快速开发
**安装**:
```bash
npm install element-plus @element-plus/icons-vue
```
### 3. **Ant Design Vue** ⭐⭐⭐⭐
**特点**: 企业级UI设计语言
**优势**:
- ✅ 设计规范统一
- ✅ 组件丰富
- ✅ 企业级应用
- ✅ 国际化支持
**适用场景**: 企业应用,设计规范要求高
**安装**:
```bash
npm install ant-design-vue @ant-design/icons-vue
```
### 4. **PrimeVue** ⭐⭐⭐
**特点**: 丰富的组件库,支持多种主题
**优势**:
- ✅ 组件数量多
- ✅ 功能强大
- ✅ 主题丰富
- ✅ 响应式设计
**适用场景**: 复杂业务场景
**安装**:
```bash
npm install primevue primeicons
```
### 5. **Vuetify** ⭐⭐⭐
**特点**: Material Design风格
**优势**:
- ✅ 设计美观
- ✅ 响应式好
- ✅ Material Design
- ✅ 组件丰富
**适用场景**: 现代化应用Material Design风格
**安装**:
```bash
npm install vuetify @mdi/font
```
## 🚀 当前项目推荐
### 推荐使用 **Naive UI**
**原因**:
1. **Vue 3原生支持**: 完全基于Vue 3 Composition API
2. **TypeScript友好**: 原生TypeScript支持
3. **组件丰富**: 满足资源管理系统需求
4. **主题系统**: 支持深色/浅色主题切换
5. **性能优秀**: 按需加载,体积小
### 集成步骤
1. **安装依赖**:
```bash
cd web
npm install naive-ui vfonts @vicons/ionicons5 @css-render/vue3-ssr @juggle/resize-observer
```
2. **配置Nuxt**:
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
build: {
transpile: ['naive-ui', 'vueuc', '@css-render/vue3-ssr', '@juggle/resize-observer']
}
})
```
3. **创建插件**:
```typescript
// plugins/naive-ui.client.ts
import { setup } from '@css-render/vue3-ssr'
export default defineNuxtPlugin((nuxtApp) => {
if (process.server) {
const { collect } = setup(nuxtApp.vueApp)
// SSR配置
}
})
```
4. **使用组件**:
```vue
<template>
<n-config-provider :theme="theme">
<n-card>
<n-button type="primary">按钮</n-button>
</n-card>
</n-config-provider>
</template>
```
## 📊 框架对比表
| 特性 | Naive UI | Element Plus | Ant Design Vue | PrimeVue | Vuetify |
|------|----------|--------------|----------------|----------|---------|
| Vue 3支持 | ✅ | ✅ | ✅ | ✅ | ✅ |
| TypeScript | ✅ | ✅ | ✅ | ✅ | ✅ |
| 组件数量 | 80+ | 60+ | 60+ | 90+ | 80+ |
| 主题系统 | ✅ | ✅ | ✅ | ✅ | ✅ |
| 中文文档 | ✅ | ✅ | ✅ | ❌ | ❌ |
| 社区活跃度 | 高 | 很高 | 高 | 中 | 中 |
| 学习曲线 | 低 | 低 | 中 | 中 | 中 |
| 性能 | 优秀 | 良好 | 良好 | 良好 | 良好 |
## 🎯 针对资源管理系统的建议
### 核心组件需求
1. **数据表格**: 资源列表展示
2. **表单组件**: 资源添加/编辑
3. **模态框**: 弹窗操作
4. **搜索组件**: 资源搜索
5. **标签组件**: 资源标签
6. **统计卡片**: 数据展示
7. **分页组件**: 列表分页
### Naive UI优势
- **n-data-table**: 功能强大的数据表格
- **n-form**: 完整的表单解决方案
- **n-modal**: 灵活的模态框
- **n-input**: 搜索输入框
- **n-tag**: 标签组件
- **n-card**: 统计卡片
- **n-pagination**: 分页组件
## 🔧 迁移指南
如果要从当前的基础组件迁移到Naive UI
1. **替换基础组件**:
```vue
<!-- 原版 -->
<button class="btn-primary">按钮</button>
<!-- Naive UI -->
<n-button type="primary">按钮</n-button>
```
2. **替换表单组件**:
```vue
<!-- 原版 -->
<input class="input-field" />
<!-- Naive UI -->
<n-input />
```
3. **替换模态框**:
```vue
<!-- 原版 -->
<div class="modal">...</div>
<!-- Naive UI -->
<n-modal>...</n-modal>
```
## 📝 总结
对于您的资源管理系统项目,我强烈推荐使用 **Naive UI**,因为:
1. **完美适配**: 完全支持Vue 3和Nuxt.js
2. **功能完整**: 提供所有需要的组件
3. **开发效率**: 减少大量自定义样式工作
4. **维护性好**: TypeScript支持代码更可靠
5. **性能优秀**: 按需加载,体积小
使用UI框架可以节省70-80%的前端开发时间,让您专注于业务逻辑而不是样式细节。

12
handlers/base.go Normal file
View File

@@ -0,0 +1,12 @@
package handlers
import (
"res_db/db/repo"
)
var repoManager *repo.RepositoryManager
// SetRepositoryManager 设置Repository管理器
func SetRepositoryManager(rm *repo.RepositoryManager) {
repoManager = rm
}

View File

@@ -1,125 +0,0 @@
package handlers
import (
"net/http"
"res_db/models"
"github.com/gin-gonic/gin"
)
// GetCategories 获取分类列表
func GetCategories(c *gin.Context) {
rows, err := models.DB.Query(`
SELECT id, name, description, created_at, updated_at
FROM categories
ORDER BY name
`)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
var categories []models.Category
for rows.Next() {
var cat models.Category
err := rows.Scan(&cat.ID, &cat.Name, &cat.Description, &cat.CreatedAt, &cat.UpdatedAt)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
categories = append(categories, cat)
}
c.JSON(http.StatusOK, categories)
}
// CreateCategory 创建分类
func CreateCategory(c *gin.Context) {
var req models.CreateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
query := `
INSERT INTO categories (name, description)
VALUES ($1, $2)
RETURNING id
`
var id int
err := models.DB.QueryRow(query, req.Name, req.Description).Scan(&id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"id": id, "message": "分类创建成功"})
}
// UpdateCategory 更新分类
func UpdateCategory(c *gin.Context) {
id := c.Param("id")
var req models.UpdateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
query := `
UPDATE categories
SET name = COALESCE($1, name),
description = COALESCE($2, description),
updated_at = CURRENT_TIMESTAMP
WHERE id = $3
`
result, err := models.DB.Exec(query, req.Name, req.Description, id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "分类更新成功"})
}
// DeleteCategory 删除分类
func DeleteCategory(c *gin.Context) {
id := c.Param("id")
// 检查是否有资源使用此分类
var count int
err := models.DB.QueryRow("SELECT COUNT(*) FROM resources WHERE category_id = $1", id).Scan(&count)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "该分类下还有资源,无法删除"})
return
}
result, err := models.DB.Exec("DELETE FROM categories WHERE id = $1", id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "分类删除成功"})
}

View File

@@ -0,0 +1,114 @@
package handlers
import (
"net/http"
"strconv"
"res_db/db/converter"
"res_db/db/dto"
"res_db/db/entity"
"github.com/gin-gonic/gin"
)
// GetCategories 获取分类列表
func GetCategories(c *gin.Context) {
categories, err := repoManager.CategoryRepository.FindAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 获取每个分类的资源数量
resourceCounts := make(map[uint]int64)
for _, category := range categories {
count, err := repoManager.CategoryRepository.GetResourceCount(category.ID)
if err != nil {
continue
}
resourceCounts[category.ID] = count
}
responses := converter.ToCategoryResponseList(categories, resourceCounts)
c.JSON(http.StatusOK, responses)
}
// CreateCategory 创建分类
func CreateCategory(c *gin.Context) {
var req dto.CreateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
category := &entity.Category{
Name: req.Name,
Description: req.Description,
}
err := repoManager.CategoryRepository.Create(category)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": category.ID,
"message": "分类创建成功",
})
}
// UpdateCategory 更新分类
func UpdateCategory(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
var req dto.UpdateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
category, err := repoManager.CategoryRepository.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"})
return
}
if req.Name != "" {
category.Name = req.Name
}
if req.Description != "" {
category.Description = req.Description
}
err = repoManager.CategoryRepository.Update(category)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "分类更新成功"})
}
// DeleteCategory 删除分类
func DeleteCategory(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
err = repoManager.CategoryRepository.Delete(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "分类删除成功"})
}

133
handlers/cks_handler.go Normal file
View File

@@ -0,0 +1,133 @@
package handlers
import (
"net/http"
"strconv"
"res_db/db/converter"
"res_db/db/dto"
"res_db/db/entity"
"github.com/gin-gonic/gin"
)
// GetCks 获取Cookie列表
func GetCks(c *gin.Context) {
cks, err := repoManager.CksRepository.FindAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
responses := converter.ToCksResponseList(cks)
c.JSON(http.StatusOK, responses)
}
// CreateCks 创建Cookie
func CreateCks(c *gin.Context) {
var req dto.CreateCksRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cks := &entity.Cks{
PanID: req.PanID,
T: req.T,
Idx: req.Idx,
Ck: req.Ck,
Remark: req.Remark,
}
err := repoManager.CksRepository.Create(cks)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": cks.ID,
"message": "Cookie创建成功",
})
}
// UpdateCks 更新Cookie
func UpdateCks(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
var req dto.UpdateCksRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cks, err := repoManager.CksRepository.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Cookie不存在"})
return
}
if req.PanID != 0 {
cks.PanID = req.PanID
}
if req.T != "" {
cks.T = req.T
}
cks.Idx = req.Idx
if req.Ck != "" {
cks.Ck = req.Ck
}
if req.Remark != "" {
cks.Remark = req.Remark
}
err = repoManager.CksRepository.Update(cks)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Cookie更新成功"})
}
// DeleteCks 删除Cookie
func DeleteCks(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
err = repoManager.CksRepository.Delete(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Cookie删除成功"})
}
// GetCksByID 根据ID获取Cookie
func GetCksByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
cks, err := repoManager.CksRepository.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Cookie不存在"})
return
}
response := converter.ToCksResponse(cks)
c.JSON(http.StatusOK, response)
}

135
handlers/pan_handler.go Normal file
View File

@@ -0,0 +1,135 @@
package handlers
import (
"net/http"
"strconv"
"res_db/db/converter"
"res_db/db/dto"
"res_db/db/entity"
"github.com/gin-gonic/gin"
)
// GetPans 获取平台列表
func GetPans(c *gin.Context) {
pans, err := repoManager.PanRepository.FindAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
responses := converter.ToPanResponseList(pans)
c.JSON(http.StatusOK, responses)
}
// CreatePan 创建平台
func CreatePan(c *gin.Context) {
var req dto.CreatePanRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
pan := &entity.Pan{
Name: req.Name,
Key: req.Key,
Ck: req.Ck,
IsValid: req.IsValid,
Space: req.Space,
LeftSpace: req.LeftSpace,
Remark: req.Remark,
}
err := repoManager.PanRepository.Create(pan)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": pan.ID,
"message": "平台创建成功",
})
}
// UpdatePan 更新平台
func UpdatePan(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
var req dto.UpdatePanRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
pan, err := repoManager.PanRepository.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "平台不存在"})
return
}
if req.Name != "" {
pan.Name = req.Name
}
pan.Key = req.Key
if req.Ck != "" {
pan.Ck = req.Ck
}
pan.IsValid = req.IsValid
pan.Space = req.Space
pan.LeftSpace = req.LeftSpace
if req.Remark != "" {
pan.Remark = req.Remark
}
err = repoManager.PanRepository.Update(pan)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "平台更新成功"})
}
// DeletePan 删除平台
func DeletePan(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
err = repoManager.PanRepository.Delete(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "平台删除成功"})
}
// GetPan 根据ID获取平台
func GetPan(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
pan, err := repoManager.PanRepository.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "平台不存在"})
return
}
response := converter.ToPanResponse(pan)
c.JSON(http.StatusOK, response)
}

View File

@@ -0,0 +1,164 @@
package handlers
import (
"net/http"
"strconv"
"strings"
"res_db/db/converter"
"res_db/db/dto"
"res_db/db/entity"
"github.com/gin-gonic/gin"
)
// GetReadyResources 获取待处理资源列表
func GetReadyResources(c *gin.Context) {
resources, err := repoManager.ReadyResourceRepository.FindAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
responses := converter.ToReadyResourceResponseList(resources)
c.JSON(http.StatusOK, responses)
}
// CreateReadyResource 创建待处理资源
func CreateReadyResource(c *gin.Context) {
var req dto.CreateReadyResourceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resource := &entity.ReadyResource{
Title: req.Title,
URL: req.URL,
IP: req.IP,
}
err := repoManager.ReadyResourceRepository.Create(resource)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": resource.ID,
"message": "待处理资源创建成功",
})
}
// BatchCreateReadyResources 批量创建待处理资源
func BatchCreateReadyResources(c *gin.Context) {
var req dto.BatchCreateReadyResourceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var resources []entity.ReadyResource
for _, reqResource := range req.Resources {
resource := entity.ReadyResource{
Title: reqResource.Title,
URL: reqResource.URL,
IP: reqResource.IP,
}
resources = append(resources, resource)
}
err := repoManager.ReadyResourceRepository.BatchCreate(resources)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "批量创建成功",
"count": len(resources),
})
}
// CreateReadyResourcesFromText 从文本创建待处理资源
func CreateReadyResourcesFromText(c *gin.Context) {
text := c.PostForm("text")
if text == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "文本内容不能为空"})
return
}
lines := strings.Split(text, "\n")
var resources []entity.ReadyResource
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// 简单的URL提取逻辑
if strings.Contains(line, "http") {
resource := entity.ReadyResource{
URL: line,
}
resources = append(resources, resource)
}
}
if len(resources) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "未找到有效的URL"})
return
}
err := repoManager.ReadyResourceRepository.BatchCreate(resources)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "从文本创建成功",
"count": len(resources),
})
}
// DeleteReadyResource 删除待处理资源
func DeleteReadyResource(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
err = repoManager.ReadyResourceRepository.Delete(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "待处理资源删除成功"})
}
// ClearReadyResources 清空所有待处理资源
func ClearReadyResources(c *gin.Context) {
resources, err := repoManager.ReadyResourceRepository.FindAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
for _, resource := range resources {
err = repoManager.ReadyResourceRepository.Delete(resource.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{
"message": "所有待处理资源已清空",
"count": len(resources),
})
}

View File

@@ -1,293 +0,0 @@
package handlers
import (
"net/http"
"strconv"
"res_db/models"
"github.com/gin-gonic/gin"
)
// GetResources 获取资源列表
func GetResources(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
categoryID := c.Query("category_id")
offset := (page - 1) * limit
query := `
SELECT r.id, r.title, r.description, r.url, r.file_path, r.file_size,
r.file_type, r.category_id, c.name as category_name, r.tags,
r.download_count, r.view_count, r.is_public, r.created_at, r.updated_at
FROM resources r
LEFT JOIN categories c ON r.category_id = c.id
WHERE r.is_public = true
`
args := []interface{}{}
if categoryID != "" {
query += " AND r.category_id = $1"
args = append(args, categoryID)
}
query += " ORDER BY r.created_at DESC LIMIT $2 OFFSET $3"
args = append(args, limit, offset)
rows, err := models.DB.Query(query, args...)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
var resources []models.Resource
for rows.Next() {
var r models.Resource
err := rows.Scan(
&r.ID, &r.Title, &r.Description, &r.URL, &r.FilePath, &r.FileSize,
&r.FileType, &r.CategoryID, &r.CategoryName, &r.Tags,
&r.DownloadCount, &r.ViewCount, &r.IsPublic, &r.CreatedAt, &r.UpdatedAt,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
resources = append(resources, r)
}
c.JSON(http.StatusOK, gin.H{
"resources": resources,
"page": page,
"limit": limit,
})
}
// GetResourceByID 根据ID获取资源
func GetResourceByID(c *gin.Context) {
id := c.Param("id")
var r models.Resource
query := `
SELECT r.id, r.title, r.description, r.url, r.file_path, r.file_size,
r.file_type, r.category_id, c.name as category_name, r.tags,
r.download_count, r.view_count, r.is_public, r.created_at, r.updated_at
FROM resources r
LEFT JOIN categories c ON r.category_id = c.id
WHERE r.id = $1 AND r.is_public = true
`
err := models.DB.QueryRow(query, id).Scan(
&r.ID, &r.Title, &r.Description, &r.URL, &r.FilePath, &r.FileSize,
&r.FileType, &r.CategoryID, &r.CategoryName, &r.Tags,
&r.DownloadCount, &r.ViewCount, &r.IsPublic, &r.CreatedAt, &r.UpdatedAt,
)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
return
}
// 增加浏览次数
models.DB.Exec("UPDATE resources SET view_count = view_count + 1 WHERE id = $1", id)
c.JSON(http.StatusOK, r)
}
// CreateResource 创建资源
func CreateResource(c *gin.Context) {
var req models.CreateResourceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
query := `
INSERT INTO resources (title, description, url, file_path, file_size, file_type, category_id, tags, is_public)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id
`
var id int
err := models.DB.QueryRow(
query,
req.Title, req.Description, req.URL, req.FilePath, req.FileSize,
req.FileType, req.CategoryID, req.Tags, req.IsPublic,
).Scan(&id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"id": id, "message": "资源创建成功"})
}
// UpdateResource 更新资源
func UpdateResource(c *gin.Context) {
id := c.Param("id")
var req models.UpdateResourceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
query := `
UPDATE resources
SET title = COALESCE($1, title),
description = COALESCE($2, description),
url = COALESCE($3, url),
file_path = COALESCE($4, file_path),
file_size = COALESCE($5, file_size),
file_type = COALESCE($6, file_type),
category_id = $7,
tags = COALESCE($8, tags),
is_public = COALESCE($9, is_public),
updated_at = CURRENT_TIMESTAMP
WHERE id = $10
`
result, err := models.DB.Exec(
query,
req.Title, req.Description, req.URL, req.FilePath, req.FileSize,
req.FileType, req.CategoryID, req.Tags, req.IsPublic, id,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "资源更新成功"})
}
// DeleteResource 删除资源
func DeleteResource(c *gin.Context) {
id := c.Param("id")
result, err := models.DB.Exec("DELETE FROM resources WHERE id = $1", id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "资源删除成功"})
}
// SearchResources 搜索资源
func SearchResources(c *gin.Context) {
query := c.Query("q")
categoryID := c.Query("category_id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
offset := (page - 1) * limit
sqlQuery := `
SELECT r.id, r.title, r.description, r.url, r.file_path, r.file_size,
r.file_type, r.category_id, c.name as category_name, r.tags,
r.download_count, r.view_count, r.is_public, r.created_at, r.updated_at
FROM resources r
LEFT JOIN categories c ON r.category_id = c.id
WHERE r.is_public = true
`
args := []interface{}{}
argCount := 0
if query != "" {
argCount++
sqlQuery += " AND (r.title ILIKE $" + strconv.Itoa(argCount) +
" OR r.description ILIKE $" + strconv.Itoa(argCount) +
" OR EXISTS (SELECT 1 FROM unnest(r.tags) tag WHERE tag ILIKE $" + strconv.Itoa(argCount) + "))"
args = append(args, "%"+query+"%")
}
if categoryID != "" {
argCount++
sqlQuery += " AND r.category_id = $" + strconv.Itoa(argCount)
args = append(args, categoryID)
}
sqlQuery += " ORDER BY r.created_at DESC LIMIT $" + strconv.Itoa(argCount+1) +
" OFFSET $" + strconv.Itoa(argCount+2)
args = append(args, limit, offset)
rows, err := models.DB.Query(sqlQuery, args...)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
var resources []models.Resource
for rows.Next() {
var r models.Resource
err := rows.Scan(
&r.ID, &r.Title, &r.Description, &r.URL, &r.FilePath, &r.FileSize,
&r.FileType, &r.CategoryID, &r.CategoryName, &r.Tags,
&r.DownloadCount, &r.ViewCount, &r.IsPublic, &r.CreatedAt, &r.UpdatedAt,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
resources = append(resources, r)
}
c.JSON(http.StatusOK, gin.H{
"resources": resources,
"page": page,
"limit": limit,
"query": query,
})
}
// GetStats 获取统计信息
func GetStats(c *gin.Context) {
var stats models.Stats
// 总资源数
err := models.DB.QueryRow("SELECT COUNT(*) FROM resources WHERE is_public = true").Scan(&stats.TotalResources)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 总分类数
err = models.DB.QueryRow("SELECT COUNT(*) FROM categories").Scan(&stats.TotalCategories)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 总下载数
err = models.DB.QueryRow("SELECT COALESCE(SUM(download_count), 0) FROM resources WHERE is_public = true").Scan(&stats.TotalDownloads)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 总浏览数
err = models.DB.QueryRow("SELECT COALESCE(SUM(view_count), 0) FROM resources WHERE is_public = true").Scan(&stats.TotalViews)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}

View File

@@ -0,0 +1,255 @@
package handlers
import (
"net/http"
"strconv"
"res_db/db/converter"
"res_db/db/dto"
"res_db/db/entity"
"github.com/gin-gonic/gin"
)
// GetResources 获取资源列表
func GetResources(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
categoryIDStr := c.Query("category_id")
panIDStr := c.Query("pan_id")
var resources []entity.Resource
var err error
if categoryIDStr != "" {
categoryID, _ := strconv.ParseUint(categoryIDStr, 10, 32)
resources, err = repoManager.ResourceRepository.FindByCategoryID(uint(categoryID))
} else if panIDStr != "" {
panID, _ := strconv.ParseUint(panIDStr, 10, 32)
resources, err = repoManager.ResourceRepository.FindByPanID(uint(panID))
} else {
resources, err = repoManager.ResourceRepository.FindWithRelations()
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 只返回公开的资源
var publicResources []entity.Resource
for _, resource := range resources {
if resource.IsPublic {
publicResources = append(publicResources, resource)
}
}
// 分页处理
start := (page - 1) * limit
end := start + limit
if start >= len(publicResources) {
start = len(publicResources)
}
if end > len(publicResources) {
end = len(publicResources)
}
pagedResources := publicResources[start:end]
responses := converter.ToResourceResponseList(pagedResources)
c.JSON(http.StatusOK, gin.H{
"resources": responses,
"page": page,
"limit": limit,
"total": len(publicResources),
})
}
// GetResourceByID 根据ID获取资源
func GetResourceByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
resource, err := repoManager.ResourceRepository.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
return
}
if !resource.IsPublic {
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
return
}
// 增加浏览次数
repoManager.ResourceRepository.IncrementViewCount(uint(id))
response := converter.ToResourceResponse(resource)
c.JSON(http.StatusOK, response)
}
// CreateResource 创建资源
func CreateResource(c *gin.Context) {
var req dto.CreateResourceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resource := &entity.Resource{
Title: req.Title,
Description: req.Description,
URL: req.URL,
PanID: req.PanID,
QuarkURL: req.QuarkURL,
FileSize: req.FileSize,
CategoryID: req.CategoryID,
IsValid: req.IsValid,
IsPublic: req.IsPublic,
}
err := repoManager.ResourceRepository.Create(resource)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 处理标签关联
if len(req.TagIDs) > 0 {
err = repoManager.ResourceRepository.UpdateWithTags(resource, req.TagIDs)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusCreated, gin.H{
"id": resource.ID,
"message": "资源创建成功",
})
}
// UpdateResource 更新资源
func UpdateResource(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
var req dto.UpdateResourceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resource, err := repoManager.ResourceRepository.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
return
}
// 更新字段
if req.Title != "" {
resource.Title = req.Title
}
if req.Description != "" {
resource.Description = req.Description
}
if req.URL != "" {
resource.URL = req.URL
}
if req.PanID != nil {
resource.PanID = req.PanID
}
if req.QuarkURL != "" {
resource.QuarkURL = req.QuarkURL
}
if req.FileSize != "" {
resource.FileSize = req.FileSize
}
if req.CategoryID != nil {
resource.CategoryID = req.CategoryID
}
resource.IsValid = req.IsValid
resource.IsPublic = req.IsPublic
err = repoManager.ResourceRepository.Update(resource)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 处理标签关联
if req.TagIDs != nil {
err = repoManager.ResourceRepository.UpdateWithTags(resource, req.TagIDs)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"message": "资源更新成功"})
}
// DeleteResource 删除资源
func DeleteResource(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
err = repoManager.ResourceRepository.Delete(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "资源删除成功"})
}
// SearchResources 搜索资源
func SearchResources(c *gin.Context) {
query := c.Query("query")
categoryIDStr := c.Query("category_id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
var categoryID *uint
if categoryIDStr != "" {
if id, err := strconv.ParseUint(categoryIDStr, 10, 32); err == nil {
temp := uint(id)
categoryID = &temp
}
}
resources, total, err := repoManager.ResourceRepository.Search(query, categoryID, page, limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 只返回公开的资源
var publicResources []entity.Resource
for _, resource := range resources {
if resource.IsPublic {
publicResources = append(publicResources, resource)
}
}
responses := converter.ToResourceResponseList(publicResources)
c.JSON(http.StatusOK, dto.SearchResponse{
Resources: responses,
Total: total,
Page: page,
Limit: limit,
})
}

46
handlers/stats_handler.go Normal file
View File

@@ -0,0 +1,46 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
)
// GetStats 获取统计信息
func GetStats(c *gin.Context) {
// 获取资源总数
totalResources, err := repoManager.ResourceRepository.FindAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 获取分类总数
totalCategories, err := repoManager.CategoryRepository.FindAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 获取标签总数
totalTags, err := repoManager.TagRepository.FindAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 计算总浏览次数
var totalViews int64
for _, resource := range totalResources {
totalViews += int64(resource.ViewCount)
}
stats := map[string]interface{}{
"total_resources": len(totalResources),
"total_categories": len(totalCategories),
"total_tags": len(totalTags),
"total_views": totalViews,
}
c.JSON(http.StatusOK, stats)
}

142
handlers/tag_handler.go Normal file
View File

@@ -0,0 +1,142 @@
package handlers
import (
"net/http"
"strconv"
"res_db/db/converter"
"res_db/db/dto"
"res_db/db/entity"
"github.com/gin-gonic/gin"
)
// GetTags 获取标签列表
func GetTags(c *gin.Context) {
tags, err := repoManager.TagRepository.FindAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
responses := converter.ToTagResponseList(tags)
c.JSON(http.StatusOK, responses)
}
// CreateTag 创建标签
func CreateTag(c *gin.Context) {
var req dto.CreateTagRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tag := &entity.Tag{
Name: req.Name,
Description: req.Description,
}
err := repoManager.TagRepository.Create(tag)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": tag.ID,
"message": "标签创建成功",
})
}
// UpdateTag 更新标签
func UpdateTag(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
var req dto.UpdateTagRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tag, err := repoManager.TagRepository.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"})
return
}
if req.Name != "" {
tag.Name = req.Name
}
if req.Description != "" {
tag.Description = req.Description
}
err = repoManager.TagRepository.Update(tag)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "标签更新成功"})
}
// DeleteTag 删除标签
func DeleteTag(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
err = repoManager.TagRepository.Delete(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "标签删除成功"})
}
// GetTagByID 根据ID获取标签
func GetTagByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
tag, err := repoManager.TagRepository.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"})
return
}
response := converter.ToTagResponse(tag)
c.JSON(http.StatusOK, response)
}
// GetResourceTags 获取资源的标签
func GetResourceTags(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
tags, err := repoManager.TagRepository.FindByResourceID(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
responses := converter.ToTagResponseList(tags)
c.JSON(http.StatusOK, responses)
}

View File

@@ -62,6 +62,21 @@ func InitDB() error {
// createTables 创建数据库表
func createTables() error {
// 创建pan表
panTable := `
CREATE TABLE IF NOT EXISTS pan (
id SERIAL PRIMARY KEY,
name VARCHAR(64) DEFAULT NULL,
key INTEGER DEFAULT NULL,
ck TEXT,
is_valid BOOLEAN DEFAULT true,
space BIGINT DEFAULT 0,
left_space BIGINT DEFAULT 0,
remark VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
// 创建分类表
categoryTable := `
CREATE TABLE IF NOT EXISTS categories (
@@ -72,33 +87,97 @@ func createTables() error {
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
// 创建资源
// 创建标签
tagTable := `
CREATE TABLE IF NOT EXISTS tags (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
// 创建资源表 - 更新后的结构
resourceTable := `
CREATE TABLE IF NOT EXISTS resources (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
url VARCHAR(500),
file_path VARCHAR(500),
file_size BIGINT,
file_type VARCHAR(100),
url VARCHAR(128),
pan_id INTEGER REFERENCES pan(id) ON DELETE SET NULL,
quark_url VARCHAR(500),
file_size VARCHAR(100),
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
tags TEXT[],
download_count INTEGER DEFAULT 0,
view_count INTEGER DEFAULT 0,
is_valid BOOLEAN DEFAULT true,
is_public BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
// 创建资源标签关联表
resourceTagTable := `
CREATE TABLE IF NOT EXISTS resource_tags (
id SERIAL PRIMARY KEY,
resource_id INTEGER NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(resource_id, tag_id)
);`
if _, err := DB.Exec(panTable); err != nil {
return err
}
if _, err := DB.Exec(categoryTable); err != nil {
return err
}
if _, err := DB.Exec(tagTable); err != nil {
return err
}
if _, err := DB.Exec(resourceTable); err != nil {
return err
}
if _, err := DB.Exec(resourceTagTable); err != nil {
return err
}
// 创建cks表
cksTable := `
CREATE TABLE IF NOT EXISTS cks (
id SERIAL PRIMARY KEY,
pan_id INTEGER NOT NULL REFERENCES pan(id) ON DELETE CASCADE,
t VARCHAR(64) DEFAULT NULL,
idx INTEGER DEFAULT NULL,
ck TEXT,
remark VARCHAR(64) NOT NULL
);`
// 创建待处理资源表
readyResourceTable := `
CREATE TABLE IF NOT EXISTS ready_resource (
id SERIAL PRIMARY KEY,
title VARCHAR(255),
url VARCHAR(500) NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip VARCHAR(45) DEFAULT NULL
);`
if _, err := DB.Exec(panTable); err != nil {
return err
}
if _, err := DB.Exec(cksTable); err != nil {
return err
}
if _, err := DB.Exec(readyResourceTable); err != nil {
return err
}
// 插入默认分类
insertDefaultCategories := `
INSERT INTO categories (name, description) VALUES

69
models/pan.go Normal file
View File

@@ -0,0 +1,69 @@
package models
import (
"time"
)
// Pan 第三方平台表
type Pan struct {
ID int `json:"id"`
Name string `json:"name"` // Qurak
Key int `json:"key"` // quark
Ck string `json:"ck"` // cookie
IsValid bool `json:"is_valid"` // 是否有效
Space int64 `json:"space"` // 空间
LeftSpace int64 `json:"left_space"` // 剩余空间
Remark string `json:"remark"` // 备注
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Cks 第三方平台账号cookie表
type Cks struct {
ID int `json:"id"`
PanID int `json:"pan_id"` // pan ID
T string `json:"t"` // cookie类型
Idx int `json:"idx"` // index
Ck string `json:"ck"` // cookie
Remark string `json:"remark"` // 备注
}
// CreatePanRequest 创建平台请求
type CreatePanRequest struct {
Name string `json:"name" binding:"required"`
Key int `json:"key"`
Ck string `json:"ck"`
IsValid bool `json:"is_valid"`
Space int64 `json:"space"`
LeftSpace int64 `json:"left_space"`
Remark string `json:"remark"`
}
// UpdatePanRequest 更新平台请求
type UpdatePanRequest struct {
Name string `json:"name"`
Key int `json:"key"`
Ck string `json:"ck"`
IsValid bool `json:"is_valid"`
Space int64 `json:"space"`
LeftSpace int64 `json:"left_space"`
Remark string `json:"remark"`
}
// CreateCksRequest 创建cookie请求
type CreateCksRequest struct {
PanID int `json:"pan_id" binding:"required"`
T string `json:"t"`
Idx int `json:"idx"`
Ck string `json:"ck"`
Remark string `json:"remark"`
}
// UpdateCksRequest 更新cookie请求
type UpdateCksRequest struct {
PanID int `json:"pan_id"`
T string `json:"t"`
Idx int `json:"idx"`
Ck string `json:"ck"`
Remark string `json:"remark"`
}

View File

@@ -1,26 +1,105 @@
package models
import (
"database/sql/driver"
"fmt"
"strings"
"time"
"github.com/lib/pq"
)
// Resource 资源模型
// Tags 自定义类型,用于处理 PostgreSQL 数组
type Tags []string
// Value 实现 driver.Valuer 接口
func (t Tags) Value() (driver.Value, error) {
if t == nil {
return nil, nil
}
return pq.Array(t), nil
}
// Scan 实现 sql.Scanner 接口
func (t *Tags) Scan(value interface{}) error {
if value == nil {
*t = nil
return nil
}
switch v := value.(type) {
case []byte:
// 处理 PostgreSQL 数组格式: {tag1,tag2,tag3}
if len(v) == 0 {
*t = Tags{}
return nil
}
// 移除 { } 并分割
s := string(v)
s = strings.Trim(s, "{}")
if s == "" {
*t = Tags{}
return nil
}
tags := strings.Split(s, ",")
*t = Tags(tags)
return nil
case string:
// 处理字符串格式
if v == "" {
*t = Tags{}
return nil
}
s := strings.Trim(v, "{}")
if s == "" {
*t = Tags{}
return nil
}
tags := strings.Split(s, ",")
*t = Tags(tags)
return nil
default:
return fmt.Errorf("cannot scan %T into Tags", value)
}
}
// Resource 资源模型 - 更新后的结构
type Resource struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
FilePath string `json:"file_path"`
FileSize int64 `json:"file_size"`
FileType string `json:"file_type"`
CategoryID *int `json:"category_id"`
CategoryName string `json:"category_name"`
Tags []string `json:"tags"`
DownloadCount int `json:"download_count"`
ViewCount int `json:"view_count"`
IsPublic bool `json:"is_public"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"` // 单个URL字符串
PanID *int `json:"pan_id"` // 平台ID标识链接类型
QuarkURL string `json:"quark_url"` // 新增字段
FileSize string `json:"file_size"` // 改为 string 类型
CategoryID *int `json:"category_id"`
CategoryName string `json:"category_name"`
ViewCount int `json:"view_count"`
IsValid bool `json:"is_valid"` // 新增字段
IsPublic bool `json:"is_public"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Tag 标签模型
type Tag struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ResourceTag 资源标签关联表
type ResourceTag struct {
ID int `json:"id"`
ResourceID int `json:"resource_id"`
TagID int `json:"tag_id"`
CreatedAt time.Time `json:"created_at"`
}
// Category 分类模型
@@ -34,28 +113,42 @@ type Category struct {
// CreateResourceRequest 创建资源请求
type CreateResourceRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
URL string `json:"url"`
FilePath string `json:"file_path"`
FileSize int64 `json:"file_size"`
FileType string `json:"file_type"`
CategoryID *int `json:"category_id"`
Tags []string `json:"tags"`
IsPublic bool `json:"is_public"`
Title string `json:"title" binding:"required"`
Description string `json:"description"`
URL string `json:"url"`
PanID *int `json:"pan_id"` // 平台ID
QuarkURL string `json:"quark_url"`
FileSize string `json:"file_size"`
CategoryID *int `json:"category_id"`
IsValid bool `json:"is_valid"`
IsPublic bool `json:"is_public"`
TagIDs []int `json:"tag_ids"` // 标签ID列表
}
// UpdateResourceRequest 更新资源请求
type UpdateResourceRequest struct {
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
FilePath string `json:"file_path"`
FileSize int64 `json:"file_size"`
FileType string `json:"file_type"`
CategoryID *int `json:"category_id"`
Tags []string `json:"tags"`
IsPublic bool `json:"is_public"`
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
PanID *int `json:"pan_id"` // 平台ID
QuarkURL string `json:"quark_url"`
FileSize string `json:"file_size"`
CategoryID *int `json:"category_id"`
IsValid bool `json:"is_valid"`
IsPublic bool `json:"is_public"`
TagIDs []int `json:"tag_ids"` // 标签ID列表
}
// CreateTagRequest 创建标签请求
type CreateTagRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}
// UpdateTagRequest 更新标签请求
type UpdateTagRequest struct {
Name string `json:"name"`
Description string `json:"description"`
}
// CreateCategoryRequest 创建分类请求
@@ -86,6 +179,27 @@ type SearchResponse struct {
Limit int `json:"limit"`
}
// ReadyResource 待处理资源模型
type ReadyResource struct {
ID int `json:"id"`
Title *string `json:"title"`
URL string `json:"url"`
CreateTime time.Time `json:"create_time"`
IP *string `json:"ip"`
}
// CreateReadyResourceRequest 创建待处理资源请求
type CreateReadyResourceRequest struct {
Title *string `json:"title"`
URL string `json:"url" binding:"required"`
IP *string `json:"ip"`
}
// BatchCreateReadyResourceRequest 批量创建待处理资源请求
type BatchCreateReadyResourceRequest struct {
Resources []CreateReadyResourceRequest `json:"resources" binding:"required"`
}
// Stats 统计信息
type Stats struct {
TotalResources int `json:"total_resources"`

1
web/.npmrc Normal file
View File

@@ -0,0 +1 @@
registry=https://repo.huaweicloud.com/repository/npm/

View File

@@ -1,41 +1,55 @@
import axios from 'axios'
const config = useRuntimeConfig()
const api = axios.create({
baseURL: config.public.apiBase,
timeout: 10000,
})
// 资源相关API
// 使用 $fetch 替代 axios更好地处理 SSR
export const useResourceApi = () => {
const config = useRuntimeConfig()
const getResources = async (params?: any) => {
const response = await api.get('/resources', { params })
return response.data
return await $fetch('/resources', {
baseURL: config.public.apiBase,
params
})
}
const getResource = async (id: number) => {
const response = await api.get(`/resources/${id}`)
return response.data
return await $fetch(`/resources/${id}`, {
baseURL: config.public.apiBase,
})
}
const createResource = async (data: any) => {
const response = await api.post('/resources', data)
return response.data
return await $fetch('/resources', {
baseURL: config.public.apiBase,
method: 'POST',
body: data
})
}
const updateResource = async (id: number, data: any) => {
const response = await api.put(`/resources/${id}`, data)
return response.data
return await $fetch(`/resources/${id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: data
})
}
const deleteResource = async (id: number) => {
const response = await api.delete(`/resources/${id}`)
return response.data
return await $fetch(`/resources/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE'
})
}
const searchResources = async (params: any) => {
const response = await api.get('/search', { params })
return response.data
return await $fetch('/search', {
baseURL: config.public.apiBase,
params
})
}
const getResourcesByPan = async (panId: number, params?: any) => {
return await $fetch('/resources', {
baseURL: config.public.apiBase,
params: { ...params, pan_id: panId }
})
}
return {
@@ -45,29 +59,41 @@ export const useResourceApi = () => {
updateResource,
deleteResource,
searchResources,
getResourcesByPan,
}
}
// 分类相关API
export const useCategoryApi = () => {
const config = useRuntimeConfig()
const getCategories = async () => {
const response = await api.get('/categories')
return response.data
return await $fetch('/categories', {
baseURL: config.public.apiBase,
})
}
const createCategory = async (data: any) => {
const response = await api.post('/categories', data)
return response.data
return await $fetch('/categories', {
baseURL: config.public.apiBase,
method: 'POST',
body: data
})
}
const updateCategory = async (id: number, data: any) => {
const response = await api.put(`/categories/${id}`, data)
return response.data
return await $fetch(`/categories/${id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: data
})
}
const deleteCategory = async (id: number) => {
const response = await api.delete(`/categories/${id}`)
return response.data
return await $fetch(`/categories/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE'
})
}
return {
@@ -78,11 +104,224 @@ export const useCategoryApi = () => {
}
}
// 平台相关API
export const usePanApi = () => {
const config = useRuntimeConfig()
const getPans = async () => {
return await $fetch('/pans', {
baseURL: config.public.apiBase,
})
}
const getPan = async (id: number) => {
return await $fetch(`/pans/${id}`, {
baseURL: config.public.apiBase,
})
}
const createPan = async (data: any) => {
return await $fetch('/pans', {
baseURL: config.public.apiBase,
method: 'POST',
body: data
})
}
const updatePan = async (id: number, data: any) => {
return await $fetch(`/pans/${id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: data
})
}
const deletePan = async (id: number) => {
return await $fetch(`/pans/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE'
})
}
return {
getPans,
getPan,
createPan,
updatePan,
deletePan,
}
}
// Cookie相关API
export const useCksApi = () => {
const config = useRuntimeConfig()
const getCks = async (params?: any) => {
return await $fetch('/cks', {
baseURL: config.public.apiBase,
params
})
}
const getCksByID = async (id: number) => {
return await $fetch(`/cks/${id}`, {
baseURL: config.public.apiBase,
})
}
const createCks = async (data: any) => {
return await $fetch('/cks', {
baseURL: config.public.apiBase,
method: 'POST',
body: data
})
}
const updateCks = async (id: number, data: any) => {
return await $fetch(`/cks/${id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: data
})
}
const deleteCks = async (id: number) => {
return await $fetch(`/cks/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE'
})
}
return {
getCks,
getCksByID,
createCks,
updateCks,
deleteCks,
}
}
// 标签相关API
export const useTagApi = () => {
const config = useRuntimeConfig()
const getTags = async () => {
return await $fetch('/tags', {
baseURL: config.public.apiBase,
})
}
const getTag = async (id: number) => {
return await $fetch(`/tags/${id}`, {
baseURL: config.public.apiBase,
})
}
const createTag = async (data: any) => {
return await $fetch('/tags', {
baseURL: config.public.apiBase,
method: 'POST',
body: data
})
}
const updateTag = async (id: number, data: any) => {
return await $fetch(`/tags/${id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: data
})
}
const deleteTag = async (id: number) => {
return await $fetch(`/tags/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE'
})
}
const getResourceTags = async (resourceId: number) => {
return await $fetch(`/resources/${resourceId}/tags`, {
baseURL: config.public.apiBase,
})
}
return {
getTags,
getTag,
createTag,
updateTag,
deleteTag,
getResourceTags,
}
}
// 统计相关API
// 待处理资源相关API
export const useReadyResourceApi = () => {
const config = useRuntimeConfig()
const getReadyResources = async () => {
return await $fetch('/ready-resources', {
baseURL: config.public.apiBase,
})
}
const createReadyResource = async (data: any) => {
return await $fetch('/ready-resources', {
baseURL: config.public.apiBase,
method: 'POST',
body: data
})
}
const batchCreateReadyResources = async (data: any) => {
return await $fetch('/ready-resources/batch', {
baseURL: config.public.apiBase,
method: 'POST',
body: data
})
}
const createReadyResourcesFromText = async (text: string) => {
return await $fetch('/ready-resources/text', {
baseURL: config.public.apiBase,
method: 'POST',
body: { text }
})
}
const deleteReadyResource = async (id: number) => {
return await $fetch(`/ready-resources/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE'
})
}
const clearReadyResources = async () => {
return await $fetch('/ready-resources', {
baseURL: config.public.apiBase,
method: 'DELETE'
})
}
return {
getReadyResources,
createReadyResource,
batchCreateReadyResources,
createReadyResourcesFromText,
deleteReadyResource,
clearReadyResources,
}
}
export const useStatsApi = () => {
const config = useRuntimeConfig()
const getStats = async () => {
const response = await api.get('/stats')
return response.data
return await $fetch('/stats', {
baseURL: config.public.apiBase,
})
}
return {

View File

@@ -12,14 +12,20 @@ export default defineNuxtConfig({
],
app: {
head: {
title: '资源管理系统',
title: '网盘资源管理系统',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: 'description', content: '一个现代化的资源管理系统' }
{ name: 'description', content: '网盘资源管理系统 - 一个现代化的资源管理系统' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
],
script: [
{
src: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js',
defer: true
}
]
}
},
@@ -30,5 +36,9 @@ export default defineNuxtConfig({
},
build: {
transpile: ['naive-ui', 'vueuc', '@css-render/vue3-ssr', '@juggle/resize-observer']
},
ssr: true,
nitro: {
preset: 'node'
}
})

View File

@@ -13,21 +13,22 @@
"devDependencies": {
"@nuxt/devtools": "latest",
"@nuxt/typescript-build": "^3.0.0",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^20.0.0",
"nuxt": "^3.8.0",
"typescript": "^5.0.0"
},
"dependencies": {
"@nuxtjs/tailwindcss": "^6.8.0",
"@pinia/nuxt": "^0.5.0",
"pinia": "^2.1.0",
"vue": "^3.3.0",
"vue-router": "^4.2.0",
"naive-ui": "^2.37.0",
"vfonts": "^0.0.3",
"@vicons/ionicons5": "^0.12.0",
"@css-render/vue3-ssr": "^0.15.12",
"@juggle/resize-observer": "^3.3.1",
"axios": "^1.6.0"
"@nuxtjs/tailwindcss": "^6.8.0",
"@pinia/nuxt": "^0.5.0",
"@vicons/ionicons5": "^0.12.0",
"naive-ui": "^2.37.0",
"pinia": "^2.1.0",
"vfonts": "^0.0.3",
"vue": "^3.3.0",
"vue-router": "^4.2.0"
}
}

308
web/pages/admin.vue Normal file
View File

@@ -0,0 +1,308 @@
<template>
<div class="min-h-screen bg-gray-50 text-gray-800 p-3 sm:p-5">
<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">
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
<NuxtLink to="/" class="text-white hover:text-gray-200 no-underline">网盘资源管理系统</NuxtLink>
</h1>
<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>
<button
@click="showAddResourceModal = 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>
</nav>
</div>
<!-- 管理功能区域 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<!-- 资源管理 -->
<div class="bg-white 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">资源管理</h3>
<p class="text-sm text-gray-600">管理所有资源</p>
</div>
</div>
<div class="space-y-2">
<button @click="goToResourceManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">查看所有资源</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</button>
<button @click="showAddResourceModal = true" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">添加新资源</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</button>
</div>
</div>
<!-- 平台管理 -->
<div class="bg-white 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">平台管理</h3>
<p class="text-sm text-gray-600">管理网盘平台</p>
</div>
</div>
<div class="space-y-2">
<button @click="goToPlatformManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">管理平台</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 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">添加平台</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</button>
</div>
</div>
<!-- 分类管理 -->
<div class="bg-white 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">分类管理</h3>
<p class="text-sm text-gray-600">管理资源分类</p>
</div>
</div>
<div class="space-y-2">
<button @click="goToCategoryManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">管理分类</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</button>
<button @click="showAddCategoryModal = true" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">添加分类</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</button>
</div>
</div>
<!-- 标签管理 -->
<div class="bg-white 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">标签管理</h3>
<p class="text-sm text-gray-600">管理资源标签</p>
</div>
</div>
<div class="space-y-2">
<button @click="goToTagManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">管理标签</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</button>
<button @click="showAddTagModal = true" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">添加标签</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</button>
</div>
</div>
<!-- 统计信息 -->
<div class="bg-white 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">统计信息</h3>
<p class="text-sm text-gray-600">系统统计数据</p>
</div>
</div>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">总资源数</span>
<span class="text-lg font-semibold text-gray-900">{{ stats?.total_resources || 0 }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">总浏览量</span>
<span class="text-lg font-semibold text-gray-900">{{ stats?.total_views || 0 }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">分类数量</span>
<span class="text-lg font-semibold text-gray-900">{{ stats?.total_categories || 0 }}</span>
</div>
</div>
</div>
<!-- 待处理资源 -->
<div class="bg-white 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">待处理资源</h3>
<p class="text-sm text-gray-600">批量添加和管理</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 transition-colors block">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">管理待处理资源</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 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">批量添加资源</span>
<i class="fas fa-plus text-gray-400"></i>
</div>
</button>
</div>
</div>
<!-- 系统设置 -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<div class="p-3 bg-gray-100 rounded-lg">
<i class="fas fa-cog text-gray-600 text-xl"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900">系统设置</h3>
<p class="text-sm text-gray-600">系统配置</p>
</div>
</div>
<div class="space-y-2">
<button @click="goToSystemSettings" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">系统配置</span>
<i class="fas fa-chevron-right text-gray-400"></i>
</div>
</button>
<button @click="goToBackupRestore" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">备份恢复</span>
<i class="fas fa-database text-gray-400"></i>
</div>
</button>
</div>
</div>
</div>
</div>
<!-- 添加资源模态框 -->
<ResourceModal
v-if="showAddResourceModal"
:resource="editingResource"
@close="closeModal"
@save="handleSaveResource"
/>
</div>
</template>
<script setup lang="ts">
const store = useResourceStore()
const { stats } = storeToRefs(store)
const showAddResourceModal = ref(false)
const showAddPlatformModal = ref(false)
const showAddCategoryModal = ref(false)
const showAddTagModal = ref(false)
const editingResource = ref(null)
// 获取统计数据
onMounted(async () => {
await store.fetchStats()
})
// 导航到资源管理
const goToResourceManagement = () => {
// 这里可以导航到资源管理页面
console.log('导航到资源管理')
}
// 导航到平台管理
const goToPlatformManagement = () => {
// 这里可以导航到平台管理页面
console.log('导航到平台管理')
}
// 导航到分类管理
const goToCategoryManagement = () => {
// 这里可以导航到分类管理页面
console.log('导航到分类管理')
}
// 导航到标签管理
const goToTagManagement = () => {
// 这里可以导航到标签管理页面
console.log('导航到标签管理')
}
// 导航到系统设置
const goToSystemSettings = () => {
// 这里可以导航到系统设置页面
console.log('导航到系统设置')
}
// 导航到备份恢复
const goToBackupRestore = () => {
// 这里可以导航到备份恢复页面
console.log('导航到备份恢复')
}
// 导航到批量添加
const goToBatchAdd = () => {
// 导航到待处理资源页面
navigateTo('/ready-resources')
}
// 关闭模态框
const closeModal = () => {
showAddResourceModal.value = false
editingResource.value = null
}
// 保存资源
const handleSaveResource = async (resourceData: any) => {
try {
if (editingResource.value) {
await store.updateResource(editingResource.value.id, resourceData)
} else {
await store.createResource(resourceData)
}
closeModal()
} catch (error) {
console.error('保存失败:', error)
}
}
</script>
<style scoped>
/* 可以添加自定义样式 */
</style>

View File

@@ -1,204 +1,189 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- 头部 -->
<header class="bg-white shadow-sm border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center py-4">
<div class="flex items-center">
<h1 class="text-2xl font-bold text-gray-900">资源管理系统</h1>
</div>
<div class="flex items-center space-x-4">
<button @click="showAddResourceModal = true" class="btn-primary">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
添加资源
</button>
</div>
</div>
</div>
</header>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="card">
<div class="flex items-center">
<div class="p-2 bg-blue-100 rounded-lg">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">总资源</p>
<p class="text-2xl font-bold text-gray-900">{{ stats?.total_resources || 0 }}</p>
</div>
</div>
</div>
<div class="card">
<div class="flex items-center">
<div class="p-2 bg-green-100 rounded-lg">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">分类</p>
<p class="text-2xl font-bold text-gray-900">{{ stats?.total_categories || 0 }}</p>
</div>
</div>
</div>
<div class="card">
<div class="flex items-center">
<div class="p-2 bg-yellow-100 rounded-lg">
<svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">下载</p>
<p class="text-2xl font-bold text-gray-900">{{ stats?.total_downloads || 0 }}</p>
</div>
</div>
</div>
<div class="card">
<div class="flex items-center">
<div class="p-2 bg-purple-100 rounded-lg">
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">浏览</p>
<p class="text-2xl font-bold text-gray-900">{{ stats?.total_views || 0 }}</p>
</div>
</div>
</div>
<div class="min-h-screen bg-gray-50 text-gray-800 p-3 sm:p-5">
<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">
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
<a href="/" class="text-white hover:text-gray-200 no-underline">网盘资源管理系统</a>
</h1>
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4">
<button
@click="showAddResourceModal = 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"
>
<i class="fas fa-plus"></i> 添加资源
</button>
<NuxtLink
to="/admin"
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-user-shield"></i> 管理员入口
</NuxtLink>
</nav>
</div>
<!-- 搜索和筛选 -->
<div class="card mb-8">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<div class="relative">
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<input
v-model="searchQuery"
@keyup.enter="handleSearch"
type="text"
placeholder="搜索资源..."
class="input-field pl-10"
/>
</div>
<!-- 搜索区域 -->
<div class="w-full max-w-3xl mx-auto mb-4 sm:mb-8 px-2 sm:px-0">
<div class="relative">
<input
v-model="searchQuery"
@keyup="debounceSearch"
type="text"
class="w-full px-4 py-3 rounded-full border-2 border-gray-300 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 transition-all"
placeholder="输入文件名或链接进行搜索..."
/>
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
<i class="fas fa-search text-gray-400"></i>
</div>
<div class="w-full md:w-48">
<select v-model="selectedCategory" class="input-field">
<option value="">全部分类</option>
<option v-for="category in categories" :key="category.id" :value="category.id">
{{ category.name }}
</option>
</select>
</div>
<!-- 平台类型筛选 -->
<div class="mt-3 flex flex-wrap gap-2" id="platformFilters">
<button
class="px-2 py-1 text-xs rounded-full bg-slate-800 text-white active-filter"
@click="filterByPlatform('')"
>
全部
</button>
<button
v-for="platform in platforms"
:key="platform.id"
class="px-2 py-1 text-xs rounded-full bg-gray-200 text-gray-800 hover:bg-gray-300 transition-colors"
@click="filterByPlatform(platform.id)"
>
{{ getPlatformIcon(platform.name) }} {{ platform.name }}
</button>
</div>
<!-- 统计信息 -->
<div class="flex justify-between mt-3 text-sm text-gray-600 px-2">
<div class="flex items-center">
<i class="fas fa-calendar-day text-pink-600 mr-1"></i>
今日更新: <span class="font-medium text-pink-600 ml-1 count-up" :data-target="todayUpdates">0</span>
</div>
<div class="flex gap-2">
<button @click="handleSearch" class="btn-primary">
搜索
</button>
<button @click="clearSearch" class="btn-secondary">
清除
</button>
<div class="flex items-center">
<i class="fas fa-database text-blue-600 mr-1"></i>
总资源数: <span class="font-medium text-blue-600 ml-1 count-up" :data-target="stats?.total_resources || 0">0</span>
</div>
</div>
</div>
<!-- 资源列表 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<div
v-for="resource in resources"
:key="resource.id"
class="resource-card cursor-pointer"
@click="viewResource(resource)"
<div class="overflow-x-auto bg-white rounded-lg shadow">
<table class="w-full">
<thead>
<tr class="bg-slate-800 text-white">
<th class="px-2 sm:px-6 py-3 sm:py-4 text-left text-xs sm:text-sm">
<div class="flex items-center">
<i class="fas fa-cloud mr-1 text-gray-300"></i> 文件名
</div>
</th>
<th class="px-2 sm:px-6 py-3 sm:py-4 text-left text-xs sm:text-sm hidden sm:table-cell">链接</th>
<th class="px-2 sm:px-6 py-3 sm:py-4 text-left text-xs sm:text-sm">更新时间</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-if="loading" class="text-center py-8">
<td colspan="3" class="text-gray-500">
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
</td>
</tr>
<tr v-else-if="resources.length === 0" class="text-center py-8">
<td colspan="3" class="text-gray-500">暂无数据</td>
</tr>
<tr
v-for="resource in (resources as unknown as ExtendedResource[])"
:key="resource.id"
:class="isUpdatedToday(resource.updated_at) ? 'hover:bg-pink-50 bg-pink-50/30' : 'hover:bg-gray-50'"
>
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm">
<div class="flex items-start">
<span class="mr-2 flex-shrink-0">{{ getPlatformIcon(getPlatformName(resource.pan_id || 0)) }}</span>
<span class="break-words">{{ resource.title }}</span>
</div>
<div class="sm:hidden mt-1">
<button
class="text-blue-600 hover:text-blue-800 text-xs flex items-center gap-1 show-link-btn"
@click="toggleLink(resource)"
>
<i class="fas fa-eye"></i> 显示链接
</button>
<a
v-if="resource.showLink"
:href="resource.url"
target="_blank"
class="text-blue-600 hover:text-blue-800 hover:underline text-xs break-all"
>
{{ resource.url }}
</a>
</div>
</td>
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm hidden sm:table-cell">
<button
class="text-blue-600 hover:text-blue-800 flex items-center gap-1 show-link-btn"
@click="toggleLink(resource)"
>
<i class="fas fa-eye"></i> 显示链接
</button>
<a
v-if="resource.showLink"
:href="resource.url"
target="_blank"
class="text-blue-600 hover:text-blue-800 hover:underline"
>
{{ resource.url }}
</a>
</td>
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-gray-500" :title="resource.updated_at">
{{ formatRelativeTime(resource.updated_at) }}
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div v-if="totalPages > 1" class="flex flex-wrap justify-center gap-1 sm:gap-2 my-4 sm:my-8 px-2">
<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"
>
<div class="flex items-start justify-between mb-3">
<div class="flex-1">
<h3 class="font-semibold text-gray-900 truncate">{{ resource.title }}</h3>
<p class="text-sm text-gray-600 mt-1 line-clamp-2">{{ resource.description }}</p>
</div>
<div class="flex items-center space-x-1 ml-2">
<button
@click.stop="editResource(resource)"
class="p-1 text-gray-400 hover:text-blue-600"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button
@click.stop="deleteResource(resource.id)"
class="p-1 text-gray-400 hover:text-red-600"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</div>
<i class="fas fa-chevron-left mr-1"></i> 上一页
</button>
<div class="flex items-center justify-between text-sm text-gray-500">
<span>{{ resource.category_name || '未分类' }}</span>
<div class="flex items-center space-x-3">
<span class="flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
{{ resource.view_count }}
</span>
<span class="flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
{{ resource.download_count }}
</span>
</div>
</div>
<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>
<div v-if="resource.tags && resource.tags.length > 0" class="mt-3">
<div class="flex flex-wrap gap-1">
<span
v-for="tag in resource.tags.slice(0, 3)"
:key="tag"
class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded"
>
{{ tag }}
</span>
<span v-if="resource.tags.length > 3" class="px-2 py-1 text-gray-400 text-xs">
+{{ resource.tags.length - 3 }}
</span>
</div>
</div>
</div>
</div>
<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>
<!-- 加载状态 -->
<div v-if="loading" class="flex justify-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
<span v-if="currentPage > 2" class="px-2 py-1 sm:px-3 sm:py-2 text-gray-500 text-sm">...</span>
<!-- 空状态 -->
<div v-if="!loading && resources.length === 0" class="text-center py-12">
<svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<h3 class="text-lg font-medium text-gray-900 mb-2">暂无资源</h3>
<p class="text-gray-600 mb-4">开始添加您的第一个资源吧</p>
<button @click="showAddResourceModal = true" class="btn-primary">
添加资源
<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>
@@ -210,6 +195,14 @@
@close="closeModal"
@save="handleSaveResource"
/>
<!-- 页脚 -->
<footer class="mt-8 py-6 border-t border-gray-200">
<div class="max-w-7xl mx-auto text-center text-gray-600 text-sm">
<p class="mb-2">本站内容由网络爬虫自动抓取本站不储存复制传播任何文件仅作个人公益学习请在获取后24小内删除!!!</p>
<p>© 2025 网盘资源管理系统 By 小七</p>
</div>
</footer>
</div>
</template>
@@ -218,9 +211,53 @@ const store = useResourceStore()
const { resources, categories, stats, loading } = storeToRefs(store)
const searchQuery = ref('')
const selectedCategory = ref('')
const selectedPlatform = ref('')
const showAddResourceModal = ref(false)
const editingResource = ref(null)
const currentPage = ref(1)
const totalPages = ref(1)
interface Platform {
id: number
name: string
key?: number
ck?: string
is_valid: boolean
space: number
left_space: number
remark: string
created_at: string
updated_at: string
}
interface ExtendedResource {
id: number
title: string
description: string
url: string
pan_id?: number
quark_url?: string
file_size?: string
category_id?: number
category_name?: string
view_count: number
is_valid?: boolean
is_public?: boolean
created_at: string
updated_at: string
showLink?: boolean
}
const platforms = ref<Platform[]>([])
const todayUpdates = ref(0)
// 防抖搜索
let searchTimeout: NodeJS.Timeout
const debounceSearch = () => {
clearTimeout(searchTimeout)
searchTimeout = setTimeout(() => {
handleSearch()
}, 500)
}
// 获取数据
onMounted(async () => {
@@ -228,25 +265,150 @@ onMounted(async () => {
store.fetchResources(),
store.fetchCategories(),
store.fetchStats(),
fetchPlatforms(),
])
animateCounters()
})
// 获取平台列表
const fetchPlatforms = async () => {
try {
const { usePanApi } = await import('~/composables/useApi')
const panApi = usePanApi()
const response = await panApi.getPans() as any
platforms.value = response.pans || []
} catch (error) {
console.error('获取平台列表失败:', error)
}
}
// 搜索处理
const handleSearch = () => {
const categoryId = selectedCategory.value ? parseInt(selectedCategory.value) : undefined
store.searchResources(searchQuery.value, categoryId)
const platformId = selectedPlatform.value ? parseInt(selectedPlatform.value) : undefined
store.searchResources(searchQuery.value, platformId)
}
// 清除搜索
const clearSearch = () => {
searchQuery.value = ''
selectedCategory.value = ''
store.clearSearch()
// 按平台筛选
const filterByPlatform = (platformId: string | number) => {
selectedPlatform.value = platformId.toString()
currentPage.value = 1
handleSearch()
}
// 查看资源
const viewResource = (resource: any) => {
console.log('查看资源:', resource)
// 获取平台图标
const getPlatformIcon = (platformName: string) => {
const icons: Record<string, string> = {
'百度网盘': '<i class="fas fa-cloud text-blue-500"></i>',
'阿里云盘': '<i class="fas fa-cloud text-orange-500"></i>',
'夸克网盘': '<i class="fas fa-atom text-purple-500"></i>',
'天翼云盘': '<i class="fas fa-cloud text-cyan-500"></i>',
'迅雷云盘': '<i class="fas fa-bolt text-yellow-500"></i>',
'微云': '<i class="fas fa-cloud text-green-500"></i>',
'蓝奏云': '<i class="fas fa-cloud text-blue-400"></i>',
'123云盘': '<i class="fas fa-cloud text-red-500"></i>',
'腾讯微云': '<i class="fas fa-cloud text-green-500"></i>',
'OneDrive': '<i class="fab fa-microsoft text-blue-600"></i>',
'Google云盘': '<i class="fab fa-google-drive text-green-600"></i>',
'Dropbox': '<i class="fab fa-dropbox text-blue-500"></i>',
'城通网盘': '<i class="fas fa-folder text-yellow-600"></i>',
'115网盘': '<i class="fas fa-cloud-upload-alt text-green-600"></i>',
'磁力链接': '<i class="fas fa-magnet text-red-600"></i>',
'UC网盘': '<i class="fas fa-cloud-download-alt text-purple-600"></i>',
'天翼云': '<i class="fas fa-cloud text-cyan-500"></i>',
'unknown': '<i class="fas fa-question-circle text-gray-400"></i>',
'其他': '<i class="fas fa-cloud text-gray-500"></i>'
}
return icons[platformName] || icons['unknown']
}
// 获取平台名称
const getPlatformName = (platformId: number) => {
const platform = platforms.value.find((p: Platform) => p.id === platformId)
return platform?.name || 'unknown'
}
// 切换链接显示
const toggleLink = (resource: any) => {
resource.showLink = !resource.showLink
}
// 格式化相对时间
const formatRelativeTime = (dateString: string) => {
const date = new Date(dateString)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffSec = Math.floor(diffMs / 1000)
const diffMin = Math.floor(diffSec / 60)
const diffHour = Math.floor(diffMin / 60)
const diffDay = Math.floor(diffHour / 24)
const diffWeek = Math.floor(diffDay / 7)
const diffMonth = Math.floor(diffDay / 30)
const diffYear = Math.floor(diffDay / 365)
const isToday = date.toDateString() === now.toDateString()
if (isToday) {
if (diffMin < 1) {
return '<span class="text-pink-600 font-medium flex items-center"><i class="fas fa-circle-dot text-xs mr-1 animate-pulse"></i>刚刚更新</span>'
} else if (diffHour < 1) {
return `<span class="text-pink-600 font-medium flex items-center"><i class="fas fa-circle-dot text-xs mr-1 animate-pulse"></i>${diffMin}分钟前</span>`
} else {
return `<span class="text-pink-600 font-medium flex items-center"><i class="fas fa-circle-dot text-xs mr-1 animate-pulse"></i>${diffHour}小时前</span>`
}
} else if (diffDay < 1) {
return `${diffHour}小时前`
} else if (diffDay < 7) {
return `${diffDay}天前`
} else if (diffWeek < 4) {
return `${diffWeek}周前`
} else if (diffMonth < 12) {
return `${diffMonth}个月前`
} else {
return `${diffYear}年前`
}
}
// 检查是否为今天更新
const isUpdatedToday = (dateString: string) => {
const date = new Date(dateString)
const now = new Date()
return date.toDateString() === now.toDateString()
}
// 数字动画效果
const animateCounters = () => {
const counters = document.querySelectorAll('.count-up')
const speed = 200
counters.forEach((counter: Element) => {
const target = parseInt(counter.getAttribute('data-target') || '0')
const increment = Math.ceil(target / speed)
let count = 0
const updateCount = () => {
if (count < target) {
count += increment
if (count > target) count = target
counter.textContent = count.toString()
setTimeout(updateCount, 1)
} else {
counter.textContent = target.toString()
}
}
updateCount()
})
}
// 页面跳转
const goToPage = (page: number) => {
currentPage.value = page
handleSearch()
window.scrollTo({
top: 0,
behavior: 'smooth'
})
}
// 编辑资源
@@ -288,10 +450,24 @@ const handleSaveResource = async (resourceData: any) => {
</script>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
.active-filter {
@apply bg-slate-800 text-white;
}
.count-up {
transition: all 0.3s ease;
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
</style>

View File

@@ -0,0 +1,269 @@
<template>
<div class="min-h-screen bg-gray-50 text-gray-800 p-3 sm:p-5">
<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">
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
<NuxtLink to="/" class="text-white hover:text-gray-200 no-underline">网盘资源管理系统</NuxtLink>
</h1>
<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>
<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>
</nav>
</div>
<!-- 批量添加模态框 -->
<div v-if="showAddModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl p-6 max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold">批量添加待处理资源</h3>
<button @click="closeModal" class="text-gray-500 hover:text-gray-800">
<i class="fas fa-times"></i>
</button>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">输入格式说明</label>
<div class="bg-gray-50 p-3 rounded text-sm text-gray-600 mb-4">
<p class="mb-2"><strong>格式1</strong>标题和URL两行一组</p>
<pre class="bg-white p-2 rounded border text-xs">
电影标题1
https://pan.baidu.com/s/123456
电影标题2
https://pan.baidu.com/s/789012</pre>
<p class="mt-2 mb-2"><strong>格式2</strong>只有URL系统自动判断</p>
<pre class="bg-white p-2 rounded border text-xs">
https://pan.baidu.com/s/123456
https://pan.baidu.com/s/789012
https://pan.baidu.com/s/345678</pre>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">资源内容</label>
<textarea
v-model="resourceText"
rows="15"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="请输入资源内容,支持两种格式..."
></textarea>
</div>
<div class="flex justify-end gap-2">
<button @click="closeModal" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
取消
</button>
<button @click="handleBatchAdd" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
批量添加
</button>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-900">待处理资源管理</h2>
<div class="flex gap-2">
<button
@click="refreshData"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center gap-2"
>
<i class="fas fa-refresh"></i> 刷新
</button>
<button
@click="clearAll"
class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 flex items-center gap-2"
>
<i class="fas fa-trash"></i> 清空全部
</button>
</div>
</div>
<!-- 资源列表 -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="bg-slate-800 text-white">
<th class="px-4 py-3 text-left text-sm">ID</th>
<th class="px-4 py-3 text-left text-sm">标题</th>
<th class="px-4 py-3 text-left text-sm">URL</th>
<th class="px-4 py-3 text-left text-sm">创建时间</th>
<th class="px-4 py-3 text-left text-sm">IP地址</th>
<th class="px-4 py-3 text-left text-sm">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-if="loading" class="text-center py-8">
<td colspan="6" class="text-gray-500">
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
</td>
</tr>
<tr v-else-if="readyResources.length === 0" class="text-center py-8">
<td colspan="6" class="text-gray-500">暂无待处理资源</td>
</tr>
<tr
v-for="resource in readyResources"
:key="resource.id"
class="hover:bg-gray-50"
>
<td class="px-4 py-3 text-sm text-gray-900">{{ resource.id }}</td>
<td class="px-4 py-3 text-sm text-gray-900">
<span v-if="resource.title">{{ resource.title }}</span>
<span v-else class="text-gray-400 italic">未设置</span>
</td>
<td class="px-4 py-3 text-sm">
<a
:href="resource.url"
target="_blank"
class="text-blue-600 hover:text-blue-800 hover:underline break-all"
>
{{ resource.url }}
</a>
</td>
<td class="px-4 py-3 text-sm text-gray-500">
{{ formatTime(resource.create_time) }}
</td>
<td class="px-4 py-3 text-sm text-gray-500">
{{ resource.ip || '-' }}
</td>
<td class="px-4 py-3 text-sm">
<button
@click="deleteResource(resource.id)"
class="text-red-600 hover:text-red-800"
>
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 统计信息 -->
<div class="mt-4 text-sm text-gray-600">
{{ readyResources.length }} 个待处理资源
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface ReadyResource {
id: number
title?: string
url: string
create_time: string
ip?: string
}
const readyResources = ref<ReadyResource[]>([])
const loading = ref(false)
const showAddModal = ref(false)
const resourceText = ref('')
// 获取待处理资源API
const { useReadyResourceApi } = await import('~/composables/useApi')
const readyResourceApi = useReadyResourceApi()
// 获取数据
const fetchData = async () => {
loading.value = true
try {
const response = await readyResourceApi.getReadyResources() as any
readyResources.value = response.resources || []
} catch (error) {
console.error('获取待处理资源失败:', error)
} finally {
loading.value = false
}
}
// 刷新数据
const refreshData = () => {
fetchData()
}
// 关闭模态框
const closeModal = () => {
showAddModal.value = false
resourceText.value = ''
}
// 批量添加
const handleBatchAdd = async () => {
if (!resourceText.value.trim()) {
alert('请输入资源内容')
return
}
try {
const response = await readyResourceApi.createReadyResourcesFromText(resourceText.value) as any
console.log('批量添加成功:', response)
closeModal()
fetchData()
alert(`成功添加 ${response.count} 个资源`)
} catch (error) {
console.error('批量添加失败:', error)
alert('批量添加失败,请检查输入格式')
}
}
// 删除资源
const deleteResource = async (id: number) => {
if (!confirm('确定要删除这个待处理资源吗?')) {
return
}
try {
await readyResourceApi.deleteReadyResource(id)
fetchData()
} catch (error) {
console.error('删除失败:', error)
alert('删除失败')
}
}
// 清空全部
const clearAll = async () => {
if (!confirm('确定要清空所有待处理资源吗?此操作不可恢复!')) {
return
}
try {
const response = await readyResourceApi.clearReadyResources() as any
console.log('清空成功:', response)
fetchData()
alert(`成功清空 ${response.deleted_count} 个资源`)
} catch (error) {
console.error('清空失败:', error)
alert('清空失败')
}
}
// 格式化时间
const formatTime = (timeString: string) => {
const date = new Date(timeString)
return date.toLocaleString('zh-CN')
}
// 页面加载时获取数据
onMounted(() => {
fetchData()
})
</script>
<style scoped>
/* 可以添加自定义样式 */
</style>

9536
web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff