Files
AIGC/demo/frontend/src/views/MemberManagement.vue
AIGC Developer 8c55f9f376 feat: 完成代码逻辑错误修复和任务清理系统实现
主要更新:
- 修复了所有主要的代码逻辑错误
- 实现了完整的任务清理系统
- 添加了系统设置页面的任务清理管理功能
- 修复了API调用认证问题
- 优化了密码加密和验证机制
- 统一了错误处理模式
- 添加了详细的文档和测试工具

新增功能:
- 任务清理管理界面
- 任务归档和清理日志
- API监控和诊断工具
- 完整的测试套件

技术改进:
- 修复了Repository方法调用错误
- 统一了模型方法调用
- 改进了类型安全性
- 优化了代码结构和可维护性
2025-10-27 10:46:49 +08:00

883 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="member-management">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<div class="logo">
<div class="logo-icon"></div>
<span>LOGO</span>
</div>
<nav class="nav-menu">
<div class="nav-item" @click="goToDashboard">
<el-icon><Grid /></el-icon>
<span>数据仪表台</span>
</div>
<div class="nav-item active">
<el-icon><User /></el-icon>
<span>会员管理</span>
</div>
<div class="nav-item" @click="goToOrders">
<el-icon><ShoppingCart /></el-icon>
<span>订单管理</span>
</div>
<div class="nav-item" @click="goToAPI">
<el-icon><Document /></el-icon>
<span>API管理</span>
</div>
<div class="nav-item" @click="goToTasks">
<el-icon><Document /></el-icon>
<span>生成任务记录</span>
</div>
<div class="nav-item" @click="goToSettings">
<el-icon><Setting /></el-icon>
<span>系统设置</span>
</div>
</nav>
<div class="sidebar-footer">
<div class="online-users">
当前在线用户: <span class="highlight">87/500</span>
</div>
<div class="system-uptime">
系统运行时间: <span class="highlight">48小时32分</span>
</div>
</div>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 顶部搜索栏 -->
<header class="top-header">
<div class="search-bar">
<el-icon class="search-icon"><Search /></el-icon>
<input type="text" placeholder="搜索你的想要的内容" class="search-input" />
</div>
<div class="header-actions">
<el-icon class="notification-icon"><Bell /></el-icon>
<el-icon class="help-icon"><QuestionFilled /></el-icon>
<div class="user-avatar">
<img src="/images/backgrounds/welcome.jpg" alt="用户头像" />
</div>
</div>
</header>
<!-- 会员列表内容 -->
<section class="member-content">
<div class="content-header">
<h2>会员列表</h2>
<div class="selection-info" v-if="selectedMembers.length > 0">
已选择{{ selectedMembers.length }}
</div>
</div>
<div class="table-toolbar">
<div class="toolbar-left">
<el-select v-model="selectedLevel" placeholder="全部等级" size="small" @change="handleLevelChange">
<el-option label="全部等级" value="all" />
<el-option label="专业会员" value="professional" />
<el-option label="标准会员" value="standard" />
</el-select>
</div>
<div class="toolbar-right">
<el-button type="danger" size="small" @click="deleteSelected" :disabled="selectedMembers.length === 0">
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</div>
<div class="table-container">
<table class="member-table">
<thead>
<tr>
<th class="checkbox-col">
<input type="checkbox" @change="toggleAllSelection" :checked="isAllSelected" />
</th>
<th>用户ID</th>
<th>用户名</th>
<th>会员等级</th>
<th>剩余资源点</th>
<th>到期时间</th>
<th>编辑</th>
<th>删除</th>
</tr>
</thead>
<tbody>
<tr v-for="member in memberList" :key="member.id" class="table-row">
<td class="checkbox-col">
<input
type="checkbox"
:checked="selectedMembers.some(m => m.id === member.id)"
@change="toggleMemberSelection(member)" />
</td>
<td>{{ member.id }}</td>
<td>{{ member.username }}</td>
<td>
<span class="level-tag" :class="member.level === '专业会员' ? 'professional' : 'standard'">
{{ member.level }}
</span>
</td>
<td>{{ member.points.toLocaleString() }}</td>
<td>{{ member.expiryDate }}</td>
<td>
<button class="action-btn edit-btn" @click="editMember(member)">编辑</button>
</td>
<td>
<button class="action-btn delete-btn" @click="deleteMember(member)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<div class="pagination">
<button class="page-btn" @click="prevPage" :disabled="currentPage === 1"></button>
<button
v-for="page in visiblePages"
:key="page"
class="page-btn"
:class="{ active: page === currentPage }"
@click="goToPage(page)">
{{ page }}
</button>
<button class="page-btn" @click="nextPage" :disabled="currentPage === totalPages"></button>
</div>
</div>
</section>
</main>
<!-- 编辑会员对话框 -->
<el-dialog
v-model="editDialogVisible"
title="编辑会员信息"
width="500px"
:before-close="handleCloseEditDialog">
<el-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-width="100px">
<el-form-item label="用户ID" prop="id">
<el-input v-model="editForm.id" disabled />
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="editForm.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="会员等级" prop="level">
<el-select v-model="editForm.level" placeholder="请选择会员等级">
<el-option label="专业会员" value="专业会员" />
<el-option label="标准会员" value="标准会员" />
</el-select>
</el-form-item>
<el-form-item label="剩余资源点" prop="points">
<el-input-number
v-model="editForm.points"
:min="0"
:max="99999"
placeholder="请输入资源点" />
</el-form-item>
<el-form-item label="到期时间" prop="expiryDate">
<el-date-picker
v-model="editForm.expiryDate"
type="date"
placeholder="选择到期时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveEdit" :loading="saveLoading">保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Grid,
User,
ShoppingCart,
Document,
Setting,
User as Search,
Bell,
User as ArrowDown,
User as Edit,
User as Delete
} from '@element-plus/icons-vue'
import * as memberAPI from '@/api/members'
const router = useRouter()
// 数据状态
const selectedMembers = ref([])
const selectedLevel = ref('all')
const currentPage = ref(1)
const pageSize = ref(10)
const totalMembers = ref(50)
// 编辑相关状态
const editDialogVisible = ref(false)
const editFormRef = ref()
const saveLoading = ref(false)
const editForm = ref({
id: '',
username: '',
level: '',
points: 0,
expiryDate: ''
})
// 表单验证规则
const editRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 20, message: '用户名长度在 2 到 20 个字符', trigger: 'blur' }
],
level: [
{ required: true, message: '请选择会员等级', trigger: 'change' }
],
points: [
{ required: true, message: '请输入资源点', trigger: 'blur' },
{ type: 'number', min: 0, message: '资源点不能小于0', trigger: 'blur' }
],
expiryDate: [
{ required: true, message: '请选择到期时间', trigger: 'change' }
]
}
// 会员数据
const memberList = ref([])
// 导航功能
const goToDashboard = () => {
router.push('/')
}
const goToOrders = () => {
router.push('/orders')
}
const goToAPI = () => {
router.push('/api-management')
}
const goToTasks = () => {
router.push('/generate-task-record')
}
const goToSettings = () => {
router.push('/system-settings')
}
// 表格操作
const isAllSelected = computed(() => {
return memberList.value.length > 0 && selectedMembers.value.length === memberList.value.length
})
const totalPages = computed(() => {
return Math.ceil(totalMembers.value / pageSize.value)
})
const visiblePages = computed(() => {
const pages = []
const start = Math.max(1, currentPage.value - 2)
const end = Math.min(totalPages.value, start + 4)
for (let i = start; i <= end; i++) {
pages.push(i)
}
return pages
})
const toggleAllSelection = () => {
if (isAllSelected.value) {
selectedMembers.value = []
} else {
selectedMembers.value = [...memberList.value]
}
}
const toggleMemberSelection = (member) => {
const index = selectedMembers.value.findIndex(m => m.id === member.id)
if (index > -1) {
selectedMembers.value.splice(index, 1)
} else {
selectedMembers.value.push(member)
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
loadMembers()
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
loadMembers()
}
}
const goToPage = (page) => {
currentPage.value = page
loadMembers()
}
const editMember = (member) => {
// 填充编辑表单
editForm.value = {
id: member.id,
username: member.username,
level: member.level,
points: member.points,
expiryDate: member.expiryDate
}
editDialogVisible.value = true
}
const handleCloseEditDialog = () => {
editDialogVisible.value = false
// 重置表单
editFormRef.value?.resetFields()
}
const saveEdit = async () => {
if (!editFormRef.value) return
try {
// 验证表单
await editFormRef.value.validate()
saveLoading.value = true
// 调用API更新会员信息
await memberAPI.updateMember(editForm.value.id, {
username: editForm.value.username,
level: editForm.value.level,
points: editForm.value.points,
expiryDate: editForm.value.expiryDate
})
// 更新本地数据
const index = memberList.value.findIndex(m => m.id === editForm.value.id)
if (index > -1) {
memberList.value[index] = { ...editForm.value }
}
ElMessage.success('会员信息更新成功')
editDialogVisible.value = false
} catch (error) {
console.error('保存失败:', error)
ElMessage.error('保存失败,请检查输入信息')
} finally {
saveLoading.value = false
}
}
const deleteMember = async (member) => {
try {
await ElMessageBox.confirm(
`确定要删除用户 ${member.username} 吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
// 调用API删除会员
await memberAPI.deleteMember(member.id)
// 从本地列表中移除
const index = memberList.value.findIndex(m => m.id === member.id)
if (index > -1) {
memberList.value.splice(index, 1)
totalMembers.value--
}
ElMessage.success('删除成功')
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
}
}
const deleteSelected = async () => {
if (selectedMembers.value.length === 0) {
ElMessage.warning('请先选择要删除的会员')
return
}
try {
await ElMessageBox.confirm(
`确定要删除选中的 ${selectedMembers.value.length} 个会员吗?`,
'批量删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
const ids = selectedMembers.value.map(m => m.id)
// 调用API批量删除
await memberAPI.deleteMembers(ids)
// 从本地列表中移除
memberList.value = memberList.value.filter(m => !ids.includes(m.id))
totalMembers.value -= ids.length
selectedMembers.value = []
ElMessage.success('批量删除成功')
} catch (error) {
if (error !== 'cancel') {
console.error('批量删除失败:', error)
ElMessage.error('批量删除失败')
}
}
}
// 监听筛选条件变化
const handleLevelChange = () => {
currentPage.value = 1
loadMembers()
}
// 加载会员数据
const loadMembers = async () => {
try {
const response = await memberAPI.getMembers({
page: currentPage.value,
pageSize: pageSize.value,
level: selectedLevel.value === 'all' ? '' : selectedLevel.value
})
// 处理API响应数据
if (response && response.list) {
memberList.value = response.list.map(member => ({
id: member.id,
username: member.username,
level: getMembershipLevel(member.membership),
points: member.points,
expiryDate: getMembershipExpiry(member.membership)
}))
totalMembers.value = response.total || 0
} else {
ElMessage.error('API返回数据格式错误')
}
} catch (error) {
console.error('加载会员数据失败:', error)
ElMessage.error('加载会员数据失败')
}
}
// 辅助函数:获取会员等级显示名称
const getMembershipLevel = (membership) => {
if (!membership) return '标准会员'
return membership.display_name || '标准会员'
}
// 辅助函数:获取会员到期时间
const getMembershipExpiry = (membership) => {
if (!membership) return '2025-12-31'
return membership.end_date ? membership.end_date.split(' ')[0] : '2025-12-31'
}
onMounted(() => {
// 初始化数据
loadMembers()
})
</script>
<style scoped>
.member-management {
display: flex;
min-height: 100vh;
background: #f8fafc;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 左侧导航栏 */
.sidebar {
width: 320px;
background: white;
border-right: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
padding: 24px 0;
}
.logo {
display: flex;
align-items: center;
padding: 0 28px;
margin-bottom: 32px;
}
.logo-icon {
width: 24px;
height: 24px;
background: #3b82f6;
border-radius: 4px;
margin-right: 12px;
}
.logo span {
font-size: 18px;
font-weight: 600;
color: #1e293b;
}
.nav-menu {
flex: 1;
padding: 0 24px;
}
.nav-item {
display: flex;
align-items: center;
padding: 18px 24px;
margin-bottom: 6px;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
color: #64748b;
font-size: 16px;
}
.nav-item:hover {
background: #f1f5f9;
color: #334155;
}
.nav-item.active {
background: #eff6ff;
color: #3b82f6;
}
.nav-item .el-icon {
margin-right: 16px;
font-size: 22px;
}
.nav-item span {
font-size: 16px;
font-weight: 500;
}
.sidebar-footer {
padding: 0 32px 20px;
margin-top: auto;
}
.online-users,
.system-uptime {
font-size: 14px;
color: #64748b;
margin-bottom: 10px;
line-height: 1.5;
}
.highlight {
color: #3b82f6;
font-weight: 600;
font-size: 15px;
}
/* 主内容区域 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
background: #f8fafc;
}
/* 顶部搜索栏 */
.top-header {
background: white;
border-bottom: 1px solid #e2e8f0;
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.search-bar {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 12px;
color: #94a3b8;
font-size: 16px;
}
.search-input {
width: 300px;
padding: 8px 12px 8px 40px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
background: #f8fafc;
outline: none;
}
.search-input:focus {
border-color: #3b82f6;
background: white;
}
.search-input::placeholder {
color: #94a3b8;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.notification-icon,
.help-icon {
font-size: 20px;
color: #64748b;
cursor: pointer;
}
.user-avatar {
display: flex;
align-items: center;
cursor: pointer;
}
.user-avatar img {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
/* 会员内容区域 */
.member-content {
padding: 24px;
flex: 1;
}
.content-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.content-header h2 {
font-size: 24px;
font-weight: 600;
color: #1e293b;
margin: 0;
}
.selection-info {
font-size: 14px;
color: #64748b;
background: #f1f5f9;
padding: 8px 16px;
border-radius: 6px;
}
.table-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 16px;
}
.table-container {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 24px;
}
.member-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.member-table thead {
background: #f8fafc;
}
.member-table th {
padding: 12px 16px;
text-align: left;
font-weight: 600;
color: #374151;
border-bottom: 1px solid #e5e7eb;
}
.member-table td {
padding: 12px 16px;
border-bottom: 1px solid #f3f4f6;
color: #374151;
}
.table-row:hover {
background: #f9fafb;
}
.checkbox-col {
width: 50px;
text-align: center;
}
.checkbox-col input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
.level-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
color: white;
}
.level-tag.professional {
background: #ec4899;
}
.level-tag.standard {
background: #3b82f6;
}
.action-btn {
background: none;
border: none;
cursor: pointer;
font-size: 14px;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s ease;
}
.edit-btn {
color: #3b82f6;
}
.edit-btn:hover {
background: #eff6ff;
}
.delete-btn {
color: #dc2626;
}
.delete-btn:hover {
background: #fef2f2;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 24px;
}
.pagination {
display: flex;
align-items: center;
gap: 8px;
}
.page-btn {
padding: 8px 12px;
border: 1px solid #d1d5db;
background: white;
color: #374151;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s ease;
}
.page-btn:hover:not(:disabled) {
background: #f3f4f6;
border-color: #9ca3af;
}
.page-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.member-management {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
}
.nav-menu {
display: flex;
overflow-x: auto;
padding: 0 16px;
}
.nav-item {
white-space: nowrap;
margin-right: 16px;
margin-bottom: 0;
}
.sidebar-footer {
display: none;
}
.search-input {
width: 200px;
}
.member-content {
padding: 16px;
}
}
</style>