feat: 新增工单中心、房间巡查、失物招领等业务模块
- 新增工单中心(ticket_center)与工单工作台(ticket_workbench)页面 - 新增房间巡查(room_inspection)与失物招领发布(lost_found_publish)页面 - 预约看样流程新增房型/楼栋/楼层选择器组件 - 新增工具模块: booking-entry、privacy、reservation-flow - api/index.js 补充楼层列表、看样提交等接口 - locales/messages.js 补充多语言文案 - manifest.json 开启微信小程序隐私检查 - 完善登录、注册、申请详情、账单等相关页面交互
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<view class="profile-page tn-safe-area-inset-bottom">
|
||||
|
||||
<!-- 头部背景与用户信息 (悬浮质感设计) -->
|
||||
@@ -574,6 +574,11 @@ export default {
|
||||
url: '/pkgService/utility_recharge/utility_recharge'
|
||||
});
|
||||
},
|
||||
goToTicketCenter() {
|
||||
uni.navigateTo({
|
||||
url: '/pkgService/ticket_center/ticket_center'
|
||||
})
|
||||
},
|
||||
goToSubmitRequest(title, desc, payload = {}, titleZh = '', titleEn = '', descZh = '', descEn = '') {
|
||||
uni.navigateTo({
|
||||
url: `/pkgService/submit_request/submit_request?title=${encodeURIComponent(title || this.t('submitRequest'))}&titleZh=${encodeURIComponent(titleZh || title || this.t('submitRequest'))}&titleEn=${encodeURIComponent(titleEn || title || this.t('submitRequest'))}&desc=${encodeURIComponent(desc || this.t('completeFormBeforeSubmit'))}&descZh=${encodeURIComponent(descZh || desc || this.t('completeFormBeforeSubmit'))}&descEn=${encodeURIComponent(descEn || desc || this.t('completeFormBeforeSubmit'))}&payload=${encodeURIComponent(JSON.stringify(payload || {}))}`
|
||||
@@ -608,7 +613,7 @@ export default {
|
||||
return
|
||||
}
|
||||
if (item.key === 'serviceOrder') {
|
||||
this.handleApplicationEntry()
|
||||
this.goToTicketCenter()
|
||||
return
|
||||
}
|
||||
if (item.key === 'registrationRecord') {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<view class="service-page tn-safe-area-inset-bottom">
|
||||
<tn-nav-bar :isBack="false" :bottomShadow="false" backgroundColor="transparent" :customBackStyle="{ transition: 'all 0.3s ease' }">
|
||||
<tn-nav-bar :isBack="false" :bottomShadow="false" :backgroundColor="navBarBackgroundColor" :customBackStyle="{ transition: 'all 0.3s ease' }">
|
||||
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('title') }}</view>
|
||||
</tn-nav-bar>
|
||||
|
||||
<scroll-view class="service-scroll" scroll-y :style="{paddingTop: vuex_custom_bar_height + 'px'}">
|
||||
<scroll-view class="service-scroll" scroll-y :style="{paddingTop: vuex_custom_bar_height + 'px'}" @scroll="handleScroll">
|
||||
<view v-if="!hasCheckedIn" class="empty-state tn-padding animation-fade-in">
|
||||
<!-- 无预订:去选房 -->
|
||||
<view v-if="!currentBooking" class="no-booking-card">
|
||||
@@ -112,21 +112,6 @@
|
||||
|
||||
<!-- 已入住状态下的服务大厅内容 -->
|
||||
<view v-else class="animation-fade-in tn-padding-bottom">
|
||||
<view class="tn-margin-top tn-margin-left tn-margin-right">
|
||||
<view class="tn-text-bold tn-text tn-color-black tn-margin-bottom">{{ t('stayService') }}</view>
|
||||
<view class="tn-flex tn-flex-row-between">
|
||||
<view
|
||||
v-for="item in stayActions"
|
||||
:key="item.key"
|
||||
class="stay-action-card"
|
||||
@click="openServicePage(item)"
|
||||
>
|
||||
<view class="stay-action-icon"><text :class="item.icon" class="stay-action-icon-symbol"></text></view>
|
||||
<view class="stay-action-title tn-margin-top-sm">{{ pickItemText(item, 'label', 'labelEn') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 融合房卡与智能控制的新卡片 -->
|
||||
<view class="integrated-room-card tn-margin-left tn-margin-right tn-margin-top">
|
||||
<!-- 房卡背景 -->
|
||||
@@ -181,6 +166,32 @@
|
||||
</view>
|
||||
|
||||
<view class="tn-margin-top tn-margin-left tn-margin-right">
|
||||
<view class="tn-text-bold tn-text tn-color-black tn-margin-bottom">{{ t('stayService') }}</view>
|
||||
|
||||
<view class="tn-flex tn-flex-wrap tn-flex-row-between">
|
||||
<view class="service-item tn-margin-bottom-sm" v-for="item in stayActions" :key="item.key" @click="openServicePage(item)">
|
||||
<view class="service-card-visual">
|
||||
<image :src="item.image" mode="aspectFill" class="service-card-bg"></image>
|
||||
<view class="service-card-mask"></view>
|
||||
<view class="service-card-content tn-padding-sm tn-flex tn-flex-direction-column tn-flex-row-between" style="height: 100%;">
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<view class="service-icon-box tn-flex tn-flex-col-center tn-flex-row-center">
|
||||
<text :class="item.icon" class="service-icon-symbol"></text>
|
||||
</view>
|
||||
<view class="status-tag tn-text-xs" :class="item.isBusy ? 'tag-busy' : 'tag-free'">
|
||||
{{ pickItemText(item, 'statusZh', 'statusEn') }}
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<view class="tn-text-bold tn-text tn-color-white">{{ pickItemText(item, 'label', 'labelEn') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tn-margin-top-sm tn-margin-left tn-margin-right">
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-bottom tn-margin-bottom">
|
||||
<view class="tn-text-bold tn-text tn-color-black">{{ t('publicService') }}</view>
|
||||
<view class="tn-text-sm tn-color-gray">{{ t('viewAll') }} <text class="tn-icon-right section-arrow-icon"></text></view>
|
||||
@@ -238,36 +249,38 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 专属管家 (原活动位置) -->
|
||||
<!-- 宿管与管理员列表 -->
|
||||
<view class="butler-card-new tn-margin-top tn-margin-left tn-margin-right">
|
||||
<view class="tn-text-bold tn-text tn-color-black tn-margin-bottom">{{ t('butler') }}</view>
|
||||
<view class="tn-text-bold tn-text tn-color-black tn-margin-bottom">{{ t('managementContacts') }}</view>
|
||||
<view class="butler-card-new__inner">
|
||||
<view v-if="areaManagers.length" class="butler-card-new__list">
|
||||
<view v-if="managerContacts.length" class="butler-card-new__list">
|
||||
<view
|
||||
v-for="(manager, index) in areaManagers"
|
||||
:key="manager.userCode || manager.userName || manager.name || index"
|
||||
v-for="(contact, index) in managerContacts"
|
||||
:key="contact.contactKey || index"
|
||||
class="butler-member-new"
|
||||
>
|
||||
<view class="butler-member-new__identity">
|
||||
<view class="butler-avatar-new">
|
||||
<image v-if="getManagerAvatar(manager)" :src="getManagerAvatar(manager)" mode="aspectFill"></image>
|
||||
<image v-if="getManagerAvatar(contact)" :src="getManagerAvatar(contact)" mode="aspectFill"></image>
|
||||
<view v-else class="butler-avatar-new-fallback tn-flex tn-flex-col-center tn-flex-row-center">
|
||||
<text class="butler-avatar-new-fallback-text">{{ getManagerInitial(manager) }}</text>
|
||||
<text class="butler-avatar-new-fallback-text">{{ getManagerInitial(contact) }}</text>
|
||||
</view>
|
||||
<view class="online-dot"></view>
|
||||
</view>
|
||||
<view class="butler-member-new__info">
|
||||
<view class="butler-member-new__name">
|
||||
{{ getManagerName(manager) }}
|
||||
<text v-if="index === 0" class="butler-member-new__tag">{{ t('primaryButlerTag') }}</text>
|
||||
{{ getManagerName(contact) }}
|
||||
<text class="butler-member-new__tag butler-member-new__tag--type" :class="contact.contactType === 'counselor' ? 'tag-counselor' : 'tag-dormitory'">
|
||||
{{ contact.contactType === 'counselor' ? t('counselorTag') : t('dormitoryManagerTag') }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="butler-member-new__phone">{{ getManagerPhone(manager) || t('noContactPhone') }}</view>
|
||||
<view class="butler-member-new__phone">{{ getManagerPhone(contact) || t('noContactPhone') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="butler-member-new__action"
|
||||
:class="{ 'disabled': !getManagerPhone(manager) }"
|
||||
@click.stop="handleAreaManagerCall(manager)"
|
||||
:class="{ 'disabled': !getManagerPhone(contact) }"
|
||||
@click.stop="handleAreaManagerCall(contact)"
|
||||
>
|
||||
<text class="tn-icon-phone-fill"></text>
|
||||
</view>
|
||||
@@ -278,12 +291,12 @@
|
||||
<view class="butler-member-new__identity">
|
||||
<view class="butler-avatar-new">
|
||||
<view class="butler-avatar-new-fallback tn-flex tn-flex-col-center tn-flex-row-center">
|
||||
<text class="tn-icon-service-fill" style="font-size: 32rpx; color: #fff;"></text>
|
||||
<text class="tn-icon-my-fill" style="font-size: 32rpx; color: #fff;"></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="butler-member-new__info">
|
||||
<view class="butler-member-new__name">{{ t('butler') }}</view>
|
||||
<view class="butler-member-new__phone">{{ isAreaManagerLoading ? t('loadingButlerInfo') : t('noButlerInfo') }}</view>
|
||||
<view class="butler-member-new__name">{{ t('managementContacts') }}</view>
|
||||
<view class="butler-member-new__phone">{{ isManagementContactsLoading ? t('loadingButlerInfo') : t('noManagerContacts') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -296,7 +309,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { accountQueryBills, facilityManagers, gatewayRoomDeviceList, orderSearchedDetail } from '../../../api/index.js'
|
||||
import { accountQueryBills, facilityManagers, myDormitoryManagers, myCounselor, gatewayRoomDeviceList, orderSearchedDetail } from '../../../api/index.js'
|
||||
import { getSessionSnapshot, saveMyRoomData } from '../../../utils/app-state'
|
||||
import { ACCOUNT_BILL_PAYMENT_STATUS, ACCOUNT_BILL_STATUS, fetchEnums, getAccountReferenceTypeLabel, getApplicationDetailStatusLabel } from '../../../utils/enum-store'
|
||||
import { getBookingStatusText } from '../../../utils/flow'
|
||||
@@ -400,14 +413,62 @@ export default {
|
||||
areaManager: null,
|
||||
areaManagers: [],
|
||||
isAreaManagerLoading: false,
|
||||
// 宿管与管理员联系人
|
||||
dormitoryManagers: [],
|
||||
counselor: null,
|
||||
isManagementContactsLoading: false,
|
||||
// 导航栏滚动背景色
|
||||
navBarBackgroundColor: 'transparent',
|
||||
unpaidBillList: [],
|
||||
smartDevices: [],
|
||||
isSmartDevicesLoading: false,
|
||||
smartDeviceError: '',
|
||||
stayActions: [
|
||||
{ key: 'change-room', label: '换房申请', labelEn: 'Change Room', icon: 'tn-icon-home-smile-fill', title: '换房申请', titleEn: 'Change Room Request', titleEnShort: 'Room Change', desc: '提交换房原因、期望房型和管家审核说明。', descEn: 'Submit your reason, expected room type, and butler review notes.' },
|
||||
{ key: 'renew-stay', label: '续住申请', labelEn: 'Renew Stay', icon: 'tn-icon-calendar-fill', title: '续住申请', titleEn: 'Renew Stay Request', titleEnShort: 'Renew Stay', desc: '提交续住时长、续住原因和新的租期需求。', descEn: 'Submit extension duration, reason, and new lease request.' },
|
||||
{ key: 'checkout', label: '退房申请', labelEn: 'Check Out', icon: 'tn-icon-home-in-fill', title: '退房申请', titleEn: 'Checkout Request', titleEnShort: 'Check Out', desc: '提交退房时间、离场说明和验房申请。', descEn: 'Submit checkout time, leave notes, and inspection request.' }
|
||||
{
|
||||
key: 'change-room',
|
||||
label: '换房申请',
|
||||
labelEn: 'Change Room',
|
||||
icon: 'tn-icon-home-smile-fill',
|
||||
statusZh: '可提交',
|
||||
statusEn: 'Available',
|
||||
isBusy: false,
|
||||
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_11.png',
|
||||
title: '换房申请',
|
||||
titleEn: 'Change Room Request',
|
||||
titleEnShort: 'Room Change',
|
||||
desc: '提交换房原因、期望房型和管家审核说明。',
|
||||
descEn: 'Submit your reason, expected room type, and butler review notes.'
|
||||
},
|
||||
{
|
||||
key: 'renew-stay',
|
||||
label: '续住申请',
|
||||
labelEn: 'Renew Stay',
|
||||
icon: 'tn-icon-calendar-fill',
|
||||
statusZh: '可提交',
|
||||
statusEn: 'Available',
|
||||
isBusy: false,
|
||||
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_16.png',
|
||||
title: '续住申请',
|
||||
titleEn: 'Renew Stay Request',
|
||||
titleEnShort: 'Renew Stay',
|
||||
desc: '提交续住时长、续住原因和新的租期需求。',
|
||||
descEn: 'Submit extension duration, reason, and new lease request.'
|
||||
},
|
||||
{
|
||||
key: 'checkout',
|
||||
label: '退房申请',
|
||||
labelEn: 'Check Out',
|
||||
icon: 'tn-icon-home-in-fill',
|
||||
statusZh: '可提交',
|
||||
statusEn: 'Available',
|
||||
isBusy: false,
|
||||
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_17.png',
|
||||
title: '退房申请',
|
||||
titleEn: 'Checkout Request',
|
||||
titleEnShort: 'Check Out',
|
||||
desc: '提交退房时间、离场说明和验房申请。',
|
||||
descEn: 'Submit checkout time, leave notes, and inspection request.'
|
||||
}
|
||||
],
|
||||
services: [
|
||||
{
|
||||
@@ -440,6 +501,21 @@ export default {
|
||||
desc: '填写备注说明需要几桶水后提交申请。',
|
||||
descEn: 'Submit the quantity of water needed and any notes.'
|
||||
},
|
||||
{
|
||||
key: 'meal-delivery',
|
||||
icon: 'tn-icon-cart-fill',
|
||||
nameZh: '送餐服务',
|
||||
nameEn: 'Food Delivery',
|
||||
statusZh: '可下单',
|
||||
statusEn: 'Order Now',
|
||||
isBusy: false,
|
||||
image: 'https://hi.aizhuhotel.com/%E7%95%99%E5%AD%A6%E7%94%9F%E5%85%AC%E5%AF%93%20-%200320%281%29_converted_15.png',
|
||||
title: '送餐服务',
|
||||
titleEn: 'Food Delivery',
|
||||
titleEnShort: 'Food',
|
||||
desc: '一键跳转点餐小程序,挑选餐品并下单配送。',
|
||||
descEn: 'Open the ordering mini-program to pick meals and get delivery.'
|
||||
},
|
||||
{
|
||||
key: 'cleaning',
|
||||
icon: 'tn-icon-clean',
|
||||
@@ -503,7 +579,7 @@ export default {
|
||||
butler: '专属社区管家',
|
||||
online: '当前在线 · 响应时间 < 5分钟',
|
||||
callNow: '拨打',
|
||||
publicService: '社区设施预约',
|
||||
publicService: '公寓精选服务',
|
||||
viewAll: '全部',
|
||||
leaseTerm: '租期',
|
||||
activeStatus: '使用中',
|
||||
@@ -525,6 +601,10 @@ export default {
|
||||
noContactPhone: '暂无联系电话',
|
||||
loadingButlerInfo: '正在同步管家信息...',
|
||||
noButlerInfo: '暂未获取到管家信息',
|
||||
managementContacts: '宿管与管理员',
|
||||
dormitoryManagerTag: '宿管',
|
||||
counselorTag: '管理员',
|
||||
noManagerContacts: '暂无管理联系人',
|
||||
managerContactSummary: '当前区域共有 {count} 位管家可联系',
|
||||
contactPhonePrefix: '联系电话 ',
|
||||
loadingShort: '同步中',
|
||||
@@ -583,7 +663,7 @@ export default {
|
||||
butler: 'Community Butler',
|
||||
online: 'Online · Response < 5 mins',
|
||||
callNow: 'Call',
|
||||
publicService: 'Facility Booking',
|
||||
publicService: 'Premium Services',
|
||||
viewAll: 'View All',
|
||||
leaseTerm: 'Lease',
|
||||
activeStatus: 'Active',
|
||||
@@ -605,6 +685,10 @@ export default {
|
||||
noContactPhone: 'No contact number',
|
||||
loadingButlerInfo: 'Syncing butler info...',
|
||||
noButlerInfo: 'No butler information yet',
|
||||
managementContacts: 'Dorm Managers & Admins',
|
||||
dormitoryManagerTag: 'Dorm Mgr',
|
||||
counselorTag: 'Admin',
|
||||
noManagerContacts: 'No management contacts',
|
||||
managerContactSummary: '{count} butlers are available in this area',
|
||||
contactPhonePrefix: 'Phone ',
|
||||
loadingShort: 'Syncing',
|
||||
@@ -884,6 +968,25 @@ export default {
|
||||
butlerPhoneNumber() {
|
||||
const manager = this.primaryAreaManager || {}
|
||||
return this.getManagerPhone(manager)
|
||||
},
|
||||
// 合并宿管列表与管理员(辅导员)为统一联系人列表
|
||||
managerContacts() {
|
||||
const contacts = Array.isArray(this.dormitoryManagers)
|
||||
? this.dormitoryManagers.map((item, index) => ({
|
||||
...this.normalizeManager(item),
|
||||
contactType: 'dormitory-manager',
|
||||
contactKey: String(item.code || item.userCode || item.phone || index)
|
||||
}))
|
||||
: []
|
||||
const counselor = this.counselor
|
||||
if (counselor && typeof counselor === 'object' && (counselor.name || counselor.phone || counselor.code)) {
|
||||
contacts.push({
|
||||
...this.normalizeManager(counselor),
|
||||
contactType: 'counselor',
|
||||
contactKey: String(counselor.code || counselor.userCode || counselor.phone || 'counselor')
|
||||
})
|
||||
}
|
||||
return contacts
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -1123,6 +1226,58 @@ export default {
|
||||
|
||||
return this._loadAreaManagerPromise
|
||||
},
|
||||
// 获取宿管列表与管理员(辅导员)联系人
|
||||
fetchManagementContacts() {
|
||||
if (!this.isCurrentLogin()) {
|
||||
this.dormitoryManagers = []
|
||||
this.counselor = null
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
if (this._fetchManagementContactsPromise) {
|
||||
return this._fetchManagementContactsPromise
|
||||
}
|
||||
|
||||
this.isManagementContactsLoading = true
|
||||
this._fetchManagementContactsPromise = Promise.allSettled([
|
||||
myDormitoryManagers(this.language),
|
||||
myCounselor(this.language)
|
||||
])
|
||||
.then(([dormitoryManagersResult, counselorResult]) => {
|
||||
this.dormitoryManagers = dormitoryManagersResult.status === 'fulfilled' && Array.isArray(dormitoryManagersResult.value && dormitoryManagersResult.value.data)
|
||||
? dormitoryManagersResult.value.data
|
||||
: []
|
||||
this.counselor = counselorResult.status === 'fulfilled' && counselorResult.value && counselorResult.value.data && typeof counselorResult.value.data === 'object'
|
||||
? counselorResult.value.data
|
||||
: null
|
||||
if (dormitoryManagersResult.status === 'rejected') {
|
||||
console.warn('获取宿管联系人失败', dormitoryManagersResult.reason)
|
||||
}
|
||||
if (counselorResult.status === 'rejected') {
|
||||
console.warn('获取管理员信息失败', counselorResult.reason)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.dormitoryManagers = []
|
||||
this.counselor = null
|
||||
console.warn('获取管理联系人失败', error)
|
||||
})
|
||||
.finally(() => {
|
||||
this.isManagementContactsLoading = false
|
||||
this._fetchManagementContactsPromise = null
|
||||
})
|
||||
|
||||
return this._fetchManagementContactsPromise
|
||||
},
|
||||
// 滚动事件处理:顶部导航栏加背景色
|
||||
handleScroll(e) {
|
||||
const scrollTop = (e && e.detail && e.detail.scrollTop) || 0
|
||||
if (scrollTop > 20) {
|
||||
this.navBarBackgroundColor = '#F8FAFF'
|
||||
} else {
|
||||
this.navBarBackgroundColor = 'transparent'
|
||||
}
|
||||
},
|
||||
fetchCurrentOrderDetail() {
|
||||
if (!this.hasCheckedIn) {
|
||||
return Promise.resolve()
|
||||
@@ -1165,10 +1320,14 @@ export default {
|
||||
this.hasCheckedIn = false
|
||||
this.currentBooking = null
|
||||
this.myRoomData = null
|
||||
this.dormitoryManagers = []
|
||||
this.counselor = null
|
||||
return
|
||||
}
|
||||
|
||||
this.currentBooking = storeUser.currentBooking || session.currentBooking || null
|
||||
// 已登录即获取宿管与管理员联系人
|
||||
this.fetchManagementContacts()
|
||||
if (this.getStoreCheckinStatus() || session.hasCheckedIn) {
|
||||
this.hasCheckedIn = true
|
||||
this.myRoomData = buildRoomDataFromOrder(storeCurrentOrder) || buildRoomDataFromOrder(session.myRoomData) || session.myRoomData || null
|
||||
@@ -1294,17 +1453,65 @@ export default {
|
||||
}
|
||||
return `/pkgService/submit_request/submit_request?title=${encodeURIComponent(title || this.t('submitRequest'))}&titleZh=${encodeURIComponent(titleZh)}&titleEn=${encodeURIComponent(titleEn)}&desc=${encodeURIComponent(desc || this.t('completeFormBeforeSubmit'))}&descZh=${encodeURIComponent(descZh)}&descEn=${encodeURIComponent(descEn)}&payload=${encodeURIComponent(JSON.stringify(payload))}`
|
||||
},
|
||||
buildTicketWorkbenchUrl(item = {}) {
|
||||
const serviceTypeMap = {
|
||||
repair: 'TicketRepairApply',
|
||||
complaint: 'TicketSuggestionApply',
|
||||
'change-room': 'TicketChangeApply',
|
||||
checkout: 'TicketCheckOutApply',
|
||||
'renew-stay': 'TicketRenewalApply',
|
||||
'water-delivery': 'TicketWaterDeliveryApply',
|
||||
cleaning: 'TicketCleaningApply'
|
||||
}
|
||||
const serviceType = serviceTypeMap[item.key]
|
||||
if (!serviceType) {
|
||||
return ''
|
||||
}
|
||||
return `/pkgService/ticket_workbench/ticket_workbench?serviceType=${encodeURIComponent(serviceType)}`
|
||||
},
|
||||
openServicePage(item) {
|
||||
if (item.key === 'meal-delivery') {
|
||||
this.openMealDeliveryMiniProgram()
|
||||
return
|
||||
}
|
||||
if (item.key === 'utility') {
|
||||
uni.navigateTo({
|
||||
url: '/pkgService/utility_recharge/utility_recharge'
|
||||
});
|
||||
return
|
||||
}
|
||||
if (item.key === 'lost-found') {
|
||||
uni.navigateTo({
|
||||
url: '/pkgService/lost_found_publish/lost_found_publish'
|
||||
});
|
||||
return
|
||||
}
|
||||
const ticketUrl = this.buildTicketWorkbenchUrl(item)
|
||||
if (ticketUrl) {
|
||||
uni.navigateTo({
|
||||
url: ticketUrl
|
||||
});
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: this.buildServiceSubmitUrl(item)
|
||||
});
|
||||
},
|
||||
// 送餐服务:跳转外部点餐小程序
|
||||
openMealDeliveryMiniProgram() {
|
||||
uni.navigateToMiniProgram({
|
||||
appId: 'wxf221ee13f2abd417',
|
||||
path: 'pages/scan/index?scene=1005&toMenuList=5&entityId=00684887&multiMenuId=006848879dd2e038019e017ba6e330d5',
|
||||
success: () => {},
|
||||
fail: (error) => {
|
||||
console.error('跳转送餐小程序失败', error)
|
||||
uni.showToast({
|
||||
title: this.t('mealDeliveryJumpFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
getManagerName(manager = {}) {
|
||||
return manager.displayName || manager.name || manager.userName || manager.nickname || manager.userCode || manager.phone || ''
|
||||
},
|
||||
@@ -1400,6 +1607,10 @@ export default {
|
||||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
/* 导航栏背景色过渡 */
|
||||
.service-page ::v-deep .tn-custom-nav-bar__bar {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.service-scroll {
|
||||
height: calc(100vh - 100rpx);
|
||||
}
|
||||
@@ -1729,43 +1940,6 @@ export default {
|
||||
box-shadow: 0 6rpx 14rpx rgba($brand-primary, 0.16);
|
||||
}
|
||||
|
||||
.stay-action-card {
|
||||
width: 31%;
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx 12rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 10rpx 26rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.stay-action-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
margin: 0 auto;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, $icon-surface-brand-soft 0%, rgba($brand-primary, 0.04) 100%);
|
||||
color: $icon-tone-brand-deep;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 34rpx;
|
||||
box-shadow: inset 0 0 0 1rpx rgba($brand-primary, 0.08);
|
||||
}
|
||||
|
||||
.stay-action-icon-symbol {
|
||||
font-size: 32rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stay-action-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
color: #222222;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.half-btn {
|
||||
width: 48%;
|
||||
height: 92rpx;
|
||||
@@ -2044,6 +2218,18 @@ export default {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
/* 宿管标签 - 绿色 */
|
||||
.butler-member-new__tag--type.tag-dormitory {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
/* 管理员标签 - 琥珀色 */
|
||||
.butler-member-new__tag--type.tag-counselor {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #F59E0B;
|
||||
}
|
||||
|
||||
.butler-member-new__phone {
|
||||
font-size: 24rpx;
|
||||
color: #888888;
|
||||
|
||||
@@ -12,6 +12,30 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view v-if="showPrivacyPopup" class="privacy-mask">
|
||||
<view class="privacy-dialog">
|
||||
<view class="privacy-title">{{ t('privacyTitle') }}</view>
|
||||
<view class="privacy-content">
|
||||
{{ t('privacyContentPrefix') }}
|
||||
<text class="privacy-link" @click="openPrivacyContract">{{ t('privacyContractName') }}</text>
|
||||
{{ t('privacyContentSuffix') }}
|
||||
</view>
|
||||
<view class="privacy-action-row">
|
||||
<view class="privacy-action-btn" @click="handlePrivacyDisagree">{{ t('privacyDisagree') }}</view>
|
||||
<button
|
||||
id="index-privacy-agree-btn"
|
||||
class="privacy-action-btn is-primary"
|
||||
open-type="agreePrivacyAuthorization"
|
||||
@agreeprivacyauthorization="handleAgreePrivacyAuthorization"
|
||||
>
|
||||
{{ t('privacyAgree') }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 二级页面 -->
|
||||
<view v-if="hasPage('select-room')" v-show="currentPageId === 'select-room'">
|
||||
<scroll-view class="custom-tabbar-page" scroll-y enable-back-to-top @scrolltolower="tabbarPageScrollLower">
|
||||
@@ -87,9 +111,24 @@
|
||||
import MyApplicationTab from './component/MyApplicationTab.vue'
|
||||
import ContractAlert from '../../components/contract-alert/contract-alert.vue'
|
||||
import { clearLegacyDemoCache, getHomePageTabId, getSessionSnapshot, saveHomePageTabId } from '../../utils/app-state'
|
||||
import { checkRecordByChecked, notificationPage, applicationFormMePage } from '@/api/index.js'
|
||||
import { applicationFormMePage, checkRecordByChecked, notificationPage, orderSearchedMePage, orderSearchedReservationFlow } from '@/api/index.js'
|
||||
import { parseOrderListResponse, getOrderStatusCode } from '@/utils/booking-helper'
|
||||
import { bindPrivacyAuthorizationListener, getPrivacyAuthorizationStatus, openWechatPrivacyContract, unbindPrivacyAuthorizationListener } from '@/utils/privacy.js'
|
||||
import { countPendingInspectionRecords, extractInspectionList, resolveNotificationUnreadTotal } from '../../utils/message-helper'
|
||||
import { syncLoginStayState } from '../../utils/session-sync'
|
||||
import { getReservationOrderNo, getReservationCurrentNode, parseReservationFlowResponse } from '@/utils/reservation-flow'
|
||||
|
||||
function formatPageResponse(response) {
|
||||
const payload = response && response.data ? response.data : response
|
||||
return payload && typeof payload === 'object' ? payload : {}
|
||||
}
|
||||
|
||||
function normalizeApplicationList(list = []) {
|
||||
if (!Array.isArray(list)) {
|
||||
return []
|
||||
}
|
||||
return list.filter(item => item && typeof item === 'object')
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -113,6 +152,7 @@
|
||||
hasCheckedIn: false,
|
||||
hasStayOrder: false,
|
||||
hasPendingApplication: false,
|
||||
hasActiveReservationFlow: false,
|
||||
pendingContracts: []
|
||||
},
|
||||
tabDefinitions: [
|
||||
@@ -125,6 +165,9 @@
|
||||
loadedPages: ['select-room'],
|
||||
isAppInitLoading: true,
|
||||
isTabSwitching: false,
|
||||
showPrivacyPopup: false,
|
||||
resolvePrivacyAuthorization: null,
|
||||
isCheckingPrivacyAuthorization: false,
|
||||
i18n: {
|
||||
zh: {
|
||||
loadingBrand: '绿居公寓',
|
||||
@@ -133,7 +176,13 @@
|
||||
tabStayService: '入 住',
|
||||
tabMyApplication: '申请单',
|
||||
tabMessageCenter: '消 息',
|
||||
tabProfileCenter: '我 的'
|
||||
tabProfileCenter: '我 的',
|
||||
privacyTitle: '隐私授权提示',
|
||||
privacyContentPrefix: '进入小程序前,请先阅读并同意',
|
||||
privacyContractName: '《用户隐私保护指引》',
|
||||
privacyContentSuffix: ',同意后才能继续使用相关功能。',
|
||||
privacyAgree: '同意并继续',
|
||||
privacyDisagree: '暂不授权'
|
||||
},
|
||||
en: {
|
||||
loadingBrand: 'Luju Apartments',
|
||||
@@ -142,7 +191,13 @@
|
||||
tabStayService: 'Stay',
|
||||
tabMyApplication: 'Applications',
|
||||
tabMessageCenter: 'Messages',
|
||||
tabProfileCenter: 'Profile'
|
||||
tabProfileCenter: 'Profile',
|
||||
privacyTitle: 'Privacy Authorization',
|
||||
privacyContentPrefix: 'Before entering the mini program, please read and agree to the ',
|
||||
privacyContractName: 'Privacy Protection Guide',
|
||||
privacyContentSuffix: ' to continue using related features.',
|
||||
privacyAgree: 'Agree and Continue',
|
||||
privacyDisagree: 'Not Now'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,12 +215,12 @@
|
||||
const tabs = []
|
||||
if (this.sessionState.hasStayOrder) {
|
||||
tabs.push('stay-service')
|
||||
} else if (this.sessionState.hasPendingApplication) {
|
||||
} else if (this.sessionState.hasPendingApplication || this.sessionState.hasActiveReservationFlow) {
|
||||
tabs.push('my-application')
|
||||
} else {
|
||||
tabs.push('select-room')
|
||||
}
|
||||
if (this.sessionState.hasPendingApplication && !tabs.includes('my-application')) {
|
||||
if ((this.sessionState.hasPendingApplication || this.sessionState.hasActiveReservationFlow) && !tabs.includes('my-application')) {
|
||||
tabs.push('my-application')
|
||||
}
|
||||
tabs.push('message-center', 'profile-center')
|
||||
@@ -186,14 +241,20 @@
|
||||
},
|
||||
onShow() {
|
||||
this.handlePageShow()
|
||||
this.checkEntryPrivacyAuthorization()
|
||||
},
|
||||
onLoad(options) {
|
||||
clearLegacyDemoCache()
|
||||
uni.$on('messageBadgeCountChange', this.handleMessageBadgeCountChange)
|
||||
uni.$on('reservation-flow-force-refresh', this.handleReservationFlowForceRefresh)
|
||||
this.initPrivacyAuthorization()
|
||||
this.checkEntryPrivacyAuthorization()
|
||||
this.handlePageLoad(options)
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('messageBadgeCountChange', this.handleMessageBadgeCountChange)
|
||||
uni.$off('reservation-flow-force-refresh', this.handleReservationFlowForceRefresh)
|
||||
this.releasePrivacyAuthorization()
|
||||
},
|
||||
onReady() {
|
||||
this.$nextTick(() => {
|
||||
@@ -204,6 +265,76 @@
|
||||
t(key) {
|
||||
return this.$tm('pages.index.index', key)
|
||||
},
|
||||
initPrivacyAuthorization() {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (typeof wx === 'undefined' || !wx.onNeedPrivacyAuthorization) {
|
||||
return
|
||||
}
|
||||
this.handlePrivacyAuthorizationRequest = (resolve) => {
|
||||
this.resolvePrivacyAuthorization = resolve
|
||||
this.showPrivacyPopup = true
|
||||
}
|
||||
bindPrivacyAuthorizationListener(this.handlePrivacyAuthorizationRequest)
|
||||
// #endif
|
||||
},
|
||||
releasePrivacyAuthorization() {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (!this.handlePrivacyAuthorizationRequest) {
|
||||
return
|
||||
}
|
||||
unbindPrivacyAuthorizationListener(this.handlePrivacyAuthorizationRequest)
|
||||
this.handlePrivacyAuthorizationRequest = null
|
||||
this.resolvePrivacyAuthorization = null
|
||||
this.showPrivacyPopup = false
|
||||
// #endif
|
||||
},
|
||||
checkEntryPrivacyAuthorization() {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (this.isCheckingPrivacyAuthorization) {
|
||||
return
|
||||
}
|
||||
this.isCheckingPrivacyAuthorization = true
|
||||
getPrivacyAuthorizationStatus()
|
||||
.then((res) => {
|
||||
if (!this.resolvePrivacyAuthorization) {
|
||||
this.showPrivacyPopup = !!(res && res.needAuthorization)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.isCheckingPrivacyAuthorization = false
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
openPrivacyContract() {
|
||||
// #ifdef MP-WEIXIN
|
||||
openWechatPrivacyContract().catch((error) => {
|
||||
console.error('open privacy contract failed', error)
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
handleAgreePrivacyAuthorization() {
|
||||
this.showPrivacyPopup = false
|
||||
if (!this.resolvePrivacyAuthorization) {
|
||||
return
|
||||
}
|
||||
const resolvePrivacyAuthorization = this.resolvePrivacyAuthorization
|
||||
this.resolvePrivacyAuthorization = null
|
||||
resolvePrivacyAuthorization({
|
||||
buttonId: 'index-privacy-agree-btn',
|
||||
event: 'agree'
|
||||
})
|
||||
},
|
||||
handlePrivacyDisagree() {
|
||||
this.showPrivacyPopup = false
|
||||
if (!this.resolvePrivacyAuthorization) {
|
||||
return
|
||||
}
|
||||
const resolvePrivacyAuthorization = this.resolvePrivacyAuthorization
|
||||
this.resolvePrivacyAuthorization = null
|
||||
resolvePrivacyAuthorization({
|
||||
event: 'disagree'
|
||||
})
|
||||
},
|
||||
getStoreUser() {
|
||||
return (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
|
||||
},
|
||||
@@ -238,6 +369,58 @@
|
||||
console.error('sync login stay state failed', error)
|
||||
})
|
||||
},
|
||||
resolveApplicationState() {
|
||||
return applicationFormMePage({
|
||||
Status: 0,
|
||||
PageIndex: 1,
|
||||
PageSize: 1
|
||||
}, this.$getLanguage())
|
||||
.then((response) => {
|
||||
const pageData = formatPageResponse(response)
|
||||
const list = normalizeApplicationList(pageData.data || [])
|
||||
const total = Number(pageData.total || 0)
|
||||
const hasPendingApplication = list.some(item => Number(item && item.status) === 0)
|
||||
return {
|
||||
hasPendingApplication: total > 0 || hasPendingApplication
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('resolve application state failed', error)
|
||||
return {
|
||||
hasPendingApplication: false
|
||||
}
|
||||
})
|
||||
},
|
||||
resolveApplicationFlowState(list = []) {
|
||||
const orderList = normalizeApplicationList(list)
|
||||
const reservationOrderList = orderList.filter(item => getOrderStatusCode(item) === 0)
|
||||
const orderNos = [...new Set(reservationOrderList.map(item => getReservationOrderNo(item)).filter(Boolean))]
|
||||
|
||||
const result = {
|
||||
hasPendingApplication: false,
|
||||
hasActiveReservationFlow: false
|
||||
}
|
||||
|
||||
if (!orderNos.length) {
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
return orderSearchedReservationFlow({
|
||||
orderNos
|
||||
}, this.$getLanguage())
|
||||
.then((response) => {
|
||||
const flowList = parseReservationFlowResponse(response)
|
||||
const hasActiveReservationFlow = flowList.some(item => !!getReservationCurrentNode(item))
|
||||
return {
|
||||
hasPendingApplication: result.hasPendingApplication || hasActiveReservationFlow,
|
||||
hasActiveReservationFlow
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('resolve application flow state failed', error)
|
||||
return result
|
||||
})
|
||||
},
|
||||
refreshShellState(force = false, options = {}) {
|
||||
const { skipMessageBadge = false } = options
|
||||
const tasks = [
|
||||
@@ -249,18 +432,30 @@
|
||||
}
|
||||
if (this.isCurrentLogin()) {
|
||||
tasks.push(
|
||||
applicationFormMePage({ Status: 0, PageIndex: 1, PageSize: 1 })
|
||||
.then(res => {
|
||||
const payload = res && res.data ? res.data : res
|
||||
const total = payload && payload.total ? Number(payload.total) : (payload && payload.data && payload.data.length ? payload.data.length : 0)
|
||||
this.sessionState.hasPendingApplication = total > 0
|
||||
this.resolveApplicationState()
|
||||
.then((applicationState = {}) => {
|
||||
return orderSearchedMePage({ PageIndex: 1, PageSize: 100, IsExport: false })
|
||||
.then(res => {
|
||||
const orderList = parseOrderListResponse(res)
|
||||
return this.resolveApplicationFlowState(orderList)
|
||||
.then((flowState = {}) => ({
|
||||
hasPendingApplication: !!applicationState.hasPendingApplication,
|
||||
hasActiveReservationFlow: !!flowState.hasActiveReservationFlow
|
||||
}))
|
||||
})
|
||||
})
|
||||
.then((flowState = {}) => {
|
||||
this.sessionState.hasPendingApplication = !!flowState.hasPendingApplication
|
||||
this.sessionState.hasActiveReservationFlow = !!flowState.hasActiveReservationFlow
|
||||
})
|
||||
.catch(() => {
|
||||
this.sessionState.hasPendingApplication = false
|
||||
this.sessionState.hasActiveReservationFlow = false
|
||||
})
|
||||
)
|
||||
} else {
|
||||
this.sessionState.hasPendingApplication = false
|
||||
this.sessionState.hasActiveReservationFlow = false
|
||||
}
|
||||
return Promise.all(tasks).finally(() => {
|
||||
this.refreshSessionState()
|
||||
@@ -314,6 +509,7 @@
|
||||
hasCheckedIn: storeHasCheckedIn || session.hasCheckedIn,
|
||||
hasStayOrder: hasStayOrder || pendingContracts.length > 0,
|
||||
hasPendingApplication: !!this.sessionState.hasPendingApplication,
|
||||
hasActiveReservationFlow: !!this.sessionState.hasActiveReservationFlow,
|
||||
pendingContracts
|
||||
}
|
||||
},
|
||||
@@ -396,6 +592,13 @@
|
||||
const stayServiceTab = list.find(item => item.id === 'stay-service')
|
||||
if (stayServiceTab) return stayServiceTab
|
||||
}
|
||||
const shouldPreferApplicationFlow = this.sessionState.isLogin
|
||||
&& !this.sessionState.hasStayOrder
|
||||
&& (this.sessionState.hasPendingApplication || this.sessionState.hasActiveReservationFlow)
|
||||
if (shouldPreferApplicationFlow && (!preferredPageId || preferredPageId === 'select-room')) {
|
||||
const applicationTab = list.find(item => item.id === 'my-application')
|
||||
if (applicationTab) return applicationTab
|
||||
}
|
||||
if (preferredPageId) {
|
||||
const matchedTab = list.find(item => item.id === preferredPageId)
|
||||
if (matchedTab) return matchedTab
|
||||
@@ -438,6 +641,22 @@
|
||||
this.getTabbarItemInfo()
|
||||
})
|
||||
},
|
||||
async handleReservationFlowForceRefresh() {
|
||||
await this.refreshShellState(true)
|
||||
this.ensurePageLoaded('my-application')
|
||||
this.$nextTick(() => {
|
||||
const myApplicationTab = this.getPageRef('myApplicationTab')
|
||||
if (myApplicationTab && typeof myApplicationTab.load === 'function') {
|
||||
myApplicationTab.load(true)
|
||||
} else {
|
||||
this.refreshTabPageState('my-application')
|
||||
}
|
||||
if (this.currentPageId) {
|
||||
this.refreshTabPageState(this.currentPageId)
|
||||
}
|
||||
this.getTabbarItemInfo()
|
||||
})
|
||||
},
|
||||
tabbarPageScrollLower(e) {
|
||||
if (this.currentPageId === 'select-room') {
|
||||
const selectRoomTab = this.getPageRef('selectRoomTab')
|
||||
@@ -587,6 +806,86 @@
|
||||
background-color: #111111;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.privacy-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100000;
|
||||
background: rgba(17, 24, 39, 0.56);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.privacy-dialog {
|
||||
width: 100%;
|
||||
max-width: 640rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 28rpx;
|
||||
padding: 40rpx 32rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 24rpx 80rpx rgba(15, 23, 42, 0.16);
|
||||
}
|
||||
|
||||
.privacy-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #1A2233;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.privacy-content {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.7;
|
||||
color: #5C6B8A;
|
||||
}
|
||||
|
||||
.privacy-link {
|
||||
color: $brand-primary;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.privacy-action-row {
|
||||
display: flex;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.privacy-action-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: $brand-primary;
|
||||
background: rgba($brand-primary, 0.08);
|
||||
border: 1rpx solid rgba($brand-primary, 0.12);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.privacy-action-btn + .privacy-action-btn {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
button.privacy-action-btn {
|
||||
padding: 0;
|
||||
line-height: 88rpx;
|
||||
}
|
||||
|
||||
button.privacy-action-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.privacy-action-btn.is-primary {
|
||||
color: #FFFFFF;
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.92) 0%, $brand-primary 100%);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.loading-logo {
|
||||
animation: pulse 2s infinite ease-in-out;
|
||||
|
||||
Reference in New Issue
Block a user