update: 控制台体验优化

This commit is contained in:
ctwj
2025-08-03 10:50:25 +08:00
parent b5b3c55573
commit 5f8d998c65
22 changed files with 704 additions and 764 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -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>
<!-- 按钮区域 -->

View File

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

View File

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

View File

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

View File

@@ -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('清除失败')
}
}
})
}
// 格式化时间

View File

@@ -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('清空失败')
}
}
})
}
// 格式化时间

View File

@@ -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('删除失败')
}
}
}
})
}
// 工具函数

View File

@@ -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 ? '保存中...' : '保存配置' }}

View File

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

View File

@@ -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)
}
}
})
}
// 显示修改密码模态框

View File

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

View File

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

View File

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

View File

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