mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 11:29:37 +08:00
update ui
This commit is contained in:
211
doc/UI_FRAMEWORKS.md
Normal file
211
doc/UI_FRAMEWORKS.md
Normal 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
12
handlers/base.go
Normal 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
|
||||
}
|
||||
@@ -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": "分类删除成功"})
|
||||
}
|
||||
114
handlers/category_handler.go
Normal file
114
handlers/category_handler.go
Normal 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
133
handlers/cks_handler.go
Normal 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
135
handlers/pan_handler.go
Normal 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)
|
||||
}
|
||||
164
handlers/ready_resource_handler.go
Normal file
164
handlers/ready_resource_handler.go
Normal 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),
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
255
handlers/resource_handler.go
Normal file
255
handlers/resource_handler.go
Normal 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
46
handlers/stats_handler.go
Normal 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
142
handlers/tag_handler.go
Normal 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)
|
||||
}
|
||||
@@ -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
69
models/pan.go
Normal 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"`
|
||||
}
|
||||
@@ -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
1
web/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
registry=https://repo.huaweicloud.com/repository/npm/
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
@@ -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
308
web/pages/admin.vue
Normal 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>
|
||||
@@ -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>
|
||||
269
web/pages/ready-resources.vue
Normal file
269
web/pages/ready-resources.vue
Normal 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
9536
web/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user