SecurityPanel.vue 7.95 KB
<template>
  <div class="security-panel">
    <div class="settings-content-header">
      <h1>{{ $t("settings.security.title") }}</h1>
      <p>{{ $t("settings.security.desc") }}</p>
    </div>
    <!-- 密码 -->
    <div class="section section-password">
      <div class="section-title">
        {{ $t("settings.security.passwordTitle") }}
      </div>
      <div class="section-desc">{{ $t("settings.security.passwordDesc") }}</div>
      <div class="form-group">
        <label class="form-label">{{
          $t("settings.security.newPassword")
        }}</label>
        <input
          class="input"
          type="password"
          v-model="newPassword"
          :placeholder="$t('settings.security.newPasswordPlaceholder')"
        />
      </div>
      <div class="password-tip">{{ $t("settings.security.passwordTip") }}</div>
      <el-button class="save-btn" @click="updatePassword">
        {{ $t("settings.security.updatePassword") }}
      </el-button>
    </div>
    <!-- 网络状态 -->
    <div class="section section-network">
      <div class="section-title">
        {{ $t("settings.security.networkTitle") }}
      </div>
      <div class="section-desc">{{ $t("settings.security.networkDesc") }}</div>
      <div class="network-status-container">
        <div class="network-status-item">
          <span class="status-label"
            >{{ $t("settings.security.networkStatus") }}:</span
          >
          <div class="status-value">
            <span
              :class="[
                'status-indicator',
                networkStore.isOnline ? 'online' : 'offline',
              ]"
            ></span>
            <span
              :class="[
                'status-text',
                networkStore.isOnline ? 'online' : 'offline',
              ]"
            >
              {{
                networkStore.isOnline
                  ? $t("settings.security.online")
                  : $t("settings.security.offline")
              }}
            </span>
          </div>
        </div>
        <div class="network-status-item" v-if="networkStore.lastStatusChange">
          <span class="status-label"
            >{{ $t("settings.security.lastUpdate") }}:</span
          >
          <span class="status-value">{{
            formatTime(networkStore.lastStatusChange)
          }}</span>
        </div>
      </div>
    </div>
    <!-- 退出登录 -->
    <div class="section section-logout">
      <div class="section-title">{{ $t("settings.security.logoutTitle") }}</div>
      <div class="section-desc">{{ $t("settings.security.logoutDesc") }}</div>
      <el-button class="logout-btn" @click="logout">
        {{ $t("settings.security.logout") }}
      </el-button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { useAuthStore } from "@/stores/auth";
import { useNetworkStore } from "@/stores/network";

const { t: _t } = useI18n();
const router = useRouter();
const authStore = useAuthStore();
const networkStore = useNetworkStore();

const newPassword = ref("");

const updatePassword = async () => {
  if (!newPassword.value || newPassword.value.length < 6) {
    ElMessage.error("密码长度不能少于6位");
    return;
  }

  try {
    // TODO: 实际API调用
    // await updatePasswordAPI(newPassword.value);

    ElMessage.success("密码更新成功");
    newPassword.value = "";
  } catch (error) {
    ElMessage.error("更新密码失败");
  }
};

const logout = async () => {
  try {
    // 先导航到登录页,避免路由守卫冲突
    await router.replace("/auth");

    // 然后清除认证状态
    await authStore.logout();
  } catch (error) {
    console.error("登出失败:", error);
    // 即使出错也要确保跳转到登录页
    router.replace("/auth");
  }
};

// 格式化时间显示
const formatTime = (date: Date | null) => {
  if (!date) return "";
  const now = new Date();
  const diff = now.getTime() - date.getTime();
  const seconds = Math.floor(diff / 1000);
  const minutes = Math.floor(seconds / 60);
  const hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);

  if (seconds < 60) {
    return `${seconds} ${_t("settings.security.secondsAgo")}`;
  } else if (minutes < 60) {
    return `${minutes} ${_t("settings.security.minutesAgo")}`;
  } else if (hours < 24) {
    return `${hours} ${_t("settings.security.hoursAgo")}`;
  } else if (days < 7) {
    return `${days} ${_t("settings.security.daysAgo")}`;
  } else {
    return date.toLocaleString("zh-CN", {
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
      hour: "2-digit",
      minute: "2-digit",
    });
  }
};
</script>

<style scoped lang="scss">
.security-panel {
  color: var(--color-text);
  background: var(--color-bg);
  max-width: 520px;
  margin-left: 0;
  margin-right: auto;
  padding: 0 0 40px 0;
}

.settings-content-header {
  margin-top: 24px;
  margin-bottom: 48px;
  text-align: left;

  h1 {
    font-size: 32px;
    font-weight: 800;
    color: var(--color-text);
    margin-bottom: 10px;
    letter-spacing: 1px;
  }

  p {
    font-size: 16px;
    color: var(--color-secondary);
    font-weight: 400;
  }
}

.section {
  width: 100%;
  margin-bottom: 56px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.section-title {
  font-size: 20px;
  font-weight: 700;
  color: var(--color-text);
  margin-bottom: 10px;
  text-align: left;
  letter-spacing: 1px;
}

.section-desc {
  font-size: 15px;
  color: var(--color-secondary);
  margin-bottom: 18px;
}

.form-group {
  margin-bottom: 18px;
  width: 100%;
}

.form-label {
  font-size: 15px;
  color: var(--color-secondary);
  margin-bottom: 8px;
  display: block;
}

.input {
  background: var(--color-card);
  color: var(--color-text);
  border: 1.5px solid var(--color-border);
  border-radius: 8px;
  padding: 12px 16px;
  font-size: 15px;
  outline: none;
  width: 100%;
  box-sizing: border-box;
  margin-bottom: 0;
  margin-top: 0;
  transition: border-color 0.2s;

  &:focus {
    border-color: var(--color-primary);
  }
}

.password-tip {
  font-size: 13px;
  color: var(--color-secondary);
  margin-bottom: 18px;
}

.save-btn {
  background: var(--color-primary);
  color: var(--color-text);
  border: none;
  padding: 10px 24px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 600;
  transition: all 0.2s;

  &:hover {
    background: var(--color-btn-hover);
  }
}

.logout-btn {
  background: #f44336;
  color: #fff;
  border: none;
  padding: 10px 24px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 600;
  transition: all 0.2s;

  &:hover {
    background: #d32f2f;
  }
}

.network-status-container {
  width: 100%;
  background: var(--color-card);
  border: 1.5px solid var(--color-border);
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 18px;
}

.network-status-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
  font-size: 14px;

  &:last-child {
    margin-bottom: 0;
  }
}

.status-label {
  color: var(--color-secondary);
  font-weight: 500;
}

.status-value {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--color-text);
  font-weight: 600;
}

.status-indicator {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  display: inline-block;
  animation: pulse 2s ease-in-out infinite;

  &.online {
    background: #4caf50;
    box-shadow: 0 0 8px rgba(76, 175, 80, 0.5);
  }

  &.offline {
    background: #f44336;
    box-shadow: 0 0 8px rgba(244, 67, 54, 0.5);
  }
}

.status-text {
  &.online {
    color: #4caf50;
  }

  &.offline {
    color: #f44336;
  }
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.6;
  }
}

.divider {
  width: calc(100% + 48px);
  height: 1px;
  background: var(--color-border);
  margin: 48px 0 56px -48px;
  opacity: 0.7;
}

@media (max-width: 1024px) {
  .security-panel {
    max-width: 100%;
    padding: 0 4px 20px 4px;
  }
}
</style>