This commit is contained in:
Leo_Ding 2025-11-25 09:37:05 +08:00
commit 386a64ee3e
6 changed files with 1303 additions and 75 deletions

View File

@ -72,6 +72,22 @@ const routes: RouteRecordRaw[] = [
name: "Security",
component: () => import("@/views/admin/account/security/index.vue"),
},
{
path: "realnameAuth",
name: "RealnameAuth",
component: () => import("@/views/admin/account/realnameAuth/index.vue"),
},
{
path: "enterRealAuth",
name: "EnterRealAuth",
component: () => import("@/views/admin/account/enterRealAuth/index.vue"),
},
{
path: "image",
name: "Image",
component: () => import("@/views/admin/image/index.vue"),
},
],
},
],

View File

@ -0,0 +1,419 @@
<!-- src/views/EnterpriseRealNameAuth.vue -->
<template>
<div class="enterprise-real-name-auth">
<!-- 步骤条 -->
<a-steps :current="0" class="steps">
<a-step title="填写证件信息" />
<a-step title="后台审核" />
<a-step title="完成认证" />
</a-steps>
<!-- 表单 -->
<a-form
ref="formRef"
:model="formState"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
layout="horizontal"
class="auth-form"
>
<!-- 企业信息 -->
<div class="section-title">企业信息</div>
<a-alert
message="企业信息、法人/被授权人信息仅用于进行实名认证,不会泄露您的信息"
type="warning"
show-icon
style="margin-bottom: 24px"
/>
<a-form-item
label="企业证件类型:"
name="certType"
:rules="[{ required: true, message: '请选择企业证件类型' }]"
>
<a-select
v-model:value="formState.certType"
placeholder="请选择"
style="width: 400px"
>
<a-select-option value="business_license">营业执照</a-select-option>
<!-- 可扩展其他类型 -->
</a-select>
</a-form-item>
<a-form-item
label="上传企业证件附件:"
:rules="[{ required: true, validator: validateBusinessLicense }]"
>
<div class="upload-area">
<a-upload
name="file"
list-type="picture-card"
:show-upload-list="false"
:before-upload="beforeUpload"
@change="handleBusinessLicenseChange"
accept="image/png,image/jpeg"
>
<div class="upload-placeholder">
<UserOutlined />
<div style="margin-top: 8px">企业法人营业执照</div>
</div>
</a-upload>
<div v-if="formState.businessLicenseUrl" class="upload-preview">
<img :src="formState.businessLicenseUrl" alt="营业执照" />
</div>
</div>
<div class="upload-tips">支持 jpgjpegpng 格式图片大小不超过 10M</div>
</a-form-item>
<a-form-item
label="企业名称:"
name="enterpriseName"
:rules="[{ required: true, message: '请输入企业名称' }]"
>
<a-input
v-model:value="formState.enterpriseName"
placeholder="请输入企业名称"
style="width: 400px"
/>
</a-form-item>
<a-form-item
label="统一社会信用代码:"
name="creditCode"
:rules="[
{ required: true, message: '请输入统一社会信用代码' },
{ pattern: /^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/, message: '请输入有效的统一社会信用代码' }
]"
>
<a-input
v-model:value="formState.creditCode"
placeholder="请输入统一社会信用代码"
style="width: 400px"
/>
</a-form-item>
<!-- 法人/被授权人信息 -->
<div class="section-title">法人/被授权人信息</div>
<a-form-item
label="请选择您的身份:"
name="identity"
:rules="[{ required: true, message: '请选择身份' }]"
>
<a-radio-group v-model:value="formState.identity">
<a-radio value="legal_representative">法定代表人</a-radio>
<a-radio value="authorized_person">被授权人</a-radio>
</a-radio-group>
</a-form-item>
<!-- 身份证正反面 -->
<a-form-item
label="上传身份证件附件:"
:rules="[
{ required: true, validator: validateIdCardFront },
{ required: true, validator: validateIdCardBack }
]"
>
<div class="id-card-uploads">
<!-- 正面 -->
<div class="upload-area">
<a-upload
name="file"
list-type="picture-card"
:show-upload-list="false"
:before-upload="beforeUpload"
@change="handleIdCardFrontChange"
accept="image/png,image/jpeg"
>
<div class="upload-placeholder">
<IdcardOutlined />
<div style="margin-top: 8px">身份证件正面</div>
</div>
</a-upload>
<div v-if="formState.idCardFrontUrl" class="upload-preview">
<img :src="formState.idCardFrontUrl" alt="身份证正面" />
</div>
</div>
<!-- 背面 -->
<div class="upload-area">
<a-upload
name="file"
list-type="picture-card"
:show-upload-list="false"
:before-upload="beforeUpload"
@change="handleIdCardBackChange"
accept="image/png,image/jpeg"
>
<div class="upload-placeholder">
<IdcardOutlined />
<div style="margin-top: 8px">身份证件背面</div>
</div>
</a-upload>
<div v-if="formState.idCardBackUrl" class="upload-preview">
<img :src="formState.idCardBackUrl" alt="身份证背面" />
</div>
</div>
</div>
<div class="upload-tips">支持 jpgjpegpng 格式图片大小不超过 10M</div>
</a-form-item>
<a-form-item
label="姓名:"
name="name"
:rules="[{ required: true, message: '请输入姓名' }]"
>
<a-input
v-model:value="formState.name"
placeholder="请输入姓名"
style="width: 400px"
/>
</a-form-item>
<a-form-item
label="证件号码:"
name="idNumber"
:rules="[
{ required: true, message: '请输入证件号码' },
{ pattern: /^\d{17}[\dXx]$/, message: '请输入有效的18位身份证号码' }
]"
>
<a-input
v-model:value="formState.idNumber"
placeholder="请输入证件号码"
style="width: 400px"
/>
</a-form-item>
<!-- 协议同意 -->
<a-form-item :wrapper-col="{ offset: 4 }">
<a-checkbox v-model:checked="formState.agreed">
同意
<a href="/docs/real_name_cert/" target="_blank">AutoDL实名认证服务协议</a>
<a href="/docs/privacy_policy/" target="_blank">隐私政策</a>
请务必提供真实信息AutoDL算力云有权自行或委托第三方审查您提供的信息是否真实/有效若提供虚假信息由此带来的后果由您承担
</a-checkbox>
</a-form-item>
<!-- 操作按钮 -->
<a-form-item :wrapper-col="{ offset: 4 }">
<a-button type="primary" @click="handleSubmit">确认提交</a-button>
<a-button style="margin-left: 12px" @click="handleCancel">取消</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import {
ref,
reactive,
UploadChangeParam,
UploadFile
} from 'vue';
import {
message,
FormInstance,
UploadProps
} from 'ant-design-vue';
import {
UserOutlined,
IdcardOutlined
} from '@ant-design/icons-vue';
//
const formRef = ref<FormInstance>();
//
interface FormState {
certType: string | null;
businessLicenseUrl: string | null;
enterpriseName: string;
creditCode: string;
identity: 'legal_representative' | 'authorized_person' | null;
idCardFrontUrl: string | null;
idCardBackUrl: string | null;
name: string;
idNumber: string;
agreed: boolean;
}
const formState = reactive<FormState>({
certType: null,
businessLicenseUrl: null,
enterpriseName: '',
creditCode: '',
identity: null,
idCardFrontUrl: null,
idCardBackUrl: null,
name: '',
idNumber: '',
agreed: false
});
// 10MB
const MAX_FILE_SIZE = 10 * 1024 * 1024;
//
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
const isLt10M = file.size < MAX_FILE_SIZE;
if (!isLt10M) {
message.error('文件大小不能超过 10MB!');
}
const isValidType = ['image/jpeg', 'image/png'].includes(file.type);
if (!isValidType) {
message.error('仅支持 JPG/PNG 格式!');
}
return isLt10M && isValidType;
};
//
const handleBusinessLicenseChange = (info: UploadChangeParam<UploadFile>) => {
if (info.file.status === 'done') {
// URL
const url = URL.createObjectURL(info.file.originFileObj!);
formState.businessLicenseUrl = url;
}
};
//
const handleIdCardFrontChange = (info: UploadChangeParam<UploadFile>) => {
if (info.file.status === 'done') {
const url = URL.createObjectURL(info.file.originFileObj!);
formState.idCardFrontUrl = url;
}
};
//
const handleIdCardBackChange = (info: UploadChangeParam<UploadFile>) => {
if (info.file.status === 'done') {
const url = URL.createObjectURL(info.file.originFileObj!);
formState.idCardBackUrl = url;
}
};
//
const validateBusinessLicense = () => {
if (!formState.businessLicenseUrl) {
return Promise.reject(new Error('请上传企业证件'));
}
return Promise.resolve();
};
const validateIdCardFront = () => {
if (!formState.idCardFrontUrl) {
return Promise.reject(new Error('请上传身份证正面'));
}
return Promise.resolve();
};
const validateIdCardBack = () => {
if (!formState.idCardBackUrl) {
return Promise.reject(new Error('请上传身份证背面'));
}
return Promise.resolve();
};
//
const handleSubmit = async () => {
try {
await formRef.value?.validateFields();
if (!formState.agreed) {
message.error('请阅读并同意协议');
return;
}
message.success('提交成功!');
// TODO: API
} catch (error) {
console.log('Validation failed:', error);
}
};
//
const handleCancel = () => {
// TODO:
message.info('已取消');
};
</script>
<style scoped>
.enterprise-real-name-auth {
padding: 24px;
background-color: #fff;
min-height: 100vh;
}
.steps {
max-width: 800px;
margin-bottom: 32px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin: 24px 0 16px;
}
.upload-area {
display: inline-block;
margin-right: 24px;
position: relative;
}
.upload-placeholder {
text-align: center;
color: #999;
}
.upload-placeholder i {
font-size: 24px;
margin-bottom: 8px;
}
.upload-preview {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.upload-preview img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.id-card-uploads {
display: flex;
gap: 24px;
}
.upload-tips {
margin-top: 8px;
font-size: 12px;
color: #666;
}
/* 响应式 */
@media (max-width: 768px) {
.auth-form :deep(.ant-form-item-label) {
text-align: left;
}
.id-card-uploads {
flex-direction: column;
}
.upload-area {
margin-right: 0;
}
}
</style>

View File

@ -0,0 +1,310 @@
<!-- src/views/RealNameAuth.vue -->
<template>
<div class="real-name-auth-page">
<div class="header">
<h1>实名认证</h1>
<p>请选择您的认证类型</p>
</div>
<div class="auth-container">
<!-- 个人认证 -->
<a-card class="auth-card" :bordered="true">
<template #title>
<div class="card-title">
<UserOutlined class="title-icon" />
<span>个人认证</span>
</div>
</template>
<div class="auth-content" @click="openPersonalAuthModal">
<div class="option-list">
<div class="option-item">
<CheckCircleOutlined class="option-icon" />
<div class="option-info">
<div class="option-title">个人身份证认证</div>
<div class="option-desc">快速认证保障账号安全</div>
</div>
</div>
</div>
</div>
</a-card>
<!-- 企业认证 -->
<a-card class="auth-card" :bordered="true">
<template #title>
<div class="card-title">
<BankOutlined class="title-icon" />
<span>企业认证</span>
</div>
</template>
<template #extra>
<span class="extra-text">支持对公打款认证证件认证</span>
</template>
<div class="auth-content">
<div class="option-list">
<div class="option-item" @click="goEnterRealAuth">
<CheckCircleOutlined class="option-icon" />
<div class="option-info">
<div class="option-title">企业证件认证</div>
<div class="option-desc">营业执照等企业资质认证</div>
</div>
</div>
<div class="option-item disabled">
<ClockCircleOutlined class="option-icon" />
<div class="option-info">
<div class="option-title">银行对公账户认证</div>
<div class="option-desc">即将上线敬请期待</div>
</div>
</div>
</div>
</div>
</a-card>
</div>
<!-- 个人实名认证弹窗 -->
<a-modal
v-model:open="personalAuthModalVisible"
title="实名认证"
width="500px"
:footer="null"
@cancel="closePersonalAuthModal"
>
<div style="padding: 24px;">
<!-- 提示信息 -->
<div
style="margin-bottom: 24px; padding: 12px; background: #fff7e6; border: 1px solid #fda4af; color: #f59e0b; border-radius: 4px; font-size: 14px; line-height: 1.5;"
>
<span style="color: #f59e0b; margin-right: 8px;"></span>
您的实名认证信息将会加密保存不会泄露给第三方
</div>
<!-- 姓名输入 -->
<div style="margin-bottom: 24px; 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="personalForm.realName" placeholder="请输入您的姓名" style="flex: 1;" />
</div>
<!-- 身份证号输入 -->
<div style="margin-bottom: 24px; 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="personalForm.idCard" placeholder="请输入您的身份证号" style="flex: 1;" />
</div>
<!-- 按钮区 -->
<div style="text-align: right; padding: 16px 0;">
<a-button @click="closePersonalAuthModal">取消</a-button>
<a-button type="primary" @click="submitPersonalAuth" style="margin-left: 8px;">确定</a-button>
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue';
import {
UserOutlined,
BankOutlined,
CheckCircleOutlined,
ClockCircleOutlined
} from '@ant-design/icons-vue';
import router from '../../../../router';
//
const personalAuthModalVisible = ref(false);
//
const personalForm = reactive({
realName: '',
idCard: '',
});
//
const openPersonalAuthModal = () => {
personalAuthModalVisible.value = true;
};
//
const goEnterRealAuth = () => {
// TODO:
router.push('/layout/admin/enterRealAuth')
};
//
const closePersonalAuthModal = () => {
personalAuthModalVisible.value = false;
personalForm.realName = '';
personalForm.idCard = '';
};
//
const submitPersonalAuth = () => {
if (!personalForm.realName.trim()) {
message.error('请输入真实姓名');
return;
}
if (!personalForm.idCard.trim()) {
message.error('请输入身份证号');
return;
}
const idCardRegex = /^\d{17}[\dXx]$/;
if (!idCardRegex.test(personalForm.idCard)) {
message.error('请输入有效的18位身份证号码');
return;
}
message.success('实名认证提交成功!');
closePersonalAuthModal();
};
</script>
<style scoped>
.real-name-auth-page {
padding: 24px;
background-color: #f5f7fa;
min-height: 100vh;
}
.header {
text-align: left;
margin-bottom: 40px;
}
.header h1 {
font-size: 28px;
color: #1f2937;
margin-bottom: 8px;
font-weight: 600;
}
.header p {
color: #6b7280;
font-size: 16px;
margin: 0;
}
.auth-container {
display: flex;
flex-direction: column;
gap: 24px;
}
.auth-card {
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
border: 1px solid #e5e7eb;
}
.auth-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.card-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: 600;
color: #1f2937;
}
.title-icon {
margin-right: 8px;
font-size: 20px;
color: #1890ff;
}
.extra-text {
color: #6b7280;
font-size: 14px;
}
.auth-content {
padding: 8px 0;
cursor: pointer;
}
.option-list {
margin-bottom: 24px;
}
.option-item {
display: flex;
align-items: flex-start;
padding: 16px;
background: #f8fafc;
border-radius: 8px;
margin-bottom: 12px;
border: 1px solid #e2e8f0;
}
.option-item:last-child {
margin-bottom: 0;
}
.option-item.disabled {
background: #f9fafb;
border-color: #f3f4f6;
}
.option-icon {
margin-right: 12px;
font-size: 20px;
margin-top: 2px;
flex-shrink: 0;
}
.option-item:not(.disabled) .option-icon {
color: #10b981;
}
.option-item.disabled .option-icon {
color: #9ca3af;
}
.option-info {
flex: 1;
}
.option-title {
font-weight: 500;
color: #374151;
margin-bottom: 4px;
}
.option-item.disabled .option-title {
color: #9ca3af;
}
.option-desc {
font-size: 13px;
color: #6b7280;
}
.option-item.disabled .option-desc {
color: #9ca3af;
}
@media (max-width: 768px) {
.real-name-auth-page {
padding: 16px;
}
.header h1 {
font-size: 24px;
}
.auth-container {
gap: 16px;
}
.option-item {
padding: 12px;
}
}
</style>

