mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
update: 控制台体验优化
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
type CategoryRepository interface {
|
||||
BaseRepository[entity.Category]
|
||||
FindByName(name string) (*entity.Category, error)
|
||||
FindByNameIncludingDeleted(name string) (*entity.Category, error)
|
||||
FindWithResources() ([]entity.Category, error)
|
||||
FindWithTags() ([]entity.Category, error)
|
||||
GetResourceCount(categoryID uint) (int64, error)
|
||||
@@ -17,6 +18,7 @@ type CategoryRepository interface {
|
||||
GetTagNames(categoryID uint) ([]string, error)
|
||||
FindWithPagination(page, pageSize int) ([]entity.Category, int64, error)
|
||||
Search(query string, page, pageSize int) ([]entity.Category, int64, error)
|
||||
RestoreDeletedCategory(id uint) error
|
||||
}
|
||||
|
||||
// CategoryRepositoryImpl Category的Repository实现
|
||||
@@ -41,6 +43,21 @@ func (r *CategoryRepositoryImpl) FindByName(name string) (*entity.Category, erro
|
||||
return &category, nil
|
||||
}
|
||||
|
||||
// FindByNameIncludingDeleted 根据名称查找(包括已删除的记录)
|
||||
func (r *CategoryRepositoryImpl) FindByNameIncludingDeleted(name string) (*entity.Category, error) {
|
||||
var category entity.Category
|
||||
err := r.db.Unscoped().Where("name = ?", name).First(&category).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &category, nil
|
||||
}
|
||||
|
||||
// RestoreDeletedCategory 恢复已删除的分类
|
||||
func (r *CategoryRepositoryImpl) RestoreDeletedCategory(id uint) error {
|
||||
return r.db.Unscoped().Model(&entity.Category{}).Where("id = ?", id).Update("deleted_at", nil).Error
|
||||
}
|
||||
|
||||
// FindWithResources 查找包含资源的分类
|
||||
func (r *CategoryRepositoryImpl) FindWithResources() ([]entity.Category, error) {
|
||||
var categories []entity.Category
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
type TagRepository interface {
|
||||
BaseRepository[entity.Tag]
|
||||
FindByName(name string) (*entity.Tag, error)
|
||||
FindByNameIncludingDeleted(name string) (*entity.Tag, error)
|
||||
FindWithResources() ([]entity.Tag, error)
|
||||
FindByCategoryID(categoryID uint) ([]entity.Tag, error)
|
||||
FindByCategoryIDPaginated(categoryID uint, page, pageSize int) ([]entity.Tag, int64, error)
|
||||
@@ -19,6 +20,7 @@ type TagRepository interface {
|
||||
Search(query string, page, pageSize int) ([]entity.Tag, int64, error)
|
||||
UpdateWithNulls(tag *entity.Tag) error
|
||||
GetByID(id uint) (*entity.Tag, error)
|
||||
RestoreDeletedTag(id uint) error
|
||||
}
|
||||
|
||||
// TagRepositoryImpl Tag的Repository实现
|
||||
@@ -43,6 +45,16 @@ func (r *TagRepositoryImpl) FindByName(name string) (*entity.Tag, error) {
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// FindByNameIncludingDeleted 根据名称查找(包括已删除的记录)
|
||||
func (r *TagRepositoryImpl) FindByNameIncludingDeleted(name string) (*entity.Tag, error) {
|
||||
var tag entity.Tag
|
||||
err := r.db.Unscoped().Where("name = ?", name).First(&tag).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// FindWithResources 查找包含资源的标签
|
||||
func (r *TagRepositoryImpl) FindWithResources() ([]entity.Tag, error) {
|
||||
var tags []entity.Tag
|
||||
@@ -155,3 +167,8 @@ func (r *TagRepositoryImpl) GetByID(id uint) (*entity.Tag, error) {
|
||||
}
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// RestoreDeletedTag 恢复已删除的标签
|
||||
func (r *TagRepositoryImpl) RestoreDeletedTag(id uint) error {
|
||||
return r.db.Unscoped().Model(&entity.Tag{}).Where("id = ?", id).Update("deleted_at", nil).Error
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -18,6 +19,8 @@ func GetCategories(c *gin.Context) {
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
search := c.Query("search")
|
||||
|
||||
utils.Debug("获取分类列表 - 分页参数: page=%d, pageSize=%d, search=%s", page, pageSize, search)
|
||||
|
||||
var categories []entity.Category
|
||||
var total int64
|
||||
var err error
|
||||
@@ -35,6 +38,8 @@ func GetCategories(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Debug("查询到分类数量: %d, 总数: %d", len(categories), total)
|
||||
|
||||
// 获取每个分类的资源数量和标签名称
|
||||
resourceCounts := make(map[uint]int64)
|
||||
tagNamesMap := make(map[uint][]string)
|
||||
@@ -73,12 +78,50 @@ func CreateCategory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 首先检查是否存在已删除的同名分类
|
||||
deletedCategory, err := repoManager.CategoryRepository.FindByNameIncludingDeleted(req.Name)
|
||||
if err == nil && deletedCategory.DeletedAt.Valid {
|
||||
utils.Debug("找到已删除的分类: ID=%d, Name=%s", deletedCategory.ID, deletedCategory.Name)
|
||||
|
||||
// 如果存在已删除的同名分类,则恢复它
|
||||
err = repoManager.CategoryRepository.RestoreDeletedCategory(deletedCategory.ID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "恢复已删除分类失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
utils.Debug("分类恢复成功: ID=%d", deletedCategory.ID)
|
||||
|
||||
// 重新获取恢复后的分类
|
||||
restoredCategory, err := repoManager.CategoryRepository.FindByID(deletedCategory.ID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取恢复的分类失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
utils.Debug("重新获取到恢复的分类: ID=%d, Name=%s", restoredCategory.ID, restoredCategory.Name)
|
||||
|
||||
// 更新分类信息
|
||||
restoredCategory.Description = req.Description
|
||||
err = repoManager.CategoryRepository.Update(restoredCategory)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "更新恢复的分类失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
utils.Debug("分类信息更新成功: ID=%d, Description=%s", restoredCategory.ID, restoredCategory.Description)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "分类恢复成功",
|
||||
"category": converter.ToCategoryResponse(restoredCategory, 0, []string{}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果不存在已删除的同名分类,则创建新分类
|
||||
category := &entity.Category{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
}
|
||||
|
||||
err := repoManager.CategoryRepository.Create(category)
|
||||
err = repoManager.CategoryRepository.Create(category)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -65,13 +65,47 @@ func CreateTag(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 首先检查是否存在已删除的同名标签
|
||||
deletedTag, err := repoManager.TagRepository.FindByNameIncludingDeleted(req.Name)
|
||||
if err == nil && deletedTag.DeletedAt.Valid {
|
||||
// 如果存在已删除的同名标签,则恢复它
|
||||
err = repoManager.TagRepository.RestoreDeletedTag(deletedTag.ID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "恢复已删除标签失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 重新获取恢复后的标签
|
||||
restoredTag, err := repoManager.TagRepository.FindByID(deletedTag.ID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取恢复的标签失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新标签信息
|
||||
restoredTag.Description = req.Description
|
||||
restoredTag.CategoryID = req.CategoryID
|
||||
err = repoManager.TagRepository.UpdateWithNulls(restoredTag)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "更新恢复的标签失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "标签恢复成功",
|
||||
"tag": converter.ToTagResponse(restoredTag, 0),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果不存在已删除的同名标签,则创建新标签
|
||||
tag := &entity.Tag{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
CategoryID: req.CategoryID,
|
||||
}
|
||||
|
||||
err := repoManager.TagRepository.Create(tag)
|
||||
err = repoManager.TagRepository.Create(tag)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -1038,15 +1038,30 @@ func (s *Scheduler) handleTags(tagStr string) ([]uint, error) {
|
||||
Debug("查找标签: %s", name)
|
||||
tag, err := s.tagRepo.FindByName(name)
|
||||
if err != nil {
|
||||
Debug("标签 %s 不存在,创建新标签", name)
|
||||
// 不存在则新建
|
||||
tag = &entity.Tag{Name: name}
|
||||
err = s.tagRepo.Create(tag)
|
||||
if err != nil {
|
||||
Error("创建标签 %s 失败: %v", name, err)
|
||||
return nil, fmt.Errorf("创建标签 %s 失败: %v", name, err)
|
||||
// 检查是否存在已删除的同名标签
|
||||
Debug("标签 %s 不存在,检查是否有已删除的同名标签", name)
|
||||
deletedTag, err2 := s.tagRepo.FindByNameIncludingDeleted(name)
|
||||
if err2 == nil && deletedTag.DeletedAt.Valid {
|
||||
// 如果存在已删除的同名标签,则恢复它
|
||||
Debug("找到已删除的同名标签 %s,正在恢复", name)
|
||||
err2 = s.tagRepo.RestoreDeletedTag(deletedTag.ID)
|
||||
if err2 != nil {
|
||||
Error("恢复已删除标签 %s 失败: %v", name, err2)
|
||||
return nil, fmt.Errorf("恢复已删除标签 %s 失败: %v", name, err2)
|
||||
}
|
||||
tag = deletedTag
|
||||
Debug("成功恢复标签: %s (ID: %d)", name, tag.ID)
|
||||
} else {
|
||||
// 如果不存在已删除的同名标签,则创建新标签
|
||||
Debug("标签 %s 不存在,创建新标签", name)
|
||||
tag = &entity.Tag{Name: name}
|
||||
err2 = s.tagRepo.Create(tag)
|
||||
if err2 != nil {
|
||||
Error("创建标签 %s 失败: %v", name, err2)
|
||||
return nil, fmt.Errorf("创建标签 %s 失败: %v", name, err2)
|
||||
}
|
||||
Debug("成功创建标签: %s (ID: %d)", name, tag.ID)
|
||||
}
|
||||
Debug("成功创建标签: %s (ID: %d)", name, tag.ID)
|
||||
} else {
|
||||
Debug("找到已存在的标签: %s (ID: %d)", name, tag.ID)
|
||||
}
|
||||
@@ -1065,8 +1080,24 @@ func (s *Scheduler) resolveCategory(categoryName string, tagIDs []uint) (*uint,
|
||||
Debug("查找分类: %s", categoryName)
|
||||
cat, err := s.categoryRepo.FindByName(categoryName)
|
||||
if err != nil {
|
||||
Debug("分类 %s 不存在: %v", categoryName, err)
|
||||
} else if cat != nil {
|
||||
// 检查是否存在已删除的同名分类
|
||||
Debug("分类 %s 不存在,检查是否有已删除的同名分类", categoryName)
|
||||
deletedCat, err2 := s.categoryRepo.FindByNameIncludingDeleted(categoryName)
|
||||
if err2 == nil && deletedCat.DeletedAt.Valid {
|
||||
// 如果存在已删除的同名分类,则恢复它
|
||||
Debug("找到已删除的同名分类 %s,正在恢复", categoryName)
|
||||
err2 = s.categoryRepo.RestoreDeletedCategory(deletedCat.ID)
|
||||
if err2 != nil {
|
||||
Error("恢复已删除分类 %s 失败: %v", categoryName, err2)
|
||||
return nil, fmt.Errorf("恢复已删除分类 %s 失败: %v", categoryName, err2)
|
||||
}
|
||||
cat = deletedCat
|
||||
Debug("成功恢复分类: %s (ID: %d)", categoryName, cat.ID)
|
||||
} else {
|
||||
Debug("分类 %s 不存在: %v", categoryName, err)
|
||||
}
|
||||
}
|
||||
if cat != nil {
|
||||
Debug("找到分类: %s (ID: %d)", categoryName, cat.ID)
|
||||
return &cat.ID, nil
|
||||
}
|
||||
|
||||
2
web/components.d.ts
vendored
2
web/components.d.ts
vendored
@@ -11,6 +11,8 @@ declare module 'vue' {
|
||||
NA: typeof import('naive-ui')['NA']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<p class="mb-2"><strong>格式要求:</strong>标题和URL为一组,标题必填, 同一标题URL支持多行</p>
|
||||
<pre class="bg-white dark:bg-gray-800 p-2 rounded border text-xs">
|
||||
电影1
|
||||
https://pan.baidu.com/s/123456 # 百度网盘 电影1
|
||||
https://pan.quark.com/s/123456 # 夸克网盘 电影1
|
||||
https://pan.baidu.com/s/123456
|
||||
https://pan.quark.com/s/123456
|
||||
电影标题2
|
||||
https://pan.baidu.com/s/789012
|
||||
电视剧标题3
|
||||
@@ -21,9 +21,9 @@ https://pan.quark.cn/s/345678</pre>
|
||||
</div>
|
||||
<div class="mb-4 flex-1 w-1">
|
||||
<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="请输入资源内容,格式:标题和URL两行为一组..."></textarea>
|
||||
<n-input v-model="batchInput" type="textarea"
|
||||
:autosize="{ minRows: 10, maxRows: 15 }"
|
||||
placeholder="请输入资源内容,格式:标题和URL为一组..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<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 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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</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 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 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>
|
||||
|
||||
<!-- 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>
|
||||
</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 { useReadyResourceApi } from '~/composables/useApi'
|
||||
|
||||
const notification = useNotification()
|
||||
const store = useResourceStore()
|
||||
const { categories } = storeToRefs(store)
|
||||
|
||||
const props = defineProps<{ resource?: any }>()
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const loading = ref(false)
|
||||
const newTag = 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: '', // 多行
|
||||
category_id: '',
|
||||
tags: [] as string[],
|
||||
file_path: '',
|
||||
file_type: '',
|
||||
file_size: 0,
|
||||
is_public: true,
|
||||
})
|
||||
|
||||
const readyResourceApi = useReadyResourceApi()
|
||||
|
||||
onMounted(() => {
|
||||
if (props.resource) {
|
||||
form.value = {
|
||||
title: props.resource.title || '',
|
||||
description: props.resource.description || '',
|
||||
url: props.resource.url || '',
|
||||
category_id: props.resource.category_id || '',
|
||||
tags: [...(props.resource.tags || [])],
|
||||
file_path: props.resource.file_path || '',
|
||||
file_type: props.resource.file_type || '',
|
||||
file_size: props.resource.file_size || 0,
|
||||
is_public: props.resource.is_public !== false,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const addTag = () => {
|
||||
const tag = newTag.value.trim()
|
||||
if (tag && !form.value.tags.includes(tag)) {
|
||||
form.value.tags.push(tag)
|
||||
newTag.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const removeTag = (tag: string) => {
|
||||
const index = form.value.tags.indexOf(tag)
|
||||
if (index > -1) {
|
||||
form.value.tags.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量添加提交
|
||||
const handleBatchSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!batchInput.value.trim()) throw new Error('请输入资源内容')
|
||||
const res: any = await readyResourceApi.createReadyResourcesFromText(batchInput.value)
|
||||
notification.success({
|
||||
content: `成功添加 ${res.count || 0} 个资源,资源已进入待处理列表,处理完成后会自动入库`,
|
||||
duration: 3000
|
||||
})
|
||||
batchInput.value = ''
|
||||
} catch (e: any) {
|
||||
notification.error({
|
||||
content: e.message || '批量添加失败',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 单个添加提交
|
||||
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],
|
||||
})
|
||||
}
|
||||
notification.success({
|
||||
content: '资源已进入待处理列表,处理完成后会自动入库',
|
||||
duration: 3000
|
||||
})
|
||||
// 清空表单
|
||||
form.value.title = ''
|
||||
form.value.description = ''
|
||||
form.value.url = ''
|
||||
form.value.tags = []
|
||||
form.value.file_type = ''
|
||||
} catch (e: any) {
|
||||
notification.error({
|
||||
content: e.message || '添加失败',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可以添加自定义样式 */
|
||||
</style>
|
||||
@@ -5,9 +5,8 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
标题 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
<n-input
|
||||
v-model="form.title"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
placeholder="输入资源标题(必填)"
|
||||
required
|
||||
/>
|
||||
@@ -18,12 +17,11 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
描述 <span class="text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<textarea
|
||||
<n-input
|
||||
v-model="form.description"
|
||||
rows="3"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
type="textarea"
|
||||
placeholder="输入资源描述,如:剧情简介、文件大小、清晰度等"
|
||||
></textarea>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- URL -->
|
||||
@@ -31,13 +29,12 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
URL <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
<n-input
|
||||
v-model="form.url"
|
||||
rows="3"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
type="textarea"
|
||||
placeholder="请输入资源链接,支持多行,每行一个链接"
|
||||
required
|
||||
></textarea>
|
||||
/>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
支持百度网盘、阿里云盘、夸克网盘等链接,每行一个链接
|
||||
</p>
|
||||
@@ -48,9 +45,8 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
分类 <span class="text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
<n-input
|
||||
v-model="form.category"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
placeholder="如:电影、电视剧、动漫、音乐等"
|
||||
/>
|
||||
</div>
|
||||
@@ -76,10 +72,9 @@
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
<n-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>
|
||||
@@ -89,9 +84,8 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
封面图片 <span class="text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
<n-input
|
||||
v-model="form.img"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
placeholder="封面图片链接"
|
||||
/>
|
||||
</div>
|
||||
@@ -101,9 +95,8 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
数据来源 <span class="text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
<n-input
|
||||
v-model="form.source"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
placeholder="如:手动添加、API导入、爬虫等"
|
||||
/>
|
||||
</div>
|
||||
@@ -113,12 +106,11 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
额外数据 <span class="text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<textarea
|
||||
<n-input
|
||||
v-model="form.extra"
|
||||
rows="3"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
type="textarea"
|
||||
placeholder="JSON格式的额外数据,如:{'size': '2GB', 'quality': '1080p'}"
|
||||
></textarea>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<ClientOnly>
|
||||
<n-notification-provider>
|
||||
<!-- 页面内容插槽 -->
|
||||
<slot />
|
||||
<n-dialog-provider>
|
||||
<!-- 页面内容插槽 -->
|
||||
<slot />
|
||||
</n-dialog-provider>
|
||||
</n-notification-provider>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
|
||||
@@ -3,24 +3,21 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex gap-2">
|
||||
<button @click="showAddModal = true"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-white text-sm flex items-center gap-2">
|
||||
<i class="fas fa-plus"></i> 添加分类
|
||||
</button>
|
||||
<n-button @click="showAddModal = true" type="success">
|
||||
<i class="fas fa-plus"></i> 添加分类
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative">
|
||||
<input v-model="searchQuery" @keyup="debounceSearch" type="text"
|
||||
class="w-64 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 transition-all text-sm"
|
||||
<n-input v-model:value="searchQuery" @input="debounceSearch" type="text"
|
||||
placeholder="搜索分类名称..." />
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<i class="fas fa-search text-gray-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="refreshData"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2">
|
||||
<n-button @click="refreshData" type="tertiary">
|
||||
<i class="fas fa-refresh"></i> 刷新
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,10 +51,9 @@
|
||||
</svg>
|
||||
<div class="text-lg font-semibold text-gray-400 dark:text-gray-500 mb-2">暂无分类</div>
|
||||
<div class="text-sm text-gray-400 dark:text-gray-600 mb-4">你可以点击上方"添加分类"按钮创建新分类</div>
|
||||
<button @click="showAddModal = true"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2">
|
||||
<n-button @click="showAddModal = true" type="primary">
|
||||
<i class="fas fa-plus"></i> 添加分类
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -85,16 +81,12 @@
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="editCategory(category)"
|
||||
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
|
||||
title="编辑分类">
|
||||
<n-button @click="editCategory(category)" type="info" size="small">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button @click="deleteCategory(category.id)"
|
||||
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||
title="删除分类">
|
||||
</n-button>
|
||||
<n-button @click="deleteCategory(category.id)" type="error" size="small">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -152,36 +144,31 @@
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ editingCategory ? '编辑分类' : '添加分类' }}
|
||||
</h3>
|
||||
<button @click="closeModal"
|
||||
class="text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<n-button @click="closeModal" type="tertiary" size="small">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">分类名称:</label>
|
||||
<input v-model="formData.name" type="text" required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
<n-input v-model:value="formData.name" type="text" required
|
||||
placeholder="请输入分类名称" />
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">描述:</label>
|
||||
<textarea v-model="formData.description" rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
placeholder="请输入分类描述(可选)"></textarea>
|
||||
<n-input v-model:value="formData.description" type="textarea"
|
||||
placeholder="请输入分类描述(可选)" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button type="button" @click="closeModal"
|
||||
class="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-600 rounded-md hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors">
|
||||
<n-button type="tertiary" @click="closeModal">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" :disabled="submitting"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
||||
</n-button>
|
||||
<n-button type="primary" :disabled="submitting" @click="handleSubmit">
|
||||
{{ submitting ? '提交中...' : (editingCategory ? '更新' : '添加') }}
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -221,6 +208,7 @@ let searchTimeout: NodeJS.Timeout | null = null
|
||||
const showAddModal = ref(false)
|
||||
const submitting = ref(false)
|
||||
const editingCategory = ref(null)
|
||||
const dialog = useDialog()
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
@@ -263,6 +251,8 @@ const fetchCategories = async () => {
|
||||
console.log('获取分类列表参数:', params)
|
||||
const response = await categoryApi.getCategories(params)
|
||||
console.log('分类接口响应:', response)
|
||||
console.log('响应类型:', typeof response)
|
||||
console.log('响应是否为数组:', Array.isArray(response))
|
||||
|
||||
// 适配后端API响应格式
|
||||
if (response && response.items) {
|
||||
@@ -283,6 +273,7 @@ const fetchCategories = async () => {
|
||||
totalPages.value = 1
|
||||
}
|
||||
console.log('最终分类数据:', categories.value)
|
||||
console.log('分类数据长度:', categories.value.length)
|
||||
} catch (error) {
|
||||
console.error('获取分类列表失败:', error)
|
||||
categories.value = []
|
||||
@@ -317,36 +308,61 @@ const goToPage = (page: number) => {
|
||||
|
||||
// 编辑分类
|
||||
const editCategory = (category: any) => {
|
||||
console.log('编辑分类:', category)
|
||||
editingCategory.value = category
|
||||
formData.value = {
|
||||
name: category.name,
|
||||
description: category.description || ''
|
||||
}
|
||||
console.log('设置表单数据:', formData.value)
|
||||
showAddModal.value = true
|
||||
}
|
||||
|
||||
// 删除分类
|
||||
const deleteCategory = async (categoryId: number) => {
|
||||
if (!confirm(`确定要删除分类吗?`)) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await categoryApi.deleteCategory(categoryId)
|
||||
await fetchCategories()
|
||||
} catch (error) {
|
||||
console.error('删除分类失败:', error)
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除分类吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await categoryApi.deleteCategory(categoryId)
|
||||
await fetchCategories()
|
||||
} catch (error) {
|
||||
console.error('删除分类失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
submitting.value = true
|
||||
let response: any
|
||||
if (editingCategory.value) {
|
||||
await categoryApi.updateCategory(editingCategory.value.id, formData.value)
|
||||
response = await categoryApi.updateCategory(editingCategory.value.id, formData.value)
|
||||
} else {
|
||||
await categoryApi.createCategory(formData.value)
|
||||
response = await categoryApi.createCategory(formData.value)
|
||||
}
|
||||
console.log('分类操作响应:', response)
|
||||
|
||||
// 检查是否是恢复操作
|
||||
if (response && response.message && response.message.includes('恢复成功')) {
|
||||
console.log('检测到分类恢复操作,延迟刷新数据')
|
||||
console.log('恢复的分类信息:', response.category)
|
||||
closeModal()
|
||||
// 延迟一点时间再刷新,确保数据库状态已更新
|
||||
setTimeout(async () => {
|
||||
console.log('开始刷新分类数据...')
|
||||
await fetchCategories()
|
||||
console.log('分类数据刷新完成')
|
||||
}, 500)
|
||||
return
|
||||
}
|
||||
|
||||
closeModal()
|
||||
await fetchCategories()
|
||||
} catch (error) {
|
||||
|
||||
@@ -19,23 +19,23 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<n-button
|
||||
@click="showCreateModal = true"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
type="success"
|
||||
>
|
||||
<i class="fas fa-plus"></i> 添加账号
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative w-40">
|
||||
<n-select v-model:value="platform" :options="platformOptions" />
|
||||
<n-select v-model:value="platform" :options="platformOptions" @update:value="onPlatformChange" />
|
||||
</div>
|
||||
<button
|
||||
<n-button
|
||||
@click="refreshData"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
|
||||
type="tertiary"
|
||||
>
|
||||
<i class="fas fa-refresh"></i> 刷新
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,12 +71,12 @@
|
||||
</svg>
|
||||
<div class="text-lg font-semibold text-gray-400 dark:text-gray-500 mb-2">暂无账号</div>
|
||||
<div class="text-sm text-gray-400 dark:text-gray-600 mb-4">你可以点击上方"添加账号"按钮创建新账号</div>
|
||||
<button
|
||||
<n-button
|
||||
@click="showCreateModal = true"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
|
||||
type="primary"
|
||||
>
|
||||
<i class="fas fa-plus"></i> 添加账号
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -228,8 +228,8 @@
|
||||
class="mt-1 block w-full border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-gray-100"
|
||||
>
|
||||
<option value="">请选择平台</option>
|
||||
<option v-for="pan in platforms" :key="pan.id" :value="pan.id">
|
||||
{{ pan.name }}
|
||||
<option v-for="pan in platforms.filter(pan => pan.name === 'quark')" :key="pan.id" :value="pan.id">
|
||||
{{ pan.remark }}
|
||||
</option>
|
||||
</select>
|
||||
<p v-if="showEditModal" class="mt-1 text-xs text-gray-500 dark:text-gray-400">编辑时不允许修改平台类型</p>
|
||||
@@ -242,49 +242,44 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Cookie <span class="text-red-500">*</span></label>
|
||||
<textarea
|
||||
v-model="form.ck"
|
||||
<n-input
|
||||
v-model:value="form.ck"
|
||||
required
|
||||
rows="4"
|
||||
class="mt-1 block w-full border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-gray-100"
|
||||
type="textarea"
|
||||
placeholder="请输入Cookie内容,系统将自动识别容量"
|
||||
></textarea>
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">备注</label>
|
||||
<input
|
||||
v-model="form.remark"
|
||||
<n-input
|
||||
v-model:value="form.remark"
|
||||
type="text"
|
||||
class="mt-1 block w-full border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-gray-100"
|
||||
placeholder="可选,备注信息"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showEditModal">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
v-model="form.is_valid"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600"
|
||||
<n-checkbox
|
||||
v-model:checked="form.is_valid"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">账号有效</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
<n-button
|
||||
type="tertiary"
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="submitting"
|
||||
class="px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ submitting ? '处理中...' : (showEditModal ? '更新' : '创建') }}
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -323,6 +318,7 @@ const loading = ref(true)
|
||||
const pageLoading = ref(true)
|
||||
const submitting = ref(false)
|
||||
const platform = ref(null)
|
||||
const dialog = useDialog()
|
||||
|
||||
import { useCksApi, usePanApi } from '~/composables/useApi'
|
||||
const cksApi = useCksApi()
|
||||
@@ -334,10 +330,18 @@ const pans = computed(() => {
|
||||
return Array.isArray(pansData.value) ? pansData.value : (pansData.value?.list || [])
|
||||
})
|
||||
const platformOptions = computed(() => {
|
||||
return pans.value.map(pan => ({
|
||||
label: pan.remark,
|
||||
value: pan.id
|
||||
}))
|
||||
const options = [
|
||||
{ label: '全部平台', value: null }
|
||||
]
|
||||
|
||||
pans.value.forEach(pan => {
|
||||
options.push({
|
||||
label: pan.remark || pan.name || `平台${pan.id}`,
|
||||
value: pan.id
|
||||
})
|
||||
})
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
// 检查认证
|
||||
@@ -383,8 +387,11 @@ const createCks = async () => {
|
||||
await fetchCks()
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
console.error('创建账号失败:', error)
|
||||
alert('创建账号失败: ' + (error.message || '未知错误'))
|
||||
dialog.error({
|
||||
title: '错误',
|
||||
content: '创建账号失败: ' + (error.message || '未知错误'),
|
||||
positiveText: '确定'
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -407,47 +414,68 @@ const updateCks = async () => {
|
||||
|
||||
// 删除账号
|
||||
const deleteCks = async (id) => {
|
||||
if (!confirm('确定要删除这个账号吗?')) return
|
||||
|
||||
try {
|
||||
await cksApi.deleteCks(id)
|
||||
await fetchCks()
|
||||
} catch (error) {
|
||||
console.error('删除账号失败:', error)
|
||||
alert('删除账号失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这个账号吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await cksApi.deleteCks(id)
|
||||
await fetchCks()
|
||||
} catch (error) {
|
||||
console.error('删除账号失败:', error)
|
||||
alert('删除账号失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新容量
|
||||
const refreshCapacity = async (id) => {
|
||||
if (!confirm('确定要刷新此账号的容量信息吗?')) return
|
||||
|
||||
try {
|
||||
await cksApi.refreshCapacity(id)
|
||||
await fetchCks()
|
||||
alert('容量信息已刷新!')
|
||||
} catch (error) {
|
||||
console.error('刷新容量失败:', error)
|
||||
alert('刷新容量失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要刷新此账号的容量信息吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await cksApi.refreshCapacity(id)
|
||||
await fetchCks()
|
||||
alert('容量信息已刷新!')
|
||||
} catch (error) {
|
||||
console.error('刷新容量失败:', error)
|
||||
alert('刷新容量失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换账号状态
|
||||
const toggleStatus = async (cks) => {
|
||||
const newStatus = !cks.is_valid
|
||||
if (!confirm(`确定要${cks.is_valid ? '禁用' : '启用'}此账号吗?`)) return
|
||||
|
||||
try {
|
||||
console.log('切换状态 - 账号ID:', cks.id, '当前状态:', cks.is_valid, '新状态:', newStatus)
|
||||
await cksApi.updateCks(cks.id, { is_valid: newStatus })
|
||||
console.log('状态更新成功,正在刷新数据...')
|
||||
await fetchCks()
|
||||
console.log('数据刷新完成')
|
||||
alert(`账号已${newStatus ? '启用' : '禁用'}!`)
|
||||
} catch (error) {
|
||||
console.error('切换账号状态失败:', error)
|
||||
alert(`切换账号状态失败: ${error.message || '未知错误'}`)
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: `确定要${cks.is_valid ? '禁用' : '启用'}此账号吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
console.log('切换状态 - 账号ID:', cks.id, '当前状态:', cks.is_valid, '新状态:', newStatus)
|
||||
await cksApi.updateCks(cks.id, { is_valid: newStatus })
|
||||
console.log('状态更新成功,正在刷新数据...')
|
||||
await fetchCks()
|
||||
console.log('数据刷新完成')
|
||||
alert(`账号已${newStatus ? '启用' : '禁用'}!`)
|
||||
} catch (error) {
|
||||
console.error('切换账号状态失败:', error)
|
||||
alert(`切换账号状态失败: ${error.message || '未知错误'}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑账号
|
||||
@@ -532,13 +560,24 @@ const formatFileSize = (bytes) => {
|
||||
// 过滤和分页计算
|
||||
const filteredCksList = computed(() => {
|
||||
let filtered = cksList.value
|
||||
console.log('原始账号数量:', filtered.length)
|
||||
|
||||
// 平台过滤
|
||||
if (platform.value !== null && platform.value !== undefined) {
|
||||
filtered = filtered.filter(cks => cks.pan_id === platform.value)
|
||||
console.log('平台过滤后数量:', filtered.length, '平台ID:', platform.value)
|
||||
}
|
||||
|
||||
// 搜索过滤
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
filtered = filtered.filter(cks =>
|
||||
cks.pan?.name?.toLowerCase().includes(query) ||
|
||||
cks.remark?.toLowerCase().includes(query)
|
||||
)
|
||||
console.log('搜索过滤后数量:', filtered.length, '搜索词:', searchQuery.value)
|
||||
}
|
||||
|
||||
totalPages.value = Math.ceil(filtered.length / itemsPerPage.value)
|
||||
const start = (currentPage.value - 1) * itemsPerPage.value
|
||||
const end = start + itemsPerPage.value
|
||||
@@ -556,9 +595,17 @@ const debounceSearch = () => {
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 平台变化处理
|
||||
const onPlatformChange = () => {
|
||||
currentPage.value = 1
|
||||
console.log('平台过滤条件变化:', platform.value)
|
||||
console.log('当前过滤后的账号数量:', filteredCksList.value.length)
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
currentPage.value = 1
|
||||
// 保持当前的过滤条件,只刷新数据
|
||||
fetchCks()
|
||||
fetchPlatforms()
|
||||
}
|
||||
|
||||
@@ -251,6 +251,7 @@ const totalPages = ref(0)
|
||||
|
||||
// 错误统计
|
||||
const errorStats = ref<Record<string, number>>({})
|
||||
const dialog = useDialog()
|
||||
|
||||
// 获取失败资源API
|
||||
import { useReadyResourceApi } from '~/composables/useApi'
|
||||
@@ -338,83 +339,108 @@ const refreshData = () => {
|
||||
|
||||
// 重试单个资源
|
||||
const retryResource = async (id: number) => {
|
||||
if (!confirm('确定要重试这个资源吗?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await readyResourceApi.clearErrorMsg(id)
|
||||
alert('错误信息已清除,资源将在下次调度时重新处理')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('重试失败:', error)
|
||||
alert('重试失败')
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要重试这个资源吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await readyResourceApi.clearErrorMsg(id)
|
||||
alert('错误信息已清除,资源将在下次调度时重新处理')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('重试失败:', error)
|
||||
alert('重试失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 清除单个资源错误
|
||||
const clearError = async (id: number) => {
|
||||
if (!confirm('确定要清除这个资源的错误信息吗?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await readyResourceApi.clearErrorMsg(id)
|
||||
alert('错误信息已清除')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('清除错误失败:', error)
|
||||
alert('清除错误失败')
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要清除这个资源的错误信息吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await readyResourceApi.clearErrorMsg(id)
|
||||
alert('错误信息已清除')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('清除错误失败:', error)
|
||||
alert('清除错误失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除资源
|
||||
const deleteResource = async (id: number) => {
|
||||
if (!confirm('确定要删除这个失败资源吗?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await readyResourceApi.deleteReadyResource(id)
|
||||
if (failedResources.value.length === 1 && currentPage.value > 1) {
|
||||
currentPage.value--
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这个失败资源吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await readyResourceApi.deleteReadyResource(id)
|
||||
if (failedResources.value.length === 1 && currentPage.value > 1) {
|
||||
currentPage.value--
|
||||
}
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
alert('删除失败')
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
alert('删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重试所有失败资源
|
||||
const retryAllFailed = async () => {
|
||||
if (!confirm('确定要重试所有可重试的失败资源吗?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await readyResourceApi.retryFailedResources() as any
|
||||
alert(`重试操作完成:\n总数量:${response.total_count}\n已清除:${response.cleared_count}\n跳过:${response.skipped_count}`)
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('重试所有失败资源失败:', error)
|
||||
alert('重试失败')
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要重试所有可重试的失败资源吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
const response = await readyResourceApi.retryFailedResources() as any
|
||||
alert(`重试操作完成:\n总数量:${response.total_count}\n已清除:${response.cleared_count}\n跳过:${response.skipped_count}`)
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('重试所有失败资源失败:', error)
|
||||
alert('重试失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 清除所有错误
|
||||
const clearAllErrors = async () => {
|
||||
if (!confirm('确定要清除所有失败资源的错误信息吗?此操作不可恢复!')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 这里需要实现批量清除错误的API
|
||||
alert('批量清除错误功能待实现')
|
||||
} catch (error) {
|
||||
console.error('清除所有错误失败:', error)
|
||||
alert('清除失败')
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要清除所有失败资源的错误信息吗?此操作不可恢复!',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
// 这里需要实现批量清除错误的API
|
||||
alert('批量清除错误功能待实现')
|
||||
} catch (error) {
|
||||
console.error('清除所有错误失败:', error)
|
||||
alert('清除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
|
||||
@@ -78,83 +78,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量添加模态框 -->
|
||||
<div v-if="showAddModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white 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">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">输入格式说明:</label>
|
||||
<div class="bg-gray-50 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 mb-2">资源内容:</label>
|
||||
<textarea
|
||||
v-model="resourceText"
|
||||
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 dark:placeholder-gray-500"
|
||||
placeholder="请输入资源内容,支持两种格式..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button @click="closeModal" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
|
||||
取消
|
||||
</button>
|
||||
<button @click="handleBatchAdd" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
|
||||
批量添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex gap-2">
|
||||
<NuxtLink
|
||||
to="/add-resource"
|
||||
to="/admin/add-resource"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
>
|
||||
<i class="fas fa-plus"></i> 添加资源
|
||||
</NuxtLink>
|
||||
<button
|
||||
@click="showAddModal = true"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
>
|
||||
<i class="fas fa-list"></i> 批量添加
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<n-button
|
||||
@click="refreshData"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
|
||||
type="tertiary"
|
||||
>
|
||||
<i class="fas fa-refresh"></i> 刷新
|
||||
</button>
|
||||
<button
|
||||
</n-button>
|
||||
<n-button
|
||||
@click="clearAll"
|
||||
class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 flex items-center gap-2"
|
||||
type="error"
|
||||
>
|
||||
<i class="fas fa-trash"></i> 清空全部
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -189,17 +137,11 @@ https://pan.baidu.com/s/345678</pre>
|
||||
<div class="text-sm text-gray-400 dark:text-gray-600 mb-4">你可以点击上方"添加资源"按钮快速导入资源</div>
|
||||
<div class="flex gap-2">
|
||||
<NuxtLink
|
||||
to="/add-resource"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
|
||||
to="/admin/add-resource"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-plus"></i> 添加资源
|
||||
</NuxtLink>
|
||||
<button
|
||||
@click="showAddModal = true"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-list"></i> 批量添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -324,8 +266,6 @@ interface ReadyResource {
|
||||
|
||||
const readyResources = ref<ReadyResource[]>([])
|
||||
const loading = ref(false)
|
||||
const showAddModal = ref(false)
|
||||
const resourceText = ref('')
|
||||
const pageLoading = ref(true) // 添加页面加载状态
|
||||
|
||||
// 分页相关状态
|
||||
@@ -344,6 +284,7 @@ const systemConfigStore = useSystemConfigStore()
|
||||
// 获取系统配置
|
||||
const systemConfig = ref<any>(null)
|
||||
const updatingConfig = ref(false) // 添加配置更新状态
|
||||
const dialog = useDialog()
|
||||
const fetchSystemConfig = async () => {
|
||||
try {
|
||||
const response = await systemConfigApi.getSystemConfig()
|
||||
@@ -449,66 +390,55 @@ const refreshConfig = () => {
|
||||
fetchSystemConfig()
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
const closeModal = () => {
|
||||
showAddModal.value = false
|
||||
resourceText.value = ''
|
||||
}
|
||||
|
||||
// 批量添加
|
||||
const handleBatchAdd = async () => {
|
||||
if (!resourceText.value.trim()) {
|
||||
alert('请输入资源内容')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await readyResourceApi.createReadyResourcesFromText(resourceText.value) as any
|
||||
console.log('批量添加成功:', response)
|
||||
closeModal()
|
||||
fetchData()
|
||||
alert(`成功添加 ${response.data.count} 个资源`)
|
||||
} catch (error) {
|
||||
console.error('批量添加失败:', error)
|
||||
alert('批量添加失败,请检查输入格式')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 删除资源
|
||||
const deleteResource = async (id: number) => {
|
||||
if (!confirm('确定要删除这个待处理资源吗?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await readyResourceApi.deleteReadyResource(id)
|
||||
// 如果当前页没有数据了,回到上一页
|
||||
if (readyResources.value.length === 1 && currentPage.value > 1) {
|
||||
currentPage.value--
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这个待处理资源吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await readyResourceApi.deleteReadyResource(id)
|
||||
// 如果当前页没有数据了,回到上一页
|
||||
if (readyResources.value.length === 1 && currentPage.value > 1) {
|
||||
currentPage.value--
|
||||
}
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
alert('删除失败')
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
alert('删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 清空全部
|
||||
const clearAll = async () => {
|
||||
if (!confirm('确定要清空所有待处理资源吗?此操作不可恢复!')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await readyResourceApi.clearReadyResources() as any
|
||||
console.log('清空成功:', response)
|
||||
currentPage.value = 1 // 清空后回到第一页
|
||||
fetchData()
|
||||
alert(`成功清空 ${response.data.deleted_count} 个资源`)
|
||||
} catch (error) {
|
||||
console.error('清空失败:', error)
|
||||
alert('清空失败')
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要清空所有待处理资源吗?此操作不可恢复!',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
const response = await readyResourceApi.clearReadyResources() as any
|
||||
console.log('清空成功:', response)
|
||||
currentPage.value = 1 // 清空后回到第一页
|
||||
fetchData()
|
||||
alert(`成功清空 ${response.data.deleted_count} 个资源`)
|
||||
} catch (error) {
|
||||
console.error('清空失败:', error)
|
||||
alert('清空失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
|
||||
@@ -22,11 +22,10 @@
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">搜索资源</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
@keyup.enter="handleSearch"
|
||||
type="text"
|
||||
class="w-full px-4 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="输入文件名或链接进行搜索..."
|
||||
/>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
@@ -69,18 +68,18 @@
|
||||
<!-- 搜索按钮 -->
|
||||
<div class="mt-4 flex justify-between items-center">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<n-button
|
||||
@click="handleSearch"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center gap-2"
|
||||
type="primary"
|
||||
>
|
||||
<i class="fas fa-search"></i> 搜索
|
||||
</button>
|
||||
<button
|
||||
</n-button>
|
||||
<n-button
|
||||
@click="clearFilters"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
|
||||
type="tertiary"
|
||||
>
|
||||
<i class="fas fa-times"></i> 清除筛选
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
共找到 <span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个资源
|
||||
@@ -91,26 +90,26 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<n-button
|
||||
@click="showBatchModal = true"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
type="primary"
|
||||
>
|
||||
<i class="fas fa-list"></i> 批量操作
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<n-button
|
||||
@click="refreshData"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
|
||||
type="tertiary"
|
||||
>
|
||||
<i class="fas fa-refresh"></i> 刷新
|
||||
</button>
|
||||
<button
|
||||
</n-button>
|
||||
<n-button
|
||||
@click="exportData"
|
||||
class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 flex items-center gap-2"
|
||||
type="info"
|
||||
>
|
||||
<i class="fas fa-download"></i> 导出
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -119,9 +118,9 @@
|
||||
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-xl p-6 max-w-2xl w-full mx-4 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="closeBatchModal" class="text-gray-500 hover:text-gray-800">
|
||||
<n-button @click="closeBatchModal" type="tertiary" size="small">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -155,11 +154,19 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">选择标签:</label>
|
||||
<div class="space-y-2">
|
||||
<div v-for="tag in tags" :key="tag.id" class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
<n-checkbox
|
||||
:value="tag.id"
|
||||
v-model="batchTags"
|
||||
class="mr-2"
|
||||
:checked="batchTags.includes(tag.id)"
|
||||
@update:checked="(checked) => {
|
||||
if (checked) {
|
||||
batchTags.push(tag.id)
|
||||
} else {
|
||||
const index = batchTags.indexOf(tag.id)
|
||||
if (index > -1) {
|
||||
batchTags.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
<span class="text-sm">{{ tag.name }}</span>
|
||||
</div>
|
||||
@@ -168,12 +175,12 @@
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-6">
|
||||
<button @click="closeBatchModal" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
|
||||
<n-button @click="closeBatchModal" type="tertiary">
|
||||
取消
|
||||
</button>
|
||||
<button @click="handleBatchAction" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
|
||||
</n-button>
|
||||
<n-button @click="handleBatchAction" type="primary">
|
||||
执行操作
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,11 +192,9 @@
|
||||
<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">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectAll"
|
||||
@change="toggleSelectAll"
|
||||
class="mr-2"
|
||||
<n-checkbox
|
||||
v-model:checked="selectAll"
|
||||
@update:checked="toggleSelectAll"
|
||||
/>
|
||||
ID
|
||||
</th>
|
||||
@@ -219,17 +224,11 @@
|
||||
<div class="text-sm text-gray-400 dark:text-gray-600 mb-4">你可以点击上方"添加资源"按钮快速导入资源</div>
|
||||
<div class="flex gap-2">
|
||||
<NuxtLink
|
||||
to="/add-resource"
|
||||
to="/admin/add-resource"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-plus"></i> 添加资源
|
||||
</NuxtLink>
|
||||
<button
|
||||
@click="showBatchModal = true"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-list"></i> 批量操作
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -240,11 +239,19 @@
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 font-medium">
|
||||
<input
|
||||
type="checkbox"
|
||||
<n-checkbox
|
||||
:value="resource.id"
|
||||
v-model="selectedResources"
|
||||
class="mr-2"
|
||||
:checked="selectedResources.includes(resource.id)"
|
||||
@update:checked="(checked) => {
|
||||
if (checked) {
|
||||
selectedResources.push(resource.id)
|
||||
} else {
|
||||
const index = selectedResources.indexOf(resource.id)
|
||||
if (index > -1) {
|
||||
selectedResources.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
{{ resource.id }}
|
||||
</td>
|
||||
@@ -419,6 +426,7 @@ const batchCategory = ref('')
|
||||
const batchTags = ref<number[]>([])
|
||||
const selectedResources = ref<number[]>([])
|
||||
const selectAll = ref(false)
|
||||
const dialog = useDialog()
|
||||
|
||||
// API
|
||||
import { useResourceApi, usePanApi, useCategoryApi, useTagApi } from '~/composables/useApi'
|
||||
@@ -577,8 +585,8 @@ const visiblePages = computed(() => {
|
||||
})
|
||||
|
||||
// 全选/取消全选
|
||||
const toggleSelectAll = () => {
|
||||
if (selectAll.value) {
|
||||
const toggleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
selectedResources.value = resources.value.map(r => r.id)
|
||||
} else {
|
||||
selectedResources.value = []
|
||||
@@ -600,10 +608,18 @@ const handleBatchAction = async () => {
|
||||
try {
|
||||
switch (batchAction.value) {
|
||||
case 'delete':
|
||||
if (confirm(`确定要删除选中的 ${selectedResources.value.length} 个资源吗?`)) {
|
||||
await resourceApi.batchDeleteResources(selectedResources.value)
|
||||
alert('批量删除成功')
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: `确定要删除选中的 ${selectedResources.value.length} 个资源吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
await resourceApi.batchDeleteResources(selectedResources.value)
|
||||
alert('批量删除成功')
|
||||
}
|
||||
})
|
||||
return
|
||||
break
|
||||
case 'update_category':
|
||||
if (!batchCategory.value) {
|
||||
@@ -649,16 +665,23 @@ const editResource = (resource: Resource) => {
|
||||
|
||||
// 删除资源
|
||||
const deleteResource = async (id: number) => {
|
||||
if (confirm('确定要删除这个资源吗?')) {
|
||||
try {
|
||||
await resourceApi.deleteResource(id)
|
||||
alert('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
alert('删除失败')
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这个资源吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await resourceApi.deleteResource(id)
|
||||
alert('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
alert('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
|
||||
@@ -278,6 +278,7 @@
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="saving"
|
||||
@click="saveConfig"
|
||||
>
|
||||
<i v-if="saving" class="fas fa-spinner fa-spin mr-2"></i>
|
||||
{{ saving ? '保存中...' : '保存配置' }}
|
||||
|
||||
@@ -29,11 +29,10 @@
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@keyup="debounceSearch"
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
@input="debounceSearch"
|
||||
type="text"
|
||||
class="w-64 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 transition-all text-sm"
|
||||
placeholder="搜索标签名称..."
|
||||
/>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
@@ -203,19 +202,18 @@
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ editingTag ? '编辑标签' : '添加标签' }}
|
||||
</h3>
|
||||
<button @click="closeModal" class="text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<n-button @click="closeModal" type="tertiary" size="small">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">标签名称:</label>
|
||||
<input
|
||||
v-model="formData.name"
|
||||
<n-input
|
||||
v-model:value="formData.name"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
placeholder="请输入标签名称"
|
||||
/>
|
||||
</div>
|
||||
@@ -235,29 +233,27 @@
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">描述:</label>
|
||||
<textarea
|
||||
v-model="formData.description"
|
||||
rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
<n-input
|
||||
v-model:value="formData.description"
|
||||
type="textarea"
|
||||
placeholder="请输入标签描述(可选)"
|
||||
></textarea>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
<n-button
|
||||
type="tertiary"
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-600 rounded-md hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="submitting"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ submitting ? '提交中...' : (editingTag ? '更新' : '添加') }}
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -308,6 +304,7 @@ let searchTimeout: NodeJS.Timeout | null = null
|
||||
const showAddModal = ref(false)
|
||||
const submitting = ref(false)
|
||||
const editingTag = ref<any>(null)
|
||||
const dialog = useDialog()
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
@@ -374,6 +371,8 @@ const fetchTags = async () => {
|
||||
search: searchQuery.value
|
||||
}
|
||||
console.log('获取标签列表参数:', params)
|
||||
console.log('搜索查询值:', searchQuery.value)
|
||||
console.log('搜索查询类型:', typeof searchQuery.value)
|
||||
|
||||
let response: any
|
||||
if (selectedCategory.value) {
|
||||
@@ -419,10 +418,12 @@ const onCategoryChange = () => {
|
||||
|
||||
// 搜索防抖
|
||||
const debounceSearch = () => {
|
||||
console.log('搜索防抖触发,当前搜索值:', searchQuery.value)
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout)
|
||||
}
|
||||
searchTimeout = setTimeout(() => {
|
||||
console.log('执行搜索,搜索值:', searchQuery.value)
|
||||
currentPage.value = 1
|
||||
fetchTags()
|
||||
}, 300)
|
||||
@@ -452,16 +453,21 @@ const editTag = (tag: any) => {
|
||||
|
||||
// 删除标签
|
||||
const deleteTag = async (tagId: number) => {
|
||||
if (!confirm(`确定要删除标签吗?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await tagApi.deleteTag(tagId)
|
||||
await fetchTags()
|
||||
} catch (error) {
|
||||
console.error('删除标签失败:', error)
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除标签吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await tagApi.deleteTag(tagId)
|
||||
await fetchTags()
|
||||
} catch (error) {
|
||||
console.error('删除标签失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
@@ -481,10 +487,24 @@ const handleSubmit = async () => {
|
||||
category_id: categoryId
|
||||
}
|
||||
|
||||
let response: any
|
||||
if (editingTag.value) {
|
||||
await tagApi.updateTag(editingTag.value.id, submitData)
|
||||
response = await tagApi.updateTag(editingTag.value.id, submitData)
|
||||
} else {
|
||||
await tagApi.createTag(submitData)
|
||||
response = await tagApi.createTag(submitData)
|
||||
}
|
||||
|
||||
console.log('标签操作响应:', response)
|
||||
|
||||
// 检查是否是恢复操作
|
||||
if (response && response.message && response.message.includes('恢复成功')) {
|
||||
console.log('检测到标签恢复操作,延迟刷新数据')
|
||||
closeModal()
|
||||
// 延迟一点时间再刷新,确保数据库状态已更新
|
||||
setTimeout(async () => {
|
||||
await fetchTags()
|
||||
}, 500)
|
||||
return
|
||||
}
|
||||
|
||||
closeModal()
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">用户管理</h2>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<n-button
|
||||
@click="showCreateModal = true"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||
type="primary"
|
||||
>
|
||||
添加用户
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,9 +52,11 @@
|
||||
{{ user.last_login ? formatDate(user.last_login) : '从未登录' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button @click="editUser(user)" class="text-indigo-600 hover:text-indigo-900 mr-3">编辑</button>
|
||||
<button @click="showChangePasswordModal(user)" class="text-yellow-600 hover:text-yellow-900 mr-3">修改密码</button>
|
||||
<button @click="deleteUser(user.id)" class="text-red-600 hover:text-red-900">删除</button>
|
||||
<n-button @click="editUser(user)" type="info" size="small" class="mr-3" :title="user.username === 'admin' ? '管理员用户信息不可修改' : '编辑用户'">
|
||||
编辑{{ user.username === 'admin' ? ' (只读)' : '' }}
|
||||
</n-button>
|
||||
<n-button @click="showChangePasswordModal(user)" type="warning" size="small" class="mr-3">修改密码</n-button>
|
||||
<n-button @click="deleteUser(user.id)" type="error" size="small">删除</n-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -69,33 +71,44 @@
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
{{ showEditModal ? '编辑用户' : '创建用户' }}
|
||||
</h3>
|
||||
<div v-if="showEditModal && editingUser?.username === 'admin'" class="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||
<p class="text-sm text-yellow-800">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||
管理员用户信息不可修改,只能通过修改密码功能来更新密码。
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="showEditModal && editingUser?.username !== 'admin'" class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<p class="text-sm text-blue-800">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
编辑模式:用户名和邮箱不可修改,只能修改角色和激活状态。
|
||||
</p>
|
||||
</div>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">用户名</label>
|
||||
<input
|
||||
v-model="form.username"
|
||||
<n-input
|
||||
v-model:value="form.username"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
:disabled="showEditModal"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">邮箱</label>
|
||||
<input
|
||||
v-model="form.email"
|
||||
<n-input
|
||||
v-model:value="form.email"
|
||||
type="email"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
:disabled="showEditModal"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showCreateModal">
|
||||
<label class="block text-sm font-medium text-gray-700">密码</label>
|
||||
<input
|
||||
v-model="form.password"
|
||||
<n-input
|
||||
v-model:value="form.password"
|
||||
type="password"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -103,20 +116,19 @@
|
||||
<select
|
||||
v-model="form.role"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
:disabled="showEditModal && editingUser?.username === 'admin'"
|
||||
>
|
||||
<option value="user">用户</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
v-model="form.is_active"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700">激活状态</span>
|
||||
</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">激活状态</label>
|
||||
<n-switch
|
||||
v-model:value="form.is_active"
|
||||
size="medium"
|
||||
:disabled="showEditModal && editingUser?.username === 'admin'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end space-x-3">
|
||||
@@ -129,7 +141,8 @@
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700"
|
||||
class="px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="showEditModal && editingUser?.username === 'admin'"
|
||||
>
|
||||
{{ showEditModal ? '更新' : '创建' }}
|
||||
</button>
|
||||
@@ -153,22 +166,20 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">新密码</label>
|
||||
<input
|
||||
v-model="passwordForm.newPassword"
|
||||
<n-input
|
||||
v-model:value="passwordForm.newPassword"
|
||||
type="password"
|
||||
required
|
||||
minlength="6"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="请输入新密码(至少6位)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">确认新密码</label>
|
||||
<input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
<n-input
|
||||
v-model:value="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</div>
|
||||
@@ -210,6 +221,7 @@ const showEditModal = ref(false)
|
||||
const showPasswordModal = ref(false)
|
||||
const editingUser = ref(null)
|
||||
const changingPasswordUser = ref(null)
|
||||
const dialog = useDialog()
|
||||
const form = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
@@ -271,16 +283,23 @@ const updateUser = async () => {
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = async (id) => {
|
||||
if (!confirm('确定要删除这个用户吗?')) return
|
||||
|
||||
try {
|
||||
const { useUserApi } = await import('~/composables/useApi')
|
||||
const userApi = useUserApi()
|
||||
await userApi.deleteUser(id)
|
||||
await fetchUsers()
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error)
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这个用户吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
const { useUserApi } = await import('~/composables/useApi')
|
||||
const userApi = useUserApi()
|
||||
await userApi.deleteUser(id)
|
||||
await fetchUsers()
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 显示修改密码模态框
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<div class="w-full max-w-3xl mx-auto mb-4 sm:mb-8 px-2 sm:px-0">
|
||||
<ClientOnly>
|
||||
<div class="relative">
|
||||
<n-input round placeholder="搜索" v-model="searchQuery" @blur="handleSearch" @keyup.enter="handleSearch">
|
||||
<n-input round placeholder="搜索" v-model:value="searchQuery" @blur="handleSearch" @keyup.enter="handleSearch">
|
||||
<template #suffix>
|
||||
<i class="fas fa-search text-gray-400"></i>
|
||||
</template>
|
||||
|
||||
@@ -12,27 +12,25 @@
|
||||
<form @submit.prevent="handleLogin" class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-100">用户名</label>
|
||||
<input
|
||||
<n-input
|
||||
type="text"
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
required
|
||||
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 dark:text-gray-100">密码</label>
|
||||
<input
|
||||
<n-input
|
||||
type="password"
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
required
|
||||
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>
|
||||
|
||||
|
||||
@@ -47,10 +47,8 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-400">自动刷新:</label>
|
||||
<input
|
||||
v-model="autoRefresh"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
<n-checkbox
|
||||
v-model:checked="autoRefresh"
|
||||
/>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">{{ autoRefreshInterval }}秒</span>
|
||||
</div>
|
||||
|
||||
@@ -10,53 +10,49 @@
|
||||
<form @submit.prevent="handleRegister" class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-100">用户名</label>
|
||||
<input
|
||||
<n-input
|
||||
type="text"
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
required
|
||||
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 dark:text-gray-100">邮箱</label>
|
||||
<input
|
||||
<n-input
|
||||
type="email"
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
required
|
||||
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 dark:text-gray-100">密码</label>
|
||||
<input
|
||||
<n-input
|
||||
type="password"
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
required
|
||||
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 dark:text-gray-100">确认密码</label>
|
||||
<input
|
||||
<n-input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
v-model="form.confirmPassword"
|
||||
required
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user