chore(config): 添加项目配置文件和隐私协议

- 添加 .editorconfig 文件统一代码风格
- 添加 .env.development 和 .env.example 环境配置文件
- 添加 .eslintignore 和 .eslintrc.js 代码检查配置
- 添加 .gitignore 版本控制忽略文件配置
- 添加 .prettierignore 格式化忽略配置
- 添加隐私协议HTML文件
- 添加API密钥管理组件基础结构
This commit is contained in:
2026-01-26 14:05:01 +08:00
commit 482e2a2718
1192 changed files with 238401 additions and 0 deletions

View File

@@ -0,0 +1,325 @@
<template>
<div class="qr-login-container">
<div class="qr-code-wrapper">
<!-- 二维码显示区域 -->
<div v-if="qrCodeStatus === 'loading'" class="qr-loading">
<a-spin size="large" />
<p class="loading-text">正在生成二维码...</p>
</div>
<div
v-else-if="qrCodeStatus === 'active'"
class="qr-active cursor-pointer"
>
<ele-qr-code-svg
:value="qrCodeData"
:size="200"
@click="refreshQrCode"
/>
<p class="qr-tip">请使用手机APP或小程序扫码登录</p>
<p class="qr-expire-tip">二维码有效期{{ formatTime(expireTime) }}</p>
</div>
<div v-else-if="qrCodeStatus === 'scanned'" class="qr-scanned">
<div class="scanned-icon">
<CheckCircleOutlined style="font-size: 48px; color: #52c41a" />
</div>
<p class="scanned-text">扫码成功请在手机上确认登录</p>
</div>
<div v-else-if="qrCodeStatus === 'expired'" class="qr-expired">
<div class="expired-icon">
<ClockCircleOutlined style="font-size: 48px; color: #ff4d4f" />
</div>
<p class="expired-text">二维码已过期</p>
<a-button type="primary" @click="refreshQrCode">刷新二维码</a-button>
</div>
<div v-else-if="qrCodeStatus === 'error'" class="qr-error">
<div class="error-icon">
<ExclamationCircleOutlined style="font-size: 48px; color: #ff4d4f" />
</div>
<p class="error-text">{{ errorMessage || '生成二维码失败' }}</p>
<a-button type="primary" @click="refreshQrCode">重新生成</a-button>
</div>
</div>
<!-- 刷新按钮 -->
<div class="qr-actions" v-if="qrCodeStatus === 'active'">
<a-button type="link" @click="refreshQrCode" :loading="refreshing">
<ReloadOutlined /> 刷新二维码
</a-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { message } from 'ant-design-vue';
import {
CheckCircleOutlined,
ClockCircleOutlined,
ExclamationCircleOutlined,
ReloadOutlined
} from '@ant-design/icons-vue';
import {
generateQrCode,
checkQrCodeStatus,
type QrCodeResponse,
QrCodeStatusResponse
} from '@/api/passport/qrLogin';
// 定义组件事件
const emit = defineEmits<{
(e: 'loginSuccess', data: QrCodeStatusResponse): void;
(e: 'loginError', error: string): void;
}>();
// 响应式数据
const qrCodeData = ref<string>('');
const qrCodeToken = ref<string>('');
const qrCodeStatus = ref<
'loading' | 'active' | 'scanned' | 'expired' | 'error'
>('loading');
const expireTime = ref<number>(0);
const errorMessage = ref<string>('');
const refreshing = ref<boolean>(false);
// 定时器
let statusCheckTimer: number | null = null;
let expireTimer: number | null = null;
// 计算属性:格式化剩余时间
const formatTime = computed(() => {
return (seconds: number) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
};
});
/**
* 生成二维码
*/
const generateQrCodeData = async () => {
try {
qrCodeStatus.value = 'loading';
const response: QrCodeResponse = await generateQrCode();
// 后端返回的qrCode是二维码内容我们需要构造完整的URL
const baseUrl = window.location.origin;
const qrCodeUrl = `${baseUrl}/qr-confirm?qrCodeKey=${response.token}`;
qrCodeData.value = qrCodeUrl;
qrCodeToken.value = response.token;
qrCodeStatus.value = 'active';
expireTime.value = response.expiresIn || 300; // 默认5分钟过期
// 开始检查二维码状态
startStatusCheck();
// 开始倒计时
startExpireCountdown();
} catch (error: any) {
qrCodeStatus.value = 'error';
errorMessage.value = error.message || '生成二维码失败';
message.error(errorMessage.value);
}
};
/**
* 开始检查二维码状态
*/
const startStatusCheck = () => {
if (statusCheckTimer) {
clearInterval(statusCheckTimer);
}
statusCheckTimer = window.setInterval(async () => {
try {
const status = await checkQrCodeStatus(qrCodeToken.value);
switch (status.status) {
case 'scanned':
qrCodeStatus.value = 'scanned';
break;
case 'confirmed':
// 登录成功
if (status.tenantId) {
localStorage.setItem('TenantId', `${status.tenantId}`);
}
qrCodeStatus.value = 'active';
stopAllTimers();
emit('loginSuccess', status);
break;
case 'expired':
qrCodeStatus.value = 'expired';
stopAllTimers();
break;
case 'pending':
// 继续等待
break;
}
// 更新剩余时间
if (status.expiresIn !== undefined) {
expireTime.value = status.expiresIn;
}
} catch (error: any) {
console.error('检查二维码状态失败:', error);
// 继续检查,不中断流程
}
}, 2000); // 每2秒检查一次
};
/**
* 开始过期倒计时
*/
const startExpireCountdown = () => {
if (expireTimer) {
clearInterval(expireTimer);
}
expireTimer = window.setInterval(() => {
if (expireTime.value <= 0) {
qrCodeStatus.value = 'expired';
stopAllTimers();
return;
}
expireTime.value--;
}, 1000);
};
/**
* 停止所有定时器
*/
const stopAllTimers = () => {
if (statusCheckTimer) {
clearInterval(statusCheckTimer);
statusCheckTimer = null;
}
if (expireTimer) {
clearInterval(expireTimer);
expireTimer = null;
}
};
/**
* 刷新二维码
*/
const refreshQrCode = async () => {
refreshing.value = true;
stopAllTimers();
try {
await generateQrCodeData();
} finally {
refreshing.value = false;
}
};
// 组件挂载时生成二维码
onMounted(() => {
generateQrCodeData();
});
// 组件卸载时清理定时器
onUnmounted(() => {
stopAllTimers();
});
</script>
<style lang="less" scoped>
.qr-login-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
min-height: 300px;
}
.qr-code-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 250px;
width: 100%;
}
.qr-loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
.loading-text {
color: #666;
margin: 0;
}
}
.qr-active {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
.qr-tip {
color: #333;
font-size: 14px;
margin: 0;
text-align: center;
}
.qr-expire-tip {
color: #999;
font-size: 12px;
margin: 0;
}
}
.qr-scanned {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
.scanned-text {
color: #52c41a;
font-size: 14px;
margin: 0;
text-align: center;
}
}
.qr-expired {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
.expired-text {
color: #ff4d4f;
font-size: 14px;
margin: 0;
}
}
.qr-error {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
.error-text {
color: #ff4d4f;
font-size: 14px;
margin: 0;
text-align: center;
}
}
.qr-actions {
margin-top: 16px;
}
</style>