View File

@ -8,54 +8,10 @@ import {
SaveOutlined,
} from '@ant-design/icons-vue';
import { Modal, message } from 'ant-design-vue';
// <script setup> router
import { useRouter } from 'vue-router';
//
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 router = useRouter();
const getStatusIcon = (status: SecurityItem['status']) => {
switch (status) {
@ -85,23 +41,92 @@ const getActionColor = (status: SecurityItem['status']) => {
return 'blue'; //
};
//
//
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: () => openEditPhoneModal(),
},
{
title: '实名认证',
description: '实名认证后可以使用AutoDL更完整的功能如打开实例的自定义服务等',
status: 'unverified',
actionText: '立即认证',
actionHandler: () => {
router.push('/layout/admin/realnameAuth');
},
},
// {
// title: '',
// description: '',
// status: 'bound',
// actionText: '',
// actionHandler: () => alert(''),
// },
{
title: '邮箱绑定(即将上线)',
description: '绑定邮箱后可接收系统消息,如余额不足、实例即将到期、实例即将释放等消息',
status: 'unset',
actionText: '绑定',
actionHandler: () => alert('跳转到绑定邮箱页面'),
},
]);
//
const setPasswordModalVisible = ref(false);
const editPhoneModalVisible = ref(false);
//
const realNameModalVisible = ref(false);
//
const openRealNameAuthModal = () => {
realNameModalVisible.value = true;
};
//
const closeRealNameAuthModal = () => {
realNameModalVisible.value = false;
};
//
const form = reactive({
phoneNumber: '+86 178****5075', //
const passwordForm = reactive({
smsCode: '',
newPassword: '',
confirmPassword: '',
});
const phoneForm = reactive({
countryCode: '+86',
newPhoneNumber: '',
smsCode: '',
password: '', //
});
//
const countdown = ref(0);
const isSending = ref(false);
//
const sendSmsCode = () => {
const sendSmsCode = (formType: 'password' | 'phone') => {
if (isSending.value) return;
isSending.value = true;
countdown.value = 60;
@ -116,54 +141,98 @@ const sendSmsCode = () => {
message.success('验证码已发送');
};
//
const resetForm = () => {
form.smsCode = '';
form.newPassword = '';
form.confirmPassword = '';
countdown.value = 0;
isSending.value = false;
};
//
const openSetPasswordModal = () => {
resetForm();
resetPasswordForm();
setPasswordModalVisible.value = true;
};
//
const openEditPhoneModal = () => {
resetPhoneForm();
editPhoneModalVisible.value = true;
};
//
const closeSetPasswordModal = () => {
setPasswordModalVisible.value = false;
resetForm();
resetPasswordForm();
};
const closeEditPhoneModal = () => {
editPhoneModalVisible.value = false;
resetPhoneForm();
};
//
const resetPasswordForm = () => {
passwordForm.smsCode = '';
passwordForm.newPassword = '';
passwordForm.confirmPassword = '';
countdown.value = 0;
isSending.value = false;
};
const resetPhoneForm = () => {
phoneForm.countryCode = '+86';
phoneForm.newPhoneNumber = '';
phoneForm.smsCode = '';
phoneForm.password = '';
countdown.value = 0;
isSending.value = false;
};
//
const savePassword = () => {
if (!form.smsCode) {
if (!passwordForm.smsCode) {
message.error('请输入验证码');
return;
}
if (!form.newPassword) {
if (!passwordForm.newPassword) {
message.error('请输入新密码');
return;
}
if (!form.confirmPassword) {
if (!passwordForm.confirmPassword) {
message.error('请确认密码');
return;
}
if (form.newPassword !== form.confirmPassword) {
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
message.error('两次输入的密码不一致');
return;
}
if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/.test(form.newPassword)) {
if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/.test(passwordForm.newPassword)) {
message.error('密码必须为8~16位且包含字母和数字');
return;
}
//
message.success('密码设置成功!');
closeSetPasswordModal();
};
//
const savePhone = () => {
if (!phoneForm.newPhoneNumber) {
message.error('请输入新手机号');
return;
}
if (!/^\d{11}$/.test(phoneForm.newPhoneNumber)) {
message.error('请输入有效的11位手机号');
return;
}
if (!phoneForm.smsCode) {
message.error('请输入验证码');
return;
}
if (!phoneForm.password) {
message.error('请输入账号登录密码');
return;
}
message.success('手机号修改成功!');
closeEditPhoneModal();
};
</script>
<template>
@ -204,21 +273,21 @@ const savePassword = () => {
>
<div style="padding: 24px 0;">
<div style="margin-bottom: 24px; font-size: 16px; color: #333;">
手机号{{ form.phoneNumber }}
手机号+86 178****5075
</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"
v-model:value="passwordForm.smsCode"
placeholder="请输入验证码"
style="flex: 1;"
/>
<a-button
type="primary"
:disabled="isSending || countdown > 0"
@click="sendSmsCode"
@click="sendSmsCode('password')"
style="width: 120px;"
>
{{ isSending || countdown > 0 ? `${countdown}s后重发` : '发送验证码' }}
@ -229,7 +298,7 @@ const savePassword = () => {
<span style="color: red;">*</span>
<label style="font-size: 14px; color: #333;">设置新密码</label>
<a-input
v-model:value="form.newPassword"
v-model:value="passwordForm.newPassword"
placeholder="请输入816位包含数字和字母的密码"
type="password"
style="flex: 1;"
@ -238,9 +307,9 @@ const savePassword = () => {
<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>
<label style="font-size: 14px; color: #333;">确认新密码</label>
<a-input
v-model:value="form.confirmPassword"
v-model:value="passwordForm.confirmPassword"
placeholder="请确认密码"
type="password"
style="flex: 1;"
@ -253,6 +322,70 @@ const savePassword = () => {
<a-button type="primary" @click="savePassword">保存</a-button>
</div>
</a-modal>
<!-- 修改手机号弹窗 -->
<a-modal
v-model:visible="editPhoneModalVisible"
title="修改手机号"
width="500px"
:footer="null"
@cancel="closeEditPhoneModal"
>
<div style="padding: 24px 0;">
<div style="margin-bottom: 24px; display: flex; align-items: center; gap: 8px;">
<span style="color: red;">*</span>
<label style="font-size: 14px; color: #333;">输入新手机号</label>
<a-select
v-model:value="phoneForm.countryCode"
style="width: 80px;"
placeholder="+86"
>
<a-select-option value="+86">+86</a-select-option>
<a-select-option value="+852">+852</a-select-option>
<a-select-option value="+853">+853</a-select-option>
</a-select>
<a-input
v-model:value="phoneForm.newPhoneNumber"
placeholder="请输入新手机号"
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: #333;">手机验证码</label>
<a-input
v-model:value="phoneForm.smsCode"
placeholder="请输入验证码"
style="flex: 1;"
/>
<a-button
type="primary"
:disabled="isSending || countdown > 0"
@click="sendSmsCode('phone')"
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="phoneForm.password"
placeholder="请输入账号登录密码"
type="password"
style="flex: 1;"
/>
</div>
</div>
<div style="text-align: right; padding: 16px 24px;">
<a-button @click="closeEditPhoneModal" style="margin-right: 8px;">取消</a-button>
<a-button type="primary" @click="savePhone">确定</a-button>
</div>
</a-modal>
</div>
</template>

View File

@ -0,0 +1,350 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import {
Table,
Pagination,
Progress,
message,
Button
} from 'ant-design-vue';
//
interface ImageItem {
uid: string;
name: string;
size: string;
status: string;
shareInfo: string;
source: string;
region: string;
baseImage: string;
createTime: string;
}
//
const imageList = ref<ImageItem[]>([
{
uid: 'img-001',
name: 'Ubuntu 20.04',
size: '2.5GB',
status: '可用',
shareInfo: '私有',
source: '系统镜像',
region: '华北1',
baseImage: 'ubuntu:20.04',
createTime: '2023-10-01 10:30:00'
},
{
uid: 'img-002',
name: 'CentOS 7',
size: '1.8GB',
status: '可用',
shareInfo: '共享',
source: '自定义',
region: '华东2',
baseImage: 'centos:7',
createTime: '2023-10-02 14:20:00'
},
{
uid: 'img-003',
name: 'Windows Server 2019',
size: '4.2GB',
status: '构建中',
shareInfo: '私有',
source: '自定义',
region: '华南1',
baseImage: 'windows:2019',
createTime: '2023-10-03 16:45:00'
},
{
uid: 'img-004',
name: 'Debian 11',
size: '1.9GB',
status: '可用',
shareInfo: '私有',
source: '系统镜像',
region: '华北2',
baseImage: 'debian:11',
createTime: '2023-10-04 09:15:00'
},
{
uid: 'img-005',
name: 'AlmaLinux 8',
size: '2.1GB',
status: '可用',
shareInfo: '共享',
source: '自定义',
region: '华东1',
baseImage: 'almalinux:8',
createTime: '2023-10-05 11:30:00'
}
]);
//
const storageUsed = ref('12.5GB');
const storagePeak = ref('13.2GB');
const expectedFee = ref('0元');
const freeStorage = ref('30.00GB');
const paidStorage = ref('0GB');
//
const storagePercent = computed(() => {
const usedGB = parseFloat(storageUsed.value.replace('GB', ''));
const totalGB = 30;
return Math.min(100, Math.round((usedGB / totalGB) * 100));
});
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: imageList.value.length,
});
//
const loading = ref(false);
//
const columns = [
{ title: '镜像UUID', dataIndex: 'uid', key: 'uid', width: 150 },
{ title: '镜像名称', dataIndex: 'name', key: 'name', width: 150 },
{ title: '大小', dataIndex: 'size', key: 'size', width: 100 },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
{ title: '共享信息', dataIndex: 'shareInfo', key: 'shareInfo', width: 100 },
{ title: '来源', dataIndex: 'source', key: 'source', width: 100 },
{ title: '缓存地区', dataIndex: 'region', key: 'region', width: 120 },
{ title: '原基础镜像信息', dataIndex: 'baseImage', key: 'baseImage', width: 180 },
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160 },
{
title: '操作',
key: 'action',
slots: { customRender: 'action' },
width: 120
},
];
//
const onPageChange = (page: number, pageSize: number) => {
console.log(`切换到第 ${page} 页,每页 ${pageSize}`);
pagination.current = page;
pagination.pageSize = pageSize;
};
const onShowSizeChange = (current: number, pageSize: number) => {
console.log(`每页显示 ${pageSize}`);
pagination.current = current;
pagination.pageSize = pageSize;
};
//
const handleView = (record: ImageItem) => {
message.info(`查看镜像: ${record.name}`);
};
const handleDelete = (record: ImageItem) => {
message.info(`删除镜像: ${record.name}`);
};
//
const getCurrentPageData = computed(() => {
const start = (pagination.current - 1) * pagination.pageSize;
const end = start + pagination.pageSize;
return imageList.value.slice(start, end);
});
onMounted(() => {
console.log('MyImages 组件已挂载');
});
import { computed } from 'vue';
</script>
<template>
<div class="my-images-page">
<!-- 顶部警告提示 -->
<div class="top-warning">
<span class="title">我的镜像</span>
<span class="warning-text">
连续3个月未登录或欠费50元以上平台保留删除数据的权利具体规则请参考
<a href="#" class="link">文档</a>
</span>
</div>
<!-- 存储容量信息 -->
<div class="storage-info">
<div class="storage-desc">
存储容量大小<strong>{{ storageUsed }}</strong>
今天容量使用峰值{{ storagePeak }}预计扣费{{ expectedFee }}
</div>
<a href="#" class="view-rule">查看计费规则</a>
</div>
<!-- 存储进度条 -->
<div class="progress-container">
<a-progress :percent="storagePercent" :show-info="true" />
</div>
<!-- 免费/付费容量 -->
<div class="capacity-info">
<span><span class="dot free"></span>免费{{ freeStorage }}</span>
<span><span class="dot paid"></span>付费{{ paidStorage }}</span>
</div>
<!-- 表格 -->
<div class="table-container">
<a-table
:data-source="getCurrentPageData"
:columns="columns"
:pagination="false"
:scroll="{ x: 'max-content', y: 400 }"
:row-key="(record) => record.uid"
:loading="loading"
class="image-table"
>
<template #action="{ record }">
<a-button type="link" size="small" @click="handleView(record)">查看</a-button>
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
</template>
<template #empty>
<div class="table-empty">暂无数据</div>
</template>
</a-table>
</div>
<!-- 分页固定在底部 -->
<div class="pagination-container">
<a-pagination
v-model:current="pagination.current"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
show-size-changer
show-jumper
show-less-items
:page-size-options="[5, 10, 20, 50]"
@change="onPageChange"
@showSizeChange="onShowSizeChange"
/>
</div>
</div>
</template>
<style scoped lang="scss">
.my-images-page {
padding: 24px;
background-color: #f5f7fa;
min-height: 100vh;
display: flex;
flex-direction: column;
.top-warning {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
font-size: 14px;
color: #333;
.title {
font-weight: bold;
color: #000;
font-size: 18px;
}
.warning-text {
color: #f56a00;
font-size: 14px;
}
.link {
color: #1890ff;
text-decoration: underline;
}
}
.storage-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px;
background: #e6f7ff;
border-radius: 4px;
color: #333;
.storage-desc {
font-size: 14px;
strong {
font-size: 16px;
color: #1890ff;
}
}
.view-rule {
color: #1890ff;
font-size: 14px;
cursor: pointer;
}
}
.progress-container {
margin: 16px 0;
width: 100%;
max-width: 600px;
}
.capacity-info {
display: flex;
gap: 32px;
margin: 16px 0;
font-size: 14px;
color: #333;
.dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.free {
background-color: #1890ff;
}
.paid {
background-color: #f56a00;
}
}
.table-container {
flex: 1;
margin: 24px 0;
}
.image-table {
background: white;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
border-radius: 4px;
overflow: hidden;
height: 100%;
}
.table-empty {
text-align: center;
padding: 60px 0;
color: #999;
font-size: 14px;
}
.pagination-container {
margin-top: auto;
padding-top: 24px;
text-align: center;
border-top: 1px solid #e8e8e8;
}
}
</style>

View File

@ -45,7 +45,7 @@ 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/admin/image', name: '镜像', icon: GlobalOutlined },
{ path: '/layout/publicData', name: '公开数据', icon: LaptopOutlined },
{
path: '/layout/fee',