| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527 |
- <template>
- <div class="login-form-wrapper">
- <!-- 登录方式切换标签 -->
- <div class="login-tabs">
- <div class="tab-item" :class="{ active: loginType === 'password' }" @click="loginType = 'password'">密码登录</div>
- <div class="tab-item" :class="{ active: loginType === 'code' }" @click="loginType = 'code'">验证码登录</div>
- </div>
- <!-- 密码登录表单 -->
- <el-form
- v-if="loginType === 'password'"
- ref="passwordFormRef"
- :model="passwordForm"
- :rules="passwordRules"
- size="large"
- class="login-form-content"
- >
- <el-form-item prop="phone">
- <el-input v-model="passwordForm.phone" placeholder="请输入手机号" maxlength="11" clearable />
- </el-form-item>
- <el-form-item prop="password">
- <el-input
- v-model="passwordForm.password"
- type="password"
- placeholder="请输入密码"
- show-password
- autocomplete="new-password"
- clearable
- />
- </el-form-item>
- <el-form-item prop="captcha">
- <div class="captcha-wrapper">
- <el-input
- v-model="passwordForm.captcha"
- placeholder="请输入图片中的数字"
- maxlength="4"
- clearable
- class="captcha-input"
- />
- <div class="captcha-image" @click="refreshCaptcha">
- <img v-if="captchaImage" :src="captchaImage" alt="验证码" />
- <div v-else class="captcha-placeholder">点击获取</div>
- </div>
- </div>
- </el-form-item>
- </el-form>
- <!-- 验证码登录表单 -->
- <el-form v-else ref="codeFormRef" :model="codeForm" :rules="codeRules" size="large" class="login-form-content">
- <el-form-item prop="phone">
- <el-input v-model="codeForm.phone" placeholder="请输入手机号" maxlength="11" clearable />
- </el-form-item>
- <el-form-item prop="code">
- <div class="code-wrapper">
- <el-input v-model="codeForm.code" placeholder="请输入验证码" maxlength="6" clearable class="code-input" />
- <el-button :disabled="codeCountdown > 0" @click="sendCode" class="code-button">
- {{ codeCountdown > 0 ? `${codeCountdown}秒后重试` : "获取验证码" }}
- </el-button>
- </div>
- </el-form-item>
- </el-form>
- <!-- 登录按钮 -->
- <el-button type="primary" size="large" :loading="loading" @click="handleLogin" class="login-button"> 登录 </el-button>
- <!-- 底部链接 -->
- <div class="form-footer">
- <span class="link-text" @click="handleForgotPassword">忘记密码?</span>
- <span class="link-text" @click="handleRegister">还没有账号,去注册</span>
- </div>
- <!-- 用户协议 -->
- <div class="agreement-wrapper">
- <el-checkbox v-model="agreed" />
- <span class="agreement-text">
- 我已阅读并同意
- <span class="link-text" @click="handleUserAgreement">《用户协议》</span>
- 和
- <span class="link-text" @click="handlePrivacyPolicy">《隐私政策》</span>
- </span>
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, reactive, watch, onMounted, onBeforeUnmount } from "vue";
- import { useRouter } from "vue-router";
- import { HOME_URL } from "@/config";
- import { Login } from "@/api/interface/index";
- import { ElMessage, ElNotification } from "element-plus";
- import { loginApi } from "@/api/modules/login";
- import { useUserStore } from "@/stores/modules/user";
- import { useTabsStore } from "@/stores/modules/tabs";
- import { useKeepAliveStore } from "@/stores/modules/keepAlive";
- import { initDynamicRouter } from "@/routers/modules/dynamicRouter";
- import type { ElForm } from "element-plus";
- import md5 from "md5";
- import { aiLogin } from "@/api/indexAi";
- import { localSet } from "@/utils";
- import { resetAuthExpiredDedupe } from "@/api/helper/handleAuthExpired";
- const router = useRouter();
- const userStore = useUserStore();
- const tabsStore = useTabsStore();
- const keepAliveStore = useKeepAliveStore();
- // 登录方式
- const loginType = ref<"password" | "code">("password");
- // 表单引用
- const passwordFormRef = ref<InstanceType<typeof ElForm>>();
- const codeFormRef = ref<InstanceType<typeof ElForm>>();
- // 加载状态
- const loading = ref(false);
- // 用户协议同意状态
- const agreed = ref(false);
- // 密码登录表单
- const passwordForm = reactive({
- phone: "",
- password: "",
- captcha: ""
- });
- // 验证码登录表单
- const codeForm = reactive({
- phone: "",
- code: ""
- });
- // 图片验证码
- const captchaImage = ref("");
- // 验证码倒计时
- const codeCountdown = ref(0);
- let countdownTimer: NodeJS.Timeout | null = null;
- // 表单验证规则
- const passwordRules = reactive({
- phone: [
- { required: true, message: "请输入手机号", trigger: "blur" },
- { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur" }
- ],
- password: [{ required: true, message: "请输入密码", trigger: "blur" }],
- captcha: [{ required: true, message: "请输入验证码", trigger: "blur" }]
- });
- const codeRules = reactive({
- phone: [
- { required: true, message: "请输入手机号", trigger: "blur" },
- { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur" }
- ],
- code: [{ required: true, message: "请输入验证码", trigger: "blur" }]
- });
- // 获取图片验证码
- const refreshCaptcha = async () => {
- try {
- // TODO: 调用获取验证码图片的API
- // const { data } = await getCaptchaApi();
- // captchaImage.value = data.imageUrl;
- // 临时使用随机数字作为验证码图片(实际应该从API获取)
- const randomNum = Math.floor(1000 + Math.random() * 9000);
- captchaImage.value = `data:image/svg+xml;base64,${btoa(`
- <svg width="120" height="40" xmlns="http://www.w3.org/2000/svg">
- <rect width="120" height="40" fill="#f5f7fa"/>
- <text x="60" y="25" font-size="20" font-weight="bold" text-anchor="middle" fill="#409eff">${randomNum}</text>
- </svg>
- `)}`;
- } catch (error) {
- ElMessage.error("获取验证码失败");
- }
- };
- // 发送短信验证码
- const sendCode = async () => {
- if (!codeForm.phone) {
- ElMessage.warning("请先输入手机号");
- return;
- }
- if (!/^1[3-9]\d{9}$/.test(codeForm.phone)) {
- ElMessage.warning("请输入正确的手机号码");
- return;
- }
- try {
- // TODO: 调用发送验证码的API
- // await sendSmsCodeApi({ phone: codeForm.phone });
- ElMessage.success("验证码已发送");
- // 开始倒计时
- codeCountdown.value = 60;
- countdownTimer = setInterval(() => {
- codeCountdown.value--;
- if (codeCountdown.value <= 0) {
- if (countdownTimer) {
- clearInterval(countdownTimer);
- countdownTimer = null;
- }
- }
- }, 1000);
- } catch (error) {
- ElMessage.error("发送验证码失败");
- }
- };
- // 登录处理
- const handleLogin = async () => {
- if (!agreed.value) {
- ElMessage.warning("请先同意用户协议和隐私政策");
- return;
- }
- let formRef: InstanceType<typeof ElForm> | undefined;
- let formData: any = {};
- if (loginType.value === "password") {
- formRef = passwordFormRef.value;
- formData = {
- username: passwordForm.phone,
- password: passwordForm.password,
- captcha: passwordForm.captcha
- };
- } else {
- formRef = codeFormRef.value;
- formData = {
- username: codeForm.phone,
- code: codeForm.code,
- loginType: "code"
- };
- }
- if (!formRef) return;
- await formRef.validate(async valid => {
- if (!valid) return;
- loading.value = true;
- try {
- // 根据登录方式调用不同的登录接口
- let loginParams: any;
- if (loginType.value === "password") {
- loginParams = {
- username: formData.username,
- password: md5(formData.password)
- };
- } else {
- // TODO: 验证码登录接口
- loginParams = {
- username: formData.username,
- code: formData.code
- };
- }
- const { data } = (await loginApi(loginParams)) as { data: Login.ResLogin };
- if (data) {
- userStore.setToken(data.token);
- resetAuthExpiredDedupe();
- console.log("AI登录");
- // 保存用户信息到localStorage,供AI登录使用
- const userInfo = {
- userInfo: {
- nickName: formData.username, // 使用登录时的用户名
- phone: formData.username, // 假设用户名就是手机号
- password: loginType.value === "password" ? formData.password : undefined // 保存密码用于AI登录
- },
- token: data.token
- };
- localSet("geeker-user", userInfo);
- // 旧登录接口成功后,调用AI登录接口
- try {
- console.log("AI登录");
- await aiLogin();
- } catch (error) {
- console.error("AI服务登录失败:", error);
- // AI登录失败不影响主系统登录流程,静默处理
- }
- // 添加动态路由
- await initDynamicRouter();
- // 清空 tabs、keepAlive 数据
- tabsStore.setTabs([]);
- keepAliveStore.setKeepAliveName([]);
- // 跳转到首页
- router.push(HOME_URL);
- ElNotification({
- title: "登录成功",
- type: "success",
- duration: 3000
- });
- } else {
- ElNotification({
- title: "登录失败,请检查账号和密码",
- type: "error",
- duration: 3000
- });
- }
- } catch (error: any) {
- ElNotification({
- title: error?.msg || "登录失败,请稍后重试",
- type: "error",
- duration: 3000
- });
- } finally {
- loading.value = false;
- }
- });
- };
- // 忘记密码
- const handleForgotPassword = () => {
- // TODO: 跳转到忘记密码页面
- ElMessage.info("忘记密码功能开发中");
- };
- // 注册
- const handleRegister = () => {
- // TODO: 跳转到注册页面
- ElMessage.info("注册功能开发中");
- };
- // 用户协议
- const handleUserAgreement = () => {
- // TODO: 打开用户协议弹窗或跳转
- ElMessage.info("用户协议");
- };
- // 隐私政策
- const handlePrivacyPolicy = () => {
- // TODO: 打开隐私政策弹窗或跳转
- ElMessage.info("隐私政策");
- };
- // 切换登录方式时重置表单
- const resetForms = () => {
- if (passwordFormRef.value) {
- passwordFormRef.value.resetFields();
- }
- if (codeFormRef.value) {
- codeFormRef.value.resetFields();
- }
- captchaImage.value = "";
- codeCountdown.value = 0;
- if (countdownTimer) {
- clearInterval(countdownTimer);
- countdownTimer = null;
- }
- };
- // 监听登录方式切换
- watch(
- () => loginType.value,
- () => {
- resetForms();
- }
- );
- onMounted(() => {
- // 密码登录时自动获取验证码
- if (loginType.value === "password") {
- refreshCaptcha();
- }
- // 监听 enter 事件
- document.onkeydown = (e: KeyboardEvent) => {
- if (e.code === "Enter" || e.code === "enter" || e.code === "NumpadEnter") {
- if (loading.value) return;
- handleLogin();
- }
- };
- });
- onBeforeUnmount(() => {
- document.onkeydown = null;
- if (countdownTimer) {
- clearInterval(countdownTimer);
- }
- });
- </script>
- <style scoped lang="scss">
- .login-form-wrapper {
- .login-tabs {
- display: flex;
- width: fit-content;
- margin: 0 auto 30px;
- border-bottom: 1px solid #e4e7ed;
- .tab-item {
- position: relative;
- padding: 12px 24px;
- font-size: 16px;
- color: #606266;
- text-align: center;
- white-space: nowrap;
- cursor: pointer;
- transition: color 0.3s;
- &:hover {
- color: #606266;
- }
- &.active {
- font-weight: 700;
- color: #000000;
- &::after {
- position: absolute;
- right: 0;
- bottom: -1px;
- left: 0;
- height: 2px;
- content: "";
- background-color: #6c8ff8;
- }
- }
- }
- }
- .login-form-content {
- :deep(.el-form-item) {
- margin-bottom: 20px;
- }
- :deep(.el-input__wrapper) {
- padding: 12px 15px;
- }
- }
- .captcha-wrapper {
- display: flex;
- gap: 10px;
- .captcha-input {
- flex: 1;
- }
- .captcha-image {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 120px;
- height: 40px;
- overflow: hidden;
- cursor: pointer;
- background-color: #f5f7fa;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- .captcha-placeholder {
- font-size: 12px;
- color: #909399;
- }
- &:hover {
- border-color: #409eff;
- }
- }
- }
- .code-wrapper {
- display: flex;
- gap: 10px;
- .code-input {
- flex: 1;
- }
- .code-button {
- width: 120px;
- white-space: nowrap;
- }
- }
- .login-button {
- width: 100%;
- height: 44px;
- margin-top: 10px;
- font-size: 16px;
- }
- :deep(.login-button.el-button--primary) {
- background-color: #6c8ff8;
- border-color: #6c8ff8;
- &:hover {
- background-color: #5a7de8;
- border-color: #5a7de8;
- }
- &:active {
- background-color: #4a6dd8;
- border-color: #4a6dd8;
- }
- }
- .form-footer {
- display: flex;
- justify-content: space-between;
- margin-top: 20px;
- font-size: 14px;
- .link-text {
- color: #409eff;
- cursor: pointer;
- &:hover {
- text-decoration: underline;
- }
- }
- }
- .agreement-wrapper {
- display: flex;
- align-items: center;
- margin-top: 20px;
- font-size: 14px;
- color: #606266;
- :deep(.el-checkbox) {
- margin-right: 8px;
- }
- .agreement-text {
- .link-text {
- color: #409eff;
- cursor: pointer;
- &:hover {
- text-decoration: underline;
- }
- }
- }
- }
- }
- </style>
|