700 lines
19 KiB
Vue
700 lines
19 KiB
Vue
<template>
|
||
<view id="app">
|
||
<!-- 加载状态 -->
|
||
<view v-if="showLoading" class="loading-container">
|
||
<view class="loading-spinner">
|
||
<view class="spinner"></view>
|
||
<text class="loading-text">正在加载...</text>
|
||
</view>
|
||
</view>
|
||
<!-- 页面内容 -->
|
||
<router-view v-else />
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
// 强制引用这些按需加载的文件,确保它们被打包
|
||
// #ifdef MP-WEIXIN
|
||
import '@/uni_modules/uni-id-pages/common/check-id-card.js';
|
||
// #endif
|
||
|
||
import initApp from '@/common/appInit.js';
|
||
import openApp from '@/common/openApp.js';
|
||
// #ifdef H5
|
||
openApp() //创建在h5端全局悬浮引导用户下载app的功能
|
||
// #endif
|
||
// uni-agree 已在子包中,主包不再引用(避免跨包引用问题)
|
||
// import checkIsAgree from '@/pages-subpackage/uni-agree/utils/uni-agree.js';
|
||
import uniIdPageInit from '@/common/uni-id-pages-init.js';
|
||
import { store, mutations } from '@/common/store.js';
|
||
|
||
export default {
|
||
globalData: {
|
||
searchText: '',
|
||
appVersion: {},
|
||
config: {},
|
||
$i18n: {},
|
||
$t: {}
|
||
},
|
||
data() {
|
||
return {
|
||
showLoading: true // 初始显示加载状态
|
||
};
|
||
},
|
||
onLaunch: async function() {
|
||
console.log('App Launch')
|
||
|
||
// 设置全局角色信息
|
||
this.setGlobalRoleInfo();
|
||
|
||
// 初始化当前页面路径
|
||
this.updateCurrentPath();
|
||
|
||
// 安全地设置 i18n,确保在 i18n 初始化完成后再赋值
|
||
// 使用 try-catch 防止访问未初始化的 i18n 对象
|
||
try {
|
||
if (this.$i18n) {
|
||
this.globalData.$i18n = this.$i18n
|
||
} else {
|
||
// 如果 i18n 还未初始化,延迟设置
|
||
setTimeout(() => {
|
||
try {
|
||
if (this.$i18n) {
|
||
this.globalData.$i18n = this.$i18n
|
||
}
|
||
} catch (e) {
|
||
console.warn('[App] i18n 初始化延迟设置失败:', e)
|
||
}
|
||
}, 100)
|
||
}
|
||
} catch (e) {
|
||
console.warn('[App] i18n 初始化失败:', e)
|
||
}
|
||
this.globalData.$t = str => {
|
||
try {
|
||
return this.$t ? this.$t(str) : str
|
||
} catch (e) {
|
||
return str
|
||
}
|
||
}
|
||
initApp();
|
||
await uniIdPageInit()
|
||
|
||
// 移除自动URL隐藏初始化,避免页面刷新时的路由冲突
|
||
|
||
// 设置全局路由拦截,检查登录状态
|
||
this.setupRouteInterceptor()
|
||
|
||
// #ifdef MP-WEIXIN
|
||
// 微信小程序:等待页面完全加载后再检查登录状态,避免扫码进入时空白页面
|
||
// 延迟检查,确保页面栈已初始化且页面已完全加载
|
||
setTimeout(() => {
|
||
// 检查页面是否已完全加载
|
||
const checkPageLoaded = () => {
|
||
const pages = getCurrentPages()
|
||
if (pages.length > 0 && pages[pages.length - 1].route) {
|
||
// 页面已加载,检查登录状态
|
||
if (!this.isLoggedIn()) {
|
||
this.showLoading = false // 隐藏加载状态
|
||
this.redirectToLogin()
|
||
} else {
|
||
// 已登录,检查当前页面
|
||
this.checkCurrentPageLogin()
|
||
// 延迟隐藏加载状态,确保页面渲染完成
|
||
setTimeout(() => {
|
||
this.showLoading = false
|
||
}, 200)
|
||
}
|
||
} else {
|
||
// 页面还没加载完成,继续等待
|
||
setTimeout(checkPageLoaded, 100)
|
||
}
|
||
}
|
||
checkPageLoaded()
|
||
}, 500)
|
||
// #endif
|
||
// #ifndef MP-WEIXIN
|
||
// 其他平台:延迟检查登录状态
|
||
setTimeout(() => {
|
||
this.checkCurrentPageLogin()
|
||
// 延迟隐藏加载状态,确保页面渲染完成
|
||
setTimeout(() => {
|
||
this.showLoading = false
|
||
}, 200)
|
||
}, 100)
|
||
// #endif
|
||
|
||
// #ifdef APP
|
||
//checkIsAgree(); APP端暂时先用原生默认生成的。目前,自定义方式启动vue界面时,原生层已经请求了部分权限这并不符合国家的法规
|
||
// #endif
|
||
|
||
// #ifdef H5
|
||
// checkIsAgree(); // 默认不开启。目前全球,仅欧盟国家有网页端同意隐私权限的需要。如果需要可以自己去掉注视后生效
|
||
// #endif
|
||
|
||
// #ifdef APP-PLUS
|
||
//idfa有需要的用户在应用首次启动时自己获取存储到storage中
|
||
/*var idfa = '';
|
||
var manager = plus.ios.invoke('ASIdentifierManager', 'sharedManager');
|
||
if(plus.ios.invoke(manager, 'isAdvertisingTrackingEnabled')){
|
||
var identifier = plus.ios.invoke(manager, 'advertisingIdentifier');
|
||
idfa = plus.ios.invoke(identifier, 'UUIDString');
|
||
plus.ios.deleteObject(identifier);
|
||
}
|
||
plus.ios.deleteObject(manager);
|
||
console.log('idfa = '+idfa);*/
|
||
// #endif
|
||
},
|
||
onShow: function() {
|
||
console.log('App Show')
|
||
|
||
// 每次应用显示时同步用户信息(解决store初始化时uni未准备好导致的问题)
|
||
try {
|
||
mutations.syncUserInfoFromStorage();
|
||
} catch (e) {
|
||
// 静默失败
|
||
}
|
||
|
||
// 每次应用显示时也检查当前页面是否需要登录
|
||
this.checkCurrentPageLogin()
|
||
|
||
// #ifdef H5
|
||
// H5平台:刷新自定义tabBar,确保显示状态正确
|
||
setTimeout(() => {
|
||
try {
|
||
uni.$emit('tabbar:refresh');
|
||
console.log('已发送tabbar刷新事件');
|
||
} catch (e) {
|
||
console.warn('发送tabbar刷新事件失败:', e);
|
||
}
|
||
}, 500);
|
||
// #endif
|
||
},
|
||
onHide: function() {
|
||
console.log('App Hide')
|
||
// #ifdef MP-WEIXIN
|
||
this.stopTabBarMonitor();
|
||
// #endif
|
||
|
||
// 清理URL保护定时器
|
||
if (this.urlProtectionInterval) {
|
||
clearInterval(this.urlProtectionInterval);
|
||
this.urlProtectionInterval = null;
|
||
}
|
||
},
|
||
onError: function(msg) {
|
||
console.error('[App Global Error]:', msg);
|
||
},
|
||
methods: {
|
||
|
||
/**
|
||
* 完美的URL隐藏系统 - 专为uni-app H5设计
|
||
*/
|
||
setupPerfectUrlHiding() {
|
||
// #ifdef H5
|
||
console.log('🎯 初始化完美URL隐藏系统');
|
||
|
||
// 路径映射:将长路径映射为短路径
|
||
this.urlPathMap = {
|
||
'pages/furniture_customer/furniture_customer': '/customer',
|
||
'pages/furniture_reception/furniture_reception': '/reception',
|
||
'pages/furniture_top_sales/furniture_top_sales': '/top-sales',
|
||
'pages/ucenter/ucenter': '/profile'
|
||
};
|
||
|
||
// 反向映射:将短路径映射回长路径(用于路由解析)
|
||
this.reversePathMap = {};
|
||
Object.keys(this.urlPathMap).forEach(key => {
|
||
this.reversePathMap[this.urlPathMap[key]] = key;
|
||
});
|
||
|
||
// 初始化标志
|
||
this.urlHidingInitialized = false;
|
||
|
||
// 延迟初始化,避免干扰uni-app初始化过程
|
||
setTimeout(() => {
|
||
console.log('⏰ 3秒延迟结束,开始检查页面栈');
|
||
// 再次检查页面栈,确保页面已加载
|
||
const pages = getCurrentPages();
|
||
console.log('📊 检查页面栈长度:', pages.length);
|
||
if (pages.length > 0) {
|
||
console.log('✅ 页面栈不为空,开始初始化URL隐藏');
|
||
this.initializeUrlHiding();
|
||
} else {
|
||
console.log('⏳ 页面栈仍为空,继续等待');
|
||
// 如果页面栈仍为空,再等一会儿
|
||
setTimeout(() => {
|
||
console.log('⏰ 额外2秒等待结束');
|
||
const finalPages = getCurrentPages();
|
||
console.log('📊 最终页面栈长度:', finalPages.length);
|
||
this.initializeUrlHiding(); // 无论如何都初始化
|
||
}, 2000);
|
||
}
|
||
}, 3000);
|
||
|
||
console.log('✅ URL隐藏系统初始化完成');
|
||
// #endif
|
||
},
|
||
|
||
/**
|
||
* 初始化URL隐藏功能
|
||
*/
|
||
initializeUrlHiding() {
|
||
// #ifdef H5
|
||
console.log('🚀 开始初始化URL隐藏功能');
|
||
|
||
// 标记为已初始化
|
||
this.urlHidingInitialized = true;
|
||
|
||
// 1. 立即尝试隐藏当前页面的URL
|
||
this.hideCurrentPageUrl();
|
||
|
||
// 2. 监听页面切换事件
|
||
this.setupPageChangeListener();
|
||
|
||
// 3. 监听浏览器导航
|
||
this.setupBrowserNavigationListener();
|
||
|
||
// 4. 设置URL保护机制
|
||
this.setupUrlProtection();
|
||
|
||
console.log('🎉 URL隐藏功能初始化完毕');
|
||
// #endif
|
||
},
|
||
|
||
/**
|
||
* 隐藏当前页面的URL显示
|
||
*/
|
||
hideCurrentPageUrl(retryCount = 0) {
|
||
// #ifdef H5
|
||
// 只有在系统完全初始化后才执行URL隐藏
|
||
if (!this.urlHidingInitialized) {
|
||
console.log('⏳ URL隐藏系统尚未初始化,跳过');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const pages = getCurrentPages();
|
||
if (pages.length === 0) {
|
||
if (retryCount < 5) { // 最多重试5次
|
||
console.log(`📄 页面栈为空,重试 ${retryCount + 1}/5`);
|
||
setTimeout(() => this.hideCurrentPageUrl(retryCount + 1), 300);
|
||
} else {
|
||
console.log('📄 页面栈仍然为空,停止URL隐藏');
|
||
}
|
||
return;
|
||
}
|
||
|
||
const currentPage = pages[pages.length - 1];
|
||
const currentPath = currentPage.route;
|
||
|
||
// 确保页面路径有效
|
||
if (!currentPath || currentPath === '') {
|
||
console.log('⚠️ 页面路径无效,等待页面完全加载');
|
||
if (retryCount < 3) {
|
||
setTimeout(() => this.hideCurrentPageUrl(retryCount + 1), 200);
|
||
}
|
||
return;
|
||
}
|
||
|
||
console.log('📍 当前页面路径:', currentPath);
|
||
|
||
if (this.urlPathMap[currentPath]) {
|
||
const shortPath = this.urlPathMap[currentPath];
|
||
const currentPathname = window.location.pathname;
|
||
|
||
// 检查当前URL是否已经是正确的短路径
|
||
if (currentPathname !== shortPath) {
|
||
console.log(`🔄 URL隐藏: ${currentPathname} -> ${shortPath}`);
|
||
|
||
// 使用history.replaceState替换URL,但保持uni-app的路由正常工作
|
||
const newUrl = `${window.location.origin}${shortPath}`;
|
||
window.history.replaceState(null, '', newUrl);
|
||
|
||
console.log('✅ URL隐藏成功');
|
||
} else {
|
||
console.log('ℹ️ URL已经是隐藏状态');
|
||
}
|
||
} else {
|
||
console.log('⚠️ 当前路径无需隐藏:', currentPath);
|
||
}
|
||
} catch (error) {
|
||
console.warn('❌ URL隐藏失败:', error);
|
||
}
|
||
// #endif
|
||
},
|
||
|
||
/**
|
||
* 设置页面切换监听器
|
||
*/
|
||
setupPageChangeListener() {
|
||
// #ifdef H5
|
||
// 监听uni-app的页面显示事件
|
||
if (typeof uni !== 'undefined' && uni.on) {
|
||
uni.on('onShow', () => {
|
||
console.log('📱 页面显示事件触发,延迟检查URL隐藏');
|
||
// 延迟执行,确保页面完全稳定
|
||
setTimeout(() => {
|
||
this.hideCurrentPageUrl();
|
||
}, 500);
|
||
});
|
||
}
|
||
|
||
// 监听popstate事件(浏览器前进后退)
|
||
window.addEventListener('popstate', () => {
|
||
console.log('🔙 浏览器前进后退事件');
|
||
setTimeout(() => {
|
||
this.handleUrlChange();
|
||
}, 100);
|
||
});
|
||
// #endif
|
||
},
|
||
|
||
/**
|
||
* 处理URL变化(在history模式下)
|
||
*/
|
||
handleUrlChange() {
|
||
// #ifdef H5
|
||
console.log('🎯 处理URL变化');
|
||
|
||
// 检查当前URL是否需要转换
|
||
const currentPathname = window.location.pathname;
|
||
const shortPaths = Object.values(this.urlPathMap);
|
||
|
||
// 如果当前路径是短路径,不需要处理
|
||
if (shortPaths.includes(currentPathname)) {
|
||
console.log('✅ URL已经是短路径格式');
|
||
return;
|
||
}
|
||
|
||
// 检查是否是uni-app的路由变化
|
||
setTimeout(() => {
|
||
this.hideCurrentPageUrl();
|
||
}, 200);
|
||
// #endif
|
||
},
|
||
|
||
/**
|
||
* 设置浏览器导航监听器
|
||
*/
|
||
setupBrowserNavigationListener() {
|
||
// #ifdef H5
|
||
// 监听前进后退按钮(已经在上面的popstate监听器中处理)
|
||
console.log('🔄 浏览器导航监听器已设置');
|
||
// #endif
|
||
},
|
||
|
||
/**
|
||
* 设置URL保护机制(防止意外的URL变化)
|
||
*/
|
||
setupUrlProtection() {
|
||
// #ifdef H5
|
||
// 定期检查并维护URL的隐藏状态
|
||
this.urlProtectionInterval = setInterval(() => {
|
||
try {
|
||
const pages = getCurrentPages();
|
||
if (pages.length > 0) {
|
||
const currentPage = pages[pages.length - 1];
|
||
const currentPath = currentPage.route;
|
||
|
||
if (this.urlPathMap[currentPath]) {
|
||
const expectedPath = this.urlPathMap[currentPath];
|
||
const currentPathname = window.location.pathname;
|
||
|
||
if (currentPathname !== expectedPath) {
|
||
console.log('🛡️ 检测到URL异常,重新隐藏');
|
||
const newUrl = `${window.location.origin}${expectedPath}`;
|
||
window.history.replaceState(null, '', newUrl);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn('🛡️ URL保护检查失败:', error);
|
||
}
|
||
}, 3000); // 每3秒检查一次
|
||
|
||
console.log('🛡️ URL保护机制已启动');
|
||
// #endif
|
||
},
|
||
|
||
/**
|
||
* 设置全局路由拦截
|
||
*/
|
||
setupRouteInterceptor() {
|
||
const app = this
|
||
|
||
// 拦截路由跳转(适用于所有平台)
|
||
uni.addInterceptor('navigateTo', {
|
||
invoke: (options) => {
|
||
if (app.shouldRedirectToLogin(options.url)) {
|
||
app.redirectToLogin()
|
||
return false // 阻止跳转
|
||
}
|
||
}
|
||
})
|
||
|
||
uni.addInterceptor('redirectTo', {
|
||
invoke: (options) => {
|
||
if (app.shouldRedirectToLogin(options.url)) {
|
||
app.redirectToLogin()
|
||
return false // 阻止跳转
|
||
}
|
||
}
|
||
})
|
||
|
||
uni.addInterceptor('switchTab', {
|
||
invoke: (options) => {
|
||
if (app.shouldRedirectToLogin(options.url)) {
|
||
app.redirectToLogin()
|
||
return false // 阻止跳转
|
||
}
|
||
}
|
||
})
|
||
|
||
uni.addInterceptor('reLaunch', {
|
||
invoke: (options) => {
|
||
if (app.shouldRedirectToLogin(options.url)) {
|
||
app.redirectToLogin()
|
||
return false // 阻止跳转
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 判断是否需要跳转到登录页
|
||
*/
|
||
shouldRedirectToLogin(url) {
|
||
if (!url) return false
|
||
|
||
// 排除登录相关页面
|
||
const excludePaths = [
|
||
'/uni_modules/uni-id-pages/pages/login',
|
||
'/uni_modules/uni-id-pages/pages/register',
|
||
'/uni_modules/uni-id-pages/pages/retrieve'
|
||
]
|
||
|
||
// 检查是否在排除列表中
|
||
for (let excludePath of excludePaths) {
|
||
if (url.includes(excludePath)) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 检查登录状态
|
||
return !this.isLoggedIn()
|
||
},
|
||
|
||
/**
|
||
* 检查是否已登录
|
||
*/
|
||
isLoggedIn() {
|
||
// 优先检查后端登录态
|
||
const backendToken = uni.getStorageSync('backend-token')
|
||
if (backendToken) {
|
||
return true
|
||
}
|
||
|
||
// 其次检查 uni-id 的登录态
|
||
return store.hasLogin
|
||
},
|
||
|
||
/**
|
||
* 跳转到登录页
|
||
*/
|
||
redirectToLogin() {
|
||
const loginPage = '/uni_modules/uni-id-pages/pages/login/login-withpwd'
|
||
const pages = getCurrentPages()
|
||
|
||
// 如果当前已经在登录页,不重复跳转
|
||
if (pages.length > 0) {
|
||
const currentPage = pages[pages.length - 1]
|
||
if (currentPage.route && currentPage.route.includes('login')) {
|
||
return
|
||
}
|
||
}
|
||
|
||
// #ifdef MP-WEIXIN
|
||
// 微信小程序:登录页在subPackage中,必须使用reLaunch
|
||
// redirectTo不能用于跳转到subPackage页面,只能用于主包页面
|
||
uni.reLaunch({
|
||
url: loginPage,
|
||
success: () => {
|
||
console.log('跳转登录页成功')
|
||
},
|
||
fail: (err) => {
|
||
console.error('跳转登录页失败:', err)
|
||
// 如果reLaunch失败,可能是路径问题,尝试不带前导斜杠
|
||
const loginPageWithoutSlash = loginPage.startsWith('/') ? loginPage.substring(1) : loginPage
|
||
uni.reLaunch({
|
||
url: loginPageWithoutSlash,
|
||
fail: (err2) => {
|
||
console.error('跳转登录页失败(重试):', err2)
|
||
}
|
||
})
|
||
}
|
||
})
|
||
// #endif
|
||
|
||
// #ifndef MP-WEIXIN
|
||
// 其他平台使用reLaunch
|
||
uni.reLaunch({
|
||
url: loginPage,
|
||
fail: (err) => {
|
||
console.error('跳转登录页失败:', err)
|
||
}
|
||
})
|
||
// #endif
|
||
},
|
||
|
||
|
||
/**
|
||
* 更新当前页面路径(用于更新 tabBar 等)
|
||
*/
|
||
updateCurrentPath() {
|
||
try {
|
||
const pages = getCurrentPages();
|
||
if (pages.length > 0) {
|
||
const currentPage = pages[pages.length - 1];
|
||
if (currentPage && currentPage.route) {
|
||
const currentPath = `/${currentPage.route}`;
|
||
// 更新 tabBar 的当前路径(如果存在)
|
||
try {
|
||
const app = getApp({ allowDefault: true });
|
||
if (app && app.globalData) {
|
||
const tabBar = app.globalData.tabBarInstance;
|
||
if (tabBar && typeof tabBar.updateCurrentPath === 'function') {
|
||
tabBar.updateCurrentPath(currentPath);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 静默失败
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 静默失败,不影响应用启动
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 检查当前页面是否需要登录
|
||
*/
|
||
setGlobalRoleInfo() {
|
||
try {
|
||
const tokenData = uni.getStorageSync('uni_id_token');
|
||
if (tokenData && tokenData.roles) {
|
||
console.log('[App] Setting global role info:', tokenData.roles);
|
||
this.globalData.roles = tokenData.roles;
|
||
this.globalData.roleName = tokenData.roleName || '';
|
||
|
||
// 通知自定义 tabBar 更新
|
||
try {
|
||
const app = getApp({ allowDefault: true });
|
||
if (app && app.globalData) {
|
||
const tabBar = app.globalData.tabBarInstance;
|
||
if (tabBar) {
|
||
tabBar.refreshRole();
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 静默失败
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('[App] Failed to set global role info:', e);
|
||
}
|
||
},
|
||
checkCurrentPageLogin() {
|
||
const pages = getCurrentPages()
|
||
if (pages.length === 0) {
|
||
// #ifdef MP-WEIXIN
|
||
// 微信小程序:如果页面栈为空,延迟重试
|
||
setTimeout(() => {
|
||
this.checkCurrentPageLogin()
|
||
}, 200)
|
||
// #endif
|
||
return
|
||
}
|
||
|
||
const currentPage = pages[pages.length - 1]
|
||
if (!currentPage || !currentPage.route) {
|
||
// #ifdef MP-WEIXIN
|
||
// 微信小程序:如果页面信息不完整,延迟重试
|
||
setTimeout(() => {
|
||
this.checkCurrentPageLogin()
|
||
}, 200)
|
||
// #endif
|
||
return
|
||
}
|
||
|
||
const currentRoute = '/' + currentPage.route
|
||
|
||
// 排除登录相关页面
|
||
if (currentRoute.includes('/uni_modules/uni-id-pages/pages/login') ||
|
||
currentRoute.includes('/uni_modules/uni-id-pages/pages/register') ||
|
||
currentRoute.includes('/uni_modules/uni-id-pages/pages/retrieve')) {
|
||
return
|
||
}
|
||
|
||
// 如果未登录,跳转到登录页
|
||
if (!this.isLoggedIn()) {
|
||
// #ifdef MP-WEIXIN
|
||
// 微信小程序:确保页面已完全加载后再跳转,使用setTimeout代替$nextTick
|
||
setTimeout(() => {
|
||
this.redirectToLogin()
|
||
}, 100)
|
||
// #endif
|
||
// #ifndef MP-WEIXIN
|
||
this.redirectToLogin()
|
||
// #endif
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
/*每个页面公共css */
|
||
|
||
/* 加载状态样式 */
|
||
.loading-container {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: #f8f8f8;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 9999;
|
||
}
|
||
|
||
.loading-spinner {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 4px solid #e0e0e0;
|
||
border-top: 4px solid #007AFF;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.loading-text {
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
</style>
|