mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
update: ui
This commit is contained in:
189
doc/API_STANDARDS.md
Normal file
189
doc/API_STANDARDS.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# API 响应格式标准化
|
||||
|
||||
## 概述
|
||||
|
||||
为了统一API响应格式,提高前后端协作效率,所有API接口都使用标准化的响应格式。
|
||||
|
||||
## 标准响应格式
|
||||
|
||||
### 基础响应结构
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "操作成功",
|
||||
"data": {},
|
||||
"error": "",
|
||||
"pagination": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
|
||||
- `success`: 布尔值,表示操作是否成功
|
||||
- `message`: 字符串,成功时的提示信息(可选)
|
||||
- `data`: 对象/数组,返回的数据内容(可选)
|
||||
- `error`: 字符串,错误信息(仅在失败时返回)
|
||||
- `pagination`: 对象,分页信息(仅在分页接口时返回)
|
||||
|
||||
## 分页响应格式
|
||||
|
||||
### 分页信息结构
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"page_size": 100,
|
||||
"total": 1002,
|
||||
"total_pages": 11
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 分页参数
|
||||
|
||||
- `page`: 当前页码(从1开始)
|
||||
- `page_size`: 每页条数
|
||||
- `total`: 总记录数
|
||||
- `total_pages`: 总页数
|
||||
|
||||
## 响应类型
|
||||
|
||||
### 1. 成功响应
|
||||
|
||||
```go
|
||||
// 普通成功响应
|
||||
SuccessResponse(c, data, "操作成功")
|
||||
|
||||
// 简单成功响应(无数据)
|
||||
SimpleSuccessResponse(c, "操作成功")
|
||||
|
||||
// 创建成功响应
|
||||
CreatedResponse(c, data, "创建成功")
|
||||
```
|
||||
|
||||
### 2. 错误响应
|
||||
|
||||
```go
|
||||
// 错误响应
|
||||
ErrorResponse(c, http.StatusBadRequest, "参数错误")
|
||||
```
|
||||
|
||||
### 3. 分页响应
|
||||
|
||||
```go
|
||||
// 分页响应
|
||||
PaginatedResponse(c, data, page, pageSize, total)
|
||||
```
|
||||
|
||||
## 接口示例
|
||||
|
||||
### 获取待处理资源列表
|
||||
|
||||
**请求:**
|
||||
```
|
||||
GET /api/ready-resources?page=1&page_size=100
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "示例资源",
|
||||
"url": "https://example.com",
|
||||
"create_time": "2024-01-01T00:00:00Z",
|
||||
"ip": "127.0.0.1"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"page_size": 100,
|
||||
"total": 1002,
|
||||
"total_pages": 11
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 创建待处理资源
|
||||
|
||||
**请求:**
|
||||
```
|
||||
POST /api/ready-resources
|
||||
{
|
||||
"title": "新资源",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "待处理资源创建成功",
|
||||
"data": {
|
||||
"id": 1003
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应示例
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "参数错误:标题不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
## 前端调用示例
|
||||
|
||||
### 获取分页数据
|
||||
|
||||
```typescript
|
||||
const response = await api.getReadyResources({
|
||||
page: 1,
|
||||
page_size: 100
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
const resources = response.data
|
||||
const pagination = response.pagination
|
||||
// 处理数据
|
||||
}
|
||||
```
|
||||
|
||||
### 处理错误
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const response = await api.createResource(data)
|
||||
if (response.success) {
|
||||
// 成功处理
|
||||
}
|
||||
} catch (error) {
|
||||
// 网络错误等
|
||||
}
|
||||
```
|
||||
|
||||
## 实施规范
|
||||
|
||||
1. **所有新接口**必须使用标准化响应格式
|
||||
2. **现有接口**逐步迁移到标准化格式
|
||||
3. **错误处理**统一使用ErrorResponse
|
||||
4. **分页接口**必须使用PaginatedResponse
|
||||
5. **前端调用**统一处理success字段
|
||||
|
||||
## 迁移计划
|
||||
|
||||
1. ✅ 待处理资源接口(ready-resources)
|
||||
2. 🔄 资源管理接口(resources)
|
||||
3. 🔄 分类管理接口(categories)
|
||||
4. 🔄 用户管理接口(users)
|
||||
5. 🔄 统计接口(stats)
|
||||
@@ -14,21 +14,38 @@ import (
|
||||
|
||||
// GetReadyResources 获取待处理资源列表
|
||||
func GetReadyResources(c *gin.Context) {
|
||||
resources, err := repoManager.ReadyResourceRepository.FindAll()
|
||||
// 获取分页参数
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
pageSizeStr := c.DefaultQuery("page_size", "100")
|
||||
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
pageSize, err := strconv.Atoi(pageSizeStr)
|
||||
if err != nil || pageSize < 1 || pageSize > 1000 {
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
resources, total, err := repoManager.ReadyResourceRepository.FindWithPagination(page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
responses := converter.ToReadyResourceResponseList(resources)
|
||||
c.JSON(http.StatusOK, responses)
|
||||
|
||||
// 使用标准化的分页响应格式
|
||||
PaginatedResponse(c, responses, page, pageSize, total)
|
||||
}
|
||||
|
||||
// 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()})
|
||||
ErrorResponse(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -40,21 +57,18 @@ func CreateReadyResource(c *gin.Context) {
|
||||
|
||||
err := repoManager.ReadyResourceRepository.Create(resource)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": resource.ID,
|
||||
"message": "待处理资源创建成功",
|
||||
})
|
||||
CreatedResponse(c, gin.H{"id": resource.ID}, "待处理资源创建成功")
|
||||
}
|
||||
|
||||
// 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()})
|
||||
ErrorResponse(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,21 +84,18 @@ func BatchCreateReadyResources(c *gin.Context) {
|
||||
|
||||
err := repoManager.ReadyResourceRepository.BatchCreate(resources)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "批量创建成功",
|
||||
"count": len(resources),
|
||||
})
|
||||
CreatedResponse(c, gin.H{"count": len(resources)}, "批量创建成功")
|
||||
}
|
||||
|
||||
// CreateReadyResourcesFromText 从文本创建待处理资源
|
||||
func CreateReadyResourcesFromText(c *gin.Context) {
|
||||
text := c.PostForm("text")
|
||||
if text == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "文本内容不能为空"})
|
||||
ErrorResponse(c, http.StatusBadRequest, "文本内容不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -107,20 +118,17 @@ func CreateReadyResourcesFromText(c *gin.Context) {
|
||||
}
|
||||
|
||||
if len(resources) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "未找到有效的URL"})
|
||||
ErrorResponse(c, http.StatusBadRequest, "未找到有效的URL")
|
||||
return
|
||||
}
|
||||
|
||||
err := repoManager.ReadyResourceRepository.BatchCreate(resources)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "从文本创建成功",
|
||||
"count": len(resources),
|
||||
})
|
||||
CreatedResponse(c, gin.H{"count": len(resources)}, "从文本创建成功")
|
||||
}
|
||||
|
||||
// DeleteReadyResource 删除待处理资源
|
||||
@@ -128,37 +136,34 @@ 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"})
|
||||
ErrorResponse(c, http.StatusBadRequest, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.ReadyResourceRepository.Delete(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "待处理资源删除成功"})
|
||||
SimpleSuccessResponse(c, "待处理资源删除成功")
|
||||
}
|
||||
|
||||
// ClearReadyResources 清空所有待处理资源
|
||||
func ClearReadyResources(c *gin.Context) {
|
||||
resources, err := repoManager.ReadyResourceRepository.FindAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, http.StatusInternalServerError, 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()})
|
||||
ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "所有待处理资源已清空",
|
||||
"count": len(resources),
|
||||
})
|
||||
SuccessResponse(c, gin.H{"deleted_count": len(resources)}, "所有待处理资源已清空")
|
||||
}
|
||||
|
||||
72
handlers/response.go
Normal file
72
handlers/response.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// StandardResponse 标准化响应结构
|
||||
type StandardResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Pagination *PaginationInfo `json:"pagination,omitempty"`
|
||||
}
|
||||
|
||||
// PaginationInfo 分页信息
|
||||
type PaginationInfo struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Total int64 `json:"total"`
|
||||
TotalPages int64 `json:"total_pages"`
|
||||
}
|
||||
|
||||
// SuccessResponse 成功响应
|
||||
func SuccessResponse(c *gin.Context, data interface{}, message string) {
|
||||
c.JSON(200, StandardResponse{
|
||||
Success: true,
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorResponse 错误响应
|
||||
func ErrorResponse(c *gin.Context, statusCode int, message string) {
|
||||
c.JSON(statusCode, StandardResponse{
|
||||
Success: false,
|
||||
Error: message,
|
||||
})
|
||||
}
|
||||
|
||||
// PaginatedResponse 分页响应
|
||||
func PaginatedResponse(c *gin.Context, data interface{}, page, pageSize int, total int64) {
|
||||
totalPages := (total + int64(pageSize) - 1) / int64(pageSize)
|
||||
|
||||
c.JSON(200, StandardResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Pagination: &PaginationInfo{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Total: total,
|
||||
TotalPages: totalPages,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SimpleSuccessResponse 简单成功响应
|
||||
func SimpleSuccessResponse(c *gin.Context, message string) {
|
||||
c.JSON(200, StandardResponse{
|
||||
Success: true,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// CreatedResponse 创建成功响应
|
||||
func CreatedResponse(c *gin.Context, data interface{}, message string) {
|
||||
c.JSON(201, StandardResponse{
|
||||
Success: true,
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
4
main.go
4
main.go
@@ -69,14 +69,14 @@ func main() {
|
||||
api.GET("/stats", handlers.GetStats)
|
||||
|
||||
// 平台管理
|
||||
api.GET("/pans", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetPans)
|
||||
api.GET("/pans", handlers.GetPans)
|
||||
api.POST("/pans", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreatePan)
|
||||
api.PUT("/pans/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdatePan)
|
||||
api.DELETE("/pans/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeletePan)
|
||||
api.GET("/pans/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetPan)
|
||||
|
||||
// Cookie管理
|
||||
api.GET("/cks", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetCks)
|
||||
api.GET("/cks", handlers.GetCks)
|
||||
api.POST("/cks", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateCks)
|
||||
api.PUT("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateCks)
|
||||
api.DELETE("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteCks)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div class="p-6">
|
||||
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-2xl w-full mx-4" style="height:600px;">
|
||||
<div class="p-6 h-full flex flex-col text-gray-900 dark:text-gray-100">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900">
|
||||
{{ resource ? '编辑资源' : '添加资源' }}
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
添加资源
|
||||
</h2>
|
||||
<button @click="$emit('close')" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -13,172 +13,172 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||
<!-- 标题 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">标题 *</label>
|
||||
<input
|
||||
v-model="form.title"
|
||||
type="text"
|
||||
required
|
||||
class="input-field"
|
||||
placeholder="输入资源标题"
|
||||
/>
|
||||
</div>
|
||||
<!-- Tab 切换 -->
|
||||
<div class="flex mb-6 border-b flex-shrink-0">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
:class="['px-4 py-2 -mb-px border-b-2', mode === tab.value ? 'border-blue-500 text-blue-600 font-bold' : 'border-transparent text-gray-500']"
|
||||
@click="mode = tab.value"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">描述</label>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
rows="3"
|
||||
class="input-field"
|
||||
placeholder="输入资源描述"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- URL -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">URL</label>
|
||||
<input
|
||||
v-model="form.url"
|
||||
type="url"
|
||||
class="input-field"
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 分类 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">分类</label>
|
||||
<select v-model="form.category_id" class="input-field">
|
||||
<option value="">选择分类</option>
|
||||
<option v-for="category in categories" :key="category.id" :value="category.id">
|
||||
{{ category.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">标签</label>
|
||||
<div class="flex flex-wrap gap-2 mb-2">
|
||||
<span
|
||||
v-for="tag in form.tags"
|
||||
:key="tag"
|
||||
class="px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full flex items-center"
|
||||
>
|
||||
{{ tag }}
|
||||
<button
|
||||
@click="removeTag(tag)"
|
||||
type="button"
|
||||
class="ml-2 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
<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="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newTag"
|
||||
@keyup.enter="addTag"
|
||||
type="text"
|
||||
class="input-field flex-1"
|
||||
placeholder="输入标签后按回车"
|
||||
/>
|
||||
<button @click="addTag" type="button" class="btn-secondary">
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件信息 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">文件路径</label>
|
||||
<input
|
||||
v-model="form.file_path"
|
||||
type="text"
|
||||
class="input-field"
|
||||
placeholder="/path/to/file"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">文件类型</label>
|
||||
<input
|
||||
v-model="form.file_type"
|
||||
type="text"
|
||||
class="input-field"
|
||||
placeholder="pdf, doc, mp4..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">文件大小 (字节)</label>
|
||||
<input
|
||||
v-model.number="form.file_size"
|
||||
type="number"
|
||||
class="input-field"
|
||||
placeholder="1024"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">是否公开</label>
|
||||
<div class="flex items-center mt-2">
|
||||
<input
|
||||
v-model="form.is_public"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label class="ml-2 text-sm text-gray-700">公开显示</label>
|
||||
<!-- 内容区域 -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<!-- 批量添加 -->
|
||||
<div v-if="mode === 'batch'">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">输入格式说明:</label>
|
||||
<div class="bg-gray-50 dark:bg-gray-800 p-3 rounded text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||
<p class="mb-2"><strong>格式1:</strong>标题和URL两行一组</p>
|
||||
<pre class="bg-white dark:bg-gray-800 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 dark:bg-gray-800 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 dark:text-gray-300 mb-2">资源内容:</label>
|
||||
<textarea
|
||||
v-model="batchInput"
|
||||
rows="15"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-900 dark:text-gray-100"
|
||||
placeholder="请输入资源内容,支持两种格式..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<div class="flex justify-end space-x-3 pt-6 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
class="btn-secondary"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-primary"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ loading ? '保存中...' : (resource ? '更新' : '创建') }}
|
||||
</button>
|
||||
<!-- 单个添加 -->
|
||||
<div v-else-if="mode === 'single'" class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">标题</label>
|
||||
<input v-model="form.title" class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700" placeholder="输入标题" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">描述</label>
|
||||
<textarea v-model="form.description" rows="3" class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700" placeholder="输入资源描述"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">类型</label>
|
||||
<select v-model="form.file_type" class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700">
|
||||
<option value="">选择类型</option>
|
||||
<option value="pan">网盘</option>
|
||||
<option value="link">直链</option>
|
||||
<option value="other">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">标签</label>
|
||||
<div class="flex flex-wrap gap-2 mb-2">
|
||||
<span v-for="tag in form.tags" :key="tag" class="bg-blue-100 text-blue-700 px-2 py-1 rounded text-xs flex items-center">
|
||||
{{ tag }}
|
||||
<button type="button" class="ml-1 text-xs" @click="removeTag(tag)">×</button>
|
||||
</span>
|
||||
</div>
|
||||
<input v-model="newTag" @keyup.enter.prevent="addTag" class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700" placeholder="输入标签后回车添加" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">链接(可多行,每行一个链接)</label>
|
||||
<textarea v-model="form.url" rows="3" class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700" placeholder="https://a.com https://b.com"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- API说明 -->
|
||||
<div v-else class="space-y-4">
|
||||
<div class="text-gray-700 dark:text-gray-300 text-sm">
|
||||
<p>你可以通过API批量添加资源:</p>
|
||||
<pre class="bg-gray-100 dark:bg-gray-800 p-3 rounded text-xs overflow-x-auto mt-2">
|
||||
POST /api/resources/batch
|
||||
Content-Type: application/json
|
||||
Body:
|
||||
[
|
||||
{ "title": "资源A", "url": "https://a.com", "file_type": "pan", ... },
|
||||
{ "title": "资源B", "url": "https://b.com", ... }
|
||||
]
|
||||
</pre>
|
||||
<p>参数说明:<br/>
|
||||
title: 标题<br/>
|
||||
url: 资源链接<br/>
|
||||
file_type: 类型(pan/link/other)<br/>
|
||||
tags: 标签数组(可选)<br/>
|
||||
description: 描述(可选)<br/>
|
||||
... 其他字段参考文档
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成功/失败提示 -->
|
||||
<SuccessToast v-if="showSuccess" :message="successMsg" @close="showSuccess = false" />
|
||||
<ErrorToast v-if="showError" :message="errorMsg" @close="showError = false" />
|
||||
</div>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<div class="flex-shrink-0 pt-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900/90 sticky bottom-0 left-0 w-full flex justify-end space-x-3 z-10 backdrop-blur">
|
||||
<template v-if="mode === 'batch'">
|
||||
<button type="button" @click="$emit('close')" class="btn-secondary">取消</button>
|
||||
<button type="button" @click="handleBatchSubmit" class="btn-primary" :disabled="loading">
|
||||
{{ loading ? '保存中...' : '批量添加' }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="mode === 'single'">
|
||||
<button type="button" @click="$emit('close')" class="btn-secondary">取消</button>
|
||||
<button type="button" @click="handleSingleSubmit" class="btn-primary" :disabled="loading">
|
||||
{{ loading ? '保存中...' : '添加' }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button type="button" @click="$emit('close')" class="btn-secondary">关闭</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useResourceStore } from '~/stores/resource'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import SuccessToast from './SuccessToast.vue'
|
||||
import ErrorToast from './ErrorToast.vue'
|
||||
import { useReadyResourceApi } from '~/composables/useApi'
|
||||
|
||||
const store = useResourceStore()
|
||||
const { categories } = storeToRefs(store)
|
||||
|
||||
const props = defineProps<{
|
||||
resource?: any
|
||||
}>()
|
||||
|
||||
const props = defineProps<{ resource?: any }>()
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const loading = ref(false)
|
||||
const newTag = ref('')
|
||||
const showSuccess = ref(false)
|
||||
const showError = ref(false)
|
||||
const successMsg = ref('')
|
||||
const errorMsg = ref('')
|
||||
|
||||
const tabs = [
|
||||
{ label: '批量添加', value: 'batch' },
|
||||
{ label: '单个添加', value: 'single' },
|
||||
{ label: 'API说明', value: 'api' },
|
||||
]
|
||||
const mode = ref('batch')
|
||||
|
||||
// 批量添加
|
||||
const batchInput = ref('')
|
||||
|
||||
// 单个添加表单
|
||||
const form = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
url: '',
|
||||
url: '', // 多行
|
||||
category_id: '',
|
||||
tags: [] as string[],
|
||||
file_path: '',
|
||||
@@ -187,7 +187,8 @@ const form = ref({
|
||||
is_public: true,
|
||||
})
|
||||
|
||||
// 初始化表单
|
||||
const readyResourceApi = useReadyResourceApi()
|
||||
|
||||
onMounted(() => {
|
||||
if (props.resource) {
|
||||
form.value = {
|
||||
@@ -204,7 +205,6 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 添加标签
|
||||
const addTag = () => {
|
||||
const tag = newTag.value.trim()
|
||||
if (tag && !form.value.tags.includes(tag)) {
|
||||
@@ -213,7 +213,6 @@ const addTag = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 移除标签
|
||||
const removeTag = (tag: string) => {
|
||||
const index = form.value.tags.indexOf(tag)
|
||||
if (index > -1) {
|
||||
@@ -221,19 +220,54 @@ const removeTag = (tag: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
// 批量添加提交
|
||||
const handleBatchSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = {
|
||||
...form.value,
|
||||
category_id: form.value.category_id ? parseInt(form.value.category_id) : null,
|
||||
}
|
||||
emit('save', data)
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
if (!batchInput.value.trim()) throw new Error('请输入资源内容')
|
||||
const res: any = await readyResourceApi.createReadyResourcesFromText(batchInput.value)
|
||||
showSuccess.value = true
|
||||
successMsg.value = `成功添加 ${res.count || 0} 个资源,资源已进入待处理列表,处理完成后会自动入库`
|
||||
batchInput.value = ''
|
||||
} catch (e: any) {
|
||||
showError.value = true
|
||||
errorMsg.value = e.message || '批量添加失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
// 单个添加提交
|
||||
const handleSingleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 多行链接
|
||||
const urls = form.value.url.split(/\r?\n/).map(l => l.trim()).filter(Boolean)
|
||||
if (!urls.length) throw new Error('请输入至少一个链接')
|
||||
for (const url of urls) {
|
||||
await store.createResource({
|
||||
...form.value,
|
||||
url,
|
||||
tags: [...form.value.tags],
|
||||
})
|
||||
}
|
||||
showSuccess.value = true
|
||||
successMsg.value = '资源已进入待处理列表,处理完成后会自动入库'
|
||||
// 清空表单
|
||||
form.value.title = ''
|
||||
form.value.description = ''
|
||||
form.value.url = ''
|
||||
form.value.tags = []
|
||||
form.value.file_type = ''
|
||||
} catch (e: any) {
|
||||
showError.value = true
|
||||
errorMsg.value = e.message || '添加失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可以添加自定义样式 */
|
||||
</style>
|
||||
@@ -352,9 +352,10 @@ export const useReadyResourceApi = () => {
|
||||
return userStore.authHeaders
|
||||
}
|
||||
|
||||
const getReadyResources = async () => {
|
||||
const getReadyResources = async (params?: any) => {
|
||||
return await $fetch('/ready-resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,19 +1,44 @@
|
||||
<template>
|
||||
<n-config-provider :theme="theme">
|
||||
<n-loading-bar-provider>
|
||||
<n-dialog-provider>
|
||||
<n-notification-provider>
|
||||
<n-message-provider>
|
||||
<slot />
|
||||
</n-message-provider>
|
||||
</n-notification-provider>
|
||||
</n-dialog-provider>
|
||||
</n-loading-bar-provider>
|
||||
</n-config-provider>
|
||||
<div>
|
||||
<button
|
||||
class="fixed top-4 right-4 z-50 w-12 h-12 flex items-center justify-center rounded-full shadow-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 transition-all duration-200 hover:bg-blue-100 dark:hover:bg-blue-900 hover:scale-110 focus:outline-none"
|
||||
@click="toggleDarkMode"
|
||||
aria-label="切换明暗模式"
|
||||
>
|
||||
<span class="text-2xl transition-transform duration-300" :class="isDark ? 'rotate-0' : 'rotate-180'">
|
||||
<svg v-if="isDark" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-7 h-7">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12.79A9 9 0 1111.21 3a7 7 0 109.79 9.79z" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-7 h-7">
|
||||
<circle cx="12" cy="12" r="5" stroke="currentColor" stroke-width="2" />
|
||||
<path stroke-linecap="round" stroke-width="2" d="M12 1v2m0 18v2m11-11h-2M3 12H1m16.95 6.95l-1.41-1.41M6.46 6.46L5.05 5.05m12.02 0l-1.41 1.41M6.46 17.54l-1.41 1.41" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { lightTheme } from 'naive-ui'
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const theme = lightTheme
|
||||
const isDark = ref(false)
|
||||
const toggleDarkMode = () => {
|
||||
isDark.value = !isDark.value
|
||||
if (isDark.value) {
|
||||
document.documentElement.classList.add('dark')
|
||||
localStorage.setItem('theme', 'dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
localStorage.setItem('theme', 'light')
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
if (localStorage.getItem('theme') === 'dark') {
|
||||
isDark.value = true
|
||||
document.documentElement.classList.add('dark')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 text-gray-800 p-3 sm:p-5">
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 头部 -->
|
||||
<div class="bg-slate-800 text-white rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center">
|
||||
@@ -39,26 +39,26 @@
|
||||
<!-- 管理功能区域 -->
|
||||
<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="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="p-3 bg-blue-100 rounded-lg">
|
||||
<i class="fas fa-cloud text-blue-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">资源管理</h3>
|
||||
<p class="text-sm text-gray-600">管理所有资源</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">资源管理</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">管理所有资源</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<button @click="goToResourceManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="goToResourceManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">查看所有资源</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">查看所有资源</span>
|
||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="showAddResourceModal = true" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="showAddResourceModal = true" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">添加新资源</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">添加新资源</span>
|
||||
<i class="fas fa-plus text-gray-400"></i>
|
||||
</div>
|
||||
</button>
|
||||
@@ -66,26 +66,26 @@
|
||||
</div>
|
||||
|
||||
<!-- 平台管理 -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="p-3 bg-green-100 rounded-lg">
|
||||
<i class="fas fa-server text-green-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">平台管理</h3>
|
||||
<p class="text-sm text-gray-600">管理网盘平台</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">平台管理</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">管理网盘平台</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<button @click="goToPlatformManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="goToPlatformManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">管理平台</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理平台</span>
|
||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="showAddPlatformModal = true" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="showAddPlatformModal = true" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">添加平台</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">添加平台</span>
|
||||
<i class="fas fa-plus text-gray-400"></i>
|
||||
</div>
|
||||
</button>
|
||||
@@ -93,26 +93,26 @@
|
||||
</div>
|
||||
|
||||
<!-- 分类管理 -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="p-3 bg-purple-100 rounded-lg">
|
||||
<i class="fas fa-folder text-purple-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">分类管理</h3>
|
||||
<p class="text-sm text-gray-600">管理资源分类</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">分类管理</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">管理资源分类</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<button @click="goToCategoryManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="goToCategoryManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">管理分类</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理分类</span>
|
||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="showAddCategoryModal = true" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="showAddCategoryModal = true" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">添加分类</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">添加分类</span>
|
||||
<i class="fas fa-plus text-gray-400"></i>
|
||||
</div>
|
||||
</button>
|
||||
@@ -120,26 +120,26 @@
|
||||
</div>
|
||||
|
||||
<!-- 标签管理 -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="p-3 bg-orange-100 rounded-lg">
|
||||
<i class="fas fa-tags text-orange-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">标签管理</h3>
|
||||
<p class="text-sm text-gray-600">管理资源标签</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">标签管理</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">管理资源标签</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<button @click="goToTagManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="goToTagManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">管理标签</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理标签</span>
|
||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="showAddTagModal = true" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="showAddTagModal = true" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">添加标签</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">添加标签</span>
|
||||
<i class="fas fa-plus text-gray-400"></i>
|
||||
</div>
|
||||
</button>
|
||||
@@ -147,53 +147,53 @@
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="p-3 bg-red-100 rounded-lg">
|
||||
<i class="fas fa-chart-bar text-red-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">统计信息</h3>
|
||||
<p class="text-sm text-gray-600">系统统计数据</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">统计信息</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">系统统计数据</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-gray-600">总资源数</span>
|
||||
<span class="text-lg font-semibold text-gray-900">{{ stats?.total_resources || 0 }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">总资源数</span>
|
||||
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ stats?.total_resources || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-gray-600">总浏览量</span>
|
||||
<span class="text-lg font-semibold text-gray-900">{{ stats?.total_views || 0 }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">总浏览量</span>
|
||||
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ stats?.total_views || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-gray-600">分类数量</span>
|
||||
<span class="text-lg font-semibold text-gray-900">{{ stats?.total_categories || 0 }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">分类数量</span>
|
||||
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ stats?.total_categories || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 待处理资源 -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="p-3 bg-yellow-100 rounded-lg">
|
||||
<i class="fas fa-clock text-yellow-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">待处理资源</h3>
|
||||
<p class="text-sm text-gray-600">批量添加和管理</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">待处理资源</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">批量添加和管理</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<NuxtLink to="/ready-resources" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors block">
|
||||
<NuxtLink to="/ready-resources" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">管理待处理资源</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">管理待处理资源</span>
|
||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<button @click="goToBatchAdd" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="goToBatchAdd" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">批量添加资源</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">批量添加资源</span>
|
||||
<i class="fas fa-plus text-gray-400"></i>
|
||||
</div>
|
||||
</button>
|
||||
@@ -201,54 +201,54 @@
|
||||
</div>
|
||||
|
||||
<!-- 搜索统计 -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="p-3 bg-indigo-100 rounded-lg">
|
||||
<i class="fas fa-search text-indigo-600 text-xl"></i>
|
||||
<div class="p-3 bg-indigo-100 dark:bg-indigo-900 rounded-lg">
|
||||
<i class="fas fa-search text-indigo-600 dark:text-indigo-300 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">搜索统计</h3>
|
||||
<p class="text-sm text-gray-600">搜索量分析和热门关键词</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">搜索统计</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">搜索量分析和热门关键词</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<NuxtLink to="/search-stats" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors block">
|
||||
<NuxtLink to="/search-stats" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">查看搜索统计</span>
|
||||
<i class="fas fa-chart-line text-gray-400"></i>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">查看搜索统计</span>
|
||||
<i class="fas fa-chart-line text-gray-400 dark:text-gray-300"></i>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<button @click="goToHotKeywords" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="goToHotKeywords" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">热门关键词</span>
|
||||
<i class="fas fa-fire text-gray-400"></i>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">热门关键词</span>
|
||||
<i class="fas fa-fire text-gray-400 dark:text-gray-300"></i>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统设置 -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="p-3 bg-gray-100 rounded-lg">
|
||||
<i class="fas fa-cog text-gray-600 text-xl"></i>
|
||||
<div class="p-3 bg-gray-100 dark:bg-gray-700 rounded-lg">
|
||||
<i class="fas fa-cog text-gray-600 dark:text-gray-300 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">系统设置</h3>
|
||||
<p class="text-sm text-gray-600">系统配置</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">系统设置</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">系统配置</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<button @click="goToSystemSettings" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="goToSystemSettings" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">系统配置</span>
|
||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">系统配置</span>
|
||||
<i class="fas fa-chevron-right text-gray-400 dark:text-gray-300"></i>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="goToUserManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="goToUserManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">用户管理</span>
|
||||
<i class="fas fa-users text-gray-400"></i>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">用户管理</span>
|
||||
<i class="fas fa-users text-gray-400 dark:text-gray-300"></i>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -262,6 +262,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ResourceModal from '~/components/ResourceModal.vue'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 text-gray-800 p-3 sm:p-5">
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
|
||||
<div class="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 relative">
|
||||
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
|
||||
<a href="/" class="text-white hover:text-gray-200 no-underline">网盘资源管理系统</a>
|
||||
<a href="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">网盘资源管理系统</a>
|
||||
</h1>
|
||||
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4 right-4 top-0 absolute">
|
||||
<NuxtLink
|
||||
@@ -31,7 +31,7 @@
|
||||
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"
|
||||
class="w-full px-4 py-3 rounded-full border-2 border-gray-300 dark:border-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 transition-all"
|
||||
placeholder="输入文件名或链接进行搜索..."
|
||||
/>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
@@ -42,7 +42,7 @@
|
||||
<!-- 平台类型筛选 -->
|
||||
<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"
|
||||
class="px-2 py-1 text-xs rounded-full bg-slate-800 dark:bg-gray-700 text-white dark:text-gray-100 active-filter"
|
||||
@click="filterByPlatform('')"
|
||||
>
|
||||
全部
|
||||
@@ -50,7 +50,7 @@
|
||||
<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"
|
||||
class="px-2 py-1 text-xs rounded-full bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-100 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
|
||||
@click="filterByPlatform(platform.id)"
|
||||
>
|
||||
{{ getPlatformIcon(platform.name) }} {{ platform.name }}
|
||||
@@ -58,7 +58,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="flex justify-between mt-3 text-sm text-gray-600 px-2">
|
||||
<div class="flex justify-between mt-3 text-sm text-gray-600 dark:text-gray-300 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>
|
||||
@@ -71,10 +71,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 资源列表 -->
|
||||
<div class="overflow-x-auto bg-white rounded-lg shadow">
|
||||
<div class="overflow-x-auto bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="bg-slate-800 text-white">
|
||||
<tr class="bg-slate-800 dark:bg-gray-700 text-white dark:text-gray-100">
|
||||
<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> 文件名
|
||||
@@ -86,17 +86,17 @@
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr v-if="loading" class="text-center py-8">
|
||||
<td colspan="3" class="text-gray-500">
|
||||
<td colspan="3" class="text-gray-500 dark:text-gray-400">
|
||||
<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>
|
||||
<td colspan="3" class="text-gray-500 dark:text-gray-400">暂无数据</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'"
|
||||
:class="isUpdatedToday(resource.updated_at) ? 'hover:bg-pink-50 dark:hover:bg-pink-900 bg-pink-50/30 dark:bg-pink-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm">
|
||||
<div class="flex items-start">
|
||||
@@ -114,7 +114,7 @@
|
||||
v-if="resource.showLink"
|
||||
:href="resource.url"
|
||||
target="_blank"
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline text-xs break-all"
|
||||
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline text-xs break-all"
|
||||
>
|
||||
{{ resource.url }}
|
||||
</a>
|
||||
@@ -131,7 +131,7 @@
|
||||
v-if="resource.showLink"
|
||||
:href="resource.url"
|
||||
target="_blank"
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline"
|
||||
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline"
|
||||
>
|
||||
{{ resource.url }}
|
||||
</a>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="bg-gray-50 min-h-screen flex items-center justify-center p-4">
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
||||
<div class="max-w-md w-full">
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 space-y-6">
|
||||
<div class="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-md w-full max-w-md text-gray-900 dark:text-gray-100">
|
||||
<div class="text-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900">管理员登录</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">管理员登录</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">请输入管理员账号密码</p>
|
||||
<div class="mt-3 p-3 bg-blue-50 rounded-lg">
|
||||
<p class="text-xs text-blue-700">
|
||||
@@ -15,26 +15,26 @@
|
||||
|
||||
<form @submit.prevent="handleLogin" class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700">用户名</label>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-100">用户名</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
:class="{ 'border-red-500': errors.username }"
|
||||
>
|
||||
<p v-if="errors.username" class="mt-1 text-sm text-red-600">{{ errors.username }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">密码</label>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-100">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
:class="{ 'border-red-500': errors.password }"
|
||||
>
|
||||
<p v-if="errors.password" class="mt-1 text-sm text-red-600">{{ errors.password }}</p>
|
||||
@@ -43,7 +43,7 @@
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="userStore.loading"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<span v-if="userStore.loading" class="inline-flex items-center">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 text-gray-800 p-3 sm:p-5">
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 头部 -->
|
||||
<div class="bg-slate-800 text-white rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center">
|
||||
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
|
||||
<NuxtLink to="/" class="text-white hover:text-gray-200 no-underline">网盘资源管理系统</NuxtLink>
|
||||
<NuxtLink to="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">网盘资源管理系统</NuxtLink>
|
||||
</h1>
|
||||
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4">
|
||||
<NuxtLink
|
||||
to="/"
|
||||
to="/admin"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
>
|
||||
<i class="fas fa-home"></i> 返回首页
|
||||
<i class="fas fa-arrow-left"></i> 返回
|
||||
</NuxtLink>
|
||||
<button
|
||||
@click="showAddModal = true"
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<!-- 批量添加模态框 -->
|
||||
<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="bg-white dark:bg-gray-900 rounded-lg shadow-xl p-6 max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto text-gray-900 dark:text-gray-100">
|
||||
<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">
|
||||
@@ -34,15 +34,15 @@
|
||||
|
||||
<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">
|
||||
<div class="bg-gray-50 dark:bg-gray-800 p-3 rounded text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||
<p class="mb-2"><strong>格式1:</strong>标题和URL两行一组</p>
|
||||
<pre class="bg-white p-2 rounded border text-xs">
|
||||
<pre class="bg-white dark:bg-gray-800 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">
|
||||
<pre class="bg-white dark:bg-gray-800 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>
|
||||
@@ -54,7 +54,7 @@ https://pan.baidu.com/s/345678</pre>
|
||||
<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"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
placeholder="请输入资源内容,支持两种格式..."
|
||||
></textarea>
|
||||
</div>
|
||||
@@ -90,57 +90,69 @@ https://pan.baidu.com/s/345678</pre>
|
||||
</div>
|
||||
|
||||
<!-- 资源列表 -->
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="bg-white dark:bg-gray-800 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>
|
||||
<thead class="bg-slate-800 dark:bg-gray-700 text-white dark:text-gray-100 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">ID</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">标题</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">URL</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">创建时间</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">IP地址</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 max-h-96 overflow-y-auto">
|
||||
<tr v-if="loading" class="text-center py-8">
|
||||
<td colspan="6" class="text-gray-500">
|
||||
<td colspan="6" class="text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="readyResources.length === 0" class="text-center py-8">
|
||||
<td colspan="6" class="text-gray-500">暂无待处理资源</td>
|
||||
<tr v-else-if="readyResources.length === 0">
|
||||
<td colspan="6">
|
||||
<div class="flex flex-col items-center justify-center py-12">
|
||||
<svg class="w-16 h-16 text-gray-300 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 48 48">
|
||||
<circle cx="24" cy="24" r="20" stroke-width="3" stroke-dasharray="6 6" />
|
||||
<path d="M16 24h16M24 16v16" stroke-width="3" stroke-linecap="round" />
|
||||
</svg>
|
||||
<div class="text-lg font-semibold text-gray-400 dark:text-gray-500 mb-2">暂无待处理资源</div>
|
||||
<div class="text-sm text-gray-400 dark:text-gray-600">你可以点击上方"批量添加"按钮快速导入资源</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="resource in readyResources"
|
||||
:key="resource.id"
|
||||
class="hover:bg-gray-50"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<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 class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 font-medium">{{ resource.id }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||||
<span v-if="resource.title" :title="resource.title">{{ escapeHtml(resource.title) }}</span>
|
||||
<span v-else class="text-gray-400 dark:text-gray-500 italic">未设置</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<a
|
||||
:href="resource.url"
|
||||
:href="checkUrlSafety(resource.url)"
|
||||
target="_blank"
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline break-all"
|
||||
rel="noopener noreferrer"
|
||||
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline break-all"
|
||||
:title="resource.url"
|
||||
>
|
||||
{{ resource.url }}
|
||||
{{ escapeHtml(resource.url) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500">
|
||||
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ formatTime(resource.create_time) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500">
|
||||
{{ resource.ip || '-' }}
|
||||
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ escapeHtml(resource.ip || '-') }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<button
|
||||
@click="deleteResource(resource.id)"
|
||||
class="text-red-600 hover:text-red-800"
|
||||
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||
title="删除此资源"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
@@ -151,9 +163,62 @@ https://pan.baidu.com/s/345678</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页组件 -->
|
||||
<div v-if="totalPages > 1" class="mt-6 flex justify-center">
|
||||
<div class="flex items-center space-x-4 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4">
|
||||
<!-- 总资源数 -->
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
共 <span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个待处理资源
|
||||
</div>
|
||||
|
||||
<div class="w-px h-6 bg-gray-300 dark:bg-gray-600"></div>
|
||||
|
||||
<!-- 上一页 -->
|
||||
<button
|
||||
@click="goToPage(currentPage - 1)"
|
||||
:disabled="currentPage <= 1"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-500 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
<span>上一页</span>
|
||||
</button>
|
||||
|
||||
<!-- 页码 -->
|
||||
<template v-for="page in visiblePages" :key="page">
|
||||
<button
|
||||
v-if="typeof page === 'number'"
|
||||
@click="goToPage(page)"
|
||||
:class="[
|
||||
'px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 min-w-[40px]',
|
||||
page === currentPage
|
||||
? 'bg-blue-600 text-white shadow-md'
|
||||
: 'text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
]"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
<span v-else class="px-3 py-2 text-sm text-gray-500">...</span>
|
||||
</template>
|
||||
|
||||
<!-- 下一页 -->
|
||||
<button
|
||||
@click="goToPage(currentPage + 1)"
|
||||
:disabled="currentPage >= totalPages"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-500 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center gap-2"
|
||||
>
|
||||
<span>下一页</span>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="mt-4 text-sm text-gray-600">
|
||||
共 {{ readyResources.length }} 个待处理资源
|
||||
<div v-if="totalPages <= 1" class="mt-4 text-center">
|
||||
<div class="inline-flex items-center bg-white dark:bg-gray-800 rounded-lg shadow px-6 py-3">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
共 <span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个待处理资源
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -173,6 +238,12 @@ const loading = ref(false)
|
||||
const showAddModal = ref(false)
|
||||
const resourceText = ref('')
|
||||
|
||||
// 分页相关状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(100)
|
||||
const totalCount = ref(0)
|
||||
const totalPages = ref(0)
|
||||
|
||||
// 获取待处理资源API
|
||||
const { useReadyResourceApi } = await import('~/composables/useApi')
|
||||
const readyResourceApi = useReadyResourceApi()
|
||||
@@ -181,8 +252,15 @@ const readyResourceApi = useReadyResourceApi()
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await readyResourceApi.getReadyResources() as any
|
||||
readyResources.value = response.resources || []
|
||||
const response = await readyResourceApi.getReadyResources({
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value
|
||||
}) as any
|
||||
|
||||
// 使用标准化的接口返回格式
|
||||
readyResources.value = response.data
|
||||
totalCount.value = response.pagination.total
|
||||
totalPages.value = response.pagination.total_pages
|
||||
} catch (error) {
|
||||
console.error('获取待处理资源失败:', error)
|
||||
} finally {
|
||||
@@ -190,6 +268,55 @@ const fetchData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到指定页面
|
||||
const goToPage = (page: number) => {
|
||||
if (page >= 1 && page <= totalPages.value) {
|
||||
currentPage.value = page
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
// 计算可见的页码
|
||||
const visiblePages = computed(() => {
|
||||
const pages: (number | string)[] = []
|
||||
const maxVisible = 5
|
||||
|
||||
if (totalPages.value <= maxVisible) {
|
||||
// 如果总页数不多,显示所有页码
|
||||
for (let i = 1; i <= totalPages.value; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
} else {
|
||||
// 如果总页数很多,显示部分页码
|
||||
if (currentPage.value <= 3) {
|
||||
// 当前页在前几页
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
pages.push('...')
|
||||
pages.push(totalPages.value)
|
||||
} else if (currentPage.value >= totalPages.value - 2) {
|
||||
// 当前页在后几页
|
||||
pages.push(1)
|
||||
pages.push('...')
|
||||
for (let i = totalPages.value - 3; i <= totalPages.value; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
} else {
|
||||
// 当前页在中间
|
||||
pages.push(1)
|
||||
pages.push('...')
|
||||
for (let i = currentPage.value - 1; i <= currentPage.value + 1; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
pages.push('...')
|
||||
pages.push(totalPages.value)
|
||||
}
|
||||
}
|
||||
|
||||
return pages
|
||||
})
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
fetchData()
|
||||
@@ -213,7 +340,7 @@ const handleBatchAdd = async () => {
|
||||
console.log('批量添加成功:', response)
|
||||
closeModal()
|
||||
fetchData()
|
||||
alert(`成功添加 ${response.count} 个资源`)
|
||||
alert(`成功添加 ${response.data.count} 个资源`)
|
||||
} catch (error) {
|
||||
console.error('批量添加失败:', error)
|
||||
alert('批量添加失败,请检查输入格式')
|
||||
@@ -228,6 +355,10 @@ const deleteResource = async (id: number) => {
|
||||
|
||||
try {
|
||||
await readyResourceApi.deleteReadyResource(id)
|
||||
// 如果当前页没有数据了,回到上一页
|
||||
if (readyResources.value.length === 1 && currentPage.value > 1) {
|
||||
currentPage.value--
|
||||
}
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
@@ -244,8 +375,9 @@ const clearAll = async () => {
|
||||
try {
|
||||
const response = await readyResourceApi.clearReadyResources() as any
|
||||
console.log('清空成功:', response)
|
||||
currentPage.value = 1 // 清空后回到第一页
|
||||
fetchData()
|
||||
alert(`成功清空 ${response.deleted_count} 个资源`)
|
||||
alert(`成功清空 ${response.data.deleted_count} 个资源`)
|
||||
} catch (error) {
|
||||
console.error('清空失败:', error)
|
||||
alert('清空失败')
|
||||
@@ -258,6 +390,29 @@ const formatTime = (timeString: string) => {
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 转义HTML防止XSS
|
||||
const escapeHtml = (text: string) => {
|
||||
if (!text) return text
|
||||
const div = document.createElement('div')
|
||||
div.textContent = text
|
||||
return div.innerHTML
|
||||
}
|
||||
|
||||
// 验证URL安全性
|
||||
const checkUrlSafety = (url: string) => {
|
||||
if (!url) return '#'
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
// 只允许http和https协议
|
||||
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
|
||||
return '#'
|
||||
}
|
||||
return url
|
||||
} catch {
|
||||
return '#'
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
@@ -265,5 +420,47 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可以添加自定义样式 */
|
||||
/* 表格滚动样式 */
|
||||
.overflow-x-auto {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 表格头部固定 */
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 分页按钮悬停效果 */
|
||||
.pagination-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 当前页码按钮效果 */
|
||||
.current-page {
|
||||
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3), 0 2px 4px -1px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
/* 表格行悬停效果 */
|
||||
tbody tr:hover {
|
||||
background-color: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
/* 暗黑模式下的表格行悬停 */
|
||||
.dark tbody tr:hover {
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* 统计信息卡片效果 */
|
||||
.stats-card {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.dark .stats-card {
|
||||
background-color: rgba(31, 41, 55, 0.9);
|
||||
}
|
||||
</style>
|
||||
@@ -1,60 +1,60 @@
|
||||
<template>
|
||||
<div class="bg-gray-50 min-h-screen flex items-center justify-center p-4">
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
||||
<div class="max-w-md w-full">
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 space-y-6">
|
||||
<div class="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-md w-full max-w-md text-gray-900 dark:text-gray-100">
|
||||
<div class="text-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900">用户注册</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">用户注册</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">创建新的用户账户</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleRegister" class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700">用户名</label>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-100">用户名</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
:class="{ 'border-red-500': errors.username }"
|
||||
>
|
||||
<p v-if="errors.username" class="mt-1 text-sm text-red-600">{{ errors.username }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">邮箱</label>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-100">邮箱</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
:class="{ 'border-red-500': errors.email }"
|
||||
>
|
||||
<p v-if="errors.email" class="mt-1 text-sm text-red-600">{{ errors.email }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">密码</label>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-100">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
:class="{ 'border-red-500': errors.password }"
|
||||
>
|
||||
<p v-if="errors.password" class="mt-1 text-sm text-red-600">{{ errors.password }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700">确认密码</label>
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 dark:text-gray-100">确认密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
v-model="form.confirmPassword"
|
||||
required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
:class="{ 'border-red-500': errors.confirmPassword }"
|
||||
>
|
||||
<p v-if="errors.confirmPassword" class="mt-1 text-sm text-red-600">{{ errors.confirmPassword }}</p>
|
||||
@@ -63,7 +63,7 @@
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="userStore.loading"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<span v-if="userStore.loading" class="inline-flex items-center">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 p-4">
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 头部 -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900">用户管理</h1>
|
||||
<div class="flex gap-2">
|
||||
|
||||
@@ -43,17 +43,18 @@ export const useUserStore = defineStore('user', {
|
||||
actions: {
|
||||
// 初始化用户状态(从localStorage恢复)
|
||||
initAuth() {
|
||||
const token = localStorage.getItem('token')
|
||||
const userStr = localStorage.getItem('user')
|
||||
|
||||
if (token && userStr) {
|
||||
try {
|
||||
this.token = token
|
||||
this.user = JSON.parse(userStr)
|
||||
this.isAuthenticated = true
|
||||
} catch (error) {
|
||||
console.error('解析用户信息失败:', error)
|
||||
this.logout()
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('token')
|
||||
const userStr = localStorage.getItem('user')
|
||||
if (token && userStr) {
|
||||
try {
|
||||
this.token = token
|
||||
this.user = JSON.parse(userStr)
|
||||
this.isAuthenticated = true
|
||||
} catch (error) {
|
||||
console.error('解析用户信息失败:', error)
|
||||
this.logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -126,10 +127,11 @@ export const useUserStore = defineStore('user', {
|
||||
this.user = null
|
||||
this.token = null
|
||||
this.isAuthenticated = false
|
||||
|
||||
// 清除localStorage
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户资料
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
"./components/**/*.{js,vue,ts}",
|
||||
"./layouts/**/*.vue",
|
||||
|
||||
Reference in New Issue
Block a user