Commit 7b04b7f8 authored by youjie's avatar youjie

授权

parent d4462fc9
......@@ -4,6 +4,9 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin-allow-popups">
<title>C-end</title>
</head>
<body>
......
......@@ -90,6 +90,7 @@ export default {
phoneCode: '区号',
googleLoginFailed: '谷歌登录失败',
isReceivePush: '「我同意接收优惠与电子报」',
loginTypeNotSupport: '暂未实现该登录方式',
},
common: {
language: '语言',
......
......@@ -23,6 +23,12 @@ const router = createRouter({
component: () => import("../views/auth/register.vue"),
meta: { title: "login.register" },
},
{
path: "/login2",
name: "login2",
component: () => import("../views/auth/register2.vue"),
meta: { title: "login.register" },
},
{
path: "/forgePassword",
name: "forgePassword",
......
import OtaRequest from '@/api/OtaRequest'
import OtaRequest,{ type HttpResponse} from '@/api/OtaRequest'
/**
* 用户服务 - 处理所有用户相关的 API 请求
......@@ -395,7 +395,7 @@ export interface ResetPasswordResponseDto {
*/
class UserService {
/**
* 代理商自助注册
* 注册
* @param data 注册信息
* @returns 注册响应
*/
......@@ -446,6 +446,27 @@ class UserService {
)
return response as unknown as DistributorLoginResultDto
}
/**
* 谷歌登录
* @param credential 谷歌token
* @returns
*/
static async GoogleLoginAsync(credential: string): Promise<HttpResponse> {
const data = {
credential
}
// OtaRequest 的响应拦截器会返回 response.data
const response = await OtaRequest.post(
'/member-auth/google-auth-bind',
data,
{
headers: {
}
}
)
return response as unknown as HttpResponse
}
/**
* 刷新访问令牌
......
......@@ -111,8 +111,8 @@ export const useUserStore = defineStore('user', {
*/
async setUserGoogleLoginAsync(credential: string): Promise<UserLoginResult> {
try {
const response = await ErpUserService.GoogleLoginAsync(credential)
const response = await UserService.GoogleLoginAsync(credential)
console.log('Google login response:', response)
if (response.data.resultCode === ApiResult.SUCCESS) {
this.token = response.data.data.token || ''
this.userInfo = response.data.data || {}
......@@ -124,7 +124,7 @@ export const useUserStore = defineStore('user', {
data: response.data.data
}
} else {
ResultMessage.Error(response.data.message || i18n.global.t('login.googleLoginFailed'))
ResultMessage.Error(response.data.message || '谷歌登录失败')
return {
status: 'ERROR',
verify: false
......@@ -132,7 +132,7 @@ export const useUserStore = defineStore('user', {
}
} catch (error: any) {
console.error('Google login error:', error)
ResultMessage.Error(error.message || i18n.global.t('login.googleLoginFailed'))
ResultMessage.Error(error.message || '谷歌登录失败')
return {
status: 'ERROR',
verify: false
......
......@@ -3,30 +3,20 @@
<div ref="loginPage"
class="light-login-bg pl-[85px] pr-[98px] pt-[33px] h-full !overflow-y-auto light-login-bg">
<loginHeader />
<!-- <div class="w-full relative h-[125px] mt-[32px] mb-[44px]">
<div class="absolute left-0 right-0 top-0 flex justify-center items-center">
<img src="../../assets/images/login-logo.png" alt="" class="h-[68px]"/>
</div>
<div class="absolute left-0 right-0 bottom-0 flex justify-center items-center">
<div class="text-[27px] primary6 SourceHanSansCN">{{ t('login.subtitle') }}</div>
</div>
</div> -->
<div class="w-full flex flex-col loginForm pt-[97px]">
<div class="flex justify-center">
<div class="w-[463px] h-[620px] ">
<!-- <div class="loginForm-bg w-full h-[620px] rounded-[18px] absolute top-0 left-0 bottom-0 z-[2]"></div> -->
<!-- absolute top-0 left-0 bottom-0 -->
<div class="w-[463px] h-[620px]">
<div class="loginForm-bg w-full h-full rounded-[18px] flex flex-col">
<div class="text-center pt-[46px] primary-6">
<div class="text-[32px] font-bold">{{ t('login.loginTo') }}</div>
<!-- <div class="text-[17px] mt-[18px]">{{ t('login.loginToSubImm') }}</div> -->
<div class="flex justify-center items-center mt-[18px]">
<img src="@/assets/images/welcome-login.png" alt="" class="h-[26px]">
</div>
</div>
<a-space direction="vertical" class="px-[72px]">
<a-form :model="loginMsg" :rules="rules" @submit="loginHandler" layout="vertical"
class="mt-[42px]">
class="mt-[42px]"
:disabled="loginMsg.reType!==0">
<a-form-item field="email" :label="t('login.account')">
<a-input class="loginMsg-input"
v-model="loginMsg.email"
......@@ -53,7 +43,7 @@
</a-input-password>
</a-form-item>
<div class="mt-[34px] flex flex-row items-center items-center-button"
<div v-if="loginMsg.reType==0" class="mt-[34px] flex flex-row items-center items-center-button"
:class="[loginMsg.password&&loginMsg.password.length>=8&&loginMsg.email?'isClick':'']">
<a-button
type="primary"
......@@ -68,35 +58,18 @@
</a-form>
</a-space>
<!-- 谷歌登录 -->
<!-- <div v-else-if="loginMsg.reType === 1" class="login-form-content google-content">
<div class="google-auth-container">
<div
id="g_id_onload"
data-client_id="13534363185-3utcasahjr950mf6uumq8upefl0fu2rl.apps.googleusercontent.com"
data-context="signin"
data-ux_mode="popup"
data-callback="googleCallback"
data-auto_select="false"
data-itp_support="true"
></div>
<div
class="g_id_signin"
data-type="standard"
data-shape="rectangular"
data-theme="outline"
data-text="signin_with"
data-size="large"
data-locale="en-US"
data-logo_alignment="center"
data-width="360"
></div>
</div>
</div> -->
<div v-show="loginMsg.reType==1" class="px-[72px] mt-[36px]">
<div
ref="googleButtonContainer"
class="g-signin2 !rounded-[13px] overflow-hidden"
data-onsuccess="onSignIn"
data-theme="dark"
></div>
</div>
<!-- Line登录 -->
<!-- <div v-else-if="loginMsg.reType === 3" class="login-form-content scan-content">
<div v-if="loginMsg.reType==3" class="login-form-content scan-content">
<div class="qr-container">
<div class="qr-box line-qr-box">
<div class="qr-code-placeholder">
......@@ -106,14 +79,14 @@
<p class="scan-instruction">{{ t('login.scanTip') }}</p>
<p class="scan-status">{{ t('login.scanWaiting') }}</p>
</div>
</div> -->
</div>
<div class="mt-[40px] flex items-center justify-center">
<a-divider orientation="center" class="text-[16px] text-[#EEEFEB]"></a-divider>
<span class="text-nowrap primary1-3 px-[14px]">{{ t('login.othenLogin') }}</span>
<a-divider orientation="center" class="text-[16px] text-[#EEEFEB]"></a-divider>
</div>
<div class="flex items-center justify-between px-[100px] mt-[20px]">
<div class="flex items-center justify-between px-[100px] mt-[20px] mb-[40px]">
<!-- loginForm-itemActive loginForm-item-->
<div class="w-[42px] h-[42px]
rounded-full bg-[#FFF]
......@@ -136,13 +109,14 @@
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from "vue";
import { ref, reactive, computed, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/index'
import { useSystemConfigStore } from '@/stores/index'
import ErpUserService from '@/services/ErpUserService'
import { ApiResult } from '@/types/ApiResult'
import { Message } from '@arco-design/web-vue'
import loginHeader from "./components/header.vue";
import f from '@/assets/images/login_f.png'
import G from '@/assets/images/login_G.png'
......@@ -151,17 +125,15 @@ import line from '@/assets/images/login_line.png'
const { t } = useI18n();
const { t,locale } = useI18n();
const userStore = useUserStore()
const systemConfigStore = useSystemConfigStore()
const loading = ref(false)
const currentTenantId = ref(0)
const needVerify = ref(false)
const validateToken = ref('')
const router = useRouter()
const invisibleHcaptcha = ref()
const googleButtonContainer = ref(null);
const loginMsg = reactive({
tenantId: systemConfigStore.tenantId || null,
......@@ -228,14 +200,72 @@ const handleClick = (path: string) => {
}
const toggleLoginType = (type: number) => {
if(type>1) return Message.error(t('login.loginTypeNotSupport'))
loginMsg.reType = type
if (type == 1) {
initGoogleLogin()
}
}
const loginHandler = async ({ values, errors }: any) => {
if (errors || loading.value) return
await handleLogin()
}
// 渲染 Google 登录按钮
const renderGoogleButton = () => {
console.log('渲染 Google 登录按钮...');
try {
// 渲染 Google 登录按钮
window.google.accounts.id.initialize({
client_id: '532164762940-vk65sge5jab1eq8mgbv1srh672ehnkff.apps.googleusercontent.com',
callback: handleSignInSuccess,
error_callback: handleSignInError,
ux_mode: 'popup' // 可选:popup/redirect
});
window.google.accounts.id.renderButton(
googleButtonContainer.value,
{ theme: 'outline', size: 'large' }
);
window.google.accounts.id.configure({
language: locale.value // 支持的语言代码
});
} catch (error) {
console.error('Error rendering Google Sign-In button:', error);
}
};
// Google 登录回调
const handleSignInSuccess = (googleUser:any) => {
console.log('Google 登录成功:', googleUser);
// 获取授权码
const authResponse = googleUser.getAuthResponse();
// 实际项目中应发送到后端
console.log('授权码:', authResponse);
// 更新状态
};
// 处理登录错误
const handleSignInError = (error) => {
console.error('登录错误:', error);
};
// 初始化谷歌登录
const initGoogleSDK = async () => {
return new Promise((resolve, reject) => {
// 动态加载 Google SDK
const script = document.createElement('script');
script.src = 'https://accounts.google.com/gsi/client';
script.async = true;
script.defer = true;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
};
const handleLogin = async () => {
loading.value = true
try {
const result = await userStore.setUserPasswordLoginAsync(
......@@ -246,47 +276,16 @@ const loginHandler = async ({ values, errors }: any) => {
loginMsg.openId?.toString() || '',
)
loading.value = false
currentTenantId.value = 0
if (result.status == 'SUCCESS') {
const forward = localStorage.getItem('forward')
localStorage.removeItem('forward')
router.push({
path: forward ? forward : '/',
})
} else {
if (!needVerify.value) needVerify.value = result.verify
if (invisibleHcaptcha.value && needVerify.value) invisibleHcaptcha.value.reset()
}
} finally {
loading.value = false
}
}
// 初始化谷歌登录
const initGoogleLogin = () => {
const cb = async (data: any) => {
loading.value = true
try {
const result = await userStore.setUserGoogleLoginAsync(data.credential)
if (result.status === 'SUCCESS') {
// 登录成功,跳转到Dashboard
const forward = localStorage.getItem('forward')
localStorage.removeItem('forward')
router.push({
path: forward || '/dashboard',
})
}
} finally {
loading.value = false
}
}
;(window as any).googleCallback = cb
const script = document.createElement('script')
script.src = 'https://accounts.google.com/gsi/client'
document.body.appendChild(script)
}
// 检查是否需要验证
......@@ -319,12 +318,21 @@ const init = async () => {
}
await checkNeedVerify()
// initGoogleLogin()
}
init()
onMounted(async () => {
try {
await initGoogleSDK();
// 确保 SDK 加载完成后渲染按钮
await new Promise(resolve => setTimeout(resolve, 1000));
renderGoogleButton()
} catch (error) {
console.error('SDK 初始化失败:', error);
}
});
</script>
<style scoped lang="scss">
:deep(.arco-form-item-content){
......@@ -390,6 +398,11 @@ init()
:deep(.arco-form-item-message){
color: rgba(255,0,0,0);
}
:deep(.nsm7Bb-HzV7m-LgbsSe){
border-radius: 13px;
height: 44px;
padding: 0 17px;
}
.light-login-bg {
background: url('../../assets/images/login-bg.png')no-repeat;
background-size: 100% 100%;
......
......@@ -34,7 +34,7 @@ import { useRouter } from 'vue-router'
const { t } = useI18n();
const systemConfigStore = useSystemConfigStore()
const currentStep = ref(inject('currentStep'))
const currentStep = ref(inject('currentStep')??0)
const router = useRouter()
const goHome = (path:string)=>{
......
<template>
<div class="card">
<div class="card-header">
<h2>Google 登录演示</h2>
</div>
<div class="card-body">
<div class="login-section">
<div class="error-message" v-if="showError">
<i class="fas fa-exclamation-circle error-icon"></i>
<div>
<h3>Google SDK 加载失败</h3>
<p>{{ errorMessage }}</p>
</div>
</div>
<div class="google-btn-container">
<button
id="google-login-btn"
class="google-btn"
@click="initGoogleLogin"
:disabled="loading"
>
<i class="fab fa-google"></i>
<span v-if="!loading">使用 Google 账号登录</span>
<span v-else><span class="loading-spinner"></span>加载中...</span>
</button>
</div>
<div class="status-panel">
<h3>登录状态</h3>
<div class="status-content">
{{ statusLog }}
</div>
<div v-if="user" class="user-info">
<img :src="user.picture" alt="用户头像" class="avatar">
<div class="user-details">
<h4>{{ user.name }}</h4>
<p>{{ user.email }}</p>
</div>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn" @click="simulateSuccess">
<i class="fas fa-check-circle"></i> 模拟成功登录
</button>
<button class="btn btn-outline" @click="resetDemo">
<i class="fas fa-redo"></i> 重置演示
</button>
</div>
</div>
</div>
</div>
<footer>
<p>© 2023 Vue3 Google 登录解决方案 | 安全可靠的企业级认证集成</p>
</footer>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from "vue";
const statusLog = ref('等待用户操作...\n');
const user = ref(null);
const loading = ref(false);
const showError = ref(false);
const errorMessage = ref('');
// 添加日志
const addLog = (message) => {
statusLog.value += `[${new Date().toLocaleTimeString()}] ${message}\n`;
};
// 检查 Google SDK 是否加载
const checkGoogleSDK = () => {
addLog('检查 Google SDK 状态...');
if (window.google && window.google.accounts && window.google.accounts.id) {
addLog('✅ Google SDK 已成功加载');
addLog(`SDK 版本: ${window.google.version || '未知'}`);
showError.value = false;
} else {
addLog('❌ Google SDK 未加载');
errorMessage.value = 'Google Identity Services SDK 未能正确加载。请检查网络连接或域名授权设置。';
showError.value = true;
}
};
// 初始化 Google 登录
const initGoogleLogin = () => {
addLog('开始初始化 Google 登录...');
loading.value = true;
showError.value = false;
// 检查 SDK 是否已加载
if (!window.google || !window.google.accounts || !window.google.accounts.id) {
addLog('⚠️ Google SDK 未加载,尝试动态加载...');
// 动态加载 Google SDK
const script = document.createElement('script');
script.src = 'https://accounts.google.com/gsi/client';
script.async = true;
script.defer = true;
script.onload = () => {
addLog('✅ Google SDK 动态加载成功');
renderGoogleButton();
};
script.onerror = () => {
addLog('❌ Google SDK 动态加载失败');
errorMessage.value = '无法加载 Google Identity Services SDK。请检查网络连接或防火墙设置。';
showError.value = true;
loading.value = false;
};
document.head.appendChild(script);
return;
}
// SDK 已加载,直接渲染按钮
renderGoogleButton();
};
// 渲染 Google 登录按钮
const renderGoogleButton = () => {
addLog('渲染 Google 登录按钮...');
try {
// 使用 Google 官方 API 渲染按钮
window.google.accounts.id.initialize({
client_id: '532164762940-vk65sge5jab1eq8mgbv1srh672ehnkff.apps.googleusercontent.com', // 示例ID,需替换
callback: handleGoogleSignIn,
auto_select: false,
cancel_on_tap_outside: false,
context: 'signin',
ux_mode: 'popup',
});
// 清除现有按钮(如果有)
const container = document.getElementById('google-login-btn');
container.innerHTML = '';
// 渲染新按钮
window.google.accounts.id.renderButton(
container,
{
theme: 'outline',
size: 'large',
text: 'signin_with',
shape: 'rectangular',
logo_alignment: 'left',
width: '100%'
}
);
addLog('✅ Google 登录按钮渲染成功');
addLog('请点击按钮继续登录流程');
loading.value = false;
} catch (error) {
addLog(`❌ 渲染 Google 按钮失败: ${error.message}`);
errorMessage.value = `渲染 Google 登录按钮时出错: ${error.message}`;
showError.value = true;
loading.value = false;
}
};
// Google 登录回调
const handleGoogleSignIn = async (response) => {
// clientId client_id credential select_by
console.log(response.credential,'----------');
addLog('Google 登录回调触发');
addLog(`收到凭证: ${response.credential.substring(0, 30)}...`);
try {
// 模拟发送到后端验证
addLog('发送凭证到后端验证...');
await new Promise(resolve => setTimeout(resolve, 1500));
// 模拟后端验证成功
user.value = {
id: '1234567890',
name: '张三',
given_name: '三',
family_name: '张',
picture: 'https://randomuser.me/api/portraits/men/32.jpg',
email: 'zhangsan@example.com',
email_verified: true,
locale: 'zh-CN'
};
addLog('✅ 后端验证成功!');
addLog(`用户信息: ${JSON.stringify(user.value, null, 2)}`);
addLog('登录成功!即将跳转到仪表盘...');
} catch (error) {
addLog(`❌ 登录失败: ${error.message}`);
}
};
// 模拟成功登录
const simulateSuccess = () => {
addLog('模拟成功登录...');
user.value = {
id: '1234567890',
name: '李四',
given_name: '四',
family_name: '李',
picture: 'https://randomuser.me/api/portraits/women/44.jpg',
email: 'lisi@example.com',
email_verified: true,
locale: 'zh-CN'
};
addLog('✅ 模拟登录成功!');
};
// 重置演示
const resetDemo = () => {
statusLog.value = '演示已重置...\n';
user.value = null;
showError.value = false;
loading.value = false;
addLog('等待用户操作...');
};
// 组件挂载时检查 SDK
onMounted(() => {
addLog('组件已挂载,检查 Google SDK 状态...');
checkGoogleSDK();
});
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
color: #333;
}
.container {
max-width: 1200px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
header {
text-align: center;
margin-bottom: 40px;
color: white;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
header h1 {
font-size: 2.8rem;
margin-bottom: 10px;
background: linear-gradient(to right, #4285f4, #34a853, #fbbc05, #ea4335);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
header p {
font-size: 1.2rem;
max-width: 800px;
margin: 0 auto;
opacity: 0.9;
}
.card {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
overflow: hidden;
width: 100%;
max-width: 1000px;
display: flex;
flex-direction: column;
}
.card-header {
background: linear-gradient(90deg, #4285f4, #34a853);
color: white;
padding: 25px 30px;
text-align: center;
}
.card-body {
padding: 30px;
display: flex;
flex-wrap: wrap;
gap: 30px;
}
.login-section {
flex: 1;
min-width: 300px;
}
.debug-section {
flex: 1;
min-width: 300px;
background: #f8f9fa;
border-radius: 12px;
padding: 25px;
}
.debug-section h3 {
color: #ea4335;
margin-bottom: 15px;
font-size: 1.4rem;
}
.google-btn-container {
display: flex;
justify-content: center;
margin: 30px 0;
position: relative;
}
.google-btn {
display: flex;
align-items: center;
justify-content: center;
background: white;
color: #444;
border: 1px solid #ddd;
border-radius: 4px;
padding: 12px 20px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
width: 100%;
max-width: 280px;
}
.google-btn:hover {
background: #f8f9fa;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.google-btn i {
font-size: 20px;
margin-right: 12px;
color: #4285f4;
}
.divider {
display: flex;
align-items: center;
margin: 25px 0;
}
.divider-line {
flex: 1;
height: 1px;
background: #eee;
}
.divider-text {
padding: 0 15px;
color: #777;
font-size: 14px;
}
.status-panel {
margin-top: 30px;
padding: 20px;
border-radius: 8px;
background: #f8f9fa;
border-left: 4px solid #4285f4;
}
.status-panel h3 {
margin-bottom: 15px;
color: #4285f4;
}
.status-content {
font-family: monospace;
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 6px;
max-height: 200px;
overflow-y: auto;
font-size: 14px;
white-space: pre-wrap;
}
.user-info {
display: flex;
align-items: center;
margin-top: 20px;
padding: 15px;
background: #e8f0fe;
border-radius: 8px;
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
margin-right: 15px;
object-fit: cover;
border: 2px solid #4285f4;
}
.user-details h4 {
font-size: 18px;
margin-bottom: 5px;
color: #202124;
}
.user-details p {
color: #5f6368;
font-size: 14px;
}
.solution-list {
margin-top: 20px;
}
.solution-item {
padding: 12px 15px;
margin-bottom: 10px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
display: flex;
align-items: flex-start;
}
.solution-icon {
margin-right: 12px;
font-size: 18px;
min-width: 24px;
color: #34a853;
}
.solution-content h4 {
margin-bottom: 5px;
color: #202124;
}
.solution-content p {
color: #5f6368;
font-size: 14px;
line-height: 1.5;
}
.btn {
display: inline-block;
padding: 12px 25px;
background: linear-gradient(90deg, #4285f4, #34a853);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin-top: 15px;
text-decoration: none;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.btn-outline {
background: transparent;
border: 2px solid #4285f4;
color: #4285f4;
margin-left: 10px;
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-message {
color: #ea4335;
background: #fce8e6;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
display: flex;
align-items: center;
}
.error-icon {
font-size: 24px;
margin-right: 10px;
}
footer {
margin-top: 40px;
text-align: center;
color: white;
font-size: 14px;
opacity: 0.8;
}
@media (max-width: 768px) {
.card-body {
flex-direction: column;
}
header h1 {
font-size: 2.2rem;
}
.btn {
display: block;
width: 100%;
margin: 10px 0;
}
.btn-outline {
margin-left: 0;
}
}
</style>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment