<template>
  <div :class="'login_form' + ((captchaObj&&captchaObj.captchaId)?' showCaptcha':'')" v-if="config.isPwdLogin || config.isScan">
    <!-- 用户名密码窗口 -->
    <div id="showQr" class="loginMainBox" :hidden="!(config.isPwdLogin && currLoginType == 'com')">
      <!-- <img
        @click="changeQr"
        class="icon-qrcode"
        :src="scanIcon"
        :hidden="!config.isScan"
      /> -->
      <i class="iconfont icon-erweima1 icon-qrcode"         @click="changeQr"   :hidden="!config.isScan"></i>
      <div class="login-tip" :hidden="!config.isScan">
        <div class="poptip">
          <div class="poptip-arrow">
            <em></em>
            <span></span>
          </div>
          <div class="poptip-content">{{$t('hivuiLogin_main_scan_login')}}</div>
        </div>
      </div>
      <div class="login_form_bd">
        <div class="error_tips"></div>
        <input type="hidden" name="directAction" value="" />
        <input type="hidden" name="refererurl" value="" />
        <div class="login_field">
          <label>{{$t('hivuiLogin_main_user')}}</label>
          <input
            class="login_field_text"
            ref="usernameRef"
            v-model="username"
            type="text"
            id="txtUserName"
            name="userName"
            :placeholder="$t('hivuiLogin_main_user_ph')"
            autofocus
            required
            autocomplete="off"
            @keyup.enter="loginFunc"
            :readonly="usernameReadonly"
          />
          <i class="fa fa-user"></i>
          <i 
            v-if="username"
            class="clear-input fa fa-times"
            @click="username='' "
          ></i>
        </div>
        <div class="login_field">
          <label>{{$t('hivuiLogin_main_pw')}}</label>
          <input
            class="login_field_text"
            ref="passwordRef"
            v-model="password"
            :type="showPassword ? 'text' : 'password'"
            id="txtPassword"
            name="password"
            :placeholder="$t('hivuiLogin_main_pw_ph')"
            required
            autocomplete="off"
            @keyup.enter="loginFunc"
          />
          <i class="fa fa-unlock-alt"></i>
          <i 
            v-if="password"
            class="toggle-password fa" 
            :class="showPassword ? 'fa-eye' : 'fa-eye-slash'" 
            @click="togglePassword"
          ></i>
                <i 
            v-if="password"
            class="clear-input fa fa-times"
            @click="password='' "
          ></i>
        </div>
        <div class="login_field captcha_field" v-if="captchaObj&&captchaObj.captchaId">
          <label>{{$t('hivuiLogin_main_captcha')}}</label>
          <div style="display:flex;align-items:center;">
            <input
              class="login_field_text"
              v-model="loginCaptcha"
              type="text"
              id="loginCaptcha"
              name="captcha"
              :placeholder="$t('hivuiLogin_main_captcha_ph')"
              autofocus
              required
              autocomplete="off"
              @keyup.enter="loginFunc"
              style="width:50%;"
            />
            <img :src="captchaObj.imageData" @click="getCaptchaPicObj">
          </div>
        </div>
        <div class="saveCheckbox_box" v-if="!miniLoginInfo">
          <label for="saveCheckbox"
            ><i
              :class="
                'fa ' + (isCheckRu ? 'fa-check-square-o' : 'fa-square-o')
              "
            ></i
            >{{$t('hivuiLogin_main_save_user')}}</label
          >
          <input
            type="checkbox"
            id="saveCheckbox"
            style="display: none"
            v-model="isCheckRu"
            @change="rememberUserEvent"
          />
          <span class="forgetPw" @click="showResetPw"
            ><i class="fa fa-lock"></i
            ><span style="margin-left: 5px">{{$t('hivuiLogin_main_forget_user')}}</span></span
          >
        </div>
        <div class="login_form_btn">
          <div class="login_btn_area">
            <button
              :style="btnStyle"
              :class="'loginBtn' + (loginLoading ? ' disabled' : '')"
              @click="loginFunc"
            >
              {{$t('hivuiLogin_main_login_btn')}}<i
                :hidden="!loginLoading"
                class="el-icon-loading"
              ></i>
            </button>
          </div>
        </div>
        <div class="login_form_bottom" v-if="!miniLoginInfo">
          <div class="register_tips" :hidden="config.hideRegister">
            {{$t('hivuiLogin_main_register1')}}
            <span @click="goRegisterPage">{{$t('hivuiLogin_main_register2')}}</span>
          </div>
          <el-dropdown @command="changeLang" class="langDropdown" :hidden="!config.showChangeLangBtn">
            <span class="el-dropdown-link">
              {{currLangDesc}}<i class="el-icon-arrow-down el-icon--right"></i>
            </span>
            <el-dropdown-menu slot="dropdown">
              <template v-for="(item,index) in langList">
                <el-dropdown-item :key="'langList'+index" :command="item.name+','+item.key">{{item.name}}</el-dropdown-item>
              </template>
            </el-dropdown-menu>
          </el-dropdown>
        </div>
        <el-popover v-if="(hasAppQRCodeConfig || config.appQRcode) && !miniLoginInfo" class="login_form_appqr" placement="bottom" width="210" popper-class="appQRPopper">
          <div ref="appQRcode" class="login_download_app">
            <template v-if="hasAppQRCodeConfig">
              <canvas v-if="!appQRCodeQRcode_img.startsWith('imgUrl:')"></canvas>
              <img v-else :src="appQRCodeQRcode_img.replace('imgUrl:','')">
            </template>
            <template v-else>
              <canvas v-if="!config.appQRcode.startsWith('imgUrl:')"></canvas>
              <img v-else :src="config.appQRcode.replace('imgUrl:','')">
            </template>
            
          </div>
          <span 
            slot="reference" 
            class="login_form_app" 
            :title="appQRCodeTitle" 
            v-if="hasAppQRCodeConfig">
            <img 
              :src="appQRCodeSrc" 
              :alt="config.appQRcodeConfig.title"  
              v-if="appQRCodeSrc" 
              :style="getQRCodeStyle()">
            <i class="miniIcon" data-type="app" v-else></i>
            {{config.appQRcodeConfig.title}}
          </span>
          <span 
            slot="reference" 
            class="login_form_app" 
            :title="$t('hivuiLogin_main_download')" 
            v-else>
            <i class="miniIcon" data-type="app"></i>
            {{$t('hivuiLogin_main_download')}}
          </span>
        </el-popover>
      </div>
    </div>
    <!-- 扫码窗口 -->
    <div
      id="showCom"
      class="loginMainBox"
      :hidden="!(config.isScan && currLoginType == 'scan')"
    >
      <div class="login_form_hd">
        <h3>{{$t('hivuiLogin_main_scan_login')}}</h3>
        <!-- <img @click="changeCom" class="icon-qrcode" :src="comIcon" alt="" :hidden="!config.isPwdLogin" /> -->
        <i class="iconfont icon-diannao1 icon-qrcode" @click="changeCom"  :hidden="!config.isPwdLogin"></i>
        <div class="login-tip" :hidden="!config.isPwdLogin">
          <div class="poptip">
            <div class="poptip-arrow">
              <em></em>
              <span></span>
            </div>
            <div class="poptip-content">{{$t('hivuiLogin_main_pw_text')}}</div>
          </div>
        </div>
      </div>
      <!-- 扫码界面 -->
      <div class="scan-page">
           <!-- 初始状态 -->
          <div class="state default" v-if="scanState ==-1">
              <div class="qr">
                  <img ref="scanQrCode" :src="scanQrCode" :load="scanLoading"/>
              </div>
              <div class="scan-info">
                 <div class="countdown">
                     <span>{{countdown}}</span> s
                  </div>
                  <div class="scan-desc">
                    <div>
                      <!-- <img :src="saomaIcon"/> -->
                       <i class="iconfont icon-scan"></i>
                    </div>
                    <div class="right">
                      <span>
                        {{$t('hivuiLogin_main_app_text1')}}
                        <a class="login-paimary-color">{{config.appName}}</a><br />
                        {{$t('hivuiLogin_main_app_text3')}}
                      </span>
                    </div>
                </div>
              </div>
           
          </div>
          <!-- 扫码中 -->
          <div class="state scanning" v-else-if="scanState == 0">
              <div class="qr">
                  <img ref="scanQrCode" :src="scanQrCode" :load="scanLoading" />
                  <div class="loading-bg"  v-loading="true">
                  </div>
              </div>
              <div class="scan-info">            
                <div class="scan-desc">            
                  <div style="text-align:center;">
                  <span class="login-paimary-color" style="font-size:16px;">正在扫码...</span><br />
                  <span>请在手机上确认登录</span>
                  </div>
                </div>
              </div>
          </div>
          
          <!-- 二维码错误 -->
          <div class="state expired" v-else>
              <div class="qr">
                  <img ref="scanQrCode" :src="scanQrCode" :load="scanLoading" />
                  <div class="loading-bg opacity">
                    <div class="loading-text">
                      <i class="el-icon-warning"></i>
                      <span >{{scanErrorMsg}}</span>
                      <el-button type="text" @click="refreshScanQrCode">点击刷新</el-button>
                    </div>
                  </div>
              </div>
              <div class="scan-info">
                  <div class="scan-desc">
                    <div>
                      <!-- <img :src="saomaIcon"/> -->
                      <i class="iconfont icon-scan"></i>
                    </div>
                    <div class="right">
                      <span>
                        {{$t('hivuiLogin_main_app_text1')}}
                        <a class="login-paimary-color" >{{config.appName}}</a><br />
                        {{$t('hivuiLogin_main_app_text3')}}
                        </span>
                    </div>
                </div>
              </div>
          </div>
      </div>
    </div>
    
    <div id="qrcode"></div>
    <!-- 重置密码 -->
    <el-dialog :title="$t('hivuiLogin_main_forget_title')" :visible.sync="isShowResetPw" width="400px" :close-on-click-modal="false" @close="cancelResetPw" :modal-append-to-body="false">
      <resetPw res="resetPw" @resetCb="cancelResetPw"></resetPw>
    </el-dialog>

    <!--选择岗位 -->
    <el-dialog :title="$t('hivuiLogin_main_bz_title')" :visible.sync="isShowUserBz" width="300px" top="40vh" :close-on-click-modal="false" @close="cancelUserBz" :modal-append-to-body="false">
      <div class="seldUserBzDialog">
        <el-select v-model="seldUserBz" :placeholder="$t('hivuiLogin_main_bz_placeholder')" @change="changeUserBz">
          <el-option
            v-for="item in userBzList"
            :key="item.fbzid"
            :label="item.fbzname+'/'+item.fbzid"
            :value="item.fbzid">
          </el-option>
        </el-select>
      </div>
    </el-dialog>
    <!-- 管理员解锁 -->
    <el-dialog :title="$t('hivuiLogin_main_admin_forget_title')" :visible.sync="isShowAdminResetPw" width="430px" :close-on-click-modal="false" @close="cancelAdminResetPw" :modal-append-to-body="false">
      <div class="unlock_field">
        <label>{{$t('hivuiLogin_main_pw')}}</label>
        <div class="unlock_field_input">
          <input
            ref="aaaa"
            class="unlock_field_text"
            v-model="unlockPassword"
            type="password"
            name="unlockPassword"
            :placeholder="$t('hivuiLogin_main_pw_ph')"
            autocomplete="off"
            @keyup.enter="adminUnlock"
          />
          <i class="fa fa-unlock-alt"></i>
        </div>
      </div>
      <div class="unlock_field" v-if="captchaObj2&&captchaObj2.captchaId">
        <label>{{$t('hivuiLogin_main_captcha')}}</label>
        <div class="unlock_field_input">
          <input
            class="unlock_field_text"
            v-model="unlockCaptcha"
            type="text"
            name="unlockCaptcha"
            :placeholder="$t('hivuiLogin_main_captcha_ph')"
            autocomplete="off"
            @keyup.enter="adminUnlock"
            style="width:50%;"
          />
          <i class="fa fa-unlock-alt"></i>
          <img :src="captchaObj2.imageData" @click="getCaptchaPicObj(2)">
        </div>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button size="small" type="primary" @click="adminUnlock">{{$t('hivuiLogin_main_admin_unlock')}}</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
import { baseLogin,getScanQrCode,adminUnlock } from "../api/login";
import { getBzList,changeBz,getCaptchaPic } from "../api/user";
import md5 from "js-md5";
import Cookies from 'js-cookie';
import QRCode from 'qrcode';
import { utils } from "hi-ui";
import { projectName } from "../config";
import WebScoketManager from '../utils/websocket.js'
import "font-awesome/css/font-awesome.css";
import comIcon from "../assets/computer2.png"
import scanIcon from "../assets/qrcode2.png"
import saomaIcon from "../assets/saoma2.png"
import _loginLogoImg from "../assets/Logo1.png"
import _loginBgImg from "../assets/background.png"
import {
  getToken,
  setToken,
  removeToken,
  getUserId,
  setUserId,
  removeUserId,
} from "../utils/auth.js";
import {setUrlValue,getUrlValue} from "../utils/index.js";

import resetPw from "../components/resetPw.vue";

export default {
  name: "LoginPanel",
  components: {resetPw},
  props: {
    miniLoginInfo:{
      type: Object,
      default: null,
    }
  },
  data() {
    return {
      comIcon: comIcon,
      scanIcon: scanIcon,
      saomaIcon: saomaIcon,
      username: "",
      usernameReadonly: false,
      password: "",
      //登录输入验证码
      loginCaptcha:"",
      config: {
        title: window.customSysCofig?.loginTitle||this.$t('hivuiLogin_main_def_title'),
        loginLogoImg: window.customSysCofig?.loginLogo||_loginLogoImg,
        loginLogoImg_height: window.customSysCofig?.loginLogo_h||56,
        loginLogoImg_width: window.customSysCofig?.loginLogo_w||206,
        loginBgImg: window.customSysCofig?.loginBgImg||_loginBgImg,
        copyright: window.customSysCofig?.copyright||this.$t('hivuiLogin_main_def_copyright'),
        isScan: window.customSysCofig?.isScan||false,
        isPwdLogin: window.customSysCofig?.isPwdLogin === false ? false : true,
        mainColor:window.customSysCofig?.mainColor,
        appQRcode:window.customSysCofig?.appQRcode||"",
        appQRcodeConfig:window.customSysCofig?.appQRcodeConfig||{},
        hideRegister:window.customSysCofig?.hideRegister||false,
        showChangeLangBtn:window.customSysCofig?.showChangeLangBtn||false,
        appName:window.customSysCofig?.appName||this.$t('hivuiLogin_main_app_text2'),
      },
      currLoginType: "com",
      rememberUserId: getUserId(),
      loginLoading: false,
      
      isShowResetPw:false,
      isShowAdminResetPw:false,
      //是否显示选择登录岗位弹窗
      isShowUserBz:false,
      //已选用户岗位
      seldUserBz:"",
      //用户岗位数据列表
      userBzList:[],
      //扫码Socket
      scanWS:null,
      //扫码UUID
      scanUUID:"",
      //登录二维码
      scanQrCode:"",
      //二维码loading
      scanLoading:false,
      //扫码报错信息
      scanErrorMsg:"",
      //当前扫码登录状态 二维码状态(type)：-1-待机 0-扫码中 1-扫码取消 2-登录中 3-已失效 4-已使用 5-登录成功 6-登录失败
      scanState:-1,
      //解锁密码
      unlockPassword:"",
      //解锁输入验证码
      unlockCaptcha:"",
      //验证码对象
      captchaObj:null,
      //解锁验证码对象
      captchaObj2:null,
      // 倒计时相关
      countdown: 60,
      countdownTimer: null,
      isCounting: false,
      showPassword: false,
    };
  },
  computed: {
    btnStyle(){
      return {
        "background-color": this.config.mainColor,
      };
    },
    isCheckRu() {
      return !!this.rememberUserId;
    },
    currLangDesc(){
      return window.localStorage.getItem("locale")?(JSON.parse(window.localStorage.getItem("locale")).desc):"中文";
    },
    langList(){
      let arr=[
        {
            name:"中文",
            key:"zh-CN",
        }, {
            name:"English",
            key:"en",
        }, 
      ]
      if(window.customSysCofig?.addLangList?.length){
        window.customSysCofig?.addLangList.forEach((item,index)=>{
          arr.push({
            name:item.name,
            key:item.key,
          });
        });
      }
      return arr;
    },
    hasAppQRCodeConfig() {
      return this.config.appQRcodeConfig && this.config.appQRcodeConfig.QRcode_img;
    },
    appQRCodeQRcode_img() {
      return this.hasAppQRCodeConfig ? this.config.appQRcodeConfig.QRcode_img : '';
    },
    appQRCodeTitle() {
      return this.hasAppQRCodeConfig ? this.config.appQRcodeConfig.title : '';
    },
    appQRCodeSrc() {
      return this.hasAppQRCodeConfig && this.config.appQRcodeConfig.icon ? 
             this.config.appQRcodeConfig.icon : '';
    }
  },
  
  async created() {
    let me=this;
    if(!me.config.isPwdLogin && me.config.isScan){
      //获取扫码登录二维码
      this.changeQr();
    }
    //获取验证码图片
    await me.getCaptchaPicObj();
  },



  mounted() {
    if(this.miniLoginInfo){
      this.username=this.miniLoginInfo.username||"";
      this.usernameReadonly= !!(this.miniLoginInfo.questType=='ajax'&&this.miniLoginInfo.oldUserName);
    }else{
      if (this.isCheckRu) {
        this.username = this.rememberUserId;
      }
      if(!window.localStorage.getItem("locale")){
          window.localStorage.setItem("locale",JSON.stringify({
              desc:"中文",
              name:"zh-CN",
          }));
          Cookies.set("locale","zh-CN");
      }
      this.$refs.scanQrCode.onload = function() {
        // 清理创建的临时链接
        //URL.revokeObjectURL(imageUrl);
      }
      //生成二维码
      if(this.config.appQRcode || this.hasAppQRCodeConfig){
        let appQRcode= this.appQRCodeQRcode_img || this.config.appQRcode;
        let isIMG=appQRcode.startsWith('imgUrl:');
        if(appQRcode.indexOf('http')==-1){
          appQRcode=(isIMG?'imgUrl:':'')+location.origin+appQRcode.replace('imgUrl:','');
        }
        if(!isIMG){
          let targetUrl=appQRcode;
          if(!window._global){//正式环境
              targetUrl=utils.string.setUrlValue(targetUrl,"pn",window.HIVUI_SETTING.projectName);
          }
          console.log("测试str",targetUrl);
          this.createDLQrCode(this.$refs.appQRcode.querySelector("canvas"),targetUrl);
        }
      }
    }

    // 使用 nextTick 确保 DOM 已经渲染完成
    this.$nextTick(() => {
      if(!this.username && this.$refs.usernameRef){
        // 添加延时，确保组件完全渲染
        setTimeout(() => {
          if (this.$refs.usernameRef) { 
            this.$refs.usernameRef.focus();
          }
        }, 100);
      }else if(this.$refs.passwordRef){
        setTimeout(() => {
          if (this.$refs.passwordRef) {
            this.$refs.passwordRef.focus();
          }
        }, 100);
      }
    });

  },
  beforeDestroy() {
    // 组件销毁前清除定时器
    this.stopCountdown();
  },
  methods: {
    // 获取二维码样式
    getQRCodeStyle() {
      const style = {};
      if (this.config.appQRcodeConfig && this.config.appQRcodeConfig.icon_width) {
        style.width = this.config.appQRcodeConfig.icon_width + 'px';
      }
      if (this.config.appQRcodeConfig && this.config.appQRcodeConfig.icon_height) {
        style.height = this.config.appQRcodeConfig.icon_height + 'px';
      }
      return style;
    },
    
    // 开始倒计时
    startCountdown() {
      // 如果已经在倒计时，则不重复启动
      if (this.isCounting) return;
      
      this.isCounting = true;
      this.countdown = 60; // 重置时间为60秒
      
      this.countdownTimer = setInterval(() => {
        this.countdown--;
        
        // 当倒计时结束
        if (this.countdown <= 0) {
          this.finishCountdown();
        }
      }, 1000);
    },
    
    // 倒计时结束处理
    finishCountdown() {
      this.stopCountdown();
      // 设置为失效状态
      this.setScanState({type:3,msg:"二维码已失效"})
    },
    
    // 停止倒计时
    stopCountdown() {
      if (this.countdownTimer) {
        clearInterval(this.countdownTimer);
        this.countdownTimer = null;
      }
      this.isCounting = false;
    },
    
    // 重置倒计时
    resetCountdown() {
      this.stopCountdown();
      this.countdown = 60;
      this.$nextTick(() => {
        this.startCountdown();
      });
    },

    getLoginData(params = {}){
      const username = this.username ? this.username.trim() : '';
      
      const userInfo = {
        username,
        password: this.password || '',
      };
      
      // 根据类型处理密码加密
      if(params.type === "md5" && this.password){
        // 注意：MD5仅用于演示，实际项目中应使用更安全的哈希算法如bcrypt或后端处理
        userInfo.password = md5(this.password);
      }
      
      return userInfo
    },
    
    togglePassword() {
      this.showPassword = !this.showPassword;
    },
    
    loginFunc() {
      if(this.loginLoading)return;
      let me=this;
      if(me.captchaObj && me.captchaObj.captchaId && !me.loginCaptcha.trim()){
        me.$message.error(me.$t('hivuiLogin_main_valid_captcha'));
        return;
      }else if(!me.username.trim()){
        me.$message.error(me.$t('hivuiLogin_main_valid_user'));
        return;
      }else if(!me.password){
        me.$message.error(me.$t('hivuiLogin_main_valid_pw'));
        return;
      }
      const loginClick = window.customSysCofig?.loginView?.loginClick;
      if(loginClick){
        loginClick.call(
          this,
          {
            getLoginData:me.getLoginData,
            setToken:setToken,
          },
        );
        if (me.isCheckRu) {
          setUserId(me.username);
        } else {
          removeUserId();
        }
      }else{
        me.defaultLogin();
      }
      
    },
    defaultLogin() {
      let me=this;
      removeToken();
      me.loginLoading = true;
      let _params={
        username: me.username.trim(),
        password: md5(me.password),
      };
      if(me.loginCaptcha && me.captchaObj && me.captchaObj.captchaId){
        _params.captchaId=me.captchaObj.captchaId;
        _params.captcha=me.loginCaptcha;
      }
      baseLogin(_params)
        .then((response) => {
          const data = response;
          setToken(data.token);
          if (me.isCheckRu) {
            setUserId(me.username);
          } else {
            removeUserId();
          }
          if(this.miniLoginInfo){
            me.$emit("loginSuccess", data);
          }else{
            if(data.isAuthorize===false){
              if(data.authorizeMsg){
                me.$message.warning(data.authorizeMsg);
              }
              this.$router.push("/authorize");
            }else if(window.customSysCofig?.isSelectUserBz){
              getBzList().then((res)=>{
                me.userBzList=res.dataPack;
                me.isShowUserBz=true;
              });
            }else{
              me.$emit("loginSuccess", data);
            }
          }
      
        })
        .catch((error) => {
          me.loginLoading = false;
          if(me.captchaObj && me.captchaObj.captchaId && error.status==500){
            me.getCaptchaPicObj();
          }
          if(me.username.trim()=="admin" && error&&error.dataPack&&error.dataPack.lock){
            me.isShowAdminResetPw=true;
            //获取验证码图片
            me.getCaptchaPicObj(2);
          }
        });
    },
    //关闭选岗弹窗
    cancelUserBz(){
      this.isShowUserBz=false;
      this.loginLoading=false;
    },
    //选择岗位
    changeUserBz(val){
      changeBz({
        bzid:val
      }).then((res)=>{
        this.$emit("loginSuccess");
      });
    },
    changeQr() {
      this.currLoginType = "scan";
      this.getScanQrCode();
    },
    changeCom() {
      this.currLoginType = "com";
    },
    
    showResetPw() {
      this.isShowResetPw=true;
    },
    cancelResetPw() {
      this.isShowResetPw=false;
    },
    cancelAdminResetPw(){
      this.isShowAdminResetPw=false;
    },
    rememberUserEvent() {
      if (this.isCheckRu) {
        this.rememberUserId = "";
      } else {
        this.rememberUserId = true;
      }
    },
    goRegisterPage(){
      this.$router.push("/register");
    },
    changeLang(command){
      let dataArr=command.split(",");
      window.localStorage.setItem("locale",JSON.stringify({
          desc:dataArr[0],
          name:dataArr[1],
      }));
      Cookies.set("locale",dataArr[1]);
      window.location.reload();
    },
    //生成下载二维码
    createDLQrCode(target,text){
      QRCode.toCanvas(target, text, function (error) { // 将文本转化为二维码并渲染到canvas上
        if (error) {
          console.error(error);
        }
      });
    },
    //解锁
    adminUnlock(){
      let me=this;
      if(!me.unlockPassword){
        me.$message.error(me.$t('hivuiLogin_main_valid_pw'));
        return;
      }
      let _params={
        userId: me.username.trim(),
        password: md5(me.unlockPassword)
      };
      if(me.unlockCaptcha && me.captchaObj2 && me.captchaObj2.captchaId){
        _params.captchaId=me.captchaObj2.captchaId;
        _params.captcha=me.unlockCaptcha;
      }
      adminUnlock(_params).then((response)=>{
        if(response.status==200){
          me.isShowAdminResetPw=false;
          if(!response.msg){
            me.$message.error(me.$t('hivuiLogin_main_unlock_success'));
          }
          me.password=me.unlockPassword;
          if(!me.captchaObj || !me.captchaObj.captchaId){
            me.loginFunc();
          }else{
            me.loginCaptcha="";
          }
        }
      }).catch((err)=>{
        if(me.captchaObj2 && me.captchaObj2.captchaId && err.status==500){
          me.getCaptchaPicObj(2);
        }else{
          me.isShowAdminResetPw=false;
        }
      });
    },
    //获取验证码
    getCaptchaPicObj(num){//num=2:管理员解锁时验证码
      let me=this;
      getCaptchaPic({
        type:"login"
      }).then((res)=>{
        if(res&&res.dataPack){
          if(num==2){
            me.captchaObj2=res.dataPack;
          }else{
            me.captchaObj=res.dataPack;
          }
        }
      });
    },

    scanInit(url,params) { 
      let me=this;
      return getScanQrCode(url,params).then((res)=>{ 
        me.scanLoading=false;
        if(res.headers.uuid){
          me.scanUUID=atob(res.headers.uuid);
          console.log("uuid:",me.scanUUID);
        }
        const blob = new Blob([res.data], { type: 'image/jpeg' });
        const imageUrl = URL.createObjectURL(blob);
        me.scanQrCode=imageUrl;
        me.resetCountdown();
        if(!url){
          me.createScanSocket();
        }
        return Promise.resolve(res);
      }).catch((error)=>{
        me.scanLoading=false;
        this.setScanState({
          type:6,
          msg:"获取二维码失败"
        });
        return Promise.reject(error);
      });
    },
    //获取扫码登录的二维码
    getScanQrCode(){
      let me=this;
      me.scanLoading=true;
      me.scanState=-1;

      if(me.scanWS){
        me.scanWS.end();
      }
      const scanInit = window.customSysCofig?.loginView?.scanInit;
      if(scanInit){
        scanInit({
          setScanQrCode:this.scanInit,
          setToken:setToken,
          setScanState:this.setScanState,
          createScanSocket:this.createScanSocket,
        })
      }else{
        me.scanInit(null,{"type":"qrLogin"})
      }
      
    },

    setScanState(state){ 
      this.scanState = (state?.type !== undefined && state?.type !== null) ? state.type : 9;
      this.scanErrorMsg = state?.msg || "";
      if(this.scanState != -1){
        this.stopCountdown();
      }
    },

    //刷新二维码
    refreshScanQrCode(){
      let me=this;
      me.getScanQrCode();
    },
    // 建立扫码登录socket
    createScanSocket(url, callback) {
        let me = this;
        return new Promise((resolve, reject) => {
            if ("WebSocket" in window) {
                try {
                    // 构造 WebSocket 路径
                    let socketHost = window.HIVUI_SETTING?.serverUrl.replace("http", "ws");
                    let path = socketHost + "/ws/qr/login/pc/" + window.HIVUI_SETTING?.projectName + "/" + me.scanUUID;

                    if (url) {
                        path = url;
                    }

                    // 创建 WebSocket 实例
                    me.scanWS = new WebScoketManager({
                        link: path,
                        //"reConnect":true,
                    });

                    // 初始化 WebSocket 事件
                    me.initWSEvent(url, callback);

                    // 监听连接错误
                    me.scanWS.on("onerror", (error) => {
                        reject(new Error("WebSocket 链接错误: " + error.message));
                    });

                    // 监听连接关闭
                    me.scanWS.on("onclose", () => {
                        reject(new Error("WebSocket 连接已关闭"));
                    });

                    // 监听连接成功
                    me.scanWS.on("onopen", () => {
                        resolve(me.scanWS);
                    });
                } catch (error) {
                    console.error("WebSocket 初始化失败:", error);
                    reject(new Error("WebSocket 初始化失败: " + error.message));
                }
            } else {
                // 浏览器不支持 WebSocket
                alert("您的浏览器不支持 WebSocket!");
                reject(new Error("浏览器不支持 WebSocket"));
            }
        });
    },
    //初始化WS事件
    initWSEvent(url, callback) {
      let me=this;
      let _ws = me.scanWS;
      if (!_ws) {
        console.log("WebSocket 实例未初始化");
        return;
      }
      console.log('开始连接websocket');
      //建立连接
      _ws.on("onopen", () => {
          console.log('数据发送中...');
      });
      //连接失败
      _ws.on("onclose", () => {
          console.log('连接已关闭...');
      });
      //超时关闭
      _ws.on("overtime", () => {
          console.log('服务器连接超时');
      });
      _ws.on("reConnect", () => {
          console.log('重新连接websocket中');
      });
      _ws.on("endReConnect", () => {
          console.log('重新连接次数超出上限，终止连接');
      });
      //接受消息
      _ws.on("onmessage", (data) => {
        if(url && callback){
          callback(data);
          return;
        }else{ 
          if(data.status + "" =="500"){
            me.setScanState({type:6,msg:data.msg})
            return;
          }else if(data.status + "" =="200"){
            if(data.token){//登录成功
              setToken(data.token);
              if(window.customSysCofig?.isSelectUserBz){
                getBzList().then((res)=>{
                  me.userBzList=res.dataPack;
                  me.isShowUserBz=true;
                });
              }else{
                me.$emit("loginSuccess");
              }
              return;
            }
          }
          if(typeof(data.type)!="undefined"){
            this.setScanState(data);
            switch (data.type + "") {//二维码状态(type)：-1-待机 0-扫码中 1-扫码取消 2-登录中 3-已失效 4-已使用 5-登录成功 6-登录失败
                case "0": 
                  break;
                case "1": 
                  me.scanErrorMsg="扫码已取消";
                  break;
                case "2": 
                  break;
                case "3": 
                  me.scanErrorMsg="二维码已失效";
                  me.stopCountdown();
                  break;
                case "4": 
                  me.scanErrorMsg="二维码已使用";
                  me.stopCountdown();
                  break;
                default: 
                  me.scanErrorMsg="扫码登录异常";
                  break;
            }  
          }
        }
      });
    },
  },
};
</script>
<style>

</style>

<style lang="scss" scoped>
.login-paimary-color{
  color: var(--el-color-primary,#06c);
}
.login_form {
  // width: 370px;
  // height: 380px;
  // margin: auto;
  // position: absolute;
  // top: 0;
  // left: 40%;
  // right: 0;
  // bottom: 0;
  // z-index: 100;
  // background-color: #fff;
  // padding: 18px 20px 0;
  // box-shadow: 0 0 5px 1px rgba(0, 0, 0, 0.5);
  .login_form_hd{
    position: relative;
    box-sizing: border-box;
    padding-left:30px;
    width: 100%;
    h3{
      font-size: 24px;
      text-align: left;
      line-height: 44px;
      color: #333;
      font-weight: normal;
    }
  }
  .login_form_bd {
    min-height: 231px;
    height: 100%;
    position: relative;
    padding: 24px 20px 0;
    .login_form_appqr {
      position: absolute;
      bottom: 0;
      left: 15px;
      .login_form_app{
        color: var(--el-color-primary,#06c);
        cursor: pointer;
        display: flex;
        align-items: center;
        gap: 5px;
        img{
          width: 20px;
        }
        &:hover {
          color: #0f406c;
          &i {
            color: #0f406c;
          }
        }
      }
      .miniIcon {
        display: inline-block;
        height: 20px;
        width: 20px;
        background-position: center center;
        background-size: 100%;
        // margin-right:5px;
        &[data-type="app"] {
          background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAbdJREFUWEftV0tOwzAQfUMoa24ARwA1XQMnoJyAVKLZUk7Q9AR0nSJRbhBOQPdtBUcoN2DdD0aTNiKOk9ZJYyGheBXF43nPz57xDCE5nPdjHC27ADUBnCrzxX7MABFgftjD8Pwr7oIUf+3xCEQXxXB2rgrg2zfZBO7Glzigt51u9jH4Fld4aowiF7IC7rQDiEfJv8ArSHwUwhR0BsK1vJYe4Nf7GQTGHkDd3wWycSESyqZED37D0yOQkKsQAeVY/xWBtbwz+HaQqY5RBdyJCIF9Ww3niFFFoFKgVAXakz4I9xDUwqA+RPwStqcOSDwDcpyjVAJuLFMyiRAQCAlF30YJMJg7GQK4zYj7F/i2I82VqkDkOZ2ECs72RgioSqSDGyXAzvlS8hjYnb9JxTrPo7Ej0AE3fgQ6JPZSAAgwt1rJSlYHN7QJK+wV5wqusDcjT0GijZTHsCJQlgJcri+sddqtLQP9ZqYsAvGKOVdDs5VASmOSeb9iPUNaQ6OzDoBcTObaCT/D4JeRvcgv4Lag2NqahfndYHPK92Zgx3JCUoEoedRW3Do1QTjJE+GZtgKf4KS2sLxkUvsBGpxuMP/zizEAAAAASUVORK5CYII=");
        }
        &[data-type="desk"] {
          background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJGRUFBRDNFOTc2NTExRUI5OTdGOTE0QkVGODlDRDIwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJGRUFBRDNGOTc2NTExRUI5OTdGOTE0QkVGODlDRDIwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkZFQUFEM0M5NzY1MTFFQjk5N0Y5MTRCRUY4OUNEMjAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkZFQUFEM0Q5NzY1MTFFQjk5N0Y5MTRCRUY4OUNEMjAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7jOkn1AAABSElEQVR42mL8//8/AymApXzd08fvfv36+58Rh4o///7LCrLZqPCEmQgCuYwMaWeINPv/TGOQDUC8OEkx2kzoHw63MTMxrjv/Yf/Nz1AnATE/JzMjIwMIYwO7rn3iZGWcHCGL0PDt1z9czrj7+qf7xNtAxslKDTMFbiCDCY+jP/3469B7C8J27L318O0vfBpuvfwhUXLpyftfEC7QFQpVl888/AZyEjsLFtdffvodGJRff/07dvcLkCvGy6IrzXnkDpCddmbN2Xf/cYD9Nz+Bwj3tTNOWZxARkA0hM+8JcDH//osSrEBuiau4uSI3hOupww8NpTJ3iXuvf37//Y8J1V3AuNeQ4AA6DMjWk+E0keeCxR9ewJJxFuiei4+/wUVwhtKbL39su28yMTGsz1QG2oCUQnCAE/e+1G58+vrzbzRxRlKTN0CAAQB1Gu32zyWjpAAAAABJRU5ErkJggg==");
        }
      }
    }
    .error_tips {
      position: absolute;
      line-height: 30px;
      font-size: 12px;
      color: var(--el-color-danger,#f60000);
      text-align: center;
      top: 0;
      left: 0;
      right: 0;
      width: 100%;
      margin: auto;
    }
    .login_field {
      position: relative;
      padding-bottom: 20px;
      label {
        line-height: 35px;
        display: block;
        height: 35px;
        font-size: 16px;
      }
      i {
        font-size: 20px;
        color: #aaa;
        width: 35px;
        text-align: center;
        line-height: 40px;
        height: 40px;
        position: absolute;
        top: 35px;
        left: 0;
        margin: auto;
        &.toggle-password{
          left: initial;
          right: 30px;
          font-size: 15px;
          cursor: pointer;
          &:hover{
            opacity: 0.8;
          }
        }
        &.clear-input{
          left: initial;
          font-size: 14px;
          right: 0px;
          cursor: pointer;
          &:hover{
            opacity: 0.8;
          }
        }
      }
      .login_field_text {
        height: 40px;
        width: 100%;
        line-height: 40px;
        border: 1px solid #ccc;
        border-radius: 5px;
        background-color: #fff;
        font-size: 16px;
        color: #000;
        padding-left: 35px;
        padding-right: 62px;
        box-sizing: border-box;
        font-family: "Microsoft YaHei", proxima-nova, "Helvetica Neue", arial,
          helvetica, clean, sans-serif;
        outline: none;
        &[readonly]{
          background-color: #eee;
          color: #666;
        }
        &::input-placeholder {
          color: #ccc;
        }
        &:focus {
          border-color: var(--el-color-primary,#06c);
        }
      }
      img{
        width:calc(50% - 10px);
        margin-left:10px;
        cursor: pointer;
      }
    }
    .saveCheckbox_box {
      height: 35px;
      line-height: 35px;
      padding-left: 30px;
      position: relative;
      top: 0px;
      label {
        cursor: pointer;
        user-select: none;
        i {
          font-size: 24px;
          position: absolute;
          top: 0;
          left: 2px;
          bottom: 0;
          margin: auto;
          height: 25px;
          width: 25px;
          color: #888;
          cursor: pointer;
          text-align: center;
        }
      }
      .occasion {
        float: right;
        &:after {
          content: "";
          background: no-repeat;
          width: 14px;
          height: 18px;
          position: absolute;
          top: 30%;
          pointer-events: none;
          right: 1px;
        }
        #selectStyle {
          border: none;
          outline: none;
          width: 62px;
          appearance: none;
          -webkit-appearance: none;
          -moz-appearance: none;
        }
      }
    }
    .forgetPw {
      float: right;
      color: var(--el-color-primary,#06c);
      cursor: pointer;
      margin-left: 5px;
      &:hover {
        color: #0f406c;
        i {
          color: #0f406c;
        }
      }
    }
    .login_form_btn {
      font-size: 13px;
      margin-top: 20px;
      .loginBtn {
        background-color: var(--el-color-primary,#06c);;
        width: 100%;
        height: 40px;
        border: none;
        border-radius: 5px;
        color: #fff;
        font-size: 16px;
        font-weight: bold;
        text-align: center;
        cursor: pointer;
        display: inline-block;
        i {
          margin-left: 5px;
        }
        &:hover {
          filter: brightness(0.8);
        }
        &.disabled {
          background-color: #ccc!important;
          &:hover {
            filter: brightness(1);
          }
        }
      }
    }
    /*二维码*/
    .qrCodeImg {
      /* position: absolute; */
      height: 139px;
      width: 139px;
      /* left:40% !important;
              right:0 !important;
              bottom:12%; */
      margin-top: 10px;
      margin-left: 85px;
      z-index: 999;
    }
    .login_form_bottom{
      padding-top: 15px;
      min-height:36px;
      .register_tips{
        font-size: 14px;
        text-align: center;
        span{
          color:var(--el-color-primary,#06c);
          cursor: pointer;
        }
      }
      .langDropdown{
        position: absolute;
        bottom:0;
        right:0;
        padding:0 20px;
      }
    }
    
  }
  .login_form_google {
    width: 280px;
    position: absolute;
    top: 317px;
    left: 0;
    bottom: 0;
    color: #fff;
    line-height: 20px;
    text-align: center;
    font-weight: bold;
    font-size: 13px;
    cursor: pointer;
    user-select: none;
    & > a {
      color: #fff;
    }
  }
  .loginMainBox {
    position: relative;
    .login-title {
      height: 18px;
      line-height: 18px;
      font-size: 16px;
      color: #889aa4;
      margin-top: 12px;
      margin-left: 12px;
      padding-bottom: 8px;
      font-weight: 700;
    }
    .login-tip{
        position: absolute;
        top: 8px;
        right: 52px;
        display: block;
        .poptip{
            background-color: var(--el-color-primary-light-9,#ecf5ff);
            line-height: 16px;
            position: relative;
            z-index: 99;
            border: 1px solid var(--el-color-primary-light-6,#b3d8ff);
            padding: 5px 10px;
            border-radius: 4px;
            .poptip-arrow {
                position: absolute;
                z-index: 10;
                top: 8px;
                right: 0;
                span{
                    position: absolute;
                    width: 0;
                    height: 0;
                    border-color: hsla(0,0%,100%,0);
                    border-style: solid;
                    overflow: hidden;
                    top: 0;
                    left: -1px;
                    border-width: 6px 0 6px 6px;
                    border-left-color: var(--el-color-primary-light-9,#ecf5ff);
                }
                em {
                    position: absolute;
                    width: 0;
                    height: 0;
                    border-color: hsla(0,0%,100%,0);
                    border-style: solid;
                    overflow: hidden;
                    top: 0;
                    left: 0;
                    border-width: 6px 0 6px 6px;
                    border-left-color: var(--el-color-primary-light-6,#b3d8ff);
                    border-right-color: var(--el-color-primary-light-6,#b3d8ff);
                }
            }
            .poptip-content {
                font-size: 12px;
                font-weight: 400;
                color: var(--el-color-primary,#06c);;
            }
        }
    }
    .qrDesc {
      font-size: 12px;
      color: #666;
      width: 160px;
      margin: auto;
      overflow: hidden;
      line-height: 17px;
      margin-top: 230px;
      .qrDesc_right {
        position: relative;
        left: 50px;
        top: -35px;
      }
    }
    .scanMain {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      .qrcode-img {
        position: relative;
        margin: 20px auto;
        font-size: 14px;
        -webkit-box-shadow: 0 0 8px #ddd;
        box-shadow: 0 0 8px #ddd;
        opacity: 1;
        width: 140px;
        height: 140px;
        .qrcode-error {
          background: hsla(0, 0%, 100%, 0.95);
          position: absolute;
          left: 0;
          top: 0;
          z-index: 99;
          width: 100%;
          height: 100%;
          p {
            font-weight: 700;
            color: #222;
            margin-top: 38px;
            margin-bottom: 8px;
            text-align: center;
          }
          .refresh {
            background: #f40;
            width: 110px;
            height: 34px;
            line-height: 34px;
            text-align: center;
            margin: 0 auto;
            background: #ff9000;
            border-color: #ff9000;
            display: block;
            color: #fff;
            border-radius: 3px;
            font-size: 14px;
            cursor: pointer;
          }
        }
        canvas {
          margin: 5px;
        }
      }
      .login-error {
        text-align: center;
        color: #222;
        font-weight: 700;
      }
    }
    .scanMask{
      position: absolute;
      top: 0;
      left: 0;
      height: 100%;
      width: 100%;
      background-color: rgba(255,255,255,0.7);
      display: flex;
      justify-content: center;
      align-items: center;
      .scanMaskMsg{
        padding: 0 10px;
        box-shadow: #000 0 0 10px 1px;
        background-color: #fff;
        line-height: 32px;
        border-radius: 5px;
        color: var(--el-color-primary,#06c);;
        font-weight: bold;
      }
    }
    /*二维码切换登录*/
    .icon-qrcode {
      position: absolute;
      right: 0px;
      top: 0px;
      cursor: pointer;
      width: 44px;
      font-size: 40px;
      line-height: 1;
      color: var(--el-color-primary,#06c);
      z-index: 99;
    }

    .login-content {
      width: 100%;
      margin: 0 auto;
      padding-top: 55px;
      .qrcode-success {
        text-align: center;
        margin-top: 20px;
        .iconfont {
          color: #c5c5c5;
          font-size: 36px;
        }
        h4,
        p {
          margin-top: 10px;
          font-size: 14px;
        }
      }
    }

    // 扫码界面
    .scan-page{
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      .state{
        .qr{
          position: relative;
          img{
            width: 200px;
            height: 200px;
          }
          .loading-bg{
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            &.opacity{
              background-color: rgba(255,255,255,0.9);
            }
            .loading-text{
              width: 100%;
              height: 100%;
              display: flex;
              flex-direction: column;
              align-items: center;
              justify-content: center;
              i{
                font-size: 34px;
                color: red;
              }
            }
          }
        }
        .scan-info{
          display: flex;
          flex-direction: column;
          gap: 5px;
          position: relative;
          top: -10px;
          z-index: 2001;
          .countdown{
            text-align: center;
          }
          .scan-desc{
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 15px;
            font-size: 12px;
            color: #666;
            margin: auto;
            i{
              font-size: 38px;
              color: var(--el-color-primary,#06c);
            }
          }
        }
      }
     
    }
  }
  &.showCaptcha{
    height:440px;
    .login_form_bd {
      .login_field{
        padding-bottom:10px;
      }
      .captcha_field{
        input{
          padding-left:10px;
        }
      }
    }
  }
}

.appQRPopper{
  text-align: center;
  .login_download_app{
    display:flex;
    justify-content:center;
    canvas{
      width:180px!important;
      height:180px!important;
    }
    img{
      max-width:180px;
    }
  }
}
.seldUserBzDialog{
  display: flex;
  justify-content: center;
  padding-bottom:10px;
}
.unlock_field {
  display:flex;
  padding-top:10px;
  label {
    line-height: 35px;
    display: block;
    height: 35px;
    width:80px;
    font-size: 14px;
    padding-left:20px;
  }
  .unlock_field_input{
    position: relative;
    margin-left:20px;
    flex: 0 0 calc(100% - 150px);
    display:flex;
    align-items: center;
    i {
      font-size: 20px;
      color: #aaa;
      width: 35px;
      text-align: center;
      line-height: 35px;
      height: 35px;
      position: absolute;
      top: 0;
      left: 0;
      margin: auto;
    }
    .unlock_field_text {
      height: 35px;
      width: 100%;
      line-height: 35px;
      border: 1px solid #ccc;
      border-radius: 5px;
      background-color: #fff;
      font-size: 14px;
      color: #000;
      padding-left: 35px;
      box-sizing: border-box;
      font-family: "Microsoft YaHei", proxima-nova, "Helvetica Neue", arial,
        helvetica, clean, sans-serif;
      &::input-placeholder {
        color: #ccc;
      }
    }
    img{
      width:calc(50% - 10px);
      margin-left:10px;
      cursor: pointer;
    }
  }
}

@supports (display:none) {
    dot {
        display: inline-block; 
        width: 3ch;
        text-indent: -1ch;
        vertical-align: bottom; 
        overflow: hidden;
        animation: dot 2s infinite step-start both;
        font-family: Consolas, Monaco, monospace;
    }
}

@keyframes dot {
    33% { text-indent: 0; }
    66% { text-indent: -2ch; }
}

</style>