补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
303 行
6.4 KiB
Plaintext
303 行
6.4 KiB
Plaintext
<template>
|
|
<!-- 访客端顶栏/右上角常驻入口:始终可见,不藏二级菜单 -->
|
|
<view class="i18n-selector-wrap i18n-selector-persist" :class="{ 'is-rtl': isCurrentRtl }">
|
|
<!-- 常驻语言切换触发按钮 (触控区 >= 44x44) -->
|
|
<view
|
|
class="i18n-trigger-btn"
|
|
role="button"
|
|
aria-label="Select Language"
|
|
@click="openModal"
|
|
>
|
|
<text class="i18n-trigger-badge">{{ currentShortCode }}</text>
|
|
<text class="i18n-trigger-arrow">▾</text>
|
|
</view>
|
|
|
|
<!-- 语言切换浮层面板 (遮罩 + 弹出列表,复用现有 languages / setAppLocale) -->
|
|
<view v-if="visible" class="i18n-modal-mask" @click="closeModal">
|
|
<view class="i18n-modal-container" @click.stop="">
|
|
<!-- 弹窗头部 -->
|
|
<view class="i18n-modal-header">
|
|
<text class="i18n-modal-title">Select Language / 语言设置</text>
|
|
<view class="i18n-modal-close" role="button" aria-label="Close" @click="closeModal">
|
|
<text class="i18n-close-icon">x</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 7 大主流语种列表 (含中/英/阿;WCAG 2.2 AA,单行 >= 44px) -->
|
|
<view class="i18n-list">
|
|
<view
|
|
v-for="item in languages"
|
|
:key="item.code"
|
|
class="i18n-item"
|
|
:class="{ 'i18n-item-active': item.code === activeLocale }"
|
|
@click="selectLanguage(item.code)"
|
|
>
|
|
<view class="i18n-item-prefix">
|
|
<text class="i18n-item-badge">{{ item.badge }}</text>
|
|
<text class="i18n-item-name">{{ item.name }}</text>
|
|
</view>
|
|
<view class="i18n-item-check-box">
|
|
<view v-if="item.code === activeLocale" class="i18n-item-check-mark"></view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
import {
|
|
OmniLocale,
|
|
getCurrentLocale,
|
|
setAppLocale,
|
|
isRtlLocale,
|
|
} from '../../common/locale.uts';
|
|
|
|
interface LanguageOption {
|
|
code: OmniLocale;
|
|
name: string;
|
|
badge: string;
|
|
}
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'change', locale: OmniLocale): void
|
|
}>();
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
});
|
|
|
|
const visible = ref<boolean>(false);
|
|
const activeLocale = ref<OmniLocale>(
|
|
props.modelValue ? (props.modelValue as OmniLocale) : (getCurrentLocale() as OmniLocale)
|
|
);
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(val: string) => {
|
|
if (val && val.trim() !== '') {
|
|
activeLocale.value = val as OmniLocale;
|
|
}
|
|
}
|
|
);
|
|
|
|
const languages: LanguageOption[] = [
|
|
{ code: 'zh-CN', name: '简体中文', badge: 'ZH' },
|
|
{ code: 'en-US', name: 'English', badge: 'EN' },
|
|
{ code: 'ar-SA', name: 'العربية', badge: 'AR' },
|
|
{ code: 'es-ES', name: 'Español', badge: 'ES' },
|
|
{ code: 'de-DE', name: 'Deutsch', badge: 'DE' },
|
|
{ code: 'fr-FR', name: 'Français', badge: 'FR' },
|
|
{ code: 'ja-JP', name: '日本語', badge: 'JA' },
|
|
];
|
|
|
|
const currentShortCode = computed((): string => {
|
|
const target = activeLocale.value;
|
|
for (let i = 0; i < languages.length; i++) {
|
|
if (languages[i].code === target) {
|
|
return languages[i].badge;
|
|
}
|
|
}
|
|
return 'EN';
|
|
});
|
|
|
|
const isCurrentRtl = computed((): boolean => {
|
|
return isRtlLocale(activeLocale.value);
|
|
});
|
|
|
|
function openModal(): void {
|
|
visible.value = true;
|
|
}
|
|
|
|
function closeModal(): void {
|
|
visible.value = false;
|
|
}
|
|
|
|
function selectLanguage(code: OmniLocale): void {
|
|
activeLocale.value = code;
|
|
setAppLocale(code);
|
|
emit('change', code);
|
|
closeModal();
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.i18n-selector-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
z-index: 20;
|
|
}
|
|
|
|
.i18n-selector-persist {
|
|
/* 顶栏常驻:防止被挤出可视区 */
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* 常驻右上角触发胶囊 (触控区 >= 44x44) */
|
|
.i18n-trigger-btn {
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
padding: 0 8px;
|
|
background-color: #FFFFFF;
|
|
border: 1px solid #E3E7ED;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.i18n-trigger-badge {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #101828;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.i18n-trigger-arrow {
|
|
font-size: 11px;
|
|
color: #667085;
|
|
}
|
|
|
|
/* 浮层遮罩 */
|
|
.i18n-modal-mask {
|
|
position: fixed;
|
|
left: 0;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(11, 13, 16, 0.45);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 9999;
|
|
padding: 24px;
|
|
}
|
|
|
|
/* 浮层面板容器 */
|
|
.i18n-modal-container {
|
|
width: 100%;
|
|
max-width: 360px;
|
|
background-color: #FFFFFF;
|
|
border: 1px solid #E3E7ED;
|
|
border-radius: 20px;
|
|
padding: 20px;
|
|
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.i18n-modal-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding-bottom: 12px;
|
|
border-bottom: 1px solid #E3E7ED;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.i18n-modal-title {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: #101828;
|
|
}
|
|
|
|
.i18n-modal-close {
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.i18n-close-icon {
|
|
font-size: 14px;
|
|
color: #667085;
|
|
}
|
|
|
|
/* 列表选项 (每行 >= 48px) */
|
|
.i18n-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.i18n-item {
|
|
min-height: 48px;
|
|
padding: 0 12px;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
border-radius: 12px;
|
|
margin-bottom: 4px;
|
|
background-color: #FFFFFF;
|
|
}
|
|
|
|
.i18n-item-active {
|
|
background-color: #EAF1FF;
|
|
}
|
|
|
|
.i18n-item-prefix {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
|
|
.i18n-item-badge {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
color: #667085;
|
|
background-color: #F7F8FA;
|
|
padding: 2px 6px;
|
|
border-radius: 6px;
|
|
margin-right: 12px;
|
|
}
|
|
|
|
.i18n-item-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #101828;
|
|
}
|
|
|
|
.i18n-item-active .i18n-item-name {
|
|
color: #0B5CFF;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.i18n-item-check-box {
|
|
min-width: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.i18n-item-check-mark {
|
|
width: 6px;
|
|
height: 12px;
|
|
border-right: 2px solid #0B5CFF;
|
|
border-bottom: 2px solid #0B5CFF;
|
|
transform: rotate(45deg);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
/* RTL 适配 */
|
|
.is-rtl .i18n-trigger-badge {
|
|
margin-right: 0;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
.is-rtl .i18n-item-badge {
|
|
margin-right: 0;
|
|
margin-left: 12px;
|
|
}
|
|
</style>
|