update: ui

This commit is contained in:
ctwj
2025-07-11 00:49:41 +08:00
parent 2c48026b51
commit 770d9b00cb
15 changed files with 892 additions and 364 deletions

189
doc/API_STANDARDS.md Normal file
View 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

View File

@@ -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
View 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,
})
}

View File

@@ -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)

View File

@@ -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&#10;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>

View File

@@ -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>
})
}

View File

@@ -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>

View File

@@ -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'
})

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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')
}
},
// 获取用户资料

View File

@@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",