630 lines
20 KiB
Vue
630 lines
20 KiB
Vue
<template>
|
||
<view class="auth-result-page tn-safe-area-inset-bottom">
|
||
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="transparent">
|
||
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('navTitle') }}</view>
|
||
</tn-nav-bar>
|
||
<view class="page-content tn-padding" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
|
||
<view class="status-card fade-in">
|
||
<!-- 状态大图标 -->
|
||
<view class="status-icon-wrapper tn-flex tn-flex-row-center tn-flex-col-center" :class="authStatus">
|
||
<text v-if="authStatus === 'approved'" class="tn-icon-success-circle-fill status-icon icon-success"></text>
|
||
<text v-else-if="authStatus === 'rejected'" class="tn-icon-close-circle-fill status-icon icon-rejected"></text>
|
||
<text v-else-if="authStatus === 'pending'" class="tn-icon-time-fill status-icon icon-pending"></text>
|
||
<text v-else class="tn-icon-warning-fill status-icon icon-unsubmitted"></text>
|
||
</view>
|
||
|
||
<!-- 状态标题与描述 -->
|
||
<view class="tn-text-center tn-margin-top-lg tn-padding-left-lg tn-padding-right-lg">
|
||
<view class="card-title">{{ titleText }}</view>
|
||
<!-- <view class="card-desc tn-margin-top-sm">{{ descText }}</view> -->
|
||
</view>
|
||
|
||
<!-- <view v-if="applicationCode" class="code-chip tn-margin-top-lg">
|
||
申请编号:{{ applicationCode }}
|
||
</view> -->
|
||
|
||
<!-- 审批备注 -->
|
||
<view v-if="approvalRemark" class="reason-box tn-margin-top-xl tn-margin-left-lg tn-margin-right-lg" :class="authStatus">
|
||
<view class="reason-title tn-flex tn-flex-row-center tn-flex-col-center">
|
||
<text :class="remarkIconClass" class="tn-margin-right-xs"></text>{{ approvalRemarkTitle }}
|
||
</view>
|
||
<view class="reason-content tn-margin-top-sm tn-text-center">{{ approvalRemark }}</view>
|
||
</view>
|
||
|
||
<!-- 提交信息摘要 -->
|
||
<view v-if="profileSummary.length" class="summary-box tn-margin-top-xl tn-margin-bottom-lg tn-margin-left-lg tn-margin-right-lg">
|
||
<view class="summary-title tn-margin-bottom-sm">{{ t('submittedInfoTitle') }}</view>
|
||
<view v-for="item in profileSummary" :key="item.label" class="summary-row tn-flex tn-flex-row-between tn-padding-top-sm tn-padding-bottom-sm">
|
||
<text class="summary-label">{{ item.label }}</text>
|
||
<text class="summary-value tn-text-ellipsis">{{ item.value }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 操作按钮 -->
|
||
<view class="action-group tn-margin-top-xl fade-in-up">
|
||
<view v-if="authStatus === 'approved'" class="custom-btn btn-primary" @click="goToLogin">
|
||
<text class="tn-text-bold">{{ t('goLogin') }}</text>
|
||
</view>
|
||
<view v-else-if="authStatus === 'rejected'" class="custom-btn btn-primary" @click="resubmit">
|
||
<text class="tn-text-bold">{{ t('resubmitRegister') }}</text>
|
||
</view>
|
||
<view v-else class="custom-btn btn-dark" @click="refreshStatus">
|
||
<text class="tn-text-bold tn-flex tn-flex-row-center tn-flex-col-center">
|
||
<text v-if="loading" class="tn-icon-loading tn-margin-right-xs spin"></text>
|
||
{{ loading ? t('refreshing') : t('refreshStatus') }}
|
||
</text>
|
||
</view>
|
||
<view class="custom-btn btn-light tn-margin-top" @click="goToRegistrationRecords">
|
||
<text class="tn-text-bold">{{ t('viewRegistrationRecords') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { saveRegisteredUser } from '../../utils/app-state'
|
||
import { registrationApplicationMePage, registrationMyorder } from '@/api/index.js'
|
||
import { fetchEnums, getApprovalStatusLabel } from '../../utils/enum-store'
|
||
import { buildUserFromRegistrationApplication, mapRegistrationApprovalToAuthStatus, resolveRegistrationApplicationCode } from '../../utils/flow'
|
||
|
||
function formatPageResponse(response) {
|
||
const payload = response && response.data ? response.data : response
|
||
return payload && typeof payload === 'object' ? payload : {}
|
||
}
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
applicationInfo: null,
|
||
targetCode: '',
|
||
sourceType: '',
|
||
i18n: {
|
||
zh: {
|
||
navTitle: '注册申请结果',
|
||
submittedInfoTitle: '已提交信息',
|
||
goLogin: '去登录',
|
||
resubmitRegister: '重新提交注册申请',
|
||
refreshing: '刷新中...',
|
||
refreshStatus: '刷新审批状态',
|
||
viewRegistrationRecords: '查看注册记录',
|
||
unsubmitted: '未提交',
|
||
titleUnsubmitted: '您还未提交注册申请',
|
||
titlePending: '注册申请已提交',
|
||
titleApproved: '注册审批已通过',
|
||
titleRejected: '注册审批未通过',
|
||
descUnsubmitted: '请先提交注册申请单,审批通过后才算注册成功。',
|
||
descPendingRecords: '当前展示的是该申请单的审批状态,可下拉或点击按钮刷新最新结果。',
|
||
descPendingDefault: '当前申请正在审批中,审批通过前无法登录。',
|
||
descApproved: '您的注册申请已审批通过,现在可以前往登录。',
|
||
descRejectedRecords: '当前展示的是该申请单的审批结果,请根据驳回原因修改信息后重新提交注册申请。',
|
||
descRejectedDefault: '请根据驳回原因修改信息后重新提交注册申请。',
|
||
remarkApproved: '通过备注',
|
||
remarkRejected: '驳回原因',
|
||
remarkPending: '审批备注',
|
||
roleForeign: '外籍人员',
|
||
roleDomestic: '中国籍',
|
||
labelIdentityType: '人员类型',
|
||
labelName: '姓名',
|
||
labelAccount: '注册账号',
|
||
labelCertificateNo: '证件号',
|
||
fallbackNone: '-',
|
||
latestDetailMissing: '未查到最新详情,已展示列表记录',
|
||
approvedPleaseLogin: '审批已通过,请登录',
|
||
fetchStatusFailed: '获取审批状态失败',
|
||
statusPending: '审核中',
|
||
statusApproved: '已通过',
|
||
statusRejected: '已驳回'
|
||
},
|
||
en: {
|
||
navTitle: 'Registration Result',
|
||
submittedInfoTitle: 'Submitted Information',
|
||
goLogin: 'Sign In',
|
||
resubmitRegister: 'Submit Registration Again',
|
||
refreshing: 'Refreshing...',
|
||
refreshStatus: 'Refresh Status',
|
||
viewRegistrationRecords: 'View Registration Records',
|
||
unsubmitted: 'Not Submitted',
|
||
titleUnsubmitted: 'No registration application submitted yet',
|
||
titlePending: 'Registration submitted',
|
||
titleApproved: 'Registration approved',
|
||
titleRejected: 'Registration was not approved',
|
||
descUnsubmitted: 'Please submit a registration application first. Registration is completed only after approval.',
|
||
descPendingRecords: 'This page shows the approval status of the selected application. Pull down or tap the button to refresh.',
|
||
descPendingDefault: 'Your application is under review and sign-in is unavailable before approval.',
|
||
descApproved: 'Your registration is approved and you can sign in now.',
|
||
descRejectedRecords: 'This page shows the approval result of the selected application. Please update the information and resubmit based on the rejection reason.',
|
||
descRejectedDefault: 'Please update the information and resubmit based on the rejection reason.',
|
||
remarkApproved: 'Approval Note',
|
||
remarkRejected: 'Rejection Reason',
|
||
remarkPending: 'Approval Note',
|
||
roleForeign: 'Foreign',
|
||
roleDomestic: 'Chinese',
|
||
labelIdentityType: 'Identity Type',
|
||
labelName: 'Name',
|
||
labelAccount: 'Account',
|
||
labelCertificateNo: 'Certificate No.',
|
||
fallbackNone: '-',
|
||
latestDetailMissing: 'Latest details are unavailable. Showing the list record instead.',
|
||
approvedPleaseLogin: 'Approved. Please sign in.',
|
||
fetchStatusFailed: 'Failed to fetch approval status',
|
||
statusPending: 'Under Review',
|
||
statusApproved: 'Approved',
|
||
statusRejected: 'Rejected'
|
||
}
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
language() {
|
||
return this.$getLanguage()
|
||
},
|
||
authStatus() {
|
||
const status = this.applicationInfo ? this.applicationInfo.approvalStatus : undefined
|
||
return mapRegistrationApprovalToAuthStatus(status)
|
||
},
|
||
applicationCode() {
|
||
return this.targetCode || resolveRegistrationApplicationCode(this.applicationInfo) || ''
|
||
},
|
||
approvalStatusText() {
|
||
const status = this.applicationInfo ? this.applicationInfo.approvalStatus : undefined
|
||
if (typeof status === 'number') {
|
||
return this.getApprovalStatusLabelLocal(status)
|
||
}
|
||
return this.t('unsubmitted')
|
||
},
|
||
titleText() {
|
||
const map = {
|
||
unsubmitted: this.t('titleUnsubmitted'),
|
||
pending: this.t('titlePending'),
|
||
approved: this.t('titleApproved'),
|
||
rejected: this.t('titleRejected')
|
||
}
|
||
return map[this.authStatus] || map.unsubmitted
|
||
},
|
||
descText() {
|
||
const map = {
|
||
unsubmitted: this.t('descUnsubmitted'),
|
||
pending: this.sourceType === 'registration-records'
|
||
? this.t('descPendingRecords')
|
||
: this.t('descPendingDefault'),
|
||
approved: this.t('descApproved'),
|
||
rejected: this.sourceType === 'registration-records'
|
||
? this.t('descRejectedRecords')
|
||
: this.t('descRejectedDefault')
|
||
}
|
||
return map[this.authStatus] || map.unsubmitted
|
||
},
|
||
approvalRemarkTitle() {
|
||
const map = {
|
||
approved: this.t('remarkApproved'),
|
||
rejected: this.t('remarkRejected'),
|
||
pending: this.t('remarkPending'),
|
||
unsubmitted: this.t('remarkPending')
|
||
}
|
||
return map[this.authStatus] || map.pending
|
||
},
|
||
approvalRemark() {
|
||
return this.applicationInfo && this.applicationInfo.approveReason
|
||
? this.applicationInfo.approveReason
|
||
: ''
|
||
},
|
||
remarkIconClass() {
|
||
const map = {
|
||
approved: 'tn-icon-success',
|
||
rejected: 'tn-icon-warning',
|
||
pending: 'tn-icon-time',
|
||
unsubmitted: 'tn-icon-warning'
|
||
}
|
||
return map[this.authStatus] || map.pending
|
||
},
|
||
profileSummary() {
|
||
const profile = this.applicationInfo || {}
|
||
const identityType = profile.passportNumber ? 'foreign' : 'domestic'
|
||
const roleText = identityType === 'foreign' ? this.t('roleForeign') : identityType === 'domestic' ? this.t('roleDomestic') : this.t('fallbackNone')
|
||
const certificateNo = profile.passportNumber || profile.idCard || this.t('fallbackNone')
|
||
return [
|
||
{ label: this.t('labelIdentityType'), value: roleText },
|
||
{ label: this.t('labelName'), value: profile.name || this.t('fallbackNone') },
|
||
{ label: this.t('labelAccount'), value: profile.account || this.t('fallbackNone') },
|
||
{ label: this.t('labelCertificateNo'), value: certificateNo },
|
||
// { label: '国籍', value: profile.country || profile.nationality || '-' },
|
||
// { label: '工作单位', value: profile.workUnit || '-' },
|
||
// { label: '兴趣爱好', value: hobbyText }
|
||
]
|
||
}
|
||
},
|
||
watch: {
|
||
language() {
|
||
this.refreshStatus()
|
||
}
|
||
},
|
||
onLoad(options = {}) {
|
||
this.sourceType = options.from || ''
|
||
this.targetCode = decodeURIComponent(options.code || '').trim()
|
||
|
||
const payloadText = options.payload ? decodeURIComponent(options.payload) : ''
|
||
if (!payloadText) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const payload = JSON.parse(payloadText)
|
||
if (payload && typeof payload === 'object') {
|
||
this.applicationInfo = payload
|
||
if (!this.targetCode) {
|
||
this.targetCode = resolveRegistrationApplicationCode(payload)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('parse auth result payload failed', error)
|
||
}
|
||
},
|
||
async onShow() {
|
||
await fetchEnums(false, this.language)
|
||
this.refreshStatus()
|
||
},
|
||
methods: {
|
||
t(key) {
|
||
return this.$tm('pkgAccount.auth_result.auth_result', key)
|
||
},
|
||
getWechatCode() {
|
||
return new Promise((resolve) => {
|
||
// #ifdef MP-WEIXIN
|
||
uni.login({
|
||
provider: 'weixin',
|
||
success: (res) => {
|
||
resolve(res.code || '')
|
||
},
|
||
fail: (err) => {
|
||
console.error('getWechatCode failed', err)
|
||
resolve('')
|
||
}
|
||
})
|
||
// #endif
|
||
// #ifndef MP-WEIXIN
|
||
resolve('')
|
||
// #endif
|
||
})
|
||
},
|
||
async fetchApplicationByCode(providerValue, applicationCode) {
|
||
if (!providerValue || !applicationCode) {
|
||
return null
|
||
}
|
||
|
||
const response = await registrationApplicationMePage({
|
||
ProviderValue: providerValue,
|
||
Search: applicationCode,
|
||
PageIndex: 1,
|
||
PageSize: 20
|
||
}, this.language)
|
||
const pageData = formatPageResponse(response)
|
||
const list = Array.isArray(pageData.data) ? pageData.data : []
|
||
return list.find(item => resolveRegistrationApplicationCode(item) === applicationCode) || list[0] || null
|
||
},
|
||
async refreshStatus() {
|
||
this.loading = true
|
||
try {
|
||
const providerValue = await this.getWechatCode()
|
||
const applicationCode = this.applicationCode
|
||
let target = null
|
||
|
||
if (applicationCode) {
|
||
target = await this.fetchApplicationByCode(providerValue || '', applicationCode)
|
||
} else {
|
||
const response = await registrationMyorder({
|
||
providerValue: providerValue || ''
|
||
}, this.language)
|
||
const rawTarget = response && response.data && typeof response.data === 'object' ? response.data : response
|
||
target = rawTarget.data !== undefined ? rawTarget.data : rawTarget
|
||
}
|
||
|
||
if (!target || typeof target !== 'object' || !Object.keys(target).length) {
|
||
if (this.applicationInfo && this.applicationCode) {
|
||
uni.showToast({
|
||
title: this.t('latestDetailMissing'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
this.applicationInfo = {
|
||
approvalStatus: 1
|
||
}
|
||
uni.showToast({
|
||
title: this.t('approvedPleaseLogin'),
|
||
icon: 'success'
|
||
})
|
||
setTimeout(() => {
|
||
this.goToLogin()
|
||
}, 800)
|
||
return
|
||
}
|
||
|
||
this.applicationInfo = target
|
||
this.targetCode = resolveRegistrationApplicationCode(target) || this.targetCode
|
||
if (target.approvalStatus === 1) {
|
||
const approvedUser = buildUserFromRegistrationApplication(target, {
|
||
password: ''
|
||
})
|
||
saveRegisteredUser(approvedUser)
|
||
}
|
||
} catch (error) {
|
||
console.error('refresh registration status failed', error)
|
||
uni.showToast({
|
||
title: this.t('fetchStatusFailed'),
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
getApprovalStatusLabelLocal(status) {
|
||
return getApprovalStatusLabel(
|
||
status,
|
||
this.authStatus === 'pending'
|
||
? this.t('statusPending')
|
||
: this.authStatus === 'approved'
|
||
? this.t('statusApproved')
|
||
: this.authStatus === 'rejected'
|
||
? this.t('statusRejected')
|
||
: this.t('unsubmitted'),
|
||
this.language
|
||
)
|
||
},
|
||
goToLogin() {
|
||
uni.reLaunch({
|
||
url: '/pkgAccount/login/login?bypass=1'
|
||
})
|
||
},
|
||
resubmit() {
|
||
uni.redirectTo({
|
||
url: '/pkgAccount/register/register?bypass=1'
|
||
})
|
||
},
|
||
goToRegistrationRecords() {
|
||
uni.navigateTo({
|
||
url: '/pkgAccount/registration_records/registration_records'
|
||
})
|
||
},
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.auth-result-page {
|
||
/* 白蓝主题渐变背景 */
|
||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.status-card {
|
||
background: #ffffff;
|
||
border-radius: 24rpx;
|
||
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
|
||
border: 1rpx solid rgba($brand-primary, 0.05);
|
||
position: relative;
|
||
z-index: 1;
|
||
margin-top: 120rpx;
|
||
padding-bottom: 40rpx;
|
||
}
|
||
|
||
.status-icon-wrapper {
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
margin: 0 auto;
|
||
border-radius: 50%;
|
||
margin-top: -70rpx;
|
||
background: #ffffff;
|
||
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.08);
|
||
}
|
||
|
||
.status-icon-wrapper.approved {
|
||
background: rgba($brand-primary, 0.1);
|
||
}
|
||
|
||
.status-icon-wrapper.rejected {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
}
|
||
|
||
.status-icon-wrapper.pending,
|
||
.status-icon-wrapper.unsubmitted {
|
||
background: rgba(245, 158, 11, 0.12);
|
||
}
|
||
|
||
.status-icon {
|
||
font-size: 80rpx;
|
||
}
|
||
|
||
.icon-success {
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.icon-rejected {
|
||
color: #cf3c3c;
|
||
}
|
||
|
||
.icon-pending,
|
||
.icon-unsubmitted {
|
||
color: #c98512;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 44rpx;
|
||
font-weight: bold;
|
||
color: #1f1f1f;
|
||
}
|
||
|
||
.card-desc {
|
||
font-size: 28rpx;
|
||
color: #888888;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.code-chip {
|
||
width: fit-content;
|
||
max-width: calc(100% - 64rpx);
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
padding: 12rpx 24rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba($brand-primary, 0.08);
|
||
color: $brand-primary;
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.reason-box {
|
||
padding: 30rpx;
|
||
border-radius: 20rpx;
|
||
}
|
||
|
||
.reason-box.approved {
|
||
background: rgba($brand-primary, 0.05);
|
||
border: 1rpx solid rgba($brand-primary, 0.1);
|
||
}
|
||
.reason-box.approved .reason-title,
|
||
.reason-box.approved .reason-content {
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.reason-box.rejected {
|
||
background: rgba(239, 68, 68, 0.05);
|
||
border: 1rpx solid rgba(239, 68, 68, 0.1);
|
||
}
|
||
.reason-box.rejected .reason-title,
|
||
.reason-box.rejected .reason-content {
|
||
color: #ef4444;
|
||
}
|
||
|
||
.reason-box.pending,
|
||
.reason-box.unsubmitted {
|
||
background: rgba(245, 158, 11, 0.05);
|
||
border: 1rpx solid rgba(245, 158, 11, 0.1);
|
||
}
|
||
.reason-box.pending .reason-title,
|
||
.reason-box.pending .reason-content,
|
||
.reason-box.unsubmitted .reason-title,
|
||
.reason-box.unsubmitted .reason-content {
|
||
color: #f59e0b;
|
||
}
|
||
|
||
.reason-title {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.reason-content {
|
||
font-size: 26rpx;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.summary-box {
|
||
padding: 30rpx;
|
||
background: rgba($brand-primary, 0.02);
|
||
border-radius: 20rpx;
|
||
border: 1rpx solid rgba($brand-primary, 0.1);
|
||
}
|
||
|
||
.summary-title {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #1A2233;
|
||
}
|
||
|
||
.summary-row {
|
||
border-bottom: 1rpx dashed rgba($brand-primary, 0.15);
|
||
}
|
||
.summary-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.summary-label {
|
||
font-size: 28rpx;
|
||
color: #6A7A99;
|
||
}
|
||
|
||
.summary-value {
|
||
font-size: 28rpx;
|
||
color: #1A2233;
|
||
font-weight: 500;
|
||
max-width: 65%;
|
||
text-align: right;
|
||
}
|
||
|
||
.custom-btn {
|
||
width: 100%;
|
||
height: 96rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 999rpx;
|
||
font-size: 32rpx;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.custom-btn:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
|
||
color: #ffffff;
|
||
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.25);
|
||
}
|
||
|
||
.btn-dark {
|
||
background: #ffffff;
|
||
color: $brand-primary;
|
||
border: 1rpx solid rgba($brand-primary, 0.3);
|
||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||
}
|
||
|
||
.btn-light {
|
||
background: #ffffff;
|
||
color: #1A2233;
|
||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.04);
|
||
}
|
||
|
||
/* 动画效果 */
|
||
.fade-in {
|
||
animation: fadeIn 0.6s ease-out forwards;
|
||
}
|
||
|
||
.fade-in-up {
|
||
animation: fadeInUp 0.6s ease-out forwards;
|
||
animation-delay: 0.2s;
|
||
opacity: 0;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
@keyframes fadeInUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(40rpx);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.spin {
|
||
display: inline-block;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
</style>
|