2
0

feat:站点列表功能拓展、绑定邮箱、修改密码

This commit is contained in:
zhongzm
2025-04-01 15:21:08 +08:00
parent 42d10809e7
commit b27aebed73
11 changed files with 1319 additions and 51 deletions

View File

@@ -0,0 +1,314 @@
// 地区选项
export const regionOptions = [
{ label: 'Afghanistan', value: 'Afghanistan/AF/+93' },
{ label: 'Albania', value: 'Albania/AL/+355' },
{ label: 'Algeria', value: 'Algeria/DZ/+213' },
{ label: 'Angola', value: 'Angola/AO/+244' },
{ label: 'Anguilla', value: 'Anguilla/AI/+1264' },
{ label: 'Antigua and Barbuda', value: 'Antigua and Barbuda/AG/+268' },
{ label: 'Argentina', value: 'Argentina/AR/+54' },
{ label: 'Armenia', value: 'Armenia/AM/+374' },
{ label: 'Aruba', value: 'Aruba/AW/+297' },
{ label: 'Australia', value: 'Australia/AU/+61' },
{ label: 'Austria', value: 'Austria/AT/+43' },
{ label: 'Azerbaijan', value: 'Azerbaijan/AZ/+994' },
{ label: 'Bahamas', value: 'Bahamas/BS/+1242' },
{ label: 'Bahrain', value: 'Bahrain/BH/+973' },
{ label: 'Bangladesh', value: 'Bangladesh/BD/+880' },
{ label: 'Barbados', value: 'Barbados/BB/+1246' },
{ label: 'Belarus', value: 'Belarus/BY/+375' },
{ label: 'Belgium', value: 'Belgium/BE/+32' },
{ label: 'Belize', value: 'Belize/BZ/+501' },
{ label: 'Benin', value: 'Benin/BJ/+229' },
{ label: 'Bermuda', value: 'Bermuda/BM/+1441' },
{ label: 'Bhutan', value: 'Bhutan/BT/+975' },
{ label: 'Bolivia', value: 'Bolivia/BO/+591' },
{ label: 'Botswana', value: 'Botswana/BW/+267' },
{ label: 'Brasil', value: 'Brasil/BR/+55' },
{ label: 'Brunei', value: 'Brunei/BN/+673' },
{ label: 'Burkina Faso', value: 'Burkina Faso/BF/+226' },
{ label: 'Burundi', value: 'Burundi/BI/+257' },
{ label: 'Cabo Verde', value: 'Cabo Verde/CV/+238' },
{ label: 'Cambodia', value: 'Cambodia/KH/+855' },
{ label: 'Cameroon', value: 'Cameroon/CM/+237' },
{ label: 'Canada', value: 'Canada/CA/+1' },
{ label: 'Cayman Islands', value: 'Cayman Islands/KY/+1345' },
{ label: 'Central African', value: 'Central African/CF/+236' },
{ label: 'Chad', value: 'Chad/TD/+235' },
{ label: 'Chile', value: 'Chile/CL/+56' },
{ label: 'China', value: 'China/CN/+86' },
{ label: 'Christmas Island', value: 'Christmas Island/CX/+618' },
{ label: 'Colombia', value: 'Colombia/CO/+57' },
{ label: 'Comoros', value: 'Comoros/KM/+269' },
{ label: 'Congo', value: 'Congo/CD/+243' },
{ label: 'Congo', value: 'Congo/CG/+242' },
{ label: 'Costa Rica', value: 'Costa Rica/CR/+506' },
{ label: "Cote d'Ivoire", value: "Cote d'Ivoire/CI/+225" },
{ label: 'Croatia', value: 'Croatia/HR/+385' },
{ label: 'Cuba', value: 'Cuba/CU/+53' },
{ label: 'Cyprus', value: 'Cyprus/CY/+357' },
{ label: 'Czech', value: 'Czech/CZ/+420' },
{ label: 'Denmark', value: 'Denmark/DK/+45' },
{ label: 'Djibouti', value: 'Djibouti/DJ/+253' },
{ label: 'Dominica', value: 'Dominica/DM/+1767' },
{ label: 'Dominican', value: 'Dominican/DO/+1809' },
{ label: 'Ecuador', value: 'Ecuador/EC/+593' },
{ label: 'Egypt', value: 'Egypt/EG/+20' },
{ label: 'Equatorial Guinea', value: 'Equatorial Guinea/GQ/+240' },
{ label: 'Eritrea', value: 'Eritrea/ER/+291' },
{ label: 'Estonia', value: 'Estonia/EE/+372' },
{ label: 'Ethiopia', value: 'Ethiopia/ET/+251' },
{ label: 'Falkland Islands', value: 'Falkland Islands/FK/+500' },
{ label: 'Fiji', value: 'Fiji/FJ/+679' },
{ label: 'Finland', value: 'Finland/FI/+358' },
{ label: 'France', value: 'France/FR/+33' },
{ label: 'French Guiana', value: 'French Guiana/GF/+594' },
{ label: 'French Polynesia', value: 'French Polynesia/PF/+689' },
{ label: 'Gabon', value: 'Gabon/GA/+241' },
{ label: 'Gambia', value: 'Gambia/GM/+220' },
{ label: 'Georgia', value: 'Georgia/GE/+995' },
{ label: 'Germany', value: 'Germany/DE/+49' },
{ label: 'Ghana', value: 'Ghana/GH/+233' },
{ label: 'Greece', value: 'Greece/GR/+30' },
{ label: 'Greenland', value: 'Greenland/GL/+45' },
{ label: 'Grenada', value: 'Grenada/GD/+1473' },
{ label: 'Guam', value: 'Guam/GU/+1' },
{ label: 'Guatemala', value: 'Guatemala/GT/+502' },
{ label: 'Guinea', value: 'Guinea/GN/+224' },
{ label: 'Guinea-Bissau', value: 'Guinea-Bissau/GW/+245' },
{ label: 'Guyana', value: 'Guyana/GY/+592' },
{ label: 'Haiti', value: 'Haiti/HT/+509' },
{ label: 'Honduras', value: 'Honduras/HN/+504' },
{ label: 'Hong Kong', value: 'Hong Kong/HK/+852' },
{ label: 'Hungary', value: 'Hungary/HU/+36' },
{ label: 'Iceland', value: 'Iceland/IS/+354' },
{ label: 'India', value: 'India/IN/+91' },
{ label: 'Indonesia', value: 'Indonesia/ID/+62' },
{ label: 'Iran', value: 'Iran/IR/+98' },
{ label: 'Iraq', value: 'Iraq/IQ/+964' },
{ label: 'Ireland', value: 'Ireland/IE/+353' },
{ label: 'Israel', value: 'Israel/IL/+972' },
{ label: 'Italy', value: 'Italy/IT/+39' },
{ label: 'Jamaica', value: 'Jamaica/JM/+876' },
{ label: 'Japan', value: 'Japan/JP/+81' },
{ label: 'Jordan', value: 'Jordan/JO/+962' },
{ label: 'Kazakhstan', value: 'Kazakhstan/KZ/+7' },
{ label: 'Kenya', value: 'Kenya/KE/+254' },
{ label: 'Kiribati', value: 'Kiribati/KI/+686' },
{ label: 'Korea', value: 'Korea/KR/+82' },
{ label: 'Kuwait', value: 'Kuwait/KW/+965' },
{ label: 'Kyrgyzstan', value: 'Kyrgyzstan/KG/+996' },
{ label: 'Lao', value: 'Lao/LA/+856' },
{ label: 'Latvia', value: 'Latvia/LV/+371' },
{ label: 'Lebanon', value: 'Lebanon/LB/+961' },
{ label: 'Lesotho', value: 'Lesotho/LS/+266' },
{ label: 'Liberia', value: 'Liberia/LR/+231' },
{ label: 'Libya', value: 'Libya/LY/+218' },
{ label: 'Liechtenstein', value: 'Liechtenstein/LI/+423' },
{ label: 'Lithuania', value: 'Lithuania/LT/+370' },
{ label: 'Luxembourg', value: 'Luxembourg/LU/+352' },
{ label: 'Macao', value: 'Macao/MO/+853' },
{ label: 'Macedonia', value: 'Macedonia/MK/+389' },
{ label: 'Madagascar', value: 'Madagascar/MG/+261' },
{ label: 'Malawi', value: 'Malawi/MW/+265' },
{ label: 'Malaysia', value: 'Malaysia/MY/+60' },
{ label: 'Maldives', value: 'Maldives/MV/+960' },
{ label: 'Mali', value: 'Mali/ML/+223' },
{ label: 'Malta', value: 'Malta/MT/+356' },
{ label: 'Marshall Islands', value: 'Marshall Islands/MH/+692' },
{ label: 'Mauritania', value: 'Mauritania/MR/+222' },
{ label: 'Mauritius', value: 'Mauritius/MU/+230' },
{ label: 'Mexico', value: 'Mexico/MX/+52' },
{ label: 'Micronesia', value: 'Micronesia/FM/+691' },
{ label: 'Moldova', value: 'Moldova/MD/+373' },
{ label: 'Monaco', value: 'Monaco/MC/+377' },
{ label: 'Mongolia', value: 'Mongolia/MN/+976' },
{ label: 'Montenegro', value: 'Montenegro/ME/+382' },
{ label: 'Morocco', value: 'Morocco/MA/+212' },
{ label: 'Mozambique', value: 'Mozambique/MZ/+258' },
{ label: 'Myanmar', value: 'Myanmar/MM/+95' },
{ label: 'Namibia', value: 'Namibia/NA/+264' },
{ label: 'Nauru', value: 'Nauru/NR/+674' },
{ label: 'Nepal', value: 'Nepal/NP/+977' },
{ label: 'Netherlands', value: 'Netherlands/NL/+31' },
{ label: 'Netherlands Antilles', value: 'Netherlands Antilles/AN/+599' },
{ label: 'New Caledonia', value: 'New Caledonia/NC/+687' },
{ label: 'New Zealand', value: 'New Zealand/NZ/+64' },
{ label: 'Nicaragua', value: 'Nicaragua/NI/+505' },
{ label: 'Niger', value: 'Niger/NE/+227' },
{ label: 'Nigeria', value: 'Nigeria/NG/+234' },
{ label: 'Niue', value: 'Niue/NU/+683' },
{ label: 'North Korea', value: 'North Korea/KP/+850' },
{ label: 'Norway', value: 'Norway/NO/+47' },
{ label: 'Oman', value: 'Oman/OM/+968' },
{ label: 'Pakistan', value: 'Pakistan/PK/+92' },
{ label: 'Palau', value: 'Palau/PW/+680' },
{ label: 'Palestine', value: 'Palestine/PS/+970' },
{ label: 'Panama', value: 'Panama/PA/+507' },
{ label: 'Papua New Guinea', value: 'Papua New Guinea/PG/+675' },
{ label: 'Paraguay', value: 'Paraguay/PY/+595' },
{ label: 'Peru', value: 'Peru/PE/+51' },
{ label: 'Philippines', value: 'Philippines/PH/+63' },
{ label: 'Pitcairn Islands', value: 'Pitcairn Islands/PN/+64' },
{ label: 'Poland', value: 'Poland/PL/+48' },
{ label: 'Portuguese', value: 'Portuguese/PT/+351' },
{ label: 'Puerto Rico', value: 'Puerto Rico/PR/+1787' },
{ label: 'Qatar', value: 'Qatar/QA/+974' },
{ label: 'Romania', value: 'Romania/RO/+40' },
{ label: 'Russia', value: 'Russia/RU/+7' },
{ label: 'Rwanda', value: 'Rwanda/RW/+250' },
{ label: 'Saint Kitts and Nevis', value: 'Saint Kitts and Nevis/KN/+1869' },
{ label: 'Saint Lucia', value: 'Saint Lucia/LC/+1758' },
{ label: 'Saint Vincent and the Grenadines', value: 'Saint Vincent and the Grenadines/VC/+1784' },
{ label: 'Saint-Martin', value: 'Saint-Martin/MF/+1721' },
{ label: 'Salvador', value: 'Salvador/SV/+503' },
{ label: 'Samoa', value: 'Samoa/WS/+685' },
{ label: 'San Marino', value: 'San Marino/SM/+378' },
{ label: 'Sao Tome and Principe', value: 'Sao Tome and Principe/ST/+239' },
{ label: 'Saudi Arabia', value: 'Saudi Arabia/SA/+966' },
{ label: 'Senegal', value: 'Senegal/SN/+221' },
{ label: 'Serbia', value: 'Serbia/RS/+381' },
{ label: 'Seychelles', value: 'Seychelles/SC/+248' },
{ label: 'Sierra Leone', value: 'Sierra Leone/SL/+232' },
{ label: 'Singapore', value: 'Singapore/SG/+65' },
{ label: 'Slovak', value: 'Slovak/SK/+421' },
{ label: 'Slovenia', value: 'Slovenia/SI/+386' },
{ label: 'Solomon Islands', value: 'Solomon Islands/SB/+677' },
{ label: 'Somalia', value: 'Somalia/SO/+252' },
{ label: 'South Africa', value: 'South Africa/ZA/+27' },
{ label: 'South Sudan', value: 'South Sudan/SS/+211' },
{ label: 'Spain', value: 'Spain/ES/+34' },
{ label: 'Sri Lanka', value: 'Sri Lanka/LK/+94' },
{ label: 'Sudan', value: 'Sudan/SD/+249' },
{ label: 'Suriname', value: 'Suriname/SR/+597' },
{ label: 'Swaziland', value: 'Swaziland/SZ/+268' },
{ label: 'Sweden', value: 'Sweden/SE/+46' },
{ label: 'Swiss', value: 'Swiss/CH/+41' },
{ label: 'Syria', value: 'Syria/SY/+963' },
{ label: 'Taiwan', value: 'Taiwan/TW/+886' },
{ label: 'Tajikistan', value: 'Tajikistan/TJ/+992' },
{ label: 'Tanzania', value: 'Tanzania/TZ/+255' },
{ label: 'Thailand', value: 'Thailand/TH/+66' },
{ label: 'The Cook Islands', value: 'The Cook Islands/CK/+682' },
{ label: 'The Turks and Caicos Islands', value: 'The Turks and Caicos Islands/TC/+1649' },
{ label: 'The United States Virgin Islands', value: 'The United States Virgin Islands/VI/+1340' },
{ label: 'Timor-Leste', value: 'Timor-Leste/TL/+670' },
{ label: 'Togo', value: 'Togo/TG/+228' },
{ label: 'Tokelau', value: 'Tokelau/TK/+690' },
{ label: 'Tonga', value: 'Tonga/TO/+676' },
{ label: 'Trinidad and Tobago', value: 'Trinidad and Tobago/TT/+1868' },
{ label: 'Tunisia', value: 'Tunisia/TN/+216' },
{ label: 'Turkey', value: 'Turkey/TR/+90' },
{ label: 'Turkmenistan', value: 'Turkmenistan/TM/+993' },
{ label: 'Tuvalu', value: 'Tuvalu/TV/+688' },
{ label: 'Uganda', value: 'Uganda/UG/+256' },
{ label: 'Ukraine', value: 'Ukraine/UA/+380' },
{ label: 'United Arab Emirates', value: 'United Arab Emirates/AE/+971' },
{ label: 'United Kingdom', value: 'United Kingdom/UK/+44' },
{ label: 'United States', value: 'United States/US/+1' },
{ label: 'Uruguay', value: 'Uruguay/UY/+598' },
{ label: 'Uzbekistan', value: 'Uzbekistan/UZ/+998' },
{ label: 'Vanuatu', value: 'Vanuatu/VU/+678' },
{ label: 'Vatican', value: 'Vatican/VA/+379' },
{ label: 'Venezuela', value: 'Venezuela/VE/+58' },
{ label: 'Vietnam', value: 'Vietnam/VN/+84' },
{ label: 'Yemen', value: 'Yemen/YE/+967' },
{ label: 'Zambia', value: 'Zambia/ZM/+260' },
{ label: 'Zimbabwe', value: 'Zimbabwe/ZW/+263' }
];
// 时区选项
export const timeZoneOptions = [
{ label: 'Pacific/Wake', value: 'Pacific/Wake' },
{ label: 'Pacific/Midway', value: 'Pacific/Midway' },
{ label: 'Pacific/Honolulu', value: 'Pacific/Honolulu' },
{ label: 'America/Anchorage', value: 'America/Anchorage' },
{ label: 'America/Los_Angeles', value: 'America/Los_Angeles' },
{ label: 'America/Phoenix', value: 'America/Phoenix' },
{ label: 'America/Chihuahua', value: 'America/Chihuahua' },
{ label: 'America/Denver', value: 'America/Denver' },
{ label: 'America/Tegucigalpa', value: 'America/Tegucigalpa' },
{ label: 'America/Chicago', value: 'America/Chicago' },
{ label: 'America/Mexico_City', value: 'America/Mexico_City' },
{ label: 'Canada/Saskatchewan', value: 'Canada/Saskatchewan' },
{ label: 'America/Bogota', value: 'America/Bogota' },
{ label: 'America/New_York', value: 'America/New_York' },
{ label: 'America/Indiana/Indianapolis', value: 'America/Indiana/Indianapolis' },
{ label: 'America/Caracas', value: 'America/Caracas' },
{ label: 'America/Asuncion', value: 'America/Asuncion' },
{ label: 'America/Halifax', value: 'America/Halifax' },
{ label: 'America/Cuiaba', value: 'America/Cuiaba' },
{ label: 'America/La_Paz', value: 'America/La_Paz' },
{ label: 'Canada/Newfoundland', value: 'Canada/Newfoundland' },
{ label: 'America/Sao_Paulo', value: 'America/Sao_Paulo' },
{ label: 'America/Buenos_Aires', value: 'America/Buenos_Aires' },
{ label: 'America/Cayenne', value: 'America/Cayenne' },
{ label: 'America/Godthab', value: 'America/Godthab' },
{ label: 'America/Montevideo', value: 'America/Montevideo' },
{ label: 'America/Santiago', value: 'America/Santiago' },
{ label: 'Atlantic/South_Georgia', value: 'Atlantic/South_Georgia' },
{ label: 'Atlantic/Azores', value: 'Atlantic/Azores' },
{ label: 'Atlantic/Cape_Verde', value: 'Atlantic/Cape_Verde' },
{ label: 'Africa/Casablanca', value: 'Africa/Casablanca' },
{ label: 'UTC', value: 'UTC' },
{ label: 'Europe/London', value: 'Europe/London' },
{ label: 'Atlantic/Reykjavik', value: 'Atlantic/Reykjavik' },
{ label: 'Europe/Amsterdam', value: 'Europe/Amsterdam' },
{ label: 'Europe/Belgrade', value: 'Europe/Belgrade' },
{ label: 'Europe/Brussels', value: 'Europe/Brussels' },
{ label: 'Europe/Sarajevo', value: 'Europe/Sarajevo' },
{ label: 'Africa/Algiers', value: 'Africa/Algiers' },
{ label: 'Europe/Athens', value: 'Europe/Athens' },
{ label: 'Asia/Beirut', value: 'Asia/Beirut' },
{ label: 'Africa/Cairo', value: 'Africa/Cairo' },
{ label: 'Asia/Damascus', value: 'Asia/Damascus' },
{ label: 'Africa/Harare', value: 'Africa/Harare' },
{ label: 'Europe/Vilnius', value: 'Europe/Vilnius' },
{ label: 'Asia/Jerusalem', value: 'Asia/Jerusalem' },
{ label: 'Asia/Amman', value: 'Asia/Amman' },
{ label: 'Asia/Baghdad', value: 'Asia/Baghdad' },
{ label: 'Europe/Minsk', value: 'Europe/Minsk' },
{ label: 'Asia/Kuwait', value: 'Asia/Kuwait' },
{ label: 'Africa/Nairobi', value: 'Africa/Nairobi' },
{ label: 'Asia/Istanbul', value: 'Asia/Istanbul' },
{ label: 'Asia/Tehran', value: 'Asia/Tehran' },
{ label: 'Asia/Muscat', value: 'Asia/Muscat' },
{ label: 'Asia/Baku', value: 'Asia/Baku' },
{ label: 'Europe/Moscow', value: 'Europe/Moscow' },
{ label: 'Asia/Tbilisi', value: 'Asia/Tbilisi' },
{ label: 'Asia/Yerevan', value: 'Asia/Yerevan' },
{ label: 'Asia/Kabul', value: 'Asia/Kabul' },
{ label: 'Asia/Karachi', value: 'Asia/Karachi' },
{ label: 'Asia/Yekaterinburg', value: 'Asia/Yekaterinburg' },
{ label: 'Asia/Tashkent', value: 'Asia/Tashkent' },
{ label: 'Asia/Kolkata', value: 'Asia/Kolkata' },
{ label: 'Asia/Colombo', value: 'Asia/Colombo' },
{ label: 'Asia/Katmandu', value: 'Asia/Katmandu' },
{ label: 'Asia/Dhaka', value: 'Asia/Dhaka' },
{ label: 'Asia/Rangoon', value: 'Asia/Rangoon' },
{ label: 'Asia/Bangkok', value: 'Asia/Bangkok' },
{ label: 'Asia/Novosibirsk', value: 'Asia/Novosibirsk' },
{ label: 'Asia/Hong_Kong', value: 'Asia/Hong_Kong' },
{ label: 'Asia/Krasnoyarsk', value: 'Asia/Krasnoyarsk' },
{ label: 'Asia/Kuala_Lumpur', value: 'Asia/Kuala_Lumpur' },
{ label: 'Australia/Perth', value: 'Australia/Perth' },
{ label: 'Asia/Taipei', value: 'Asia/Taipei' },
{ label: 'Asia/Ulaanbaatar', value: 'Asia/Ulaanbaatar' },
{ label: 'Asia/Irkutsk', value: 'Asia/Irkutsk' },
{ label: 'Asia/Tokyo', value: 'Asia/Tokyo' },
{ label: 'Asia/Seoul', value: 'Asia/Seoul' },
{ label: 'Australia/Adelaide', value: 'Australia/Adelaide' },
{ label: 'Australia/Darwin', value: 'Australia/Darwin' },
{ label: 'Australia/Brisbane', value: 'Australia/Brisbane' },
{ label: 'Australia/Canberra', value: 'Australia/Canberra' },
{ label: 'Pacific/Guam', value: 'Pacific/Guam' },
{ label: 'Australia/Hobart', value: 'Australia/Hobart' },
{ label: 'Asia/Yakutsk', value: 'Asia/Yakutsk' },
{ label: 'Pacific/Noumea', value: 'Pacific/Noumea' },
{ label: 'Asia/Vladivostok', value: 'Asia/Vladivostok' },
{ label: 'Pacific/Auckland', value: 'Pacific/Auckland' },
{ label: 'Pacific/Fiji', value: 'Pacific/Fiji' },
{ label: 'Asia/Magadan', value: 'Asia/Magadan' },
{ label: 'Asia/Kamchatka', value: 'Asia/Kamchatka' },
{ label: 'Pacific/Tongatapu', value: 'Pacific/Tongatapu' }
];

View File

@@ -3,13 +3,16 @@ import { Modal } from 'ant-design-vue';
import { useAuthStore } from '@/store/modules/auth';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
import {useRouter} from "vue-router";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const router = useRouter();
defineOptions({
name: 'UserAvatar'
});
const authStore = useAuthStore();
const { routerPushByKey, toLogin } = useRouterPush();
const { toLogin } = useRouterPush();
function loginOrRegister() {
toLogin();
@@ -37,13 +40,17 @@ function logout() {
</ButtonIcon>
<template #overlay>
<AMenu>
<!-- <AMenuItem @click="routerPushByKey('user-center')">-->
<!-- <div class="flex-center gap-8px">-->
<!-- <SvgIcon icon="ph:user-circle" class="text-icon" />-->
<!-- {{ $t('common.userCenter') }}-->
<!-- </div>-->
<!-- </AMenuItem>-->
<!-- <AMenuDivider />-->
<AMenuItem @click="router.push('/user-center/email')">
<div class="flex-center gap-8px">
{{ $t('common.setemail') }}
</div>
</AMenuItem>
<AMenuItem @click="router.push('/user-center/resetpwd')">
<div class="flex-center gap-8px">
{{ $t('common.resetpwd') }}
</div>
</AMenuItem>
<AMenuDivider />
<AMenuItem @click="logout">
<div class="flex-center gap-8px">
<SvgIcon icon="ph:sign-out" class="text-icon" />

View File

@@ -24,6 +24,7 @@ const local: any = {
columnSetting: 'Column Setting',
config: 'Config',
confirm: 'Confirm',
submit:'Confirm',
delete: 'Delete',
view: 'View',
exportOk: "Export Completed",
@@ -52,6 +53,8 @@ const local: any = {
update: 'Update',
updateSuccess: 'Update Success',
userCenter: 'User Center',
setemail:'Bind Email',
resetpwd:'Reset Password',
yesOrNo: {
yes: 'Yes',
no: 'No'
@@ -670,6 +673,24 @@ const local: any = {
clients:'CLIENTS',
search:'Search site name',
total:'Total',
addsite:'Add New Site',
region:'Country/Region',
timezone:'TimeZone',
scenario:'Scenario',
username:'Username',
password:'Password',
deleteConfirmTitle:'Are you sure to delete',
deleteConfirmContent:'Are you sure to delete {name}',
confirm:'Confirm',
cancel:'Cancel',
deleteSuccess:'Delete Success',
editsite:'Edit Site',
office:'Office',
hotel:'Hotel',
education:'Education',
retail:'Retail',
other:'Other',
updateSuccess:'Update Success',
},
headerbanner:{
controller:'Controller Overview',
@@ -1105,7 +1126,42 @@ const local: any = {
confirm:'Determine',
close:'Cancel',
nozero:'The sort must be greater than or equal to 0'
}
},
resetPwd:{
title:'Change your password',
byEmail:'Change by email',
byPassword:'Change by old password',
email:'Email',
getCode:'Get code',
code:'Code',
oldPassword:'Old password',
newPassword:'New password',
confirmPassword:'confirm password',
emailPlaceholder:'Please enter the email',
codePlaceholder:'Please enter the code',
passwordPlaceholder:'Please enter new password',
oldPasswordPlaceholder:'Please enter old password',
confirmPasswordPlaceholder:'Please enter again',
submit:'Confirm',
emailRequired:'Email can not be empty',
codeSent:'Code enter',
success:'Reset Success',
},
email:{
title:'Bind Email',
currentEmail:'Current Email',
email:'Email',
code:'Code',
getCode:'Get Code',
submit:'Submit',
codeSent:'Code Sent',
codeRequired:'Code can not be empty',
updateSuccess:'Update Success',
updateFailed:'Update Failed',
emailRequired:'Email can not be empty',
emailInvalid:'Email Invalid',
codeLength:'Code Invalid',
},
},
form: {
required: 'Cannot be empty',

View File

@@ -24,6 +24,7 @@ const local:any = {
columnSetting: '列设置',
config: '配置',
confirm: '确认',
submit:'确定',
delete: '删除',
view:'查看',
exportOk: "已完成导出",
@@ -52,6 +53,8 @@ const local:any = {
update: '更新',
updateSuccess: '更新成功',
userCenter: '个人中心',
setemail:'绑定邮箱',
resetpwd:'修改密码',
yesOrNo: {
yes: '是',
no: '否'
@@ -670,6 +673,24 @@ const local:any = {
clients:'装置',
search:'输入站点名称',
total:'共',
addsite:'添加站点',
region:'国家/地区',
timezone:'时区',
scenario:'场景',
username:'用户名',
password:'密码',
deleteConfirmTitle:'确定要删除吗',
deleteConfirmContent:'确定要删除站点{name}吗?',
confirm:'确定',
cancel:'取消',
deleteSuccess:'删除成功',
editsite:'修改配置',
office:'办公室',
hotel:'医院',
education:'教育',
retail:'零售业',
other:'其他',
updateSuccess:'更新成功',
},
headerbanner:{
controller:'控制仪表盘',
@@ -1106,7 +1127,42 @@ const local:any = {
confirm:'确定',
close:'取消',
nozero:'排序必须大于等于0'
}
},
resetPwd:{
title:'修改密码',
byEmail:'通过邮箱修改密码',
byPassword:'通过原密码修改密码',
email:'邮箱',
getCode:'获取验证码',
code:'验证码',
oldPassword:'原密码',
newPassword:'新密码',
confirmPassword:'确认密码',
emailPlaceholder:'请输入邮箱',
codePlaceholder:'请输入验证码',
passwordPlaceholder:'请输入新密码',
oldPasswordPlaceholder:'请输入旧密码',
confirmPasswordPlaceholder:'请再次输入新密码',
submit:'确认',
emailRequired:'邮箱不能为空',
codeSent:'验证码发送',
success:'修改成功',
},
email:{
title:'绑定邮箱',
currentEmail:'当前邮箱',
email:'新邮箱',
code:'验证码',
getCode:'获取验证码',
submit:'确定',
codeSent:'验证码发送',
codeRequired:'验证码不能为空',
updateSuccess:'修改成功',
updateFailed:'修改失败',
emailRequired:'邮箱不能为空',
emailInvalid:'邮箱格式错误',
codeLength:'验证码长度错误',
},
},
form: {
required: '不能为空',

View File

@@ -200,7 +200,9 @@ const routeMap: RouteMap = {
'function_toggle-auth': '/function/toggle-auth',
'dashboard': '/dashboard',
'login': '/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?',
'user-center': '/user-center'
'user-center': '/user-center',
'reset-pwd':'/reset-pwd',
'forgot-pwd':'/user-center/resetpwd'
};
/**

View File

@@ -407,12 +407,6 @@ export function deletePortal(siteId: string, portalId: string) {
});
}
/** 首页邮箱重置密码 */
export function fetcodeReset(data:any) {
return request({
@@ -421,6 +415,81 @@ export function fetcodeReset(data:any) {
data
});
}
/** 获取邮箱验证码 */
export function getEmailCode(email: string) {
return request<any>({
url: `/system/email/code`,
method: 'get',
params: { email }
});
}
/** 重置密码 */
export function resetPassword(data: { email: string; code: string; password: string }) {
return request<any>({
url: '/system/user/profile/forgotPwd',
method: 'put',
data
});
}
/** 通过原密码修改密码 */
export function updatePasswordByOld(data: { oldPassword: string; newPassword: string }) {
return request<any>({
url: '/system/user/profile/updatePwd',
method: 'put',
data
});
}
/** 添加站点 */
export function addSite(data: Api.Site.AddSiteParams) {
return request<any>({
url: '/system/site',
method: 'post',
data
});
}
/** 删除站点 */
export function deleteSite(siteId: string) {
return request<any>({
url: `/system/site/${siteId}`,
method: 'delete'
});
}
/** 获取站点配置 */
export function getSiteConfig(siteId: string) {
return request<Api.Site.SiteConfig>({
url: `/system/site/${siteId}`,
method: 'get'
});
}
/** 更新站点配置 */
export function updateSite(siteId: string, data: Api.Site.UpdateSiteParams) {
return request<any>({
url: `/system/site/${siteId}`,
method: 'put',
data
});
}
/** 获取用户信息 */
export function getUserProfile() {
return request<any>({
url: '/system/user/profile',
method: 'get'
});
}
/** 更新用户信息 */
export function updateUserProfile(data: { email: string; code: string }) {
return request<any>({
url: '/system/user/profile',
method: 'put',
data
});
}

24
src/typings/api.d.ts vendored
View File

@@ -788,6 +788,30 @@ declare namespace Api {
supportL2: boolean;
}
interface AddSiteParams {
name: string;
region: string;
timeZone: string;
scenario: string;
deviceAccountSetting: {
username: string;
password: string;
};
}
interface SiteConfig {
name: string;
region: string;
timeZone: string;
scenario: string;
}
interface UpdateSiteParams {
name: string;
region: string;
timeZone: string;
scenario: string;
}
interface SiteParams {
pageNum: number;
pageSize: number;

View File

@@ -16,6 +16,7 @@ declare global {
const addPackage: typeof import('../service/api/auth')['addPackage']
const addPortal: typeof import('../service/api/auth')['addPortal']
const addRateLimit: typeof import('../service/api/auth')['addRateLimit']
const addSite: typeof import('../service/api/auth')['addSite']
const addThemeVarsToHtml: typeof import('../store/modules/theme/shared')['addThemeVarsToHtml']
const addWlanSsid: typeof import('../service/api/auth')['addWlanSsid']
const adoptApDevice: typeof import('../service/api/auth')['adoptApDevice']
@@ -68,6 +69,7 @@ declare global {
const deleteApDevices: typeof import('../service/api/auth')['deleteApDevices']
const deletePackage: typeof import('../service/api/auth')['deletePackage']
const deletePortal: typeof import('../service/api/auth')['deletePortal']
const deleteSite: typeof import('../service/api/auth')['deleteSite']
const deleteWlanSsid: typeof import('../service/api/auth')['deleteWlanSsid']
const describe: typeof import('vitest')['describe']
const dict: typeof import('../store/modules/dict/index')['default']
@@ -176,6 +178,7 @@ declare global {
const getDefaultHomeTab: typeof import('../store/modules/tab/shared')['getDefaultHomeTab']
const getDictDataType: typeof import('../service/api/dict')['getDictDataType']
const getDictOptionselect: typeof import('../service/api/dict')['getDictOptionselect']
const getEmailCode: typeof import('../service/api/auth')['getEmailCode']
const getFixedTabIds: typeof import('../store/modules/tab/shared')['getFixedTabIds']
const getFixedTabs: typeof import('../store/modules/tab/shared')['getFixedTabs']
const getGlobalMenusByAuthRoutes: typeof import('../store/modules/route/shared')['getGlobalMenusByAuthRoutes']
@@ -184,9 +187,11 @@ declare global {
const getRouteIcons: typeof import('../store/modules/tab/shared')['getRouteIcons']
const getSelectedMenuKeyPathByKey: typeof import('../store/modules/route/shared')['getSelectedMenuKeyPathByKey']
const getServiceBaseURL: typeof import('../utils/service')['getServiceBaseURL']
const getSiteConfig: typeof import('../service/api/auth')['getSiteConfig']
const getTabByRoute: typeof import('../store/modules/tab/shared')['getTabByRoute']
const getTabIdByRoute: typeof import('../store/modules/tab/shared')['getTabIdByRoute']
const getToken: typeof import('../store/modules/auth/shared')['getToken']
const getUserProfile: typeof import('../service/api/auth')['getUserProfile']
const getWlanSsidConfig: typeof import('../service/api/auth')['getWlanSsidConfig']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
@@ -255,6 +260,7 @@ declare global {
const rejectKyc: typeof import('../service/api/auth')['rejectKyc']
const removeEmptyChildren: typeof import('../utils/menu')['removeEmptyChildren']
const removeRateLimit: typeof import('../service/api/auth')['removeRateLimit']
const resetPassword: typeof import('../service/api/auth')['resetPassword']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
@@ -306,9 +312,12 @@ declare global {
const updateJob: typeof import('../service/api/job')['updateJob']
const updateLocaleOfGlobalMenus: typeof import('../store/modules/route/shared')['updateLocaleOfGlobalMenus']
const updatePackage: typeof import('../service/api/auth')['updatePackage']
const updatePasswordByOld: typeof import('../service/api/auth')['updatePasswordByOld']
const updatePortalConfig: typeof import('../service/api/auth')['updatePortalConfig']
const updateSite: typeof import('../service/api/auth')['updateSite']
const updateTabByI18nKey: typeof import('../store/modules/tab/shared')['updateTabByI18nKey']
const updateTabsByI18nKey: typeof import('../store/modules/tab/shared')['updateTabsByI18nKey']
const updateUserProfile: typeof import('../service/api/auth')['updateUserProfile']
const updateWlanSsid: typeof import('../service/api/auth')['updateWlanSsid']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']

View File

@@ -4,15 +4,107 @@ import type { TableColumnType } from 'ant-design-vue';
import {
EnvironmentOutlined,
SearchOutlined,
PlusOutlined,
EditOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import { getDashboardSiteList } from '@/service/api/auth';
import { getDashboardSiteList, addSite, deleteSite, getSiteConfig, updateSite } from '@/service/api/auth';
import { useI18n } from 'vue-i18n';
import { Form, Modal } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { regionOptions, timeZoneOptions } from '@/constants/site-options';
const { t } = useI18n();
defineOptions({
name: 'CardData'
});
// 表单实例
const formRef = ref();
const useForm = Form.useForm;
// 弹窗控制
const showAddDialog = ref(false);
// 表单数据
const formData = ref({
name: '',
region: '',
timeZone: '',
scenario: '',
deviceAccountSetting: {
username: '',
password: ''
}
});
// 表单验证规则
const { validate, validateInfos } = useForm(formData, {
name: [
{ required: true, message: t('page.carddata.nameRequired') },
{
pattern: /^[^ \+\-\@\=]$|^[^ \+\-\@\=].{0,62}[^ ]$/,
message: t('page.carddata.nameInvalid')
}
],
region: [{ required: true, message: t('page.carddata.regionRequired') }],
timeZone: [{ required: true, message: t('page.carddata.timeZoneRequired') }],
scenario: [{ required: true, message: t('page.carddata.scenarioRequired') }],
'deviceAccountSetting.username': [
{ required: true, message: t('page.carddata.usernameRequired') },
{
pattern: /^[\x21-\x7E]{1,64}$/,
message: t('page.carddata.usernameInvalid')
}
],
'deviceAccountSetting.password': [
{ required: true, message: t('page.carddata.passwordRequired') },
{
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\!\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\@\[\\\]\^\_\`\{\|\}\~])(?!.*[\00-\040\042\077\0177]).{8,64}$/,
message: t('page.carddata.passwordInvalid')
}
]
});
// 使用场景选项
const scenarioOptions = [
{ label: t('page.carddata.office'), value: 'office' },
{ label: t('page.carddata.hotel'), value: 'hotel' },
{ label: t('page.carddata.education'), value: 'education' },
{ label: t('page.carddata.retail'), value: 'retail' },
{ label: t('page.carddata.other'), value: 'other' }
];
// 处理添加站点
const handleAddSite = async () => {
try {
await validate();
const response = await addSite(formData.value);
if (response) {
message.success(t('page.carddata.addSuccess'));
showAddDialog.value = false;
fetchSiteList(); // 刷新列表
}
} catch (error) {
console.error('Add site failed:', error);
message.error(t('page.carddata.addFailed'));
}
};
// 打开添加对话框
const openAddDialog = () => {
formData.value = {
name: '',
region: '',
timeZone: '',
scenario: '',
deviceAccountSetting: {
username: '',
password: ''
}
};
showAddDialog.value = true;
};
// 搜索和分页状态
const searchValue = ref('');
@@ -100,25 +192,37 @@ const columns: TableColumnType<Api.DashboardSite>[] = [
key: 'clients',
width: 150
},
// {
// title: 'ACTION',
// key: 'action',
// width: 150,
// fixed: 'right'
// }
{
title: 'ACTION',
key: 'action',
width: 100,
fixed: 'right'
}
];
// 按钮操作处理函数
// const handleEdit = (record: Api.DashboardSite) => {
// console.log('Edit:', record);
// };
//
// const handleCopy = (record: Api.DashboardSite) => {
// console.log('Copy:', record);
// };
//
// const handleDelete = (record: Api.DashboardSite) => {
// console.log('Delete:', record);
// };
const handleDelete = (record: Api.DashboardSite) => {
Modal.confirm({
title: t('page.carddata.deleteConfirmTitle'),
content: t('page.carddata.deleteConfirmContent', { name: record.name }),
okText: t('page.carddata.confirm'),
cancelText: t('page.carddata.cancel'),
okType: 'danger',
async onOk() {
try {
await deleteSite(record.siteId);
message.success(t('page.carddata.deleteSuccess'));
fetchSiteList(); // 刷新列表
} catch (error) {
}
}
});
};
//
// const handleHome = (record: Api.DashboardSite) => {
// console.log('Home:', record);
@@ -136,6 +240,68 @@ const handlePageSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
};
// 编辑弹窗控制
const showEditDialog = ref(false);
// 编辑表单数据
const editFormData = ref({
name: '',
region: '',
timeZone: '',
scenario: '',
});
// 当前编辑的站点ID
const currentEditSiteId = ref('');
// 编辑表单验证规则
const { validate: validateEdit, validateInfos: validateEditInfos } = useForm(editFormData, {
name: [
{ required: true, message: t('page.carddata.nameRequired') },
{
pattern: /^[^ \+\-\@\=]$|^[^ \+\-\@\=].{0,62}[^ ]$/,
message: t('page.carddata.nameInvalid')
}
],
region: [{ required: true, message: t('page.carddata.regionRequired') }],
timeZone: [{ required: true, message: t('page.carddata.timeZoneRequired') }],
scenario: [{ required: true, message: t('page.carddata.scenarioRequired') }]
});
// 处理编辑按钮点击
const handleEdit = async (record: Api.DashboardSite) => {
try {
currentEditSiteId.value = record.siteId;
const response = await getSiteConfig(record.siteId);
if (response.data) {
editFormData.value = {
name: response.data.name,
region: response.data.region,
timeZone: response.data.timeZone,
scenario: response.data.scenario
};
showEditDialog.value = true;
}
} catch (error) {
console.error('Get site config failed:', error);
message.error(t('page.carddata.getConfigFailed'));
}
};
// 处理更新站点
const handleUpdateSite = async () => {
try {
await validateEdit();
await updateSite(currentEditSiteId.value, editFormData.value);
message.success(t('page.carddata.updateSuccess'));
showEditDialog.value = false;
fetchSiteList(); // 刷新列表
} catch (error) {
console.error('Update site failed:', error);
message.error(t('page.carddata.updateFailed'));
}
};
</script>
<template>
@@ -155,18 +321,12 @@ const handlePageSizeChange = (size: number) => {
<search-outlined />
</template>
</AInput>
<!-- <AButton type="primary">-->
<!-- <template #icon>-->
<!-- <plus-outlined />-->
<!-- </template>-->
<!-- Import Site-->
<!-- </AButton>-->
<!-- <AButton type="primary">-->
<!-- <template #icon>-->
<!-- <plus-outlined />-->
<!-- </template>-->
<!-- Add New Site-->
<!-- </AButton>-->
<AButton type="primary" @click="openAddDialog">
<template #icon>
<plus-outlined />
</template>
{{ t('page.carddata.addsite') }}
</AButton>
</div>
</div>
@@ -221,16 +381,157 @@ const handlePageSizeChange = (size: number) => {
<template v-else-if="column.key === 'clients'">
<span>{{ record.wiredClientNum }}/{{ record.wirelessClientNum }}/{{ record.guestNum }}</span>
</template>
<!-- <template v-else-if="column.key === 'action'">-->
<!-- <div class="flex items-center gap-8px">-->
<!-- <edit-outlined class="cursor-pointer text-primary" @click="handleEdit(record)" />-->
<!-- <copy-outlined class="cursor-pointer text-primary" @click="handleCopy(record)" />-->
<!-- <delete-outlined class="cursor-pointer text-red-500" @click="handleDelete(record)" />-->
<!-- <home-outlined class="cursor-pointer text-primary" @click="handleHome(record)" />-->
<!-- </div>-->
<!-- </template>-->
<template v-else-if="column.key === 'action'">
<div class="flex items-center gap-8px">
<edit-outlined class="cursor-pointer text-primary" @click="handleEdit(record)" />
<!-- <copy-outlined class="cursor-pointer text-primary" @click="handleCopy(record)" />-->
<delete-outlined class="cursor-pointer text-red-500" @click="handleDelete(record)" />
<!-- <home-outlined class="cursor-pointer text-primary" @click="handleHome(record)" />-->
</div>
</template>
</template>
</ATable>
<!-- 添加站点对话框 -->
<AModal
v-model:visible="showAddDialog"
:title="t('page.carddata.addsite')"
@ok="handleAddSite"
@cancel="showAddDialog = false"
:maskClosable="false"
>
<AForm
ref="formRef"
:model="formData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<AFormItem
name="name"
:label="t('page.carddata.sitename')"
v-bind="validateInfos.name"
>
<AInput v-model:value="formData.name" />
</AFormItem>
<AFormItem
name="region"
:label="t('page.carddata.region')"
v-bind="validateInfos.region"
>
<ASelect
v-model:value="formData.region"
:options="regionOptions"
show-search
:filter-option="(input, option) =>
option?.label?.toLowerCase().includes(input.toLowerCase())"
/>
</AFormItem>
<AFormItem
name="timeZone"
:label="t('page.carddata.timezone')"
v-bind="validateInfos.timeZone"
>
<ASelect
v-model:value="formData.timeZone"
:options="timeZoneOptions"
show-search
:filter-option="(input, option) =>
option?.label?.toLowerCase().includes(input.toLowerCase())"
/>
</AFormItem>
<AFormItem
name="scenario"
:label="t('page.carddata.scenario')"
v-bind="validateInfos.scenario"
>
<ASelect
v-model:value="formData.scenario"
:options="scenarioOptions"
/>
</AFormItem>
<AFormItem
name="deviceAccountSetting.username"
:label="t('page.carddata.username')"
v-bind="validateInfos['deviceAccountSetting.username']"
>
<AInput v-model:value="formData.deviceAccountSetting.username" />
</AFormItem>
<AFormItem
name="deviceAccountSetting.password"
:label="t('page.carddata.password')"
v-bind="validateInfos['deviceAccountSetting.password']"
>
<AInputPassword v-model:value="formData.deviceAccountSetting.password" />
</AFormItem>
</AForm>
</AModal>
<!-- 编辑站点对话框 -->
<AModal
v-model:visible="showEditDialog"
:title="t('page.carddata.editsite')"
@ok="handleUpdateSite"
@cancel="showEditDialog = false"
:maskClosable="false"
>
<AForm
:model="editFormData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<AFormItem
name="name"
:label="t('page.carddata.sitename')"
v-bind="validateEditInfos.name"
>
<AInput v-model:value="editFormData.name" />
</AFormItem>
<AFormItem
name="region"
:label="t('page.carddata.region')"
v-bind="validateEditInfos.region"
>
<ASelect
v-model:value="editFormData.region"
:options="regionOptions"
show-search
:filter-option="(input, option) =>
option?.label?.toLowerCase().includes(input.toLowerCase())"
/>
</AFormItem>
<AFormItem
name="timeZone"
:label="t('page.carddata.timezone')"
v-bind="validateEditInfos.timeZone"
>
<ASelect
v-model:value="editFormData.timeZone"
:options="timeZoneOptions"
show-search
:filter-option="(input, option) =>
option?.label?.toLowerCase().includes(input.toLowerCase())"
/>
</AFormItem>
<AFormItem
name="scenario"
:label="t('page.carddata.scenario')"
v-bind="validateEditInfos.scenario"
>
<ASelect
v-model:value="editFormData.scenario"
:options="scenarioOptions"
/>
</AFormItem>
</AForm>
</AModal>
</ACard>
</template>

View File

@@ -0,0 +1,211 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { Form, message } from 'ant-design-vue';
import { useI18n } from 'vue-i18n';
import { getEmailCode, getUserProfile, updateUserProfile } from '@/service/api/auth';
import request from '@/service/request';
const { t } = useI18n();
const useForm = Form.useForm;
// 表单数据
const formData = ref({
email: '',
code: ''
});
// 当前邮箱
const currentEmail = ref('');
// 倒计时
const countdown = ref(0);
const timer = ref<NodeJS.Timeout | null>(null);
// 表单验证规则
const { validate, validateInfos } = useForm(formData, {
email: [
{ required: true, message: t('page.email.emailRequired') },
{ type: 'email', message: t('page.email.emailInvalid') }
],
code: [
{ required: true, message: t('page.email.codeRequired') },
{ len: 4, message: t('page.email.codeLength') }
]
});
// 获取当前用户邮箱
const getCurrentEmail = async () => {
try {
const response = await getUserProfile();
if (response.data) {
currentEmail.value = response.data.email || '';
if (currentEmail.value) {
formData.value.email = currentEmail.value;
}
}
} catch (error) {
}
};
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
timer.value = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
}
}, 1000);
};
// 获取验证码
const handleGetCode = async () => {
try {
// 验证邮箱
await validate(['email']);
// 发送获取验证码请求
await getEmailCode(formData.value.email);
message.success(t('page.email.codeSent'));
startCountdown();
} catch (error) {
}
};
// 提交表单
const handleSubmit = async () => {
try {
await validate();
await updateUserProfile({
email: formData.value.email,
code: formData.value.code
});
message.success(t('page.email.updateSuccess'));
formData.value.code = ''; // 清空验证码
setTimeout(async () => {
await getCurrentEmail(); // 刷新当前邮箱
}, 500);
} catch (error) {
}
};
// 组件挂载时获取当前邮箱
onMounted(() => {
getCurrentEmail();
});
// 组件卸载时清除定时器
const onUnmounted = () => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
};
</script>
<template>
<div class="email-binding">
<ACard :bordered="false">
<h2 class="mb-4">{{ t('page.email.title') }}</h2>
<div v-if="currentEmail" class="mb-4">
{{ t('page.email.currentEmail') }}: {{ currentEmail }}
</div>
<AForm
:model="formData"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 16 }"
>
<AFormItem
name="email"
:label="t('page.email.email')"
v-bind="validateInfos.email"
class="form-item-gap"
>
<AInput v-model:value="formData.email" />
</AFormItem>
<AFormItem
name="code"
:label="t('page.email.code')"
v-bind="validateInfos.code"
class="form-item-gap"
>
<div class="verification-code-wrapper">
<AInput v-model:value="formData.code" class="verification-input" />
<AButton
type="primary"
:disabled="countdown > 0"
@click="handleGetCode"
class="verification-button"
>
{{ countdown > 0 ? `${countdown}s` : t('page.email.getCode') }}
</AButton>
</div>
</AFormItem>
<AFormItem :wrapper-col="{ offset: 4, span: 16 }">
<AButton type="primary" @click="handleSubmit">
{{ t('page.email.submit') }}
</AButton>
</AFormItem>
</AForm>
</ACard>
</div>
</template>
<style scoped>
.email-binding {
max-width: 800px;
margin: 0 auto;
padding: 24px;
}
.flex {
display: flex;
}
.gap-4 {
gap: 1rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.form-item-gap {
margin-bottom: 24px;
}
:deep(.ant-form-item-label) {
padding-right: 24px;
}
.verification-code-wrapper {
display: flex;
gap: 12px;
align-items: center;
}
.verification-input {
flex: 1;
}
.verification-button {
width: 120px;
flex-shrink: 0;
}
:deep(.ant-form-item) {
margin-bottom: 32px;
}
:deep(.ant-input-affix-wrapper) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,219 @@
<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
import { message } from 'ant-design-vue';
import { useI18n } from 'vue-i18n';
import { getEmailCode, resetPassword, updatePasswordByOld } from '@/service/api/auth';
import { useRouterPush } from '@/hooks/common/router';
import { useFormRules } from '@/hooks/common/form';
const { t } = useI18n();
const { routerPushByKey } = useRouterPush();
// 修改方式
const resetType = ref('email'); // 'email' | 'password'
// 邮箱验证表单数据
const emailFormData = reactive({
email: '',
code: '',
password: '',
confirmPassword: ''
});
// 原密码表单数据
const pwdFormData = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
});
// 验证码按钮状态
const codeButtonLoading = ref(false);
const countdown = ref(0);
const timer = ref<NodeJS.Timeout>();
// 邮箱验证表单规则
const emailRules = computed(() => {
const { formRules, createConfirmPwdRule } = useFormRules();
return {
email: formRules.email,
code: formRules.code,
password: formRules.pwd,
confirmPassword: createConfirmPwdRule(emailFormData.password)
};
});
// 原密码表单规则
const pwdRules = computed(() => {
const { formRules, createConfirmPwdRule } = useFormRules();
return {
oldPassword: formRules.pwd,
newPassword: formRules.pwd,
confirmPassword: createConfirmPwdRule(pwdFormData.newPassword)
};
});
// 获取验证码
const handleGetCode = async () => {
try {
if (!emailFormData.email) {
message.error(t('page.resetPwd.emailRequired'));
return;
}
codeButtonLoading.value = true;
await getEmailCode(emailFormData.email);
message.success(t('page.resetPwd.codeSent'));
// 开始倒计时
countdown.value = 60;
timer.value = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
clearInterval(timer.value);
}
}, 1000);
} catch (error) {
console.error('Failed to get code:', error);
} finally {
codeButtonLoading.value = false;
}
};
// 提交表单
const emailFormRef = ref();
const pwdFormRef = ref();
const handleSubmit = async () => {
try {
if (resetType.value === 'email') {
await emailFormRef.value?.validate();
const hide = message.loading(t('common.loading'), 0);
await resetPassword({
email: emailFormData.email,
code: emailFormData.code,
password: emailFormData.password
});
hide();
} else {
await pwdFormRef.value?.validate();
const hide = message.loading(t('common.loading'), 0);
await updatePasswordByOld({
oldPassword: pwdFormData.oldPassword,
newPassword: pwdFormData.newPassword
});
hide();
}
message.success(t('page.resetPwd.success'));
routerPushByKey('login');
} catch (error) {
}
};
</script>
<template>
<div class="flex-col-stretch items-center justify-center min-h-500px p-24px">
<div class="w-full max-w-400px">
<h2 class="text-24px font-bold mb-24px text-center">{{ t('page.resetPwd.title') }}</h2>
<!-- 修改方式选择 -->
<a-radio-group v-model:value="resetType" class="mb-16px">
<a-radio value="email">{{ t('page.resetPwd.byEmail') }}</a-radio>
<a-radio value="password">{{ t('page.resetPwd.byPassword') }}</a-radio>
</a-radio-group>
<!-- 邮箱验证表单 -->
<a-form
v-if="resetType === 'email'"
ref="emailFormRef"
:model="emailFormData"
:rules="emailRules"
layout="vertical"
>
<a-form-item name="email" :label="t('page.resetPwd.email')">
<div class="flex gap-8px">
<a-input
v-model:value="emailFormData.email"
:placeholder="t('page.resetPwd.emailPlaceholder')"
/>
<a-button
:loading="codeButtonLoading"
:disabled="countdown > 0"
@click="handleGetCode"
>
{{ countdown > 0 ? `${countdown}s` : t('page.resetPwd.getCode') }}
</a-button>
</div>
</a-form-item>
<a-form-item name="code" :label="t('page.resetPwd.code')">
<a-input
v-model:value="emailFormData.code"
:placeholder="t('page.resetPwd.codePlaceholder')"
/>
</a-form-item>
<a-form-item name="password" :label="t('page.resetPwd.newPassword')">
<a-input-password
v-model:value="emailFormData.password"
:placeholder="t('page.resetPwd.passwordPlaceholder')"
/>
</a-form-item>
<a-form-item name="confirmPassword" :label="t('page.resetPwd.confirmPassword')">
<a-input-password
v-model:value="emailFormData.confirmPassword"
:placeholder="t('page.resetPwd.confirmPasswordPlaceholder')"
/>
</a-form-item>
</a-form>
<!-- 原密码表单 -->
<a-form
v-else
ref="pwdFormRef"
:model="pwdFormData"
:rules="pwdRules"
layout="vertical"
>
<a-form-item name="oldPassword" :label="t('page.resetPwd.oldPassword')">
<a-input-password
v-model:value="pwdFormData.oldPassword"
:placeholder="t('page.resetPwd.oldPasswordPlaceholder')"
/>
</a-form-item>
<a-form-item name="newPassword" :label="t('page.resetPwd.newPassword')">
<a-input-password
v-model:value="pwdFormData.newPassword"
:placeholder="t('page.resetPwd.passwordPlaceholder')"
/>
</a-form-item>
<a-form-item name="confirmPassword" :label="t('page.resetPwd.confirmPassword')">
<a-input-password
v-model:value="pwdFormData.confirmPassword"
:placeholder="t('page.resetPwd.confirmPasswordPlaceholder')"
/>
</a-form-item>
</a-form>
<!-- 提交按钮 -->
<a-form-item>
<a-button type="primary" block @click="handleSubmit">
{{ t('common.submit') }}
</a-button>
</a-form-item>
</div>
</div>
</template>
<style scoped>
.max-w-400px {
max-width: 400px;
}
</style>