All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m34s
Co-authored-by: Cursor <cursoragent@cursor.com>
403 lines
10 KiB
Vue
403 lines
10 KiB
Vue
<template>
|
|
<div class="banner" :class="{ 'is-compact': compact }">
|
|
<div class="banner-row">
|
|
<div class="banner-hello">
|
|
<a-avatar :size="compact ? 32 : 40" class="banner-hello__avatar">
|
|
<img :src="avatarUrl" alt="" />
|
|
</a-avatar>
|
|
<p class="banner-hello__line">
|
|
<span class="banner-hello__greet">{{ greetText }}</span>
|
|
<span class="banner-hello__name">{{ displayName }}</span>
|
|
</p>
|
|
<div v-if="!compact" class="banner-local-clock" aria-live="polite">
|
|
<span class="banner-local-clock__label">{{ t('workplace.clock.local') }}</span>
|
|
<span class="banner-local-clock__date">{{ dateText }}</span>
|
|
<span class="banner-local-clock__time">{{ timeText }}</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
v-if="showBalance"
|
|
type="button"
|
|
class="balance-chip"
|
|
:class="{ 'balance-chip--low': isLowBalance, 'balance-chip--loading': balanceLoading }"
|
|
:title="t('workplace.rechargeNow')"
|
|
@click="goRecharge"
|
|
>
|
|
<span class="balance-chip__label">{{ t('workplace.balance') }}</span>
|
|
<span class="balance-chip__value">
|
|
<template v-if="balanceLoading">···</template>
|
|
<template v-else>{{ balanceDisplay }}</template>
|
|
</span>
|
|
<span class="balance-chip__cta">{{ t('workplace.rechargeNow') }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useRouter } from 'vue-router';
|
|
import { useUserStore } from '@/store';
|
|
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
|
import { resolveCurrentCustomer, peekCachedCurrentCustomer } from '@/api/customer/resolve';
|
|
import { CUSTOMER_BALANCE_CHANGED_EVENT } from '@/api/customer/accountFlow';
|
|
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
|
import { isValidAvatarUrl, resolveAvatarUrl } from '@/utils/avatar';
|
|
|
|
defineProps<{
|
|
/** 员工工作台:一行问候,避免与关照重复 */
|
|
compact?: boolean
|
|
}>()
|
|
|
|
const LOW_BALANCE_THRESHOLD = 100;
|
|
|
|
const { t, locale } = useI18n();
|
|
const router = useRouter();
|
|
const userStore = useUserStore();
|
|
const rbacUserStore = useRbacUserStore();
|
|
|
|
const showBalance = computed(() => rbacUserStore.isCustomerPortal());
|
|
const customerBalance = ref<number | null>(null);
|
|
const balanceLoading = ref(false);
|
|
const wechatAvatarRaw = ref('');
|
|
const now = ref(new Date());
|
|
let timerId: number | undefined;
|
|
|
|
const displayName = computed(
|
|
() => userStore.name?.trim() || rbacUserStore.userInfo?.account || '—',
|
|
);
|
|
|
|
const avatarUrl = computed(() => {
|
|
if (showBalance.value && isValidAvatarUrl(wechatAvatarRaw.value)) {
|
|
return resolveAvatarUrl(wechatAvatarRaw.value);
|
|
}
|
|
return resolveAvatarUrl(userStore.avatar || rbacUserStore.userInfo?.avatar);
|
|
});
|
|
|
|
const dateLocale = computed(() =>
|
|
String(locale.value).toLowerCase().startsWith('en') ? 'en-US' : 'zh-CN',
|
|
);
|
|
|
|
const dateText = computed(() =>
|
|
new Intl.DateTimeFormat(dateLocale.value, {
|
|
timeZone: 'Asia/Shanghai',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
weekday: 'short',
|
|
}).format(now.value),
|
|
);
|
|
|
|
const timeText = computed(() =>
|
|
new Intl.DateTimeFormat(dateLocale.value, {
|
|
timeZone: 'Asia/Shanghai',
|
|
hour12: false,
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
}).format(now.value),
|
|
);
|
|
const balanceDisplay = computed(() => {
|
|
const raw = customerBalance.value;
|
|
if (raw == null) return '--';
|
|
const parsed = Number(raw);
|
|
return Number.isFinite(parsed)
|
|
? parsed.toLocaleString('zh-CN', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
})
|
|
: '--';
|
|
});
|
|
|
|
const isLowBalance = computed(() => {
|
|
const raw = customerBalance.value;
|
|
if (raw == null) return false;
|
|
return Number(raw) < LOW_BALANCE_THRESHOLD;
|
|
});
|
|
|
|
const greetText = computed(() => {
|
|
const hour = now.value.getHours();
|
|
if (hour < 6) return t('workplace.greet.night');
|
|
if (hour < 12) return t('workplace.greet.morning');
|
|
if (hour < 14) return t('workplace.greet.noon');
|
|
if (hour < 18) return t('workplace.greet.afternoon');
|
|
return t('workplace.greet.evening');
|
|
});
|
|
|
|
function applyWechatAvatar(data: { wechatAvatarUrl?: string; wechatAvatar?: string } | null) {
|
|
const next = data?.wechatAvatarUrl || data?.wechatAvatar || '';
|
|
wechatAvatarRaw.value = isValidAvatarUrl(next) ? String(next).trim() : '';
|
|
}
|
|
|
|
async function loadCustomerProfile() {
|
|
if (!rbacUserStore.isCustomerPortal() || !rbacUserStore.isLoggedIn) {
|
|
customerBalance.value = null;
|
|
wechatAvatarRaw.value = '';
|
|
return;
|
|
}
|
|
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken);
|
|
const account = rbacUserStore.userInfo?.account?.trim() || '';
|
|
const cached = peekCachedCurrentCustomer({ mapId, account });
|
|
if (cached) {
|
|
applyWechatAvatar(cached);
|
|
const cachedBal = cached.accountBalance ?? cached.balance;
|
|
if (cachedBal != null && cachedBal !== '') {
|
|
const parsed = Number(cachedBal);
|
|
if (Number.isFinite(parsed)) customerBalance.value = parsed;
|
|
}
|
|
}
|
|
|
|
balanceLoading.value = true;
|
|
try {
|
|
const data = await resolveCurrentCustomer({ mapId, account, force: true });
|
|
applyWechatAvatar(data);
|
|
const raw = data?.accountBalance ?? data?.balance;
|
|
if (raw == null || raw === '') {
|
|
customerBalance.value = 0;
|
|
return;
|
|
}
|
|
const parsed = Number(raw);
|
|
customerBalance.value = Number.isFinite(parsed) ? parsed : 0;
|
|
} catch {
|
|
customerBalance.value = null;
|
|
if (!isValidAvatarUrl(wechatAvatarRaw.value)) {
|
|
wechatAvatarRaw.value = '';
|
|
}
|
|
} finally {
|
|
balanceLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function goRecharge() {
|
|
router.push({ name: 'VipRechargeDeposit' }).catch(() => undefined);
|
|
}
|
|
|
|
function onBalanceChanged() {
|
|
void loadCustomerProfile();
|
|
}
|
|
|
|
onMounted(() => {
|
|
timerId = window.setInterval(() => {
|
|
now.value = new Date();
|
|
}, 1000);
|
|
void loadCustomerProfile();
|
|
window.addEventListener(CUSTOMER_BALANCE_CHANGED_EVENT, onBalanceChanged);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
if (timerId) {
|
|
clearInterval(timerId);
|
|
timerId = undefined;
|
|
}
|
|
window.removeEventListener(CUSTOMER_BALANCE_CHANGED_EVENT, onBalanceChanged);
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.banner {
|
|
width: 100%;
|
|
padding: 16px 20px 4px;
|
|
background: transparent;
|
|
|
|
&.is-compact {
|
|
padding: 2px 0;
|
|
|
|
.banner-row {
|
|
min-height: 0;
|
|
}
|
|
|
|
.banner-hello__greet {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.banner-hello__name {
|
|
font-size: 16px;
|
|
font-weight: 650;
|
|
}
|
|
}
|
|
}
|
|
|
|
.banner-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px 24px;
|
|
min-height: 56px;
|
|
}
|
|
|
|
.banner-hello {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
|
|
.banner-hello__avatar {
|
|
flex-shrink: 0;
|
|
border: 1px solid var(--app-acrylic-border);
|
|
background: var(--app-acrylic-bg);
|
|
box-shadow: none;
|
|
|
|
:deep(img) {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
}
|
|
|
|
.banner-hello__line {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
margin: 0;
|
|
min-width: 0;
|
|
}
|
|
|
|
.banner-hello__greet {
|
|
flex-shrink: 0;
|
|
color: var(--color-text-3);
|
|
font-size: 14px;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.banner-hello__name {
|
|
margin: 0;
|
|
color: var(--color-text-1);
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
line-height: 1.3;
|
|
letter-spacing: 0.01em;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.banner-local-clock {
|
|
--aw-ink: #1a2e38;
|
|
--aw-soft: rgba(26, 46, 56, 0.48);
|
|
|
|
display: inline-flex;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
min-width: 0;
|
|
margin-left: 4px;
|
|
padding: 8px 12px;
|
|
border-radius: 12px;
|
|
border: 1px solid var(--app-acrylic-border-strong);
|
|
background: var(--app-acrylic-hover-bg);
|
|
backdrop-filter: var(--app-acrylic-blur);
|
|
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.banner-local-clock__label {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--aw-soft);
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.banner-local-clock__date {
|
|
font-size: 12px;
|
|
color: var(--aw-soft);
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.banner-local-clock__time {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: 0.03em;
|
|
color: var(--aw-ink);
|
|
line-height: 1.15;
|
|
}
|
|
|
|
.balance-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-shrink: 0;
|
|
height: 44px;
|
|
padding: 0 12px 0 14px;
|
|
border: 1px solid var(--app-acrylic-border);
|
|
border-radius: 12px;
|
|
background: var(--app-acrylic-bg);
|
|
backdrop-filter: var(--app-acrylic-blur);
|
|
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
|
cursor: pointer;
|
|
transition: border-color 0.2s, background 0.2s;
|
|
|
|
&:hover {
|
|
border-color: var(--app-acrylic-border-strong);
|
|
background: var(--app-acrylic-hover-bg);
|
|
box-shadow: none;
|
|
|
|
.balance-chip__cta {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
&--low {
|
|
border-color: rgba(255, 125, 0, 0.45);
|
|
background: rgba(255, 125, 0, 0.06);
|
|
|
|
.balance-chip__value {
|
|
color: #ff7d00;
|
|
}
|
|
}
|
|
|
|
&--loading {
|
|
.balance-chip__value {
|
|
opacity: 0.55;
|
|
}
|
|
}
|
|
|
|
&__label {
|
|
color: var(--color-text-3);
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
&__value {
|
|
color: var(--app-primary-color, rgb(var(--primary-6)));
|
|
font-size: 18px;
|
|
font-weight: 650;
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 1;
|
|
letter-spacing: 0.02em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
&__cta {
|
|
margin-left: 2px;
|
|
padding: 3px 10px;
|
|
border-radius: 8px;
|
|
background: var(--app-primary-color, rgb(var(--primary-6)));
|
|
color: #fff;
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
white-space: nowrap;
|
|
opacity: 0.92;
|
|
transition: opacity 0.2s;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.banner-row {
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
.banner-hello {
|
|
width: 100%;
|
|
}
|
|
|
|
.balance-chip {
|
|
margin-left: auto;
|
|
}
|
|
}
|
|
</style>
|