feat(ui): add status avatar interactions
This commit is contained in:
parent
359caca672
commit
4a54d6fbb7
@ -6,6 +6,7 @@
|
|||||||
:style="{ width: size + 'px', height: size + 'px' }"
|
:style="{ width: size + 'px', height: size + 'px' }"
|
||||||
viewBox="0 0 200 200"
|
viewBox="0 0 200 200"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
@click="handleAvatarClick"
|
||||||
>
|
>
|
||||||
<!-- flat-top 圆角正六边形外框 -->
|
<!-- flat-top 圆角正六边形外框 -->
|
||||||
<path
|
<path
|
||||||
@ -82,6 +83,7 @@ const props = withDefaults(
|
|||||||
);
|
);
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'active-index', index: number): void;
|
(event: 'active-index', index: number): void;
|
||||||
|
(event: 'poke'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// 每个实例独立的 clipPath id,避免多实例冲突
|
// 每个实例独立的 clipPath id,避免多实例冲突
|
||||||
@ -139,15 +141,23 @@ function buildFaceInner(tool: ToolDef): string {
|
|||||||
// ---- 眼睛 morph(idle 竖线 ↔ work 提示符),照搬 demo ----
|
// ---- 眼睛 morph(idle 竖线 ↔ work 提示符),照搬 demo ----
|
||||||
const EYE_SHAPES: Record<string, string> = {
|
const EYE_SHAPES: Record<string, string> = {
|
||||||
idle: 'M 0,-11 Q 0,-5.5 0,0 Q 0,5.5 0,11 Q 0,5.5 0,0 Q 0,-5.5 0,-11',
|
idle: 'M 0,-11 Q 0,-5.5 0,0 Q 0,5.5 0,11 Q 0,5.5 0,0 Q 0,-5.5 0,-11',
|
||||||
|
blink: 'M 0,0 Q 0,0 0,0 Q 0,0 0,0 Q 0,0 0,0 Q 0,0 0,0',
|
||||||
|
nervousLeft: 'M -10,-12 Q -2,-6 6,0 Q -2,6 -10,12 Q -2,6 6,0 Q -2,-6 -10,-12',
|
||||||
|
nervousRight: 'M 10,-12 Q 2,-6 -6,0 Q 2,6 10,12 Q 2,6 -6,0 Q 2,-6 10,-12',
|
||||||
work: 'M 0,0 Q 8.5,0 17,0 Q 25.5,0 34,0 Q 25.5,0 17,0 Q 8.5,0 0,0'
|
work: 'M 0,0 Q 8.5,0 17,0 Q 25.5,0 34,0 Q 25.5,0 17,0 Q 8.5,0 0,0'
|
||||||
};
|
};
|
||||||
const EYE_LEFT_ORIGIN = { x: 84, y: 96 };
|
const EYE_LEFT_ORIGIN = { x: 84, y: 96 };
|
||||||
const EYE_RIGHT_ORIGIN = { x: 116, y: 96 };
|
const EYE_RIGHT_ORIGIN = { x: 116, y: 96 };
|
||||||
|
const NERVOUS_LEFT_ORIGIN = { x: 82, y: 96 };
|
||||||
|
const NERVOUS_RIGHT_ORIGIN = { x: 118, y: 96 };
|
||||||
const WORK_ORIGIN = { x: 117, y: 100 };
|
const WORK_ORIGIN = { x: 117, y: 100 };
|
||||||
const WORK_ROTATION_LEFT = 150;
|
const WORK_ROTATION_LEFT = 150;
|
||||||
const WORK_ROTATION_RIGHT = 210;
|
const WORK_ROTATION_RIGHT = 210;
|
||||||
|
const DEFAULT_EYE_TRANSITION = 'transform 0.55s cubic-bezier(0.22, 1, 0.36, 1)';
|
||||||
|
const NERVOUS_EYE_TRANSITION = 'transform 0.22s cubic-bezier(0.65, 0, 0.35, 1)';
|
||||||
|
|
||||||
let currentEyeShape = 'idle';
|
let currentLeftEyeShape = 'idle';
|
||||||
|
let currentRightEyeShape = 'idle';
|
||||||
|
|
||||||
function cubicBezier(p1x: number, p1y: number, p2x: number, p2y: number) {
|
function cubicBezier(p1x: number, p1y: number, p2x: number, p2y: number) {
|
||||||
const cx = 3 * p1x;
|
const cx = 3 * p1x;
|
||||||
@ -170,6 +180,7 @@ function cubicBezier(p1x: number, p1y: number, p2x: number, p2y: number) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const easeOut = cubicBezier(0.22, 1, 0.36, 1);
|
const easeOut = cubicBezier(0.22, 1, 0.36, 1);
|
||||||
|
const easeInOut = cubicBezier(0.65, 0, 0.35, 1);
|
||||||
|
|
||||||
function parsePathNumbers(d: string): number[] {
|
function parsePathNumbers(d: string): number[] {
|
||||||
return (d.match(/[-\d.]+/g) || []).map(Number);
|
return (d.match(/[-\d.]+/g) || []).map(Number);
|
||||||
@ -179,7 +190,13 @@ function buildPath(n: number[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const morphRaf = new Map<SVGPathElement, number>();
|
const morphRaf = new Map<SVGPathElement, number>();
|
||||||
function morphShape(eye: SVGPathElement | null, fromShape: string, toShape: string, duration = 550) {
|
function morphShape(
|
||||||
|
eye: SVGPathElement | null,
|
||||||
|
fromShape: string,
|
||||||
|
toShape: string,
|
||||||
|
duration = 550,
|
||||||
|
easing = easeOut
|
||||||
|
) {
|
||||||
if (!eye) return;
|
if (!eye) return;
|
||||||
eye.setAttribute('d', EYE_SHAPES[fromShape]);
|
eye.setAttribute('d', EYE_SHAPES[fromShape]);
|
||||||
const fromNums = parsePathNumbers(EYE_SHAPES[fromShape]);
|
const fromNums = parsePathNumbers(EYE_SHAPES[fromShape]);
|
||||||
@ -189,7 +206,7 @@ function morphShape(eye: SVGPathElement | null, fromShape: string, toShape: stri
|
|||||||
if (prev) cancelAnimationFrame(prev);
|
if (prev) cancelAnimationFrame(prev);
|
||||||
const step = (now: number) => {
|
const step = (now: number) => {
|
||||||
const progress = Math.min((now - startTime) / duration, 1);
|
const progress = Math.min((now - startTime) / duration, 1);
|
||||||
const eased = easeOut(progress);
|
const eased = easing(progress);
|
||||||
const cur = fromNums.map((v, i) => v + (toNums[i] - v) * eased);
|
const cur = fromNums.map((v, i) => v + (toNums[i] - v) * eased);
|
||||||
eye.setAttribute('d', buildPath(cur));
|
eye.setAttribute('d', buildPath(cur));
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
@ -202,16 +219,34 @@ function morphShape(eye: SVGPathElement | null, fromShape: string, toShape: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyWorkTransform() {
|
function applyWorkTransform() {
|
||||||
if (eyeLeftRef.value)
|
if (eyeLeftRef.value) {
|
||||||
|
eyeLeftRef.value.style.transition = DEFAULT_EYE_TRANSITION;
|
||||||
eyeLeftRef.value.style.transform = `translate(${WORK_ORIGIN.x}px, ${WORK_ORIGIN.y}px) rotate(${WORK_ROTATION_LEFT}deg)`;
|
eyeLeftRef.value.style.transform = `translate(${WORK_ORIGIN.x}px, ${WORK_ORIGIN.y}px) rotate(${WORK_ROTATION_LEFT}deg)`;
|
||||||
if (eyeRightRef.value)
|
}
|
||||||
|
if (eyeRightRef.value) {
|
||||||
|
eyeRightRef.value.style.transition = DEFAULT_EYE_TRANSITION;
|
||||||
eyeRightRef.value.style.transform = `translate(${WORK_ORIGIN.x}px, ${WORK_ORIGIN.y}px) rotate(${WORK_ROTATION_RIGHT}deg)`;
|
eyeRightRef.value.style.transform = `translate(${WORK_ORIGIN.x}px, ${WORK_ORIGIN.y}px) rotate(${WORK_ROTATION_RIGHT}deg)`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function applyIdleTransform() {
|
function applyIdleTransform() {
|
||||||
if (eyeLeftRef.value)
|
if (eyeLeftRef.value) {
|
||||||
|
eyeLeftRef.value.style.transition = DEFAULT_EYE_TRANSITION;
|
||||||
eyeLeftRef.value.style.transform = `translate(${EYE_LEFT_ORIGIN.x}px, ${EYE_LEFT_ORIGIN.y}px)`;
|
eyeLeftRef.value.style.transform = `translate(${EYE_LEFT_ORIGIN.x}px, ${EYE_LEFT_ORIGIN.y}px)`;
|
||||||
if (eyeRightRef.value)
|
}
|
||||||
|
if (eyeRightRef.value) {
|
||||||
|
eyeRightRef.value.style.transition = DEFAULT_EYE_TRANSITION;
|
||||||
eyeRightRef.value.style.transform = `translate(${EYE_RIGHT_ORIGIN.x}px, ${EYE_RIGHT_ORIGIN.y}px)`;
|
eyeRightRef.value.style.transform = `translate(${EYE_RIGHT_ORIGIN.x}px, ${EYE_RIGHT_ORIGIN.y}px)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function applyNervousTransform() {
|
||||||
|
if (eyeLeftRef.value) {
|
||||||
|
eyeLeftRef.value.style.transition = NERVOUS_EYE_TRANSITION;
|
||||||
|
eyeLeftRef.value.style.transform = `translate(${NERVOUS_LEFT_ORIGIN.x}px, ${NERVOUS_LEFT_ORIGIN.y}px)`;
|
||||||
|
}
|
||||||
|
if (eyeRightRef.value) {
|
||||||
|
eyeRightRef.value.style.transition = NERVOUS_EYE_TRANSITION;
|
||||||
|
eyeRightRef.value.style.transform = `translate(${NERVOUS_RIGHT_ORIGIN.x}px, ${NERVOUS_RIGHT_ORIGIN.y}px)`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 眼睛鼠标追踪 ----
|
// ---- 眼睛鼠标追踪 ----
|
||||||
@ -231,7 +266,8 @@ function stopTracking() {
|
|||||||
if (faceRef.value) faceRef.value.style.transform = 'translate(0px, 0px)';
|
if (faceRef.value) faceRef.value.style.transform = 'translate(0px, 0px)';
|
||||||
}
|
}
|
||||||
function trackEyes() {
|
function trackEyes() {
|
||||||
if (!(props.tracking && props.mode === 'idle') || !svgRef.value) {
|
const mode = internalMode.value || props.mode;
|
||||||
|
if (!(props.tracking && (mode === 'idle' || mode === 'nervous')) || !svgRef.value) {
|
||||||
stopTracking();
|
stopTracking();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -254,7 +290,11 @@ function trackEyes() {
|
|||||||
// ---- 并行工具轮播 ----
|
// ---- 并行工具轮播 ----
|
||||||
let carouselTimer: number | null = null;
|
let carouselTimer: number | null = null;
|
||||||
let faceSwitchTimer: number | null = null;
|
let faceSwitchTimer: number | null = null;
|
||||||
|
let blinkTimer: number | null = null;
|
||||||
|
let nervousTimer: number | null = null;
|
||||||
|
let isBlinking = false;
|
||||||
let carouselIdx = 0;
|
let carouselIdx = 0;
|
||||||
|
const internalMode = ref<string | null>(null);
|
||||||
function stopCarousel() {
|
function stopCarousel() {
|
||||||
if (carouselTimer) clearInterval(carouselTimer);
|
if (carouselTimer) clearInterval(carouselTimer);
|
||||||
carouselTimer = null;
|
carouselTimer = null;
|
||||||
@ -263,6 +303,73 @@ function stopFaceSwitchTimer() {
|
|||||||
if (faceSwitchTimer) clearTimeout(faceSwitchTimer);
|
if (faceSwitchTimer) clearTimeout(faceSwitchTimer);
|
||||||
faceSwitchTimer = null;
|
faceSwitchTimer = null;
|
||||||
}
|
}
|
||||||
|
function stopAutoBlink() {
|
||||||
|
if (blinkTimer) clearTimeout(blinkTimer);
|
||||||
|
blinkTimer = null;
|
||||||
|
}
|
||||||
|
function stopNervousTimer() {
|
||||||
|
if (nervousTimer) clearTimeout(nervousTimer);
|
||||||
|
nervousTimer = null;
|
||||||
|
}
|
||||||
|
function wait(ms: number) {
|
||||||
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
function scheduleAutoBlink() {
|
||||||
|
stopAutoBlink();
|
||||||
|
if (props.mode !== 'idle' || internalMode.value) return;
|
||||||
|
const delay = 2800 + Math.random() * 3600;
|
||||||
|
blinkTimer = window.setTimeout(async () => {
|
||||||
|
await blinkEyes();
|
||||||
|
scheduleAutoBlink();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
async function blinkEyes() {
|
||||||
|
if (props.mode !== 'idle' || internalMode.value || isBlinking) return;
|
||||||
|
isBlinking = true;
|
||||||
|
const blinkCount = Math.random() < 0.5 ? 1 : 2;
|
||||||
|
for (let i = 0; i < blinkCount; i++) {
|
||||||
|
morphShape(eyeLeftRef.value, 'idle', 'blink', 150, easeInOut);
|
||||||
|
morphShape(eyeRightRef.value, 'idle', 'blink', 150, easeInOut);
|
||||||
|
await wait(120);
|
||||||
|
if (props.mode !== 'idle' || internalMode.value) {
|
||||||
|
isBlinking = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await wait(45);
|
||||||
|
morphShape(eyeLeftRef.value, 'blink', 'idle', 210, easeInOut);
|
||||||
|
morphShape(eyeRightRef.value, 'blink', 'idle', 210, easeInOut);
|
||||||
|
await wait(i === blinkCount - 1 ? 200 : 250);
|
||||||
|
}
|
||||||
|
isBlinking = false;
|
||||||
|
}
|
||||||
|
function triggerNervous() {
|
||||||
|
if (props.mode !== 'idle') return;
|
||||||
|
internalMode.value = 'nervous';
|
||||||
|
stopAutoBlink();
|
||||||
|
stopNervousTimer();
|
||||||
|
stopTracking();
|
||||||
|
stopCarousel();
|
||||||
|
stopFaceSwitchTimer();
|
||||||
|
activeFaceKey.value = null;
|
||||||
|
sharedVisible.value = true;
|
||||||
|
applyNervousTransform();
|
||||||
|
morphShape(eyeLeftRef.value, currentLeftEyeShape, 'nervousLeft', 220, easeInOut);
|
||||||
|
morphShape(eyeRightRef.value, currentRightEyeShape, 'nervousRight', 220, easeInOut);
|
||||||
|
currentLeftEyeShape = 'nervousLeft';
|
||||||
|
currentRightEyeShape = 'nervousRight';
|
||||||
|
if (props.tracking) trackEyes();
|
||||||
|
nervousTimer = window.setTimeout(() => {
|
||||||
|
if (internalMode.value === 'nervous') {
|
||||||
|
internalMode.value = null;
|
||||||
|
applyState();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
function handleAvatarClick() {
|
||||||
|
if (props.mode !== 'idle') return;
|
||||||
|
emit('poke');
|
||||||
|
triggerNervous();
|
||||||
|
}
|
||||||
function setIconFace(key: string, animated = true) {
|
function setIconFace(key: string, animated = true) {
|
||||||
const fromShared = sharedVisible.value && !activeFaceKey.value;
|
const fromShared = sharedVisible.value && !activeFaceKey.value;
|
||||||
sharedVisible.value = false;
|
sharedVisible.value = false;
|
||||||
@ -306,7 +413,12 @@ function startCarousel(keys: string[]) {
|
|||||||
|
|
||||||
// ---- 状态应用 ----
|
// ---- 状态应用 ----
|
||||||
function applyState() {
|
function applyState() {
|
||||||
const mode = props.mode;
|
if (props.mode !== 'idle' && internalMode.value) {
|
||||||
|
internalMode.value = null;
|
||||||
|
stopNervousTimer();
|
||||||
|
}
|
||||||
|
const mode = internalMode.value || props.mode;
|
||||||
|
if (mode !== 'idle') stopAutoBlink();
|
||||||
if (mode === 'tool') {
|
if (mode === 'tool') {
|
||||||
stopTracking();
|
stopTracking();
|
||||||
const keys = (props.toolKeys || []).filter(Boolean);
|
const keys = (props.toolKeys || []).filter(Boolean);
|
||||||
@ -320,17 +432,21 @@ function applyState() {
|
|||||||
setIconFace('think');
|
setIconFace('think');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (mode === 'nervous') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// idle / work:显示 shared 眼睛并 morph
|
// idle / work:显示 shared 眼睛并 morph
|
||||||
const target = mode === 'work' ? 'work' : 'idle';
|
const target = mode === 'work' ? 'work' : 'idle';
|
||||||
const fromIcon = !!activeFaceKey.value;
|
const fromIcon = !!activeFaceKey.value;
|
||||||
activeFaceKey.value = null;
|
activeFaceKey.value = null;
|
||||||
const revealShared = () => {
|
const revealShared = () => {
|
||||||
sharedVisible.value = true;
|
sharedVisible.value = true;
|
||||||
morphShape(eyeLeftRef.value, currentEyeShape, target);
|
morphShape(eyeLeftRef.value, currentLeftEyeShape, target);
|
||||||
morphShape(eyeRightRef.value, currentEyeShape, target);
|
morphShape(eyeRightRef.value, currentRightEyeShape, target);
|
||||||
if (target === 'work') applyWorkTransform();
|
if (target === 'work') applyWorkTransform();
|
||||||
else applyIdleTransform();
|
else applyIdleTransform();
|
||||||
currentEyeShape = target;
|
currentLeftEyeShape = target;
|
||||||
|
currentRightEyeShape = target;
|
||||||
};
|
};
|
||||||
if (fromIcon) {
|
if (fromIcon) {
|
||||||
faceSwitchTimer = window.setTimeout(() => {
|
faceSwitchTimer = window.setTimeout(() => {
|
||||||
@ -343,12 +459,14 @@ function applyState() {
|
|||||||
if (target === 'idle' && props.tracking) {
|
if (target === 'idle' && props.tracking) {
|
||||||
stopTracking();
|
stopTracking();
|
||||||
trackEyes();
|
trackEyes();
|
||||||
|
scheduleAutoBlink();
|
||||||
} else {
|
} else {
|
||||||
stopTracking();
|
stopTracking();
|
||||||
|
stopAutoBlink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => [props.mode, props.toolKeys, props.tracking], () => applyState(), { deep: true });
|
watch(() => [props.mode, props.toolKeys, props.tracking, internalMode.value], () => applyState(), { deep: true });
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
applyIdleTransform();
|
applyIdleTransform();
|
||||||
@ -360,6 +478,8 @@ onBeforeUnmount(() => {
|
|||||||
stopTracking();
|
stopTracking();
|
||||||
stopCarousel();
|
stopCarousel();
|
||||||
stopFaceSwitchTimer();
|
stopFaceSwitchTimer();
|
||||||
|
stopAutoBlink();
|
||||||
|
stopNervousTimer();
|
||||||
morphRaf.forEach((id) => cancelAnimationFrame(id));
|
morphRaf.forEach((id) => cancelAnimationFrame(id));
|
||||||
morphRaf.clear();
|
morphRaf.clear();
|
||||||
});
|
});
|
||||||
@ -368,6 +488,7 @@ onBeforeUnmount(() => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.status-avatar {
|
.status-avatar {
|
||||||
display: block;
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@ -401,6 +522,11 @@ onBeforeUnmount(() => {
|
|||||||
.sa-shared.hidden {
|
.sa-shared.hidden {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-avatar--work .sa-shared {
|
||||||
|
animation: sa-bob 2.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.sa-icon {
|
.sa-icon {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.92);
|
transform: scale(0.92);
|
||||||
|
|||||||
@ -632,24 +632,28 @@
|
|||||||
:tracking="avatarStatusSafe.tracking"
|
:tracking="avatarStatusSafe.tracking"
|
||||||
:size="34"
|
:size="34"
|
||||||
@active-index="handleStatusAvatarActiveIndex"
|
@active-index="handleStatusAvatarActiveIndex"
|
||||||
|
@poke="handleStatusAvatarPoke"
|
||||||
/>
|
/>
|
||||||
<span
|
<Transition name="conversation-status-avatar-text-fade" mode="out-in">
|
||||||
v-if="statusAvatarText"
|
<span
|
||||||
class="conversation-status-avatar__text"
|
v-if="statusAvatarText"
|
||||||
:class="{ 'conversation-status-avatar__text--animated': statusAvatarTextAnimated }"
|
:key="statusAvatarText"
|
||||||
>
|
class="conversation-status-avatar__text"
|
||||||
<template v-if="statusAvatarTextAnimated">
|
:class="{ 'conversation-status-avatar__text--animated': statusAvatarTextAnimated }"
|
||||||
<span
|
>
|
||||||
v-for="(letter, letterIndex) in statusAvatarLetters"
|
<template v-if="statusAvatarTextAnimated">
|
||||||
:key="`${letter}-${letterIndex}`"
|
<span
|
||||||
class="conversation-status-avatar__letter"
|
v-for="(letter, letterIndex) in statusAvatarLetters"
|
||||||
:style="{ animationDelay: `${letterIndex * 0.08}s` }"
|
:key="`${letter}-${letterIndex}`"
|
||||||
>
|
class="conversation-status-avatar__letter"
|
||||||
{{ letter }}
|
:style="{ animationDelay: `${letterIndex * 0.08}s` }"
|
||||||
</span>
|
>
|
||||||
</template>
|
{{ letter }}
|
||||||
<template v-else>{{ statusAvatarText }}</template>
|
</span>
|
||||||
</span>
|
</template>
|
||||||
|
<template v-else>{{ statusAvatarText }}</template>
|
||||||
|
</span>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="messages-bottom-spacer" aria-hidden="true"></div>
|
<div class="messages-bottom-spacer" aria-hidden="true"></div>
|
||||||
@ -1032,6 +1036,30 @@ const getFilteredMessagesSafe = () =>
|
|||||||
const latestMessageIndex = computed(() => getFilteredMessagesSafe().length - 1);
|
const latestMessageIndex = computed(() => getFilteredMessagesSafe().length - 1);
|
||||||
const statusAvatarActiveIndex = ref(0);
|
const statusAvatarActiveIndex = ref(0);
|
||||||
const statusAvatarRef = ref<HTMLElement | null>(null);
|
const statusAvatarRef = ref<HTMLElement | null>(null);
|
||||||
|
const statusAvatarPokeText = ref('');
|
||||||
|
let statusAvatarPokeTimer: number | null = null;
|
||||||
|
const STATUS_AVATAR_POKE_TEXTS = [
|
||||||
|
'不要!',
|
||||||
|
'停下来!',
|
||||||
|
'别戳啦!',
|
||||||
|
'哎呀!',
|
||||||
|
'你戳到我了!',
|
||||||
|
'轻一点嘛!',
|
||||||
|
'我不是按钮!',
|
||||||
|
'不许乱点!',
|
||||||
|
'呜……别戳我了……',
|
||||||
|
'六边形也是会痛的!',
|
||||||
|
'再戳我要变形了!',
|
||||||
|
'我会紧张的……',
|
||||||
|
'正在假装镇定……',
|
||||||
|
'Token 掉了一地!',
|
||||||
|
'上下文被你戳乱了!',
|
||||||
|
'防戳模式启动失败',
|
||||||
|
'系统提示:AI 被戳了一下',
|
||||||
|
'警告:检测到手欠行为',
|
||||||
|
'我记住你了!',
|
||||||
|
'戳回去!'
|
||||||
|
];
|
||||||
const avatarStatusSafe = computed(() => ({
|
const avatarStatusSafe = computed(() => ({
|
||||||
mode: props.avatarStatus?.mode || 'idle',
|
mode: props.avatarStatus?.mode || 'idle',
|
||||||
toolKeys: Array.isArray(props.avatarStatus?.toolKeys) ? props.avatarStatus!.toolKeys! : [],
|
toolKeys: Array.isArray(props.avatarStatus?.toolKeys) ? props.avatarStatus!.toolKeys! : [],
|
||||||
@ -1040,6 +1068,9 @@ const avatarStatusSafe = computed(() => ({
|
|||||||
tracking: props.avatarStatus?.tracking !== false
|
tracking: props.avatarStatus?.tracking !== false
|
||||||
}));
|
}));
|
||||||
const statusAvatarText = computed(() => {
|
const statusAvatarText = computed(() => {
|
||||||
|
if (statusAvatarPokeText.value && avatarStatusSafe.value.mode === 'idle') {
|
||||||
|
return statusAvatarPokeText.value;
|
||||||
|
}
|
||||||
if (avatarStatusSafe.value.mode === 'tool' && avatarStatusSafe.value.toolTexts.length > 1) {
|
if (avatarStatusSafe.value.mode === 'tool' && avatarStatusSafe.value.toolTexts.length > 1) {
|
||||||
const idx = statusAvatarActiveIndex.value % avatarStatusSafe.value.toolTexts.length;
|
const idx = statusAvatarActiveIndex.value % avatarStatusSafe.value.toolTexts.length;
|
||||||
return avatarStatusSafe.value.toolTexts[idx] || avatarStatusSafe.value.text;
|
return avatarStatusSafe.value.toolTexts[idx] || avatarStatusSafe.value.text;
|
||||||
@ -1050,13 +1081,31 @@ const statusAvatarTextAnimated = computed(() => {
|
|||||||
return avatarStatusSafe.value.mode === 'work' && !!statusAvatarText.value;
|
return avatarStatusSafe.value.mode === 'work' && !!statusAvatarText.value;
|
||||||
});
|
});
|
||||||
const statusAvatarLetters = computed(() => Array.from(statusAvatarText.value || ''));
|
const statusAvatarLetters = computed(() => Array.from(statusAvatarText.value || ''));
|
||||||
|
const clearStatusAvatarPokeText = () => {
|
||||||
|
if (statusAvatarPokeTimer !== null) {
|
||||||
|
window.clearTimeout(statusAvatarPokeTimer);
|
||||||
|
statusAvatarPokeTimer = null;
|
||||||
|
}
|
||||||
|
statusAvatarPokeText.value = '';
|
||||||
|
};
|
||||||
const handleStatusAvatarActiveIndex = (index: number) => {
|
const handleStatusAvatarActiveIndex = (index: number) => {
|
||||||
statusAvatarActiveIndex.value = Number.isFinite(index) ? Math.max(0, index) : 0;
|
statusAvatarActiveIndex.value = Number.isFinite(index) ? Math.max(0, index) : 0;
|
||||||
};
|
};
|
||||||
|
const handleStatusAvatarPoke = () => {
|
||||||
|
if (avatarStatusSafe.value.mode !== 'idle') return;
|
||||||
|
const texts = STATUS_AVATAR_POKE_TEXTS;
|
||||||
|
statusAvatarPokeText.value = texts[Math.floor(Math.random() * texts.length)] || '不要!';
|
||||||
|
if (statusAvatarPokeTimer !== null) window.clearTimeout(statusAvatarPokeTimer);
|
||||||
|
statusAvatarPokeTimer = window.setTimeout(() => {
|
||||||
|
statusAvatarPokeText.value = '';
|
||||||
|
statusAvatarPokeTimer = null;
|
||||||
|
}, 1800);
|
||||||
|
};
|
||||||
watch(
|
watch(
|
||||||
() => [props.avatarStatus?.mode, props.avatarStatus?.toolKeys?.join('|')],
|
() => [props.avatarStatus?.mode, props.avatarStatus?.toolKeys?.join('|')],
|
||||||
() => {
|
() => {
|
||||||
statusAvatarActiveIndex.value = 0;
|
statusAvatarActiveIndex.value = 0;
|
||||||
|
if (avatarStatusSafe.value.mode !== 'idle') clearStatusAvatarPokeText();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const nowMs = ref(Date.now());
|
const nowMs = ref(Date.now());
|
||||||
@ -1959,6 +2008,7 @@ onBeforeUnmount(() => {
|
|||||||
clearInterval(timerHandle);
|
clearInterval(timerHandle);
|
||||||
timerHandle = null;
|
timerHandle = null;
|
||||||
}
|
}
|
||||||
|
clearStatusAvatarPokeText();
|
||||||
});
|
});
|
||||||
|
|
||||||
const isStackable = (action: any) =>
|
const isStackable = (action: any) =>
|
||||||
|
|||||||
@ -151,6 +151,21 @@
|
|||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conversation-status-avatar-text-fade-enter-active,
|
||||||
|
.conversation-status-avatar-text-fade-leave-active {
|
||||||
|
transition: opacity 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-status-avatar-text-fade-enter-from,
|
||||||
|
.conversation-status-avatar-text-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-status-avatar-text-fade-enter-to,
|
||||||
|
.conversation-status-avatar-text-fade-leave-from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.conversation-status-avatar__text--animated {
|
.conversation-status-avatar__text--animated {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user