新增订单页面、用户管理、系统设置
This commit is contained in:
@@ -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>
|
||||
|
||||
<!-- 标题 -->
|
||||
|
||||
@@ -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
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
811
src/views/admin/Setting.vue
Normal 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">​</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">​</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">​</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
986
src/views/admin/Users.vue
Normal 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">​</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">
|
||||
支持JPG、PNG格式,大小不超过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">​</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">​</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">​</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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user