Merge branch 'main' of https://gitlab.guxuan.icu/Leo_Ding/GPU_Web
This commit is contained in:
commit
41f69742e6
@ -57,6 +57,21 @@ const routes: RouteRecordRaw[] = [
|
||||
name: "AdminHome",
|
||||
component: () => import("@/views/admin/home/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "fileStore",
|
||||
name: "FileStore",
|
||||
component: () => import("@/views/admin/fileStore/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "history",
|
||||
name: "History",
|
||||
component: () => import("@/views/admin/account/history/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "security",
|
||||
name: "Security",
|
||||
component: () => import("@/views/admin/account/security/index.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
116
src/views/admin/account/history/index.vue
Normal file
116
src/views/admin/account/history/index.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<!-- src/components/AccessLog.vue -->
|
||||
<template>
|
||||
<div class="access-log">
|
||||
<div class="title-wrapper">
|
||||
<h3>访问记录</h3>
|
||||
<span class="desc">以下显示内容为账号及其子账号近3个月的登录记录</span>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="loginRecords"
|
||||
:pagination="false"
|
||||
size="middle"
|
||||
:bordered="true"
|
||||
class="login-table"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
// 定义类型
|
||||
interface LoginRecord {
|
||||
id: number;
|
||||
time: string;
|
||||
ip: string;
|
||||
city: string;
|
||||
type: string;
|
||||
operator: string;
|
||||
}
|
||||
|
||||
// 模拟数据
|
||||
const loginRecords = ref<LoginRecord[]>([
|
||||
{
|
||||
id: 1,
|
||||
time: '2025-11-21 09:58:38',
|
||||
ip: '183.209.69.36',
|
||||
city: '中国 江苏省',
|
||||
type: 'Web微信登录',
|
||||
operator: '炼丹师5075',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
time: '2025-11-19 17:07:13',
|
||||
ip: '183.209.69.36',
|
||||
city: '中国 江苏省',
|
||||
type: 'Web微信登录',
|
||||
operator: '炼丹师5075',
|
||||
},
|
||||
]);
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '访问时间',
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '访问IP地址',
|
||||
dataIndex: 'ip',
|
||||
key: 'ip',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '访问城市',
|
||||
dataIndex: 'city',
|
||||
key: 'city',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '登录类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'operator',
|
||||
key: 'operator',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.access-log {
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title-wrapper h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #faad14;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.login-table {
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
346
src/views/admin/account/security/index.vue
Normal file
346
src/views/admin/account/security/index.vue
Normal file
@ -0,0 +1,346 @@
|
||||
<!-- src/components/AccountSecurity.vue -->
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
CloseOutlined,
|
||||
SaveOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
|
||||
// 定义安全项接口
|
||||
interface SecurityItem {
|
||||
title: string;
|
||||
description: string;
|
||||
status: 'unset' | 'bound' | 'unverified' | 'verified';
|
||||
actionText: string;
|
||||
actionHandler: () => void;
|
||||
}
|
||||
|
||||
const securityItems = ref<SecurityItem[]>([
|
||||
{
|
||||
title: '登录密码',
|
||||
description: '安全性高的密码可以使账号更安全。建议您定期更换密码,设置一个包含字母和数字且长度超过8位的密码',
|
||||
status: 'unset',
|
||||
actionText: '设置',
|
||||
actionHandler: () => openSetPasswordModal(),
|
||||
},
|
||||
{
|
||||
title: '手机绑定',
|
||||
description: '您已绑定了手机178****5075您的手机号可以直接用于登录、找回密码等',
|
||||
status: 'bound',
|
||||
actionText: '修改',
|
||||
actionHandler: () => alert('跳转到修改手机号页面'),
|
||||
},
|
||||
{
|
||||
title: '实名认证',
|
||||
description: '实名认证后可以使用AutoDL更完整的功能,如打开实例的自定义服务等',
|
||||
status: 'unverified',
|
||||
actionText: '立即认证',
|
||||
actionHandler: () => alert('跳转到实名认证页面'),
|
||||
},
|
||||
{
|
||||
title: '微信绑定',
|
||||
description: '您已绑定微信,可快速扫码登录',
|
||||
status: 'bound',
|
||||
actionText: '解绑',
|
||||
actionHandler: () => alert('确认解绑微信?'),
|
||||
},
|
||||
{
|
||||
title: '邮箱绑定',
|
||||
description: '绑定邮箱后可接收系统消息,如余额不足、实例即将到期、实例即将释放等消息',
|
||||
status: 'unset',
|
||||
actionText: '绑定',
|
||||
actionHandler: () => alert('跳转到绑定邮箱页面'),
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
const getStatusIcon = (status: SecurityItem['status']) => {
|
||||
switch (status) {
|
||||
case 'bound':
|
||||
return CheckCircleOutlined;
|
||||
case 'unverified':
|
||||
case 'unset':
|
||||
return ExclamationCircleOutlined;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: SecurityItem['status']) => {
|
||||
switch (status) {
|
||||
case 'bound':
|
||||
return 'green';
|
||||
case 'unverified':
|
||||
case 'unset':
|
||||
return 'red';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getActionColor = (status: SecurityItem['status']) => {
|
||||
return 'blue'; // 统一蓝色即可
|
||||
};
|
||||
|
||||
// 弹窗控制
|
||||
const setPasswordModalVisible = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
phoneNumber: '+86 178****5075', // 示例手机号
|
||||
smsCode: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
// 验证码倒计时
|
||||
const countdown = ref(0);
|
||||
const isSending = ref(false);
|
||||
|
||||
// 发送验证码
|
||||
const sendSmsCode = () => {
|
||||
if (isSending.value) return;
|
||||
isSending.value = true;
|
||||
countdown.value = 60;
|
||||
const timer = setInterval(() => {
|
||||
countdown.value--;
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer);
|
||||
isSending.value = false;
|
||||
countdown.value = 0;
|
||||
}
|
||||
}, 1000);
|
||||
message.success('验证码已发送');
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
form.smsCode = '';
|
||||
form.newPassword = '';
|
||||
form.confirmPassword = '';
|
||||
countdown.value = 0;
|
||||
isSending.value = false;
|
||||
};
|
||||
|
||||
// 打开设置密码弹窗
|
||||
const openSetPasswordModal = () => {
|
||||
resetForm();
|
||||
setPasswordModalVisible.value = true;
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const closeSetPasswordModal = () => {
|
||||
setPasswordModalVisible.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// 保存密码
|
||||
const savePassword = () => {
|
||||
if (!form.smsCode) {
|
||||
message.error('请输入验证码');
|
||||
return;
|
||||
}
|
||||
if (!form.newPassword) {
|
||||
message.error('请输入新密码');
|
||||
return;
|
||||
}
|
||||
if (!form.confirmPassword) {
|
||||
message.error('请确认密码');
|
||||
return;
|
||||
}
|
||||
if (form.newPassword !== form.confirmPassword) {
|
||||
message.error('两次输入的密码不一致');
|
||||
return;
|
||||
}
|
||||
if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/.test(form.newPassword)) {
|
||||
message.error('密码必须为8~16位,且包含字母和数字');
|
||||
return;
|
||||
}
|
||||
|
||||
// 模拟提交成功
|
||||
message.success('密码设置成功!');
|
||||
closeSetPasswordModal();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="account-security">
|
||||
<h2>账号安全</h2>
|
||||
<div v-for="(item, index) in securityItems" :key="index" class="security-item">
|
||||
<div class="item-header">
|
||||
<span class="title">{{ item.title }}</span>
|
||||
<div class="status">
|
||||
<component
|
||||
:is="getStatusIcon(item.status)"
|
||||
v-if="getStatusIcon(item.status)"
|
||||
:class="['icon', `color-${getStatusColor(item.status)}`]"
|
||||
/>
|
||||
<span :class="['text', `color-${getStatusColor(item.status)}`]">
|
||||
{{ item.status === 'bound' ? '已绑定' : item.status === 'unverified' ? '未认证' : '未设置' }}
|
||||
</span>
|
||||
<span class="divider">|</span>
|
||||
<button
|
||||
class="action-btn"
|
||||
:class="`color-${getActionColor(item.status)}`"
|
||||
@click="item.actionHandler"
|
||||
>
|
||||
{{ item.actionText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="description">{{ item.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 设置密码弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="setPasswordModalVisible"
|
||||
title="设置密码"
|
||||
width="500px"
|
||||
:footer="null"
|
||||
@cancel="closeSetPasswordModal"
|
||||
>
|
||||
<div style="padding: 24px 0;">
|
||||
<div style="margin-bottom: 24px; font-size: 16px; color: #333;">
|
||||
手机号:{{ form.phoneNumber }}
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px; display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: red;">*</span>
|
||||
<label style="font-size: 14px; color: #333;">手机验证码:</label>
|
||||
<a-input
|
||||
v-model:value="form.smsCode"
|
||||
placeholder="请输入验证码"
|
||||
style="flex: 1;"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="isSending || countdown > 0"
|
||||
@click="sendSmsCode"
|
||||
style="width: 120px;"
|
||||
>
|
||||
{{ isSending || countdown > 0 ? `${countdown}s后重发` : '发送验证码' }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px; display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: red;">*</span>
|
||||
<label style="font-size: 14px; color: #333;">设置新密码:</label>
|
||||
<a-input
|
||||
v-model:value="form.newPassword"
|
||||
placeholder="请输入8~16位包含数字和字母的密码"
|
||||
type="password"
|
||||
style="flex: 1;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px; display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: red;">*</span>
|
||||
<label style="font-size: 14px; color: #3 33;">确认新密码:</label>
|
||||
<a-input
|
||||
v-model:value="form.confirmPassword"
|
||||
placeholder="请确认密码"
|
||||
type="password"
|
||||
style="flex: 1;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: right; padding: 16px 24px;">
|
||||
<a-button @click="closeSetPasswordModal" style="margin-right: 8px;">取消</a-button>
|
||||
<a-button type="primary" @click="savePassword">保存</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.account-security {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.account-security h2 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.security-item {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #0066cc;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
margin-left: 8px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.color-green {
|
||||
color: #52c41a !important;
|
||||
}
|
||||
|
||||
.color-red {
|
||||
color: #ff4d4f !important;
|
||||
}
|
||||
|
||||
.color-blue {
|
||||
color: #0066cc !important;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
255
src/views/admin/fileStore/index.vue
Normal file
255
src/views/admin/fileStore/index.vue
Normal file
@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 标题 -->
|
||||
<div class="header">
|
||||
<h1>文件存储</h1>
|
||||
</div>
|
||||
|
||||
<!-- 区域选择 -->
|
||||
<div class="region-section">
|
||||
<h2 class="section-title">选择存储区域</h2>
|
||||
<div class="region-tabs">
|
||||
<button
|
||||
v-for="region in regions"
|
||||
:key="region.id"
|
||||
:class="['region-tab', { active: selectedRegion === region.id }]"
|
||||
@click="selectRegion(region.id)"
|
||||
>
|
||||
{{ region.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中心图标 -->
|
||||
<div class="center-section">
|
||||
<div class="center-icon">
|
||||
<i class="icon-book"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要操作按钮 -->
|
||||
<div class="action-section">
|
||||
<button class="btn-primary" @click="initializeStorage">
|
||||
初始化文件存储
|
||||
</button>
|
||||
<p class="action-hint">将在 {{ getSelectedRegionName() }} 创建存储空间</p>
|
||||
</div>
|
||||
|
||||
<!-- 底部帮助链接 -->
|
||||
<div class="help-section">
|
||||
<div class="help-links">
|
||||
<a href="#" class="link" @click.prevent="viewHelp">文件存储使用介绍</a>
|
||||
<a href="#" class="link" @click.prevent="viewPricing">查看计费规则</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 定义区域选项
|
||||
interface Region {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const regions = [
|
||||
{ id: 'a100', name: 'A100专区' },
|
||||
{ id: 'v100', name: 'V100专区' },
|
||||
{ id: 'foshan', name: '佛山区' },
|
||||
{ id: 'beijing', name: '北京B区' }
|
||||
]
|
||||
|
||||
const selectedRegion = ref('a100') // 默认选中 A100
|
||||
|
||||
// 方法:获取选中区域名称
|
||||
const getSelectedRegionName = () => {
|
||||
const region = regions.find(r => r.id === selectedRegion.value)
|
||||
return region ? region.name : ''
|
||||
}
|
||||
|
||||
// 方法:选择区域
|
||||
const selectRegion = (id: string) => {
|
||||
selectedRegion.value = id
|
||||
}
|
||||
|
||||
// 方法:初始化存储
|
||||
const initializeStorage = () => {
|
||||
alert(`正在初始化 ${selectedRegion.value} 的文件存储...`)
|
||||
// 这里可以跳转到下一步流程
|
||||
}
|
||||
|
||||
// 方法:查看帮助
|
||||
const viewHelp = () => {
|
||||
alert('文件存储使用介绍')
|
||||
}
|
||||
|
||||
// 方法:查看计费规则
|
||||
const viewPricing = () => {
|
||||
alert('计费规则详情')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
/* max-width: 800px;
|
||||
margin: 0 auto; */
|
||||
padding: 40px 30px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 标题区域 */
|
||||
.header {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 区域选择 */
|
||||
.region-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.region-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.region-tab {
|
||||
padding: 12px 24px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 14px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.region-tab:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.region-tab.active {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
/* 中心图标区域 */
|
||||
.center-section {
|
||||
margin: 40px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.center-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.icon-book {
|
||||
font-size: 36px;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.icon-book::before {
|
||||
content: "📁";
|
||||
}
|
||||
|
||||
/* 操作区域 */
|
||||
.action-section {
|
||||
margin: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 14px 32px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
.action-hint {
|
||||
margin-top: 12px;
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 帮助区域 */
|
||||
.help-section {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.help-links {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
color: #3498db;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: #2980b9;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.region-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.region-tab {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.help-links {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.center-section {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,67 +1,144 @@
|
||||
<template>
|
||||
<div class="admin-layout">
|
||||
<a-menu v-model:openKeys="state.openKeys" v-model:selectedKeys="state.selectedKeys" style="width: 256px"
|
||||
:mode="state.mode" :items="items" :theme="state.theme"></a-menu>
|
||||
<div class="admin-contain">
|
||||
<router-view />
|
||||
</div>
|
||||
<div class="admin-layout">
|
||||
<!-- 左侧菜单 -->
|
||||
<a-menu
|
||||
v-model:openKeys="state.openKeys"
|
||||
v-model:selectedKeys="state.selectedKeys"
|
||||
class="admin-sidebar"
|
||||
:mode="state.mode"
|
||||
:items="items"
|
||||
:theme="state.theme"
|
||||
@select="handleMenuSelect"
|
||||
/>
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="admin-contain">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { h, reactive } from 'vue';
|
||||
import { h, reactive, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
MailOutlined,
|
||||
CalendarOutlined,
|
||||
AppstoreOutlined,
|
||||
SettingOutlined,
|
||||
HomeOutlined,
|
||||
FolderOpenOutlined,
|
||||
ConsoleSqlOutlined,
|
||||
GlobalOutlined,
|
||||
LaptopOutlined,
|
||||
MoneyCollectOutlined,
|
||||
TeamOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { MenuMode, MenuTheme } from 'ant-design-vue';
|
||||
import { ItemType } from 'ant-design-vue';
|
||||
|
||||
const state = reactive({
|
||||
mode: 'inline' as MenuMode,
|
||||
theme: 'light' as MenuTheme,
|
||||
selectedKeys: ['1'],
|
||||
openKeys: ['sub1'],
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
function getItem(
|
||||
label: string,
|
||||
key: string,
|
||||
icon?: any,
|
||||
children?: ItemType[],
|
||||
type?: 'group',
|
||||
): ItemType {
|
||||
return {
|
||||
key,
|
||||
icon,
|
||||
children,
|
||||
label,
|
||||
type,
|
||||
} as ItemType;
|
||||
// 定义菜单原始数据
|
||||
interface MenuItem {
|
||||
path: string;
|
||||
name: string;
|
||||
icon?: any;
|
||||
children?: Omit<MenuItem, 'icon'>[]; // 子项不需要 icon
|
||||
}
|
||||
|
||||
const items: ItemType[] = reactive([
|
||||
getItem('Navigation One', '1', h(MailOutlined)),
|
||||
getItem('Navigation Two', '2', h(CalendarOutlined)),
|
||||
getItem('Navigation Two', 'sub1', h(AppstoreOutlined), [
|
||||
getItem('Option 3', '3'),
|
||||
getItem('Option 4', '4'),
|
||||
getItem('Submenu', 'sub1-2', null, [getItem('Option 5', '5'), getItem('Option 6', '6')]),
|
||||
]),
|
||||
getItem('Navigation Three', 'sub2', h(SettingOutlined), [
|
||||
getItem('Option 7', '7'),
|
||||
getItem('Option 8', '8'),
|
||||
getItem('Option 9', '9'),
|
||||
getItem('Option 10', '10'),
|
||||
]),
|
||||
]);
|
||||
const menuItems: MenuItem[] = [
|
||||
{ path: '/layout/overview', name: '总览', icon: HomeOutlined },
|
||||
{ path: '/layout/container', name: '容器实例', icon: ConsoleSqlOutlined },
|
||||
{ path: '/layout/admin/fileStore', name: '文件存储', icon: FolderOpenOutlined },
|
||||
{ path: '/layout/image', name: '镜像', icon: GlobalOutlined },
|
||||
{ path: '/layout/publicData', name: '公开数据', icon: LaptopOutlined },
|
||||
{
|
||||
path: '/layout/fee',
|
||||
name: '费用',
|
||||
icon: MoneyCollectOutlined,
|
||||
children: [
|
||||
{ path: '/layout/fee/detail', name: '详情' },
|
||||
{ path: '/layout/fee/bill', name: '账单' },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/layout/admin',
|
||||
name: '账号',
|
||||
icon: TeamOutlined,
|
||||
children: [
|
||||
{ path: '/layout/admin/account/security', name: '账号安全' },
|
||||
{ path: '/layout/admin/account/history', name: '访问记录' },
|
||||
{ path: '/controlPanel/account/security', name: '安全设置' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 状态
|
||||
const state = reactive({
|
||||
mode: 'inline' as MenuMode,
|
||||
theme: 'light' as MenuTheme,
|
||||
selectedKeys: ['/controlPanel/overview'], // 初始选中第一个
|
||||
openKeys: ['/controlPanel/fee', '/controlPanel/account'], // 默认展开有子菜单的项
|
||||
});
|
||||
|
||||
// 工具函数:生成菜单项
|
||||
function getItem(
|
||||
label: string,
|
||||
key: string,
|
||||
icon?: any,
|
||||
children?: ItemType[],
|
||||
type?: 'group'
|
||||
): ItemType {
|
||||
return {
|
||||
key,
|
||||
icon,
|
||||
children,
|
||||
label,
|
||||
type,
|
||||
} as ItemType;
|
||||
}
|
||||
|
||||
// 将 menuItems 转为 a-menu 所需的 items
|
||||
const items = computed(() => {
|
||||
return menuItems.map((item) => {
|
||||
if (item.children && item.children.length > 0) {
|
||||
const childItems = item.children.map((child) =>
|
||||
getItem(child.name, child.path)
|
||||
);
|
||||
return getItem(item.name, item.path, h(item.icon), childItems);
|
||||
} else {
|
||||
return getItem(item.name, item.path, h(item.icon));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const handleMenuSelect = ({ key }: { key: string }) => {
|
||||
console.log("11!",key);
|
||||
router.push(key);
|
||||
};
|
||||
|
||||
const changeMode = (checked: boolean) => {
|
||||
state.mode = checked ? 'vertical' : 'inline';
|
||||
state.mode = checked ? 'vertical' : 'inline';
|
||||
};
|
||||
|
||||
const changeTheme = (checked: boolean) => {
|
||||
state.theme = checked ? 'dark' : 'light';
|
||||
state.theme = checked ? 'dark' : 'light';
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
height: 100vh; /* 全屏高度 */
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
width: 256px; /* 和你原来设定一致 */
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.admin-contain {
|
||||
flex: 1; /* 占据剩余空间 */
|
||||
padding: 20px;
|
||||
overflow-y: auto; /* 内容超出可滚动 */
|
||||
background-color: #f0f2f5; /* 可选:和 Ant Design 风格一致 */
|
||||
}
|
||||
</style>
|
||||
@ -1,7 +1,13 @@
|
||||
<!-- src/components/AccountSecurity.vue -->
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { ref, reactive } from 'vue';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
CloseOutlined,
|
||||
SaveOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
|
||||
// 定义安全项接口
|
||||
interface SecurityItem {
|
||||
@ -18,7 +24,7 @@ const securityItems = ref<SecurityItem[]>([
|
||||
description: '安全性高的密码可以使账号更安全。建议您定期更换密码,设置一个包含字母和数字且长度超过8位的密码',
|
||||
status: 'unset',
|
||||
actionText: '设置',
|
||||
actionHandler: () => alert('跳转到设置密码页面'),
|
||||
actionHandler: () => openSetPasswordModal(),
|
||||
},
|
||||
{
|
||||
title: '手机绑定',
|
||||
@ -50,7 +56,7 @@ const securityItems = ref<SecurityItem[]>([
|
||||
},
|
||||
]);
|
||||
|
||||
// 根据状态返回对应图标和颜色
|
||||
|
||||
const getStatusIcon = (status: SecurityItem['status']) => {
|
||||
switch (status) {
|
||||
case 'bound':
|
||||
@ -76,10 +82,87 @@ const getStatusColor = (status: SecurityItem['status']) => {
|
||||
};
|
||||
|
||||
const getActionColor = (status: SecurityItem['status']) => {
|
||||
if (status === 'bound') {
|
||||
return 'blue';
|
||||
return 'blue'; // 统一蓝色即可
|
||||
};
|
||||
|
||||
// 弹窗控制
|
||||
const setPasswordModalVisible = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
phoneNumber: '+86 178****5075', // 示例手机号
|
||||
smsCode: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
// 验证码倒计时
|
||||
const countdown = ref(0);
|
||||
const isSending = ref(false);
|
||||
|
||||
// 发送验证码
|
||||
const sendSmsCode = () => {
|
||||
if (isSending.value) return;
|
||||
isSending.value = true;
|
||||
countdown.value = 60;
|
||||
const timer = setInterval(() => {
|
||||
countdown.value--;
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer);
|
||||
isSending.value = false;
|
||||
countdown.value = 0;
|
||||
}
|
||||
}, 1000);
|
||||
message.success('验证码已发送');
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
form.smsCode = '';
|
||||
form.newPassword = '';
|
||||
form.confirmPassword = '';
|
||||
countdown.value = 0;
|
||||
isSending.value = false;
|
||||
};
|
||||
|
||||
// 打开设置密码弹窗
|
||||
const openSetPasswordModal = () => {
|
||||
resetForm();
|
||||
setPasswordModalVisible.value = true;
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const closeSetPasswordModal = () => {
|
||||
setPasswordModalVisible.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// 保存密码
|
||||
const savePassword = () => {
|
||||
if (!form.smsCode) {
|
||||
message.error('请输入验证码');
|
||||
return;
|
||||
}
|
||||
return 'blue'; // 默认蓝色
|
||||
if (!form.newPassword) {
|
||||
message.error('请输入新密码');
|
||||
return;
|
||||
}
|
||||
if (!form.confirmPassword) {
|
||||
message.error('请确认密码');
|
||||
return;
|
||||
}
|
||||
if (form.newPassword !== form.confirmPassword) {
|
||||
message.error('两次输入的密码不一致');
|
||||
return;
|
||||
}
|
||||
if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/.test(form.newPassword)) {
|
||||
message.error('密码必须为8~16位,且包含字母和数字');
|
||||
return;
|
||||
}
|
||||
|
||||
// 模拟提交成功
|
||||
message.success('密码设置成功!');
|
||||
closeSetPasswordModal();
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -110,6 +193,66 @@ const getActionColor = (status: SecurityItem['status']) => {
|
||||
</div>
|
||||
<p class="description">{{ item.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 设置密码弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="setPasswordModalVisible"
|
||||
title="设置密码"
|
||||
width="500px"
|
||||
:footer="null"
|
||||
@cancel="closeSetPasswordModal"
|
||||
>
|
||||
<div style="padding: 24px 0;">
|
||||
<div style="margin-bottom: 24px; font-size: 16px; color: #333;">
|
||||
手机号:{{ form.phoneNumber }}
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px; display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: red;">*</span>
|
||||
<label style="font-size: 14px; color: #333;">手机验证码:</label>
|
||||
<a-input
|
||||
v-model:value="form.smsCode"
|
||||
placeholder="请输入验证码"
|
||||
style="flex: 1;"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="isSending || countdown > 0"
|
||||
@click="sendSmsCode"
|
||||
style="width: 120px;"
|
||||
>
|
||||
{{ isSending || countdown > 0 ? `${countdown}s后重发` : '发送验证码' }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px; display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: red;">*</span>
|
||||
<label style="font-size: 14px; color: #333;">设置新密码:</label>
|
||||
<a-input
|
||||
v-model:value="form.newPassword"
|
||||
placeholder="请输入8~16位包含数字和字母的密码"
|
||||
type="password"
|
||||
style="flex: 1;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px; display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: red;">*</span>
|
||||
<label style="font-size: 14px; color: #3 33;">确认新密码:</label>
|
||||
<a-input
|
||||
v-model:value="form.confirmPassword"
|
||||
placeholder="请确认密码"
|
||||
type="password"
|
||||
style="flex: 1;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: right; padding: 16px 24px;">
|
||||
<a-button @click="closeSetPasswordModal" style="margin-right: 8px;">取消</a-button>
|
||||
<a-button type="primary" @click="savePassword">保存</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user