All checks were successful
Build and Deploy Front / deploy (push) Successful in 7m24s
Co-authored-by: Cursor <cursoragent@cursor.com>
755 lines
18 KiB
Vue
755 lines
18 KiB
Vue
<template>
|
|
<a-drawer
|
|
:visible="visible"
|
|
placement="bottom"
|
|
height="auto"
|
|
:footer="false"
|
|
:header="false"
|
|
unmount-on-close
|
|
:mask-closable="!saving"
|
|
:drawer-style="drawerStyle"
|
|
popup-container="body"
|
|
class="h5-addr-drawer"
|
|
@cancel="close"
|
|
>
|
|
<div class="h5-addr">
|
|
<div class="h5-addr__handle" aria-hidden="true" />
|
|
|
|
<header class="h5-addr__head">
|
|
<button
|
|
v-if="mode === 'form'"
|
|
type="button"
|
|
class="h5-addr__back"
|
|
:disabled="saving"
|
|
@click="backToList"
|
|
>
|
|
<icon-left />
|
|
</button>
|
|
<h3>{{ headTitle }}</h3>
|
|
<button type="button" class="h5-addr__close" :disabled="saving" @click="close">
|
|
<icon-close />
|
|
</button>
|
|
</header>
|
|
|
|
<!-- 列表 -->
|
|
<template v-if="mode === 'list'">
|
|
<div class="h5-addr__tabs">
|
|
<button
|
|
v-for="tab in typeTabs"
|
|
:key="tab"
|
|
type="button"
|
|
class="h5-addr__tab"
|
|
:class="{ 'is-active': activeType === tab }"
|
|
@click="switchType(tab)"
|
|
>
|
|
{{ t(`h5.address.type.${tab}`) }}
|
|
</button>
|
|
</div>
|
|
|
|
<a-spin :loading="loading" class="h5-addr__body">
|
|
<H5EmptyState
|
|
v-if="!rows.length && !loading"
|
|
:icon="IconLocation"
|
|
:text="t('h5.address.empty')"
|
|
/>
|
|
|
|
<article v-for="row in rows" :key="row.id" class="addr-card">
|
|
<div class="addr-card__main" @click="openEdit(row)">
|
|
<div class="addr-card__top">
|
|
<strong>{{ row.senderName || '--' }}</strong>
|
|
<span v-if="row.isDefault" class="addr-card__badge">{{ t('h5.address.default') }}</span>
|
|
</div>
|
|
<p class="addr-card__phone">{{ row.phone || '--' }}</p>
|
|
<p class="addr-card__line">
|
|
{{ [row.province, row.city, row.streetAddress1].filter(Boolean).join(' ') || '--' }}
|
|
</p>
|
|
<p v-if="row.addressName" class="addr-card__alias">{{ row.addressName }}</p>
|
|
</div>
|
|
<div class="addr-card__actions">
|
|
<button
|
|
v-if="!row.isDefault"
|
|
type="button"
|
|
class="addr-card__act"
|
|
@click="setDefault(row)"
|
|
>
|
|
{{ t('h5.address.setDefault') }}
|
|
</button>
|
|
<button type="button" class="addr-card__act" @click="openEdit(row)">
|
|
{{ t('h5.address.edit') }}
|
|
</button>
|
|
<button type="button" class="addr-card__act is-danger" @click="remove(row)">
|
|
{{ t('h5.address.delete') }}
|
|
</button>
|
|
</div>
|
|
</article>
|
|
</a-spin>
|
|
|
|
<footer class="h5-addr__foot">
|
|
<button type="button" class="h5-addr__btn" @click="openCreate">
|
|
{{ t('h5.address.add') }}
|
|
</button>
|
|
</footer>
|
|
</template>
|
|
|
|
<!-- 表单 -->
|
|
<template v-else>
|
|
<div class="h5-addr__body">
|
|
<section class="h5-addr__card">
|
|
<label class="h5-addr__label">{{ t('h5.address.typeLabel') }}</label>
|
|
<div class="h5-addr__type-seg">
|
|
<button
|
|
v-for="tab in typeTabs"
|
|
:key="tab"
|
|
type="button"
|
|
class="h5-addr__seg"
|
|
:class="{ 'is-active': form.addressType === tab }"
|
|
@click="form.addressType = tab"
|
|
>
|
|
{{ t(`h5.address.type.${tab}`) }}
|
|
</button>
|
|
</div>
|
|
|
|
<label class="h5-addr__label">{{ t('h5.address.senderName') }}</label>
|
|
<a-input v-model="form.senderName" allow-clear :placeholder="t('h5.address.senderNamePh')" />
|
|
|
|
<label class="h5-addr__label">{{ t('h5.address.phone') }}</label>
|
|
<a-input v-model="form.phone" allow-clear />
|
|
|
|
<label class="h5-addr__label">{{ t('h5.address.addressName') }}</label>
|
|
<a-input v-model="form.addressName" allow-clear :placeholder="t('h5.address.addressNamePh')" />
|
|
|
|
<label class="h5-addr__label">{{ t('h5.address.countryCode') }}</label>
|
|
<a-input v-model="form.countryCode" allow-clear />
|
|
|
|
<div class="h5-addr__two">
|
|
<div>
|
|
<label class="h5-addr__label">{{ t('h5.address.province') }}</label>
|
|
<a-input v-model="form.province" allow-clear />
|
|
</div>
|
|
<div>
|
|
<label class="h5-addr__label">{{ t('h5.address.city') }}</label>
|
|
<a-input v-model="form.city" allow-clear />
|
|
</div>
|
|
</div>
|
|
|
|
<label class="h5-addr__label">{{ t('h5.address.street') }}</label>
|
|
<a-textarea
|
|
v-model="form.streetAddress1"
|
|
:auto-size="{ minRows: 2, maxRows: 4 }"
|
|
:placeholder="t('h5.address.streetPh')"
|
|
/>
|
|
|
|
<label class="h5-addr__label">{{ t('h5.address.zipCode') }}</label>
|
|
<a-input v-model="form.zipCode" allow-clear />
|
|
|
|
<button
|
|
type="button"
|
|
class="h5-addr__default-toggle"
|
|
:class="{ 'is-on': form.isDefault }"
|
|
@click="form.isDefault = !form.isDefault"
|
|
>
|
|
<span class="dot" aria-hidden="true" />
|
|
{{ t('h5.address.setDefaultTip') }}
|
|
</button>
|
|
</section>
|
|
</div>
|
|
|
|
<footer class="h5-addr__foot">
|
|
<button type="button" class="h5-addr__btn" :disabled="saving" @click="save">
|
|
<span v-if="saving" class="h5-addr__loading" aria-hidden="true" />
|
|
{{ t('h5.me.save') }}
|
|
</button>
|
|
</footer>
|
|
</template>
|
|
</div>
|
|
</a-drawer>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { Message, Modal } from '@arco-design/web-vue';
|
|
import { IconClose, IconLeft, IconLocation } from '@arco-design/web-vue/es/icon';
|
|
import {
|
|
createSenderAddress,
|
|
deleteSenderAddress,
|
|
getSenderAddressList,
|
|
setDefaultSenderAddress,
|
|
updateSenderAddress,
|
|
} from '@/api/order/senderAddress';
|
|
import { pickField } from '@/utils/warehouseListMapper';
|
|
import H5EmptyState from './H5EmptyState.vue';
|
|
|
|
defineOptions({ name: 'H5AddressSheet' });
|
|
|
|
type AddressType = 'sender' | 'receiver' | 'return' | 'pickup';
|
|
|
|
type AddressRow = {
|
|
id: number;
|
|
addressType: AddressType;
|
|
addressName: string;
|
|
senderName: string;
|
|
phone: string;
|
|
countryCode: string;
|
|
province: string;
|
|
city: string;
|
|
zipCode: string;
|
|
streetAddress1: string;
|
|
isDefault: boolean;
|
|
_raw: Record<string, unknown>;
|
|
};
|
|
|
|
const props = defineProps<{
|
|
visible: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
'update:visible': [value: boolean];
|
|
}>();
|
|
|
|
const { t } = useI18n();
|
|
const typeTabs: AddressType[] = ['sender', 'pickup', 'return', 'receiver'];
|
|
const activeType = ref<AddressType>('sender');
|
|
const mode = ref<'list' | 'form'>('list');
|
|
const loading = ref(false);
|
|
const saving = ref(false);
|
|
const rows = ref<AddressRow[]>([]);
|
|
const editingId = ref(0);
|
|
|
|
const form = reactive({
|
|
addressType: 'sender' as AddressType,
|
|
addressName: '',
|
|
senderName: '',
|
|
phone: '',
|
|
countryCode: 'CN',
|
|
province: '',
|
|
city: '',
|
|
zipCode: '',
|
|
streetAddress1: '',
|
|
isDefault: false,
|
|
});
|
|
|
|
const drawerStyle = {
|
|
borderRadius: '20px 20px 0 0',
|
|
overflow: 'hidden',
|
|
maxHeight: 'min(92vh, 780px)',
|
|
};
|
|
|
|
const headTitle = computed(() => {
|
|
if (mode.value === 'list') return t('h5.address.title');
|
|
return editingId.value ? t('h5.address.editTitle') : t('h5.address.addTitle');
|
|
});
|
|
|
|
function close() {
|
|
if (saving.value) return;
|
|
emit('update:visible', false);
|
|
}
|
|
|
|
function backToList() {
|
|
if (saving.value) return;
|
|
mode.value = 'list';
|
|
}
|
|
|
|
function resetForm(type: AddressType = activeType.value) {
|
|
editingId.value = 0;
|
|
Object.assign(form, {
|
|
addressType: type,
|
|
addressName: '',
|
|
senderName: '',
|
|
phone: '',
|
|
countryCode: 'CN',
|
|
province: '',
|
|
city: '',
|
|
zipCode: '',
|
|
streetAddress1: '',
|
|
isDefault: rows.value.length === 0,
|
|
});
|
|
}
|
|
|
|
function mapRow(raw: Record<string, unknown>): AddressRow | null {
|
|
const id = Number(pickField(raw, 'id', 'Id'));
|
|
if (!Number.isFinite(id) || id <= 0) return null;
|
|
return {
|
|
id,
|
|
addressType: String(pickField(raw, 'addressType', 'AddressType') || 'sender') as AddressType,
|
|
addressName: String(pickField(raw, 'addressName', 'AddressName') || ''),
|
|
senderName: String(pickField(raw, 'senderName', 'SenderName') || ''),
|
|
phone: String(pickField(raw, 'phone', 'Phone') || ''),
|
|
countryCode: String(pickField(raw, 'countryCode', 'CountryCode') || 'CN'),
|
|
province: String(pickField(raw, 'province', 'Province') || ''),
|
|
city: String(pickField(raw, 'city', 'City') || ''),
|
|
zipCode: String(pickField(raw, 'zipCode', 'ZipCode') || ''),
|
|
streetAddress1: String(pickField(raw, 'streetAddress1', 'StreetAddress1') || ''),
|
|
isDefault: Boolean(pickField(raw, 'isDefault', 'IsDefault')),
|
|
_raw: raw,
|
|
};
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true;
|
|
try {
|
|
const res = await getSenderAddressList({
|
|
page: 1,
|
|
pageSize: 100,
|
|
addressType: activeType.value,
|
|
});
|
|
rows.value = (res.items || [])
|
|
.map((raw: any) => mapRow(raw))
|
|
.filter(Boolean) as AddressRow[];
|
|
} catch {
|
|
rows.value = [];
|
|
Message.error(t('h5.address.loadFailed'));
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function switchType(tab: AddressType) {
|
|
activeType.value = tab;
|
|
void load();
|
|
}
|
|
|
|
function openCreate() {
|
|
resetForm(activeType.value);
|
|
mode.value = 'form';
|
|
}
|
|
|
|
function openEdit(row: AddressRow) {
|
|
editingId.value = row.id;
|
|
Object.assign(form, {
|
|
addressType: row.addressType || activeType.value,
|
|
addressName: row.addressName,
|
|
senderName: row.senderName,
|
|
phone: row.phone,
|
|
countryCode: row.countryCode || 'CN',
|
|
province: row.province,
|
|
city: row.city,
|
|
zipCode: row.zipCode,
|
|
streetAddress1: row.streetAddress1,
|
|
isDefault: row.isDefault,
|
|
});
|
|
mode.value = 'form';
|
|
}
|
|
|
|
async function save() {
|
|
if (!form.senderName.trim()) {
|
|
Message.warning(t('h5.address.nameRequired'));
|
|
return;
|
|
}
|
|
if (!form.streetAddress1.trim()) {
|
|
Message.warning(t('h5.address.streetRequired'));
|
|
return;
|
|
}
|
|
saving.value = true;
|
|
try {
|
|
const body = {
|
|
id: editingId.value || undefined,
|
|
addressType: form.addressType,
|
|
addressName: form.addressName.trim() || form.senderName.trim(),
|
|
senderName: form.senderName.trim(),
|
|
phone: form.phone.trim(),
|
|
countryCode: (form.countryCode || 'CN').trim().toUpperCase(),
|
|
province: form.province.trim(),
|
|
city: form.city.trim(),
|
|
zipCode: form.zipCode.trim(),
|
|
streetAddress1: form.streetAddress1.trim(),
|
|
isDefault: form.isDefault,
|
|
};
|
|
if (editingId.value) {
|
|
await updateSenderAddress({ ...body, id: editingId.value });
|
|
} else {
|
|
await createSenderAddress(body);
|
|
}
|
|
Message.success(t('h5.me.updateSuccess'));
|
|
activeType.value = form.addressType;
|
|
mode.value = 'list';
|
|
await load();
|
|
} catch (e: any) {
|
|
Message.error(e?.message || t('h5.address.saveFailed'));
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
|
|
async function setDefault(row: AddressRow) {
|
|
try {
|
|
await setDefaultSenderAddress(row.id);
|
|
Message.success(t('h5.address.setDefaultOk'));
|
|
await load();
|
|
} catch (e: any) {
|
|
Message.error(e?.message || t('h5.address.saveFailed'));
|
|
}
|
|
}
|
|
|
|
function remove(row: AddressRow) {
|
|
Modal.confirm({
|
|
title: t('h5.address.delete'),
|
|
content: row.senderName || t('h5.address.title'),
|
|
onOk: async () => {
|
|
await deleteSenderAddress(row.id);
|
|
Message.success(t('h5.address.deleteOk'));
|
|
await load();
|
|
},
|
|
});
|
|
}
|
|
|
|
watch(
|
|
() => props.visible,
|
|
(visible) => {
|
|
if (!visible) return;
|
|
mode.value = 'list';
|
|
activeType.value = 'sender';
|
|
void load();
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
</script>
|
|
|
|
<style lang="less">
|
|
.h5-addr-drawer {
|
|
.arco-drawer-content {
|
|
border-radius: 20px 20px 0 0;
|
|
overflow: hidden;
|
|
max-height: min(92vh, 780px);
|
|
}
|
|
.arco-drawer-body {
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-height: min(92vh, 780px);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style lang="less" scoped>
|
|
.h5-addr {
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-height: min(92vh, 780px);
|
|
background: #f5f7f8;
|
|
}
|
|
|
|
.h5-addr__handle {
|
|
width: 36px;
|
|
height: 4px;
|
|
margin: 10px auto 0;
|
|
border-radius: 999px;
|
|
background: #c9cdd4;
|
|
}
|
|
|
|
.h5-addr__head {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 48px;
|
|
flex-shrink: 0;
|
|
|
|
h3 {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #1d2129;
|
|
}
|
|
}
|
|
|
|
.h5-addr__back,
|
|
.h5-addr__close {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 36px;
|
|
height: 36px;
|
|
border: none;
|
|
border-radius: 10px;
|
|
background: transparent;
|
|
color: #86909c;
|
|
font-size: 18px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
|
|
&:disabled {
|
|
opacity: 0.5;
|
|
}
|
|
}
|
|
|
|
.h5-addr__back {
|
|
left: 8px;
|
|
color: #1d2129;
|
|
}
|
|
|
|
.h5-addr__close {
|
|
right: 12px;
|
|
}
|
|
|
|
.h5-addr__tabs {
|
|
display: flex;
|
|
gap: 4px;
|
|
margin: 0 16px 12px;
|
|
padding: 3px;
|
|
border-radius: 12px;
|
|
background: #eef0f3;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.h5-addr__tab {
|
|
flex: 1;
|
|
height: 32px;
|
|
border: none;
|
|
border-radius: 10px;
|
|
background: transparent;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #86909c;
|
|
cursor: pointer;
|
|
|
|
&.is-active {
|
|
background: #fff;
|
|
color: #1d2129;
|
|
font-weight: 600;
|
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
|
|
}
|
|
}
|
|
|
|
.h5-addr__body {
|
|
flex: 1;
|
|
min-height: 220px;
|
|
overflow: auto;
|
|
display: block;
|
|
width: 100%;
|
|
padding: 0 16px 12px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.addr-card {
|
|
background: #fff;
|
|
border-radius: 16px;
|
|
padding: 14px;
|
|
margin-bottom: 10px;
|
|
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
|
|
}
|
|
|
|
.addr-card__main {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.addr-card__top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
|
|
strong {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: #1d2129;
|
|
}
|
|
}
|
|
|
|
.addr-card__badge {
|
|
height: 22px;
|
|
padding: 0 8px;
|
|
border-radius: 8px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--app-primary-color);
|
|
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
|
}
|
|
|
|
.addr-card__phone {
|
|
margin: 6px 0 0;
|
|
font-size: 13px;
|
|
color: #4e5969;
|
|
}
|
|
|
|
.addr-card__line {
|
|
margin: 4px 0 0;
|
|
font-size: 13px;
|
|
color: #1d2129;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.addr-card__alias {
|
|
margin: 6px 0 0;
|
|
font-size: 12px;
|
|
color: #86909c;
|
|
}
|
|
|
|
.addr-card__actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.addr-card__act {
|
|
height: 30px;
|
|
padding: 0 12px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: #f2f3f5;
|
|
color: #4e5969;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
|
|
&.is-danger {
|
|
color: #f53f3f;
|
|
background: color-mix(in srgb, #f53f3f 10%, #fff);
|
|
}
|
|
}
|
|
|
|
.h5-addr__card {
|
|
background: #fff;
|
|
border-radius: 16px;
|
|
padding: 16px;
|
|
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
|
|
|
|
:deep(.arco-input-wrapper),
|
|
:deep(.arco-textarea-wrapper) {
|
|
background: #f7f8fa !important;
|
|
border: none !important;
|
|
box-shadow: none !important;
|
|
border-radius: 12px;
|
|
min-height: 44px;
|
|
}
|
|
|
|
:deep(.arco-input-wrapper.arco-input-focus),
|
|
:deep(.arco-textarea-wrapper.arco-textarea-focus) {
|
|
background: color-mix(in srgb, var(--app-primary-color) 8%, #fff) !important;
|
|
}
|
|
}
|
|
|
|
.h5-addr__label {
|
|
display: block;
|
|
margin: 14px 0 8px;
|
|
font-size: 13px;
|
|
color: #4e5969;
|
|
|
|
&:first-child {
|
|
margin-top: 0;
|
|
}
|
|
}
|
|
|
|
.h5-addr__type-seg {
|
|
display: flex;
|
|
gap: 4px;
|
|
padding: 3px;
|
|
border-radius: 12px;
|
|
background: #eef0f3;
|
|
}
|
|
|
|
.h5-addr__seg {
|
|
flex: 1;
|
|
height: 34px;
|
|
border: none;
|
|
border-radius: 10px;
|
|
background: transparent;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #86909c;
|
|
cursor: pointer;
|
|
|
|
&.is-active {
|
|
background: #fff;
|
|
color: #1d2129;
|
|
font-weight: 600;
|
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
|
|
}
|
|
}
|
|
|
|
.h5-addr__two {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 10px;
|
|
}
|
|
|
|
.h5-addr__default-toggle {
|
|
width: 100%;
|
|
margin-top: 16px;
|
|
height: 44px;
|
|
border: none;
|
|
border-radius: 12px;
|
|
background: #f7f8fa;
|
|
color: #4e5969;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 0 14px;
|
|
cursor: pointer;
|
|
|
|
.dot {
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: #e5e6eb;
|
|
position: relative;
|
|
|
|
&::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 4px;
|
|
border-radius: 50%;
|
|
background: #fff;
|
|
}
|
|
}
|
|
|
|
&.is-on {
|
|
background: color-mix(in srgb, var(--app-primary-color) 10%, #fff);
|
|
color: var(--app-primary-color);
|
|
|
|
.dot {
|
|
background: var(--app-primary-color);
|
|
}
|
|
}
|
|
}
|
|
|
|
.h5-addr__foot {
|
|
flex-shrink: 0;
|
|
padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px));
|
|
background: #fff;
|
|
box-shadow: 0 -6px 20px rgba(15, 23, 42, 0.04);
|
|
}
|
|
|
|
.h5-addr__btn {
|
|
width: 100%;
|
|
height: 48px;
|
|
border: none;
|
|
border-radius: 12px;
|
|
background: var(--app-primary-color);
|
|
color: #fff;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
box-shadow: 0 8px 18px color-mix(in srgb, var(--app-primary-color) 28%, transparent);
|
|
|
|
&:disabled {
|
|
opacity: 0.7;
|
|
box-shadow: none;
|
|
}
|
|
}
|
|
|
|
.h5-addr__loading {
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 2px solid rgba(255, 255, 255, 0.35);
|
|
border-top-color: #fff;
|
|
border-radius: 50%;
|
|
animation: h5-addr-spin 0.7s linear infinite;
|
|
}
|
|
|
|
@keyframes h5-addr-spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
</style>
|