feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
这个提交包含在:
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<view
|
||||
class="audio-capsule"
|
||||
:class="{ 'audio-capsule-active': playerState.waveformActive }"
|
||||
@click="handleCapsuleClick"
|
||||
>
|
||||
<!-- 极简几何扬声器图标 -->
|
||||
<view class="speaker-icon">
|
||||
<view class="speaker-base" :class="{ 'speaker-base-active': playerState.waveformActive }"></view>
|
||||
<view class="speaker-cone" :class="{ 'speaker-cone-active': playerState.waveformActive }"></view>
|
||||
</view>
|
||||
|
||||
<!-- 文本标签 (若提供) -->
|
||||
<text
|
||||
v-if="resolvedLabel !== ''"
|
||||
class="capsule-label"
|
||||
:class="{ 'capsule-label-active': playerState.waveformActive }"
|
||||
>{{ resolvedLabel }}</text>
|
||||
|
||||
<!-- 3柱极简波形脉冲条 -->
|
||||
<view class="waveform-box">
|
||||
<view
|
||||
class="wave-bar bar-1"
|
||||
:class="{ 'wave-bar-pulse pulse-1': playerState.waveformActive, 'bar-active': playerState.waveformActive }"
|
||||
></view>
|
||||
<view
|
||||
class="wave-bar bar-2"
|
||||
:class="{ 'wave-bar-pulse pulse-2': playerState.waveformActive, 'bar-active': playerState.waveformActive }"
|
||||
></view>
|
||||
<view
|
||||
class="wave-bar bar-3"
|
||||
:class="{ 'wave-bar-pulse pulse-3': playerState.waveformActive, 'bar-active': playerState.waveformActive }"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import {
|
||||
AudioPlayerState,
|
||||
createAudioPlayerState,
|
||||
toggleAudio,
|
||||
releaseAudioPlayer,
|
||||
} from '../../pages/portal/useAudioPlayer.uts';
|
||||
|
||||
const props = defineProps({
|
||||
audioUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const playerState = reactive<AudioPlayerState>(createAudioPlayerState());
|
||||
|
||||
const resolvedLabel = computed((): string => {
|
||||
if (props.label != null && props.label.trim() !== '') {
|
||||
return props.label.trim();
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
function handleCapsuleClick(): void {
|
||||
const url = props.audioUrl != null ? props.audioUrl.trim() : '';
|
||||
if (url === '') {
|
||||
return;
|
||||
}
|
||||
toggleAudio(playerState, url);
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
releaseAudioPlayer(playerState);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.audio-capsule {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 32px;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border-radius: 16px;
|
||||
background-color: #EAF1FF;
|
||||
border: 1px solid #E3E7ED;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
|
||||
transition: background-color 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
.audio-capsule-active {
|
||||
background-color: #0B5CFF;
|
||||
border-color: #0B5CFF;
|
||||
}
|
||||
|
||||
/* 极简几何扬声器 */
|
||||
.speaker-icon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.speaker-base {
|
||||
width: 3px;
|
||||
height: 6px;
|
||||
background-color: #0B5CFF;
|
||||
border-radius: 1px 0 0 1px;
|
||||
}
|
||||
|
||||
.speaker-cone {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-right: 6px solid #0B5CFF;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.speaker-base-active {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.speaker-cone-active {
|
||||
border-right-color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* 标签文案 */
|
||||
.capsule-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #0B5CFF;
|
||||
margin-right: 6px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.capsule-label-active {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* 3柱波形 */
|
||||
.waveform-box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.wave-bar {
|
||||
width: 2px;
|
||||
background-color: #0B5CFF;
|
||||
border-radius: 1px;
|
||||
margin: 0 1.5px;
|
||||
}
|
||||
|
||||
.bar-1 {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.bar-2 {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.bar-3 {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.bar-active {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* 律动脉冲关键帧动画 */
|
||||
.wave-bar-pulse {
|
||||
animation-duration: 400ms;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
.pulse-1 {
|
||||
animation-name: waveAnim1;
|
||||
}
|
||||
|
||||
.pulse-2 {
|
||||
animation-name: waveAnim2;
|
||||
animation-delay: 150ms;
|
||||
}
|
||||
|
||||
.pulse-3 {
|
||||
animation-name: waveAnim3;
|
||||
animation-delay: 300ms;
|
||||
}
|
||||
|
||||
@keyframes waveAnim1 {
|
||||
0% {
|
||||
height: 4px;
|
||||
}
|
||||
100% {
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes waveAnim2 {
|
||||
0% {
|
||||
height: 6px;
|
||||
}
|
||||
100% {
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes waveAnim3 {
|
||||
0% {
|
||||
height: 5px;
|
||||
}
|
||||
100% {
|
||||
height: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wave-bar-pulse {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<view class="i18n-selector-wrap" :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>
|
||||
|
||||
<!-- 语言切换浮层面板 (遮罩 + 弹出列表) -->
|
||||
<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;
|
||||
}
|
||||
|
||||
/* 常驻右上角触发胶囊 (触控区 >= 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);
|
||||
}
|
||||
|
||||
.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>
|
||||
在新工单中引用
屏蔽一个用户