新增订单页面、用户管理、系统设置

This commit is contained in:
2025-04-25 11:11:43 +08:00
parent 5b0541520d
commit 02a9c72c06
7 changed files with 3381 additions and 147 deletions

View File

@@ -31,18 +31,24 @@ const menuData = [
icon: 'layout-grid',
title: '项目管理'
},
{
path: '/admin/orders',
name: 'OrdersManage',
icon: 'orders',
title: '订单管理'
},
{
path: '/admin/users',
name: 'UserManage',
icon: 'users',
title: '用户管理'
},
// {
// path: '/admin/settings',
// name: 'Settings',
// icon: 'settings',
// title: '系统设置'
// }
{
path: '/admin/setting',
name: 'Setting',
icon: 'setting',
title: '系统设置'
}
]
const navigateTo = (path) => {
@@ -88,10 +94,25 @@ const navigateTo = (path) => {
<svg v-else-if="item.icon === 'users'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<svg v-else-if="item.icon === 'settings'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg v-else-if="item.icon === 'setting'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<svg v-else-if="item.icon === 'orders'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<!-- 订单文档主体带优雅折角 -->
<path d="M19 4.5a2.25 2.25 0 00-2.25-2.25h-11.5A2.25 2.25 0 003 4.5v15a2.25 2.25 0 002.25 2.25h11.5a2.25 2.25 0 002.25-2.25V8.25M16.5 18.75h-12m12-10.5h-12m12-6.75v3.75"
stroke-linecap="round"
stroke-linejoin="round"/>
<!-- 订单详情箭头带渐变描边 -->
<path d="M12 15l3-3m0-3l-3-3"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="4 4"/>
<!-- 精致点缀订单编号角标 -->
<circle cx="19.5" cy="6" r="1.5" fill="currentColor" opacity="0.08"/>
</svg>
</div>
<!-- 标题 -->

View File

@@ -40,8 +40,10 @@ const routes = [
children: [
{ path: '', redirect: '/admin/dashboard' },
{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/admin/Dashboard.vue') },
{ path: 'projects', name: 'ProjectManage', component: () => import('@/views/admin/ProjectsCRUD.vue') },
{ path: 'users', name: 'UsersManage', component: () => import('@/views/admin/Users.vue') }
{ path: 'projects', name: 'ProjectManage', component: () => import('@/views/admin/Projects.vue') },
{ path: 'users', name: 'UsersManage', component: () => import('@/views/admin/Users.vue') },
{ path: 'setting', name: 'SettingManage', component: () => import('@/views/admin/Setting.vue') },
{ path: 'orders', name: 'OrdersManage', component: () => import('@/views/admin/Orders.vue') }
]
},
]

1413
src/views/admin/Orders.vue Normal file

File diff suppressed because it is too large Load Diff

811
src/views/admin/Setting.vue Normal file
View File

@@ -0,0 +1,811 @@
<script setup>
import { ref, onMounted } from "vue"
import AOS from "aos"
import "aos/dist/aos.css"
import { message, Upload } from "ant-design-vue"
onMounted(() => {
AOS.init({
duration: 800,
offset: 120,
easing: "ease-out-cubic",
once: false,
})
})
// 语言选项
const languageOptions = [
{ value: "zh-CN", label: "简体中文" },
{ value: "en-US", label: "English" },
{ value: "ja-JP", label: "日本語" },
]
// 时区选项
const timezoneOptions = [
{ value: "Asia/Shanghai", label: "(GMT+08:00) 北京, 上海, 香港" },
{ value: "Asia/Tokyo", label: "(GMT+09:00) 东京" },
{ value: "America/New_York", label: "(GMT-05:00) 纽约" },
{ value: "Europe/London", label: "(GMT+00:00) 伦敦" },
{ value: "Europe/Paris", label: "(GMT+01:00) 巴黎" },
]
// 密码复杂度选项
const passwordComplexityOptions = [
{ value: "low", label: "低 (仅字母和数字)" },
{ value: "medium", label: "中 (字母、数字和特殊字符)" },
{ value: "high", label: "高 (大小写字母、数字和特殊字符)" },
]
// 系统设置数据
const settings = ref({
// 网站基本设置
siteSettings: {
siteName: "企业管理系统",
logo: "https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280",
favicon: "https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280",
description: "一个功能强大的企业管理系统,提供项目管理、用户管理、订单管理等功能。",
keywords: "企业管理,项目管理,用户管理,订单管理",
copyright: "© 2024 企业管理系统. 保留所有权利。"
},
// 邮件设置
emailSettings: {
smtpServer: "smtp.example.com",
smtpPort: 587,
smtpUsername: "admin@example.com",
smtpPassword: "********",
senderEmail: "noreply@example.com",
senderName: "企业管理系统",
enableEmailNotifications: true
},
// 安全设置
securitySettings: {
loginAttempts: 5,
lockoutTime: 30,
passwordExpiry: 90,
twoFactorAuth: true,
passwordComplexity: "medium"
},
// 其他设置
generalSettings: {
maintenance: false,
maintenanceMessage: "系统正在维护中,请稍后再试。",
defaultLanguage: "zh-CN",
defaultTimezone: "Asia/Shanghai"
}
})
// 当前激活的标签
const activeTab = ref("site")
// Logo上传相关
const logoFileList = ref([])
const logoUploading = ref(false)
const logoPreviewVisible = ref(false)
const logoPreviewImage = ref("")
// Favicon上传相关
const faviconFileList = ref([])
const faviconUploading = ref(false)
const faviconPreviewVisible = ref(false)
const faviconPreviewImage = ref("")
const handleLogoPreview = (file) => {
logoPreviewImage.value = file.url || file.preview
logoPreviewVisible.value = true
}
const handleFaviconPreview = (file) => {
faviconPreviewImage.value = file.url || file.preview
faviconPreviewVisible.value = true
}
const handleLogoUploadChange = (info) => {
logoFileList.value = info.fileList.slice(-1)
if (info.file.status === "uploading") {
logoUploading.value = true
return
}
if (info.file.status === "done") {
// 模拟上传成功后获取图片URL
settings.value.siteSettings.logo = "https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280"
logoUploading.value = false
message.success("Logo上传成功")
}
if (info.file.status === "error") {
logoUploading.value = false
message.error("Logo上传失败")
}
}
const handleFaviconUploadChange = (info) => {
faviconFileList.value = info.fileList.slice(-1)
if (info.file.status === "uploading") {
faviconUploading.value = true
return
}
if (info.file.status === "done") {
// 模拟上传成功后获取图片URL
settings.value.siteSettings.favicon = "https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280"
faviconUploading.value = false
message.success("Favicon上传成功")
}
if (info.file.status === "error") {
faviconUploading.value = false
message.error("Favicon上传失败")
}
}
const beforeUpload = (file) => {
const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png"
if (!isJpgOrPng) {
message.error("只能上传JPG或PNG格式的图片!")
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
message.error("图片大小不能超过2MB!")
}
return isJpgOrPng && isLt2M
}
const customRequest = ({ onSuccess }) => {
// 模拟上传
setTimeout(() => {
onSuccess("ok", { status: "done" })
}, 800)
}
// 初始化文件列表
onMounted(() => {
if (settings.value.siteSettings.logo) {
logoFileList.value = [
{
uid: "-1",
name: "logo.png",
status: "done",
url: settings.value.siteSettings.logo,
},
]
}
if (settings.value.siteSettings.favicon) {
faviconFileList.value = [
{
uid: "-1",
name: "favicon.png",
status: "done",
url: settings.value.siteSettings.favicon,
},
]
}
})
// 保存设置
const saveLoading = ref(false)
const saveSettings = () => {
saveLoading.value = true
// 模拟API请求
setTimeout(() => {
message.success("设置保存成功")
saveLoading.value = false
}, 800)
}
// 测试邮件设置
const testEmailLoading = ref(false)
const testEmailSettings = () => {
testEmailLoading.value = true
// 模拟API请求
setTimeout(() => {
message.success("测试邮件发送成功")
testEmailLoading.value = false
}, 1500)
}
// 发送测试邮件模态框
const showTestEmailModal = ref(false)
const testEmailAddress = ref("")
const openTestEmailModal = () => {
testEmailAddress.value = ""
showTestEmailModal.value = true
}
const closeTestEmailModal = () => {
showTestEmailModal.value = false
}
const sendTestEmail = () => {
if (!testEmailAddress.value) {
message.error("请输入测试邮箱地址")
return
}
testEmailLoading.value = true
// 模拟API请求
setTimeout(() => {
message.success(`测试邮件已发送至 ${testEmailAddress.value}`)
testEmailLoading.value = false
closeTestEmailModal()
}, 1500)
}
</script>
<template>
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-6" data-aos="fade-up">
<!-- 页面标题 -->
<div class="mb-6">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">系统设置</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">管理系统的全局配置和参数设置</p>
</div>
<!-- 设置选项卡 -->
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
<button
v-for="tab in [
{ id: 'site', name: '网站设置' },
{ id: 'email', name: '邮件设置' },
{ id: 'security', name: '安全设置' },
{ id: 'general', name: '其他设置' }
]"
:key="tab.id"
@click="activeTab = tab.id"
:class="[
activeTab === tab.id
? 'border-rose-500 text-rose-600 dark:text-rose-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:border-gray-600',
'whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm cursor-pointer'
]"
>
{{ tab.name }}
</button>
</nav>
</div>
<!-- 网站设置 -->
<div v-if="activeTab === 'site'" class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 网站名称 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">网站名称</label>
<input
v-model="settings.siteSettings.siteName"
type="text"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入网站名称"
/>
</div>
<!-- 版权信息 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">版权信息</label>
<input
v-model="settings.siteSettings.copyright"
type="text"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入版权信息"
/>
</div>
</div>
<!-- Logo上传 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">网站Logo</label>
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
<Upload
v-model:file-list="logoFileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="customRequest"
@preview="handleLogoPreview"
@change="handleLogoUploadChange"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
:maxCount="1"
>
<div v-if="logoFileList.length < 1">
<div class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>点击上传Logo</span>
</div>
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
建议尺寸: 200x60px支持JPG、PNG格式大小不超过2MB
</div>
</div>
</div>
<!-- Favicon上传 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">网站图标 (Favicon)</label>
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
<Upload
v-model:file-list="faviconFileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="customRequest"
@preview="handleFaviconPreview"
@change="handleFaviconUploadChange"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
:maxCount="1"
>
<div v-if="faviconFileList.length < 1">
<div class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>点击上传Favicon</span>
</div>
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
建议尺寸: 32x32px支持JPG、PNG格式大小不超过2MB
</div>
</div>
</div>
<!-- 网站描述 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">网站描述</label>
<textarea
v-model="settings.siteSettings.description"
rows="3"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入网站描述"
></textarea>
</div>
<!-- 网站关键词 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">网站关键词</label>
<input
v-model="settings.siteSettings.keywords"
type="text"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入网站关键词,用逗号分隔"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">多个关键词请用英文逗号分隔</p>
</div>
</div>
<!-- 邮件设置 -->
<div v-if="activeTab === 'email'" class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- SMTP服务器 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP服务器</label>
<input
v-model="settings.emailSettings.smtpServer"
type="text"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="例如: smtp.example.com"
/>
</div>
<!-- SMTP端口 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP端口</label>
<input
v-model.number="settings.emailSettings.smtpPort"
type="number"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="例如: 587"
/>
</div>
<!-- SMTP用户名 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP用户名</label>
<input
v-model="settings.emailSettings.smtpUsername"
type="text"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入SMTP用户名"
/>
</div>
<!-- SMTP密码 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP密码</label>
<input
v-model="settings.emailSettings.smtpPassword"
type="password"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入SMTP密码"
/>
</div>
<!-- 发件人邮箱 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">发件人邮箱</label>
<input
v-model="settings.emailSettings.senderEmail"
type="email"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入发件人邮箱"
/>
</div>
<!-- 发件人名称 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">发件人名称</label>
<input
v-model="settings.emailSettings.senderName"
type="text"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入发件人名称"
/>
</div>
</div>
<!-- 启用邮件通知 -->
<div class="flex items-center">
<input
id="enableEmailNotifications"
v-model="settings.emailSettings.enableEmailNotifications"
type="checkbox"
class="h-4 w-4 text-rose-600 focus:ring-rose-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
/>
<label for="enableEmailNotifications" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">
启用邮件通知
</label>
</div>
<!-- 测试邮件设置按钮 -->
<div class="pt-4">
<button
type="button"
class="px-5 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
@click="openTestEmailModal"
:disabled="testEmailLoading"
>
<span v-if="testEmailLoading" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
测试邮件设置
</button>
</div>
</div>
<!-- 安全设置 -->
<div v-if="activeTab === 'security'" class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 最大登录尝试次数 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">最大登录尝试次数</label>
<input
v-model.number="settings.securitySettings.loginAttempts"
type="number"
min="1"
max="10"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">超过此次数将锁定账户</p>
</div>
<!-- 锁定时间 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">锁定时间(分钟)</label>
<input
v-model.number="settings.securitySettings.lockoutTime"
type="number"
min="5"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">账户锁定后需等待的时间</p>
</div>
<!-- 密码过期时间 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">密码过期时间(天)</label>
<input
v-model.number="settings.securitySettings.passwordExpiry"
type="number"
min="0"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">0表示永不过期</p>
</div>
<!-- 密码复杂度 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">密码复杂度</label>
<select
v-model="settings.securitySettings.passwordComplexity"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option v-for="option in passwordComplexityOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
<!-- 双因素认证 -->
<div class="flex items-center">
<input
id="twoFactorAuth"
v-model="settings.securitySettings.twoFactorAuth"
type="checkbox"
class="h-4 w-4 text-rose-600 focus:ring-rose-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
/>
<label for="twoFactorAuth" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">
启用双因素认证
</label>
</div>
</div>
<!-- 其他设置 -->
<div v-if="activeTab === 'general'" class="space-y-6">
<!-- 维护模式 -->
<div class="flex items-center">
<input
id="maintenance"
v-model="settings.generalSettings.maintenance"
type="checkbox"
class="h-4 w-4 text-rose-600 focus:ring-rose-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
/>
<label for="maintenance" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">
启用维护模式
</label>
</div>
<!-- 维护模式提示信息 -->
<div v-if="settings.generalSettings.maintenance">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">维护模式提示信息</label>
<textarea
v-model="settings.generalSettings.maintenanceMessage"
rows="3"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入维护模式提示信息"
></textarea>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 默认语言 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">默认语言</label>
<select
v-model="settings.generalSettings.defaultLanguage"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option v-for="option in languageOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
<!-- 默认时区 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">默认时区</label>
<select
v-model="settings.generalSettings.defaultTimezone"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option v-for="option in timezoneOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
</div>
<!-- 保存按钮 -->
<div class="mt-8 flex justify-end">
<button
type="button"
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
@click="saveSettings"
:disabled="saveLoading"
>
<span v-if="saveLoading" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
保存设置
</button>
</div>
</div>
<!-- Logo预览模态框 -->
<div v-if="logoPreviewVisible" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="logoPreviewVisible = false"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-transparent rounded-lg text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl sm:w-full relative z-50">
<div class="relative">
<button
class="absolute top-2 right-2 p-2 rounded-full bg-gray-800/50 text-white hover:bg-gray-700/50 transition-colors cursor-pointer"
@click="logoPreviewVisible = false"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<img :src="logoPreviewImage" class="max-h-[80vh] max-w-full rounded-lg shadow-xl" />
</div>
</div>
</div>
</div>
<!-- Favicon预览模态框 -->
<div v-if="faviconPreviewVisible" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="faviconPreviewVisible = false"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-transparent rounded-lg text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl sm:w-full relative z-50">
<div class="relative">
<button
class="absolute top-2 right-2 p-2 rounded-full bg-gray-800/50 text-white hover:bg-gray-700/50 transition-colors cursor-pointer"
@click="faviconPreviewVisible = false"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<img :src="faviconPreviewImage" class="max-h-[80vh] max-w-full rounded-lg shadow-xl" />
</div>
</div>
</div>
</div>
<!-- 测试邮件模态框 -->
<div v-if="showTestEmailModal" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="closeTestEmailModal"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full relative z-50">
<div class="bg-white dark:bg-gray-800 px-6 pt-5 pb-4 sm:p-6">
<div class="flex justify-between items-center mb-5">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white" id="modal-title">
发送测试邮件
</h3>
<button
class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 cursor-pointer"
@click="closeTestEmailModal"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form @submit.prevent="sendTestEmail" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">测试邮箱地址</label>
<input
v-model="testEmailAddress"
type="email"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入接收测试邮件的邮箱地址"
required
/>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button
type="button"
class="px-5 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
@click="closeTestEmailModal"
>
取消
</button>
<button
type="submit"
class="px-5 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
:disabled="testEmailLoading"
>
<span v-if="testEmailLoading" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
发送测试邮件
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
/* 自定义样式 */
/* 毛玻璃效果增强 */
.backdrop-blur-xl {
backdrop-filter: blur(16px);
}
.backdrop-blur-md {
backdrop-filter: blur(12px);
}
.backdrop-blur-sm {
backdrop-filter: blur(4px);
}
/* 上传组件暗黑模式适配 */
:deep(.ant-upload-list-picture-card .ant-upload-list-item) {
border-color: #4b5563;
}
:deep(.ant-upload.ant-upload-select-picture-card) {
background-color: transparent;
border-color: #4b5563;
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 20px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.7);
}
/* 暗黑模式滚动条 */
.dark ::-webkit-scrollbar-thumb {
background-color: rgba(75, 85, 99, 0.5);
}
.dark ::-webkit-scrollbar-thumb:hover {
background-color: rgba(75, 85, 99, 0.7);
}
</style>

986
src/views/admin/Users.vue Normal file
View File

@@ -0,0 +1,986 @@
<script setup>
import { ref, onMounted, computed } from "vue"
import AOS from "aos"
import "aos/dist/aos.css"
import { message, Upload } from "ant-design-vue"
onMounted(() => {
AOS.init({
duration: 800,
offset: 120,
easing: "ease-out-cubic",
once: false,
})
})
// 角色数据
const roleOptions = [
{ id: 1, name: "管理员" },
{ id: 2, name: "编辑" },
{ id: 3, name: "普通用户" },
{ id: 4, name: "访客" },
]
// 权限数据
const allPermissions = [
{ id: 1, name: "用户管理" },
{ id: 2, name: "内容管理" },
{ id: 3, name: "订单管理" },
{ id: 4, name: "系统设置" },
{ id: 5, name: "数据统计" },
{ id: 6, name: "财务管理" },
{ id: 7, name: "客户管理" },
{ id: 8, name: "营销管理" },
]
// 用户状态选项
const statusOptions = [
{ value: "启用", label: "启用" },
{ value: "禁用", label: "禁用" },
]
// 模拟用户数据
const dataSource = ref([
{
id: 1,
username: "admin",
avatar: "https://randomuser.me/api/portraits/men/32.jpg",
email: "admin@example.com",
phone: "13800138000",
role: roleOptions[0],
status: "启用",
createdAt: "2024-01-15T08:30:00Z",
lastLogin: "2024-03-28T10:15:30Z",
permissions: [
allPermissions[0],
allPermissions[1],
allPermissions[2],
allPermissions[3],
allPermissions[4],
],
},
{
id: 2,
username: "editor",
avatar: "https://randomuser.me/api/portraits/women/44.jpg",
email: "editor@example.com",
phone: "13900139000",
role: roleOptions[1],
status: "启用",
createdAt: "2024-02-10T14:20:00Z",
lastLogin: "2024-03-27T16:45:12Z",
permissions: [allPermissions[1], allPermissions[2]],
},
{
id: 3,
username: "user1",
avatar: "https://randomuser.me/api/portraits/men/22.jpg",
email: "user1@example.com",
phone: "13700137000",
role: roleOptions[2],
status: "启用",
createdAt: "2024-02-20T09:15:00Z",
lastLogin: "2024-03-25T11:30:45Z",
permissions: [allPermissions[2]],
},
{
id: 4,
username: "user2",
avatar: "https://randomuser.me/api/portraits/women/24.jpg",
email: "user2@example.com",
phone: "13600136000",
role: roleOptions[2],
status: "禁用",
createdAt: "2024-03-05T16:40:00Z",
lastLogin: "2024-03-20T09:10:22Z",
permissions: [allPermissions[2]],
},
{
id: 5,
username: "guest",
avatar: "https://randomuser.me/api/portraits/men/67.jpg",
email: "guest@example.com",
phone: "13500135000",
role: roleOptions[3],
status: "启用",
createdAt: "2024-03-10T11:25:00Z",
lastLogin: "2024-03-15T14:20:18Z",
permissions: [],
},
])
// 分页
const currentPage = ref(1)
const pageSize = ref(10)
const totalItems = ref(dataSource.value.length)
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return dataSource.value.slice(start, end)
})
const changePage = (page) => {
currentPage.value = page
}
// 搜索和筛选
const searchQuery = ref("")
const filterRole = ref("all")
const filterStatus = ref("all")
const filteredData = computed(() => {
return dataSource.value.filter(
(user) =>
(searchQuery.value === "" ||
user.username.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
user.phone.includes(searchQuery.value)) &&
(filterRole.value === "all" || user.role.id === parseInt(filterRole.value)) &&
(filterStatus.value === "all" || user.status === filterStatus.value)
)
})
const filteredAndPaginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredData.value.slice(start, end)
})
// 模态框
const showModal = ref(false)
const currentItem = ref(null)
const formLoading = ref(false)
// 头像上传相关
const fileList = ref([])
const uploading = ref(false)
const previewVisible = ref(false)
const previewImage = ref("")
const handlePreview = (file) => {
previewImage.value = file.url || file.preview
previewVisible.value = true
}
const handleUploadChange = (info) => {
if (info.file.status === "uploading") {
uploading.value = true
return
}
if (info.file.status === "done") {
// 模拟上传成功后获取图片URL
currentItem.value.avatar = "https://randomuser.me/api/portraits/men/32.jpg"
uploading.value = false
message.success("头像上传成功")
}
if (info.file.status === "error") {
uploading.value = false
message.error("头像上传失败")
}
}
const beforeUpload = (file) => {
const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png"
if (!isJpgOrPng) {
message.error("只能上传JPG或PNG格式的图片!")
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
message.error("图片大小不能超过2MB!")
}
return isJpgOrPng && isLt2M
}
const customRequest = ({ onSuccess }) => {
// 模拟上传
setTimeout(() => {
onSuccess("ok", { status: "done" })
}, 800)
}
const openModal = (item = null) => {
currentItem.value = item
? JSON.parse(JSON.stringify(item)) // 深拷贝避免直接修改原对象
: {
username: "",
avatar: "",
email: "",
phone: "",
role: roleOptions[2], // 默认为普通用户
status: "启用",
permissions: [],
}
// 重置文件列表
fileList.value = []
if (currentItem.value.avatar) {
fileList.value = [
{
uid: "-1",
name: "avatar.jpg",
status: "done",
url: currentItem.value.avatar,
},
]
}
showModal.value = true
}
const closeModal = () => {
showModal.value = false
}
// 确认删除模态框
const showDeleteConfirm = ref(false)
const itemToDelete = ref(null)
const openDeleteConfirm = (id) => {
itemToDelete.value = id
showDeleteConfirm.value = true
}
const closeDeleteConfirm = () => {
showDeleteConfirm.value = false
itemToDelete.value = null
}
const handleDelete = () => {
if (itemToDelete.value) {
dataSource.value = dataSource.value.filter((item) => item.id !== itemToDelete.value)
totalItems.value = dataSource.value.length
closeDeleteConfirm()
message.success("用户删除成功")
}
}
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" })
}
const handleSubmit = () => {
formLoading.value = true
// 模拟API请求
setTimeout(() => {
if (currentItem.value.id) {
// 更新现有用户
const index = dataSource.value.findIndex((item) => item.id === currentItem.value.id)
if (index !== -1) {
dataSource.value[index] = { ...currentItem.value }
}
message.success("用户更新成功")
} else {
// 添加新用户
const newId = Math.max(...dataSource.value.map((item) => item.id), 0) + 1
dataSource.value.push({
...currentItem.value,
id: newId,
createdAt: new Date().toISOString(),
lastLogin: null,
})
totalItems.value = dataSource.value.length
message.success("用户创建成功")
}
formLoading.value = false
closeModal()
}, 800)
}
// 权限管理
const permissionInputValue = ref("")
const handleAddPermission = () => {
if (permissionInputValue.value) {
// 检查是否已存在该权限
const existingPermission = allPermissions.find(
(permission) => permission.name.toLowerCase() === permissionInputValue.value.toLowerCase()
)
// 如果存在且未添加到当前用户,则添加
if (
existingPermission &&
!currentItem.value.permissions.some((permission) => permission.id === existingPermission.id)
) {
currentItem.value.permissions.push(existingPermission)
}
// 如果不存在,创建新权限并添加
else if (!existingPermission) {
const newPermissionId = Math.max(...allPermissions.map((permission) => permission.id), 0) + 1
const newPermission = { id: newPermissionId, name: permissionInputValue.value }
allPermissions.push(newPermission)
currentItem.value.permissions.push(newPermission)
}
permissionInputValue.value = ""
}
}
const removePermission = (index) => {
currentItem.value.permissions.splice(index, 1)
}
// 重置密码模态框
const showResetPasswordModal = ref(false)
const resetPasswordUserId = ref(null)
const resetPasswordUsername = ref("")
const newPassword = ref("")
const confirmPassword = ref("")
const resetPasswordLoading = ref(false)
const passwordError = ref("")
const openResetPasswordModal = (user) => {
resetPasswordUserId.value = user.id
resetPasswordUsername.value = user.username
newPassword.value = ""
confirmPassword.value = ""
passwordError.value = ""
showResetPasswordModal.value = true
}
const closeResetPasswordModal = () => {
showResetPasswordModal.value = false
}
const handleResetPassword = () => {
// 验证密码
if (!newPassword.value) {
passwordError.value = "请输入新密码"
return
}
if (newPassword.value.length < 6) {
passwordError.value = "密码长度不能少于6位"
return
}
if (newPassword.value !== confirmPassword.value) {
passwordError.value = "两次输入的密码不一致"
return
}
resetPasswordLoading.value = true
// 模拟API请求
setTimeout(() => {
message.success(`用户 ${resetPasswordUsername.value} 的密码已重置`)
resetPasswordLoading.value = false
closeResetPasswordModal()
}, 800)
}
</script>
<template>
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-6" data-aos="fade-up">
<!-- 页面标题和新建按钮 -->
<div class="mb-6 flex justify-between items-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">用户管理</h2>
<button
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
@click="openModal()"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
新建用户
</button>
</div>
<!-- 搜索和筛选 -->
<div class="mb-6 grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="relative">
<input
v-model="searchQuery"
type="text"
class="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="搜索用户名、邮箱或电话"
/>
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
<div>
<select
v-model="filterRole"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option value="all">所有角色</option>
<option v-for="role in roleOptions" :key="role.id" :value="role.id">{{ role.name }}</option>
</select>
</div>
<div>
<select
v-model="filterStatus"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option value="all">所有状态</option>
<option v-for="status in statusOptions" :key="status.value" :value="status.value">{{ status.label }}</option>
</select>
</div>
</div>
<!-- 表格 -->
<div class="rounded-2xl border border-gray-200 dark:border-gray-700">
<div class="overflow-x-auto" style="height: 60vh">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<!-- 表头 -->
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
用户信息
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
联系方式
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
角色
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
状态
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
最后登录
</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<!-- 表格内容 -->
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="user in filteredAndPaginatedData" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center space-x-3">
<div class="flex-shrink-0 h-10 w-10 rounded-full overflow-hidden">
<img :src="user.avatar" :alt="user.username" class="h-full w-full object-cover" />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ user.username }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">ID: {{ user.id }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900 dark:text-white">{{ user.email }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">{{ user.phone }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{{ user.role.name }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
:class="`px-2 py-1 text-xs font-medium rounded-full ${
user.status === '启用'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'
}`"
>
{{ user.status }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{{ user.lastLogin ? formatDate(user.lastLogin) : '从未登录' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex justify-end space-x-2">
<button
class="p-1.5 text-amber-600 hover:text-amber-800 dark:text-amber-400 dark:hover:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-900/20 rounded-lg transition-colors cursor-pointer"
title="编辑"
@click="openModal(user)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
class="p-1.5 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors cursor-pointer"
title="重置密码"
@click="openResetPasswordModal(user)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
</button>
<button
class="p-1.5 text-rose-600 hover:text-rose-800 dark:text-rose-400 dark:hover:text-rose-300 hover:bg-rose-50 dark:hover:bg-rose-900/20 rounded-lg transition-colors cursor-pointer"
title="删除"
@click="openDeleteConfirm(user.id)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>
</tr>
<tr v-if="filteredAndPaginatedData.length === 0">
<td colspan="6" class="px-6 py-10 text-center text-gray-500 dark:text-gray-400">
没有找到匹配的用户
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="bg-white dark:bg-gray-800 px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div class="text-sm text-gray-700 dark:text-gray-300">
{{ filteredData.length }} 条记录
</div>
<div class="flex space-x-1">
<button
class="px-3 py-1 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 cursor-pointer"
:disabled="currentPage === 1"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === 1 }"
@click="currentPage > 1 && changePage(currentPage - 1)"
>
上一页
</button>
<button
v-for="page in Math.ceil(filteredData.length / pageSize)"
:key="page"
class="px-3 py-1 border rounded-md text-sm font-medium cursor-pointer"
:class="page === currentPage
? 'bg-rose-600 text-white border-rose-600 dark:bg-rose-600 dark:border-rose-600'
: 'border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'"
@click="changePage(page)"
>
{{ page }}
</button>
<button
class="px-3 py-1 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 cursor-pointer"
:disabled="currentPage === Math.ceil(filteredData.length / pageSize)"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === Math.ceil(filteredData.length / pageSize) }"
@click="currentPage < Math.ceil(filteredData.length / pageSize) && changePage(currentPage + 1)"
>
下一页
</button>
</div>
</div>
</div>
</div>
<!-- 用户表单模态框 -->
<div v-if="showModal" class="fixed inset-0 z-50 overflow-y-scroll" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="closeModal"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full relative z-50">
<div class="bg-white dark:bg-gray-800 px-6 pt-5 pb-4 sm:p-6">
<div class="flex justify-between items-center mb-5">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white" id="modal-title">
{{ currentItem?.id ? '编辑用户' : '新建用户' }}
</h3>
<button
class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 cursor-pointer"
@click="closeModal"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form @submit.prevent="handleSubmit" class="space-y-6">
<!-- 基本信息 -->
<div class="grid grid-cols-1 gap-6">
<!-- 用户名 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">用户名</label>
<input
v-model="currentItem.username"
type="text"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入用户名"
required
/>
</div>
<!-- 邮箱 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">邮箱</label>
<input
v-model="currentItem.email"
type="email"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入邮箱"
required
/>
</div>
<!-- 电话 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">电话</label>
<input
v-model="currentItem.phone"
type="text"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入电话"
/>
</div>
<!-- 角色 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">角色</label>
<select
v-model="currentItem.role"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option v-for="role in roleOptions" :key="role.id" :value="role">
{{ role.name }}
</option>
</select>
</div>
<!-- 状态 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">状态</label>
<select
v-model="currentItem.status"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
<!-- 头像上传 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">头像</label>
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
<Upload
v-model:file-list="fileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="customRequest"
@preview="handlePreview"
@change="handleUploadChange"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
:maxCount="1"
>
<div v-if="fileList.length < 1">
<div class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>点击上传头像</span>
</div>
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持JPGPNG格式大小不超过2MB
</div>
</div>
</div>
<!-- 权限 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">权限</label>
<div class="flex flex-wrap gap-2 mb-2">
<span
v-for="(permission, index) in currentItem.permissions"
:key="index"
class="px-3 py-1.5 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-300 text-sm font-medium rounded-lg flex items-center"
>
{{ permission.name }}
<button
type="button"
class="ml-1.5 text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-200 cursor-pointer"
@click="removePermission(index)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</span>
</div>
<div class="flex">
<select
v-model="permissionInputValue"
class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-l-xl focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="">选择权限</option>
<option v-for="permission in allPermissions" :key="permission.id" :value="permission.name">
{{ permission.name }}
</option>
</select>
<button
type="button"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-r-xl transition-colors cursor-pointer"
@click="handleAddPermission"
>
添加
</button>
</div>
</div>
</div>
<!-- 表单按钮 -->
<div class="flex justify-end space-x-3 pt-4">
<button
type="button"
class="px-5 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
@click="closeModal"
>
取消
</button>
<button
type="submit"
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
:disabled="formLoading"
>
<span v-if="formLoading" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
{{ currentItem?.id ? '保存修改' : '创建用户' }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="closeDeleteConfirm"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full relative z-50">
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/30 sm:mx-0 sm:h-10 sm:w-10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white" id="modal-title">
确认删除
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
确定要删除这个用户吗此操作无法撤销
</p>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm cursor-pointer"
@click="handleDelete"
>
确认
</button>
<button
type="button"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm cursor-pointer"
@click="closeDeleteConfirm"
>
取消
</button>
</div>
</div>
</div>
</div>
<!-- 重置密码模态框 -->
<div v-if="showResetPasswordModal" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="closeResetPasswordModal"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full relative z-50">
<div class="bg-white dark:bg-gray-800 px-6 pt-5 pb-4 sm:p-6">
<div class="flex justify-between items-center mb-5">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white" id="modal-title">
重置密码 - {{ resetPasswordUsername }}
</h3>
<button
class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 cursor-pointer"
@click="closeResetPasswordModal"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form @submit.prevent="handleResetPassword" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">新密码</label>
<input
v-model="newPassword"
type="password"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入新密码"
required
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">确认密码</label>
<input
v-model="confirmPassword"
type="password"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请再次输入新密码"
required
/>
</div>
<div v-if="passwordError" class="text-sm text-red-600 dark:text-red-400">
{{ passwordError }}
</div>
<div class="flex justify-end space-x-3 pt-4">
<button
type="button"
class="px-5 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
@click="closeResetPasswordModal"
>
取消
</button>
<button
type="submit"
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
:disabled="resetPasswordLoading"
>
<span v-if="resetPasswordLoading" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
确认重置
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 图片预览模态框 -->
<div v-if="previewVisible" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="previewVisible = false"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-transparent rounded-lg text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl sm:w-full relative z-50">
<div class="relative">
<button
class="absolute top-2 right-2 p-2 rounded-full bg-gray-800/50 text-white hover:bg-gray-700/50 transition-colors cursor-pointer"
@click="previewVisible = false"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<img :src="previewImage" class="max-h-[80vh] max-w-full rounded-lg shadow-xl" />
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
/* 自定义样式 */
/* 毛玻璃效果增强 */
.backdrop-blur-xl {
backdrop-filter: blur(16px);
}
.backdrop-blur-md {
backdrop-filter: blur(12px);
}
.backdrop-blur-sm {
backdrop-filter: blur(4px);
}
/* 上传组件暗黑模式适配 */
:deep(.ant-upload-list-picture-card .ant-upload-list-item) {
border-color: #4b5563;
}
:deep(.ant-upload.ant-upload-select-picture-card) {
background-color: transparent;
border-color: #4b5563;
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 20px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.7);
}
/* 暗黑模式滚动条 */
.dark ::-webkit-scrollbar-thumb {
background-color: rgba(75, 85, 99, 0.5);
}
.dark ::-webkit-scrollbar-thumb:hover {
background-color: rgba(75, 85, 99, 0.7);
}
</style>

View File

@@ -15,7 +15,7 @@ const userData = ref({
lastLogin: '2023-06-20',
email: 'user@example.com',
phone: '138****1234',
role: '企业客户',
role: '超级管理员',
projects: 5,
favorites: 12
})
@@ -141,179 +141,180 @@ const activeTab = ref('profile')
<nav class="flex space-x-8">
<button
@click="activeTab = 'profile'"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200 cursor-pointer"
:class="activeTab === 'profile' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'"
>
个人资料
</button>
<button
@click="activeTab = 'projects'"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200 cursor-pointer"
:class="activeTab === 'projects' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'"
>
我的项目
</button>
<button
@click="activeTab = 'settings'"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200 cursor-pointer"
:class="activeTab === 'settings' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'"
>
账户设置
</button>
</nav>
</div>
</div>
</div>
</div>
<!-- 标签页内容 -->
<div class="mb-10" data-aos="fade-up">
<!-- 个人资料 -->
<div v-if="activeTab === 'profile'" class="space-y-6">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-white">基本信息</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">用户名</p>
<p class="text-gray-900 dark:text-white">{{ userData.username }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">邮箱</p>
<p class="text-gray-900 dark:text-white">{{ userData.email }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">手机号</p>
<p class="text-gray-900 dark:text-white">{{ userData.phone }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">注册时间</p>
<p class="text-gray-900 dark:text-white">{{ userData.createdAt }}</p>
</div>
</div>
</div>
<!-- 标签页内容 -->
<div class="mb-10" data-aos="fade-up">
<!-- 个人资料 -->
<div v-if="activeTab === 'profile'" class="space-y-6">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-white">基本信息</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">用户名</p>
<p class="text-gray-900 dark:text-white">{{ userData.username }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">邮箱</p>
<p class="text-gray-900 dark:text-white">{{ userData.email }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">手机号</p>
<p class="text-gray-900 dark:text-white">{{ userData.phone }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">注册时间</p>
<p class="text-gray-900 dark:text-white">{{ userData.createdAt }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-white">账户安全</h2>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">登录密码</p>
<p class="text-sm text-gray-500 dark:text-gray-400">上次修改时间: 2023-03-15</p>
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-white">账户安全</h2>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">登录密码</p>
<p class="text-sm text-gray-500 dark:text-gray-400">上次修改时间: 2023-03-15</p>
</div>
<button class="text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 font-medium">
修改
</button>
</div>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">手机验证</p>
<p class="text-sm text-gray-500 dark:text-gray-400">已绑定: {{ userData.phone }}</p>
</div>
<button class="text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 font-medium">
修改
</button>
</div>
</div>
</div>
</div>
<button class="text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 font-medium">
修改
</button>
</div>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">手机验证</p>
<p class="text-sm text-gray-500 dark:text-gray-400">已绑定: {{ userData.phone }}</p>
</div>
<button class="text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 font-medium">
修改
</button>
</div>
</div>
</div>
</div>
<!-- 我的项目 -->
<div v-else-if="activeTab === 'projects'" class="space-y-6">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<h2 class="text-xl font-bold mb-6 text-gray-900 dark:text-white">最近项目</h2>
<div class="space-y-4">
<div
v-for="project in recentProjects"
:key="project.id"
class="flex items-center justify-between p-4 border border-gray-100 dark:border-gray-700 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<div>
<h3 class="font-medium text-gray-900 dark:text-white">{{ project.title }}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ project.date }}</p>
</div>
<div class="flex items-center">
<!-- 我的项目 -->
<div v-else-if="activeTab === 'projects'" class="space-y-6">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<h2 class="text-xl font-bold mb-6 text-gray-900 dark:text-white">最近项目</h2>
<div class="space-y-4">
<div
v-for="project in recentProjects"
:key="project.id"
class="flex items-center justify-between p-4 border border-gray-100 dark:border-gray-700 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<div>
<h3 class="font-medium text-gray-900 dark:text-white">{{ project.title }}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ project.date }}</p>
</div>
<div class="flex items-center">
<span
class="px-3 py-1 rounded-full text-xs font-medium"
:class="project.status === '进行中' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' : 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'"
>
{{ project.status }}
</span>
<button class="ml-4 text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
</div>
<button class="ml-4 text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
</div>
<div class="mt-6 text-center">
<button class="text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 font-medium flex items-center mx-auto">
查看全部项目
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</button>
</div>
</div>
</div>
<div class="mt-6 text-center">
<button class="text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 font-medium flex items-center mx-auto">
查看全部项目
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</button>
</div>
</div>
</div>
<!-- 账户设置 -->
<div v-else-if="activeTab === 'settings'" class="space-y-6">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<h2 class="text-xl font-bold mb-6 text-gray-900 dark:text-white">通知设置</h2>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">系统通知</p>
<p class="text-sm text-gray-500 dark:text-gray-400">接收系统更新和安全提醒</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" value="" class="sr-only peer" checked>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-rose-300 dark:peer-focus:ring-rose-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-rose-600"></div>
</label>
</div>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">项目更新</p>
<p class="text-sm text-gray-500 dark:text-gray-400">接收项目进度和更新通知</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" value="" class="sr-only peer" checked>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-rose-300 dark:peer-focus:ring-rose-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-rose-600"></div>
</label>
</div>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">营销信息</p>
<p class="text-sm text-gray-500 dark:text-gray-400">接收优惠和促销信息</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" value="" class="sr-only peer">
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-rose-300 dark:peer-focus:ring-rose-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-rose-600"></div>
</label>
</div>
</div>
</div>
<!-- 账户设置 -->
<div v-else-if="activeTab === 'settings'" class="space-y-6">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<h2 class="text-xl font-bold mb-6 text-gray-900 dark:text-white">通知设置</h2>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">系统通知</p>
<p class="text-sm text-gray-500 dark:text-gray-400">接收系统更新和安全提醒</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" value="" class="sr-only peer" checked>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-rose-300 dark:peer-focus:ring-rose-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-rose-600"></div>
</label>
</div>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">项目更新</p>
<p class="text-sm text-gray-500 dark:text-gray-400">接收项目进度和更新通知</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" value="" class="sr-only peer" checked>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-rose-300 dark:peer-focus:ring-rose-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-rose-600"></div>
</label>
</div>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">营销信息</p>
<p class="text-sm text-gray-500 dark:text-gray-400">接收优惠和促销信息</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" value="" class="sr-only peer">
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-rose-300 dark:peer-focus:ring-rose-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-rose-600"></div>
</label>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<h2 class="text-xl font-bold mb-6 text-gray-900 dark:text-white">隐私设置</h2>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">个人资料可见性</p>
<p class="text-sm text-gray-500 dark:text-gray-400">控制谁可以查看您的个人资料</p>
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<h2 class="text-xl font-bold mb-6 text-gray-900 dark:text-white">隐私设置</h2>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">个人资料可见性</p>
<p class="text-sm text-gray-500 dark:text-gray-400">控制谁可以查看您的个人资料</p>
</div>
<select class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-rose-500 focus:border-rose-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
<option value="public">公开</option>
<option value="friends">仅好友</option>
<option value="private">私密</option>
</select>
</div>
</div>
</div>
</div>
<select class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-rose-500 focus:border-rose-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
<option value="public">公开</option>
<option value="friends">仅好友</option>
<option value="private">私密</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>