virtual-human-cf 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/components/VirtualHumanEventAdapter.vue.d.ts +2 -0
- package/dist/style.css +1 -1
- package/dist/virtual-human-cf.es.js +172 -158
- package/dist/virtual-human-cf.umd.js +1 -1
- package/package.json +1 -1
- package/src/components/VirtualHumanEventAdapter.vue +36 -6
- package/src/components/VirtualHumanPersona.vue +79 -28
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ declare const __VLS_component: import('vue').DefineComponent<import('vue').Extra
|
|
|
17
17
|
showDialog: (...args: any[]) => void;
|
|
18
18
|
end: (...args: any[]) => void;
|
|
19
19
|
connected: (...args: any[]) => void;
|
|
20
|
+
play_complete: (...args: any[]) => void;
|
|
20
21
|
}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
|
|
21
22
|
screenClientId: {
|
|
22
23
|
type: StringConstructor;
|
|
@@ -33,6 +34,7 @@ declare const __VLS_component: import('vue').DefineComponent<import('vue').Extra
|
|
|
33
34
|
onShowDialog?: ((...args: any[]) => any) | undefined;
|
|
34
35
|
onEnd?: ((...args: any[]) => any) | undefined;
|
|
35
36
|
onConnected?: ((...args: any[]) => any) | undefined;
|
|
37
|
+
onPlay_complete?: ((...args: any[]) => any) | undefined;
|
|
36
38
|
}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
37
39
|
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, ReturnType<typeof __VLS_template>>;
|
|
38
40
|
export default _default;
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.fade-enter-active[data-v-
|
|
1
|
+
.fade-enter-active[data-v-5d741303],.fade-leave-active[data-v-5d741303]{transition:opacity .5s ease,transform .5s ease}.fade-enter-from[data-v-5d741303],.fade-leave-to[data-v-5d741303]{opacity:0;transform:translateY(10px)}.virtual-human-container[data-v-5d741303]{position:fixed;z-index:2147483647;overflow:visible}.video-wrapper[data-v-5d741303]{position:relative;width:100%;height:100%;border-radius:1rem;overflow:hidden;box-shadow:0 10px 25px #0000001a;background:#fff;border:2px solid transparent;transition:background-color .3s ease,box-shadow .3s ease,border-color .3s ease}.video-wrapper[data-v-5d741303]:hover{border-color:#409eff}.virtual-human-container.is-dark .video-wrapper[data-v-5d741303]{background:#1f2937;box-shadow:0 10px 25px #00000080}.persona-video[data-v-5d741303]{width:100%;height:100%;object-fit:cover;transform-origin:center;transition:transform .1s ease-out}.resize-handle[data-v-5d741303]{position:absolute;width:20px;height:20px;background:#00000080;border:2px solid rgba(255,255,255,.8);border-radius:4px;cursor:pointer;z-index:20;opacity:0;pointer-events:none;transition:opacity .3s ease,background .2s,border-color .2s}.video-wrapper:hover .resize-handle[data-v-5d741303]{opacity:1;pointer-events:auto}.resize-handle[data-v-5d741303]:hover{background:#000000b3;border-color:#fff}.resize-handle.top-left[data-v-5d741303]{top:10px;left:10px;cursor:nwse-resize}.resize-handle.top-right[data-v-5d741303]{top:10px;right:10px;cursor:nesw-resize}.resize-handle.bottom-left[data-v-5d741303]{bottom:10px;left:10px;cursor:nesw-resize}.resize-handle.bottom-right[data-v-5d741303]{bottom:10px;right:10px;cursor:nwse-resize}.overlay[data-v-5d741303]{position:absolute;top:1rem;right:1rem;z-index:10}.status-badge[data-v-5d741303]{display:flex;align-items:center;gap:.5rem;padding:.25rem .75rem;border-radius:9999px;font-size:.75rem;font-weight:500;color:#fff;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.status-badge.paused[data-v-5d741303]{background:#ef4444cc}.status-badge.playing[data-v-5d741303]{background:#22c55ecc}.dot[data-v-5d741303]{width:6px;height:6px;border-radius:50%;background-color:#fff}@keyframes pulse-5d741303{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse[data-v-5d741303]{animation:pulse-5d741303 2s cubic-bezier(.4,0,.6,1) infinite}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { defineComponent as
|
|
2
|
-
const
|
|
1
|
+
import { defineComponent as j, ref as d, watch as X, onMounted as G, onUnmounted as K, openBlock as P, createBlock as Q, Transition as Z, withCtx as _, createElementBlock as V, normalizeStyle as ee, normalizeClass as te, createElementVNode as R, createCommentVNode as T, createTextVNode as J, renderSlot as ne } from "vue";
|
|
2
|
+
const ae = ["src", "muted"], se = { class: "overlay" }, le = {
|
|
3
3
|
key: 0,
|
|
4
4
|
class: "status-badge paused"
|
|
5
|
-
},
|
|
5
|
+
}, oe = {
|
|
6
6
|
key: 1,
|
|
7
7
|
class: "status-badge playing"
|
|
8
|
-
},
|
|
8
|
+
}, re = /* @__PURE__ */ j({
|
|
9
9
|
__name: "VirtualHumanPersona",
|
|
10
10
|
props: {
|
|
11
11
|
// 视频源URL
|
|
@@ -46,131 +46,136 @@ const j = ["src", "muted"], G = { class: "overlay" }, K = {
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
emits: ["update:isPlaying", "ended", "update:visible"],
|
|
49
|
-
setup(
|
|
50
|
-
const t =
|
|
49
|
+
setup(c, { emit: x }) {
|
|
50
|
+
const t = c, l = x, u = d(null), Y = d(null), n = d(null), y = d(!1), h = d(400), w = d(500), W = d(16), E = d(16), z = d(!1), b = d(null), A = d(0), D = d(0), s = d(0), o = d(0), r = d(0), v = d(0), g = () => {
|
|
51
51
|
if (n.value && n.value.close(), !(!t.wsUrl || !t.screenClientId))
|
|
52
52
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
const i = new URL(t.wsUrl);
|
|
54
|
+
i.searchParams.append("sessionId", t.screenClientId + "-persona"), n.value = new WebSocket(i.toString()), n.value.onopen = () => {
|
|
55
|
+
y.value = !0, console.log(`[VirtualHumanPersona] Connected to ${t.wsUrl} for session ${t.screenClientId}-persona`);
|
|
56
56
|
}, n.value.onmessage = (e) => {
|
|
57
57
|
try {
|
|
58
58
|
const a = JSON.parse(e.data);
|
|
59
|
-
|
|
59
|
+
B(a);
|
|
60
60
|
} catch (a) {
|
|
61
61
|
console.error("[VirtualHumanPersona] Failed to parse message:", e.data, a);
|
|
62
62
|
}
|
|
63
63
|
}, n.value.onerror = (e) => {
|
|
64
64
|
console.error("[VirtualHumanPersona] WebSocket error:", e);
|
|
65
65
|
}, n.value.onclose = () => {
|
|
66
|
-
|
|
66
|
+
y.value = !1, console.log("[VirtualHumanPersona] WebSocket disconnected");
|
|
67
67
|
};
|
|
68
|
-
} catch (
|
|
69
|
-
console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",
|
|
68
|
+
} catch (i) {
|
|
69
|
+
console.error("[VirtualHumanPersona] Failed to initialize WebSocket:", i);
|
|
70
70
|
}
|
|
71
|
-
},
|
|
72
|
-
const { type: e, action: a } =
|
|
73
|
-
e === "control" && (a === "play" || a === "resume" ? (
|
|
71
|
+
}, B = (i) => {
|
|
72
|
+
const { type: e, action: a } = i;
|
|
73
|
+
e === "control" && (a === "play" || a === "resume" ? (l("update:isPlaying", !0), l("update:visible", !0)) : a === "pause" ? l("update:isPlaying", !1) : a === "stop" && (l("update:isPlaying", !1), l("update:visible", !1), u.value && (u.value.currentTime = 0)));
|
|
74
74
|
};
|
|
75
|
-
|
|
76
|
-
t.screenClientId && t.wsUrl &&
|
|
77
|
-
}),
|
|
78
|
-
t.screenClientId && t.wsUrl &&
|
|
79
|
-
}),
|
|
80
|
-
|
|
75
|
+
X(() => t.screenClientId, () => {
|
|
76
|
+
t.screenClientId && t.wsUrl && g();
|
|
77
|
+
}), X(() => t.wsUrl, () => {
|
|
78
|
+
t.screenClientId && t.wsUrl && g();
|
|
79
|
+
}), X(() => t.isPlaying, (i) => {
|
|
80
|
+
u.value && (i ? u.value.play().catch((e) => console.error("Video play failed:", e)) : u.value.pause());
|
|
81
81
|
});
|
|
82
|
-
const
|
|
83
|
-
t.isPlaying ||
|
|
84
|
-
},
|
|
85
|
-
t.isPlaying &&
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
const a = "clientX" in e ? e.clientX : e.touches[0].clientX,
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
if (!
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
},
|
|
99
|
-
|
|
82
|
+
const L = () => {
|
|
83
|
+
t.isPlaying || l("update:isPlaying", !0);
|
|
84
|
+
}, U = () => {
|
|
85
|
+
t.isPlaying && l("update:isPlaying", !1);
|
|
86
|
+
}, f = () => {
|
|
87
|
+
l("update:isPlaying", !1), l("ended");
|
|
88
|
+
}, k = (i, e) => {
|
|
89
|
+
e.preventDefault(), z.value = !0, b.value = i;
|
|
90
|
+
const a = "clientX" in e ? e.clientX : e.touches[0].clientX, C = "clientY" in e ? e.clientY : e.touches[0].clientY;
|
|
91
|
+
A.value = a, D.value = C, s.value = h.value, o.value = w.value, r.value = W.value, v.value = E.value, document.addEventListener("mousemove", M), document.addEventListener("mouseup", $), document.addEventListener("touchmove", M, { passive: !1 }), document.addEventListener("touchend", $);
|
|
92
|
+
}, M = (i) => {
|
|
93
|
+
if (!z.value) return;
|
|
94
|
+
i.preventDefault();
|
|
95
|
+
const e = "clientX" in i ? i.clientX : i.touches[0].clientX, a = "clientY" in i ? i.clientY : i.touches[0].clientY, C = e - A.value, I = a - D.value, F = 200, q = 250, N = 800, O = 1e3;
|
|
96
|
+
let m = s.value, p = o.value, S = r.value, H = v.value;
|
|
97
|
+
b.value === "bottom-right" ? (m = s.value + C, H = v.value - I, p = o.value + I) : b.value === "bottom-left" ? (S = r.value + C, m = s.value - C, H = v.value - I, p = o.value + I) : b.value === "top-right" ? (m = s.value + C, p = o.value - I) : b.value === "top-left" && (S = r.value + C, m = s.value - C, p = o.value - I), m < F && (S !== r.value && (S -= F - m), m = F), p < q && (H !== v.value && (H -= q - p), p = q), m > N && (S !== r.value && (S += m - N), m = N), p > O && (H !== v.value && (H += p - O), p = O), h.value = m, w.value = p, W.value = S, E.value = H;
|
|
98
|
+
}, $ = () => {
|
|
99
|
+
z.value = !1, b.value = null, document.removeEventListener("mousemove", M), document.removeEventListener("mouseup", $), document.removeEventListener("touchmove", M), document.removeEventListener("touchend", $);
|
|
100
100
|
};
|
|
101
|
-
return
|
|
102
|
-
t.isPlaying &&
|
|
103
|
-
}),
|
|
101
|
+
return G(() => {
|
|
102
|
+
t.isPlaying && u.value && u.value.play().catch((i) => console.error("Video play failed:", i)), t.screenClientId && t.wsUrl && g();
|
|
103
|
+
}), K(() => {
|
|
104
104
|
n.value && n.value.close();
|
|
105
|
-
}), (
|
|
106
|
-
default:
|
|
107
|
-
|
|
105
|
+
}), (i, e) => (P(), Q(Z, { name: "fade" }, {
|
|
106
|
+
default: _(() => [
|
|
107
|
+
c.visible ? (P(), V("div", {
|
|
108
108
|
key: 0,
|
|
109
|
-
class:
|
|
109
|
+
class: te(["virtual-human-container", { "is-dark": c.isDark }]),
|
|
110
|
+
style: ee({
|
|
111
|
+
width: h.value + "px",
|
|
112
|
+
height: w.value + "px",
|
|
113
|
+
left: W.value + "px",
|
|
114
|
+
bottom: E.value + "px"
|
|
115
|
+
})
|
|
110
116
|
}, [
|
|
111
|
-
|
|
117
|
+
R("div", {
|
|
112
118
|
class: "video-wrapper",
|
|
113
119
|
ref_key: "wrapperRef",
|
|
114
|
-
ref:
|
|
115
|
-
style: O({ transform: `scale(${V.value})`, transformOrigin: "left bottom" })
|
|
120
|
+
ref: Y
|
|
116
121
|
}, [
|
|
117
|
-
|
|
122
|
+
R("video", {
|
|
118
123
|
ref_key: "videoRef",
|
|
119
|
-
ref:
|
|
120
|
-
src:
|
|
124
|
+
ref: u,
|
|
125
|
+
src: c.videoSrc,
|
|
121
126
|
class: "persona-video",
|
|
122
|
-
muted:
|
|
127
|
+
muted: c.muted,
|
|
123
128
|
playsinline: "",
|
|
124
129
|
loop: "",
|
|
125
|
-
onPlay:
|
|
126
|
-
onPause:
|
|
127
|
-
onEnded:
|
|
128
|
-
}, null, 40,
|
|
129
|
-
|
|
130
|
+
onPlay: L,
|
|
131
|
+
onPause: U,
|
|
132
|
+
onEnded: f
|
|
133
|
+
}, null, 40, ae),
|
|
134
|
+
c.visible ? (P(), V("div", {
|
|
130
135
|
key: 0,
|
|
131
136
|
class: "resize-handle top-left",
|
|
132
|
-
onMousedown: e[0] || (e[0] = (a) =>
|
|
133
|
-
onTouchstart: e[1] || (e[1] = (a) =>
|
|
134
|
-
}, null, 32)) :
|
|
135
|
-
|
|
137
|
+
onMousedown: e[0] || (e[0] = (a) => k("top-left", a)),
|
|
138
|
+
onTouchstart: e[1] || (e[1] = (a) => k("top-left", a))
|
|
139
|
+
}, null, 32)) : T("", !0),
|
|
140
|
+
c.visible ? (P(), V("div", {
|
|
136
141
|
key: 1,
|
|
137
142
|
class: "resize-handle top-right",
|
|
138
|
-
onMousedown: e[2] || (e[2] = (a) =>
|
|
139
|
-
onTouchstart: e[3] || (e[3] = (a) =>
|
|
140
|
-
}, null, 32)) :
|
|
141
|
-
|
|
143
|
+
onMousedown: e[2] || (e[2] = (a) => k("top-right", a)),
|
|
144
|
+
onTouchstart: e[3] || (e[3] = (a) => k("top-right", a))
|
|
145
|
+
}, null, 32)) : T("", !0),
|
|
146
|
+
c.visible ? (P(), V("div", {
|
|
142
147
|
key: 2,
|
|
143
148
|
class: "resize-handle bottom-left",
|
|
144
|
-
onMousedown: e[4] || (e[4] = (a) =>
|
|
145
|
-
onTouchstart: e[5] || (e[5] = (a) =>
|
|
146
|
-
}, null, 32)) :
|
|
147
|
-
|
|
149
|
+
onMousedown: e[4] || (e[4] = (a) => k("bottom-left", a)),
|
|
150
|
+
onTouchstart: e[5] || (e[5] = (a) => k("bottom-left", a))
|
|
151
|
+
}, null, 32)) : T("", !0),
|
|
152
|
+
c.visible ? (P(), V("div", {
|
|
148
153
|
key: 3,
|
|
149
154
|
class: "resize-handle bottom-right",
|
|
150
|
-
onMousedown: e[6] || (e[6] = (a) =>
|
|
151
|
-
onTouchstart: e[7] || (e[7] = (a) =>
|
|
152
|
-
}, null, 32)) :
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
])])) : (
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
onMousedown: e[6] || (e[6] = (a) => k("bottom-right", a)),
|
|
156
|
+
onTouchstart: e[7] || (e[7] = (a) => k("bottom-right", a))
|
|
157
|
+
}, null, 32)) : T("", !0),
|
|
158
|
+
R("div", se, [
|
|
159
|
+
c.isPlaying ? (P(), V("div", oe, [...e[9] || (e[9] = [
|
|
160
|
+
R("span", { class: "dot animate-pulse" }, null, -1),
|
|
161
|
+
J(" 播放中 ", -1)
|
|
162
|
+
])])) : (P(), V("div", le, [...e[8] || (e[8] = [
|
|
163
|
+
R("span", { class: "dot" }, null, -1),
|
|
164
|
+
J(" 暂停中 ", -1)
|
|
160
165
|
])]))
|
|
161
166
|
])
|
|
162
|
-
],
|
|
163
|
-
],
|
|
167
|
+
], 512)
|
|
168
|
+
], 6)) : T("", !0)
|
|
164
169
|
]),
|
|
165
170
|
_: 1
|
|
166
171
|
}));
|
|
167
172
|
}
|
|
168
|
-
}),
|
|
169
|
-
const t =
|
|
170
|
-
for (const [
|
|
171
|
-
t[
|
|
173
|
+
}), ie = (c, x) => {
|
|
174
|
+
const t = c.__vccOpts || c;
|
|
175
|
+
for (const [l, u] of x)
|
|
176
|
+
t[l] = u;
|
|
172
177
|
return t;
|
|
173
|
-
},
|
|
178
|
+
}, ue = /* @__PURE__ */ ie(re, [["__scopeId", "data-v-5d741303"]]), ce = /* @__PURE__ */ j({
|
|
174
179
|
__name: "VirtualHumanEventAdapter",
|
|
175
180
|
props: {
|
|
176
181
|
// 屏幕客户端ID
|
|
@@ -184,113 +189,122 @@ const j = ["src", "muted"], G = { class: "overlay" }, K = {
|
|
|
184
189
|
required: !0
|
|
185
190
|
}
|
|
186
191
|
},
|
|
187
|
-
emits: ["highlight", "showDialog", "end", "pause", "connected", "error"],
|
|
188
|
-
setup(
|
|
189
|
-
const t =
|
|
190
|
-
let n = null,
|
|
191
|
-
const
|
|
192
|
+
emits: ["highlight", "showDialog", "end", "pause", "connected", "error", "play_complete"],
|
|
193
|
+
setup(c, { emit: x }) {
|
|
194
|
+
const t = c, l = x, u = d(null), Y = d(!1);
|
|
195
|
+
let n = null, y = 0, h = !1, w = 0;
|
|
196
|
+
const W = () => {
|
|
192
197
|
n || (n = new (window.AudioContext || window.webkitAudioContext)({
|
|
193
198
|
sampleRate: 24e3
|
|
194
199
|
})), n.state === "suspended" && n.resume();
|
|
195
|
-
}, E = (
|
|
196
|
-
|
|
200
|
+
}, E = () => {
|
|
201
|
+
h && w === 0 && (l("play_complete", t.screenClientId), h = !1);
|
|
202
|
+
}, z = (s) => {
|
|
203
|
+
if (W(), !!n)
|
|
197
204
|
try {
|
|
198
|
-
const
|
|
199
|
-
for (let
|
|
200
|
-
|
|
201
|
-
const g = new Int16Array(
|
|
202
|
-
for (let
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
const o = window.atob(s), r = o.length, v = new Uint8Array(r);
|
|
206
|
+
for (let f = 0; f < r; f++)
|
|
207
|
+
v[f] = o.charCodeAt(f);
|
|
208
|
+
const g = new Int16Array(v.buffer), B = new Float32Array(g.length);
|
|
209
|
+
for (let f = 0; f < g.length; f++)
|
|
210
|
+
B[f] = g[f] / 32768;
|
|
211
|
+
const L = n.createBuffer(1, B.length, 24e3);
|
|
212
|
+
L.getChannelData(0).set(B);
|
|
213
|
+
const U = n.createBufferSource();
|
|
214
|
+
U.buffer = L, U.connect(n.destination), y < n.currentTime && (y = n.currentTime), U.start(y), y += L.duration, w++, U.onended = () => {
|
|
215
|
+
w--, E();
|
|
216
|
+
};
|
|
217
|
+
} catch (o) {
|
|
218
|
+
console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:", o);
|
|
210
219
|
}
|
|
211
|
-
},
|
|
212
|
-
if (n)
|
|
213
|
-
switch (
|
|
220
|
+
}, b = (s) => {
|
|
221
|
+
if (!(!n && s !== "stop"))
|
|
222
|
+
switch (s) {
|
|
214
223
|
case "play":
|
|
224
|
+
h = !1, w = 0, y = 0, n && n.state === "suspended" && n.resume();
|
|
225
|
+
break;
|
|
215
226
|
case "resume":
|
|
216
|
-
n.state === "suspended" && n.resume();
|
|
227
|
+
n && n.state === "suspended" && n.resume();
|
|
217
228
|
break;
|
|
218
229
|
case "pause":
|
|
219
|
-
n.state === "running" && n.suspend();
|
|
230
|
+
n && n.state === "running" && n.suspend();
|
|
220
231
|
break;
|
|
221
232
|
case "stop":
|
|
222
|
-
n.close(), n = null,
|
|
233
|
+
n && (n.close(), n = null), y = 0, h = !1, w = 0;
|
|
234
|
+
break;
|
|
235
|
+
case "tts_complete":
|
|
236
|
+
h = !0, E();
|
|
223
237
|
break;
|
|
224
238
|
default:
|
|
225
|
-
console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${
|
|
239
|
+
console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${s}`);
|
|
226
240
|
}
|
|
227
|
-
},
|
|
228
|
-
|
|
241
|
+
}, A = () => {
|
|
242
|
+
u.value && u.value.close();
|
|
229
243
|
try {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
},
|
|
244
|
+
const s = new URL(t.wsUrl);
|
|
245
|
+
s.searchParams.append("sessionId", t.screenClientId + "-event"), u.value = new WebSocket(s.toString()), u.value.onopen = () => {
|
|
246
|
+
Y.value = !0, l("connected"), console.log(`[VirtualHumanEventAdapter] Connected to ${t.wsUrl} for session ${t.screenClientId}-event`);
|
|
247
|
+
}, u.value.onmessage = (o) => {
|
|
234
248
|
try {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
} catch (
|
|
238
|
-
console.error("[VirtualHumanEventAdapter] Failed to parse message:",
|
|
249
|
+
const r = JSON.parse(o.data);
|
|
250
|
+
D(r);
|
|
251
|
+
} catch (r) {
|
|
252
|
+
console.error("[VirtualHumanEventAdapter] Failed to parse message:", o.data, r);
|
|
239
253
|
}
|
|
240
|
-
},
|
|
241
|
-
console.error("[VirtualHumanEventAdapter] WebSocket error:",
|
|
242
|
-
},
|
|
243
|
-
|
|
254
|
+
}, u.value.onerror = (o) => {
|
|
255
|
+
console.error("[VirtualHumanEventAdapter] WebSocket error:", o), l("error", o);
|
|
256
|
+
}, u.value.onclose = () => {
|
|
257
|
+
Y.value = !1, console.log("[VirtualHumanEventAdapter] WebSocket disconnected");
|
|
244
258
|
};
|
|
245
|
-
} catch (
|
|
246
|
-
console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",
|
|
259
|
+
} catch (s) {
|
|
260
|
+
console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:", s), l("error", s);
|
|
247
261
|
}
|
|
248
|
-
},
|
|
249
|
-
const { type:
|
|
250
|
-
switch (console.log("msgmsg",
|
|
262
|
+
}, D = (s) => {
|
|
263
|
+
const { type: o, payload: r, action: v } = s;
|
|
264
|
+
switch (console.log("msgmsg", s), o) {
|
|
251
265
|
case "audio":
|
|
252
|
-
const g = (
|
|
253
|
-
g &&
|
|
266
|
+
const g = (r == null ? void 0 : r.data) || s.data;
|
|
267
|
+
g && z(g);
|
|
254
268
|
break;
|
|
255
269
|
case "dialog_event":
|
|
256
|
-
|
|
270
|
+
s.event && l(s.event, s.params);
|
|
257
271
|
break;
|
|
258
272
|
case "control":
|
|
259
|
-
|
|
273
|
+
v && b(v);
|
|
260
274
|
break;
|
|
261
275
|
case "highlight":
|
|
262
|
-
|
|
276
|
+
l("highlight", r);
|
|
263
277
|
break;
|
|
264
278
|
case "showDialog":
|
|
265
|
-
|
|
279
|
+
l("showDialog", r);
|
|
266
280
|
break;
|
|
267
281
|
case "end":
|
|
268
|
-
|
|
282
|
+
l("end", r);
|
|
269
283
|
break;
|
|
270
284
|
case "pause":
|
|
271
|
-
|
|
285
|
+
l("pause", r);
|
|
272
286
|
break;
|
|
273
287
|
default:
|
|
274
|
-
console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${
|
|
288
|
+
console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${o}`);
|
|
275
289
|
}
|
|
276
290
|
};
|
|
277
|
-
return
|
|
278
|
-
t.screenClientId && t.wsUrl &&
|
|
279
|
-
}),
|
|
280
|
-
t.screenClientId && t.wsUrl &&
|
|
281
|
-
}),
|
|
282
|
-
t.screenClientId && t.wsUrl &&
|
|
283
|
-
}),
|
|
284
|
-
|
|
285
|
-
}), (
|
|
291
|
+
return X(() => t.screenClientId, () => {
|
|
292
|
+
t.screenClientId && t.wsUrl && A();
|
|
293
|
+
}), X(() => t.wsUrl, () => {
|
|
294
|
+
t.screenClientId && t.wsUrl && A();
|
|
295
|
+
}), G(() => {
|
|
296
|
+
t.screenClientId && t.wsUrl && A();
|
|
297
|
+
}), K(() => {
|
|
298
|
+
u.value && u.value.close(), n && n.close();
|
|
299
|
+
}), (s, o) => ne(s.$slots, "default");
|
|
286
300
|
}
|
|
287
|
-
}),
|
|
288
|
-
|
|
289
|
-
},
|
|
290
|
-
install:
|
|
301
|
+
}), de = (c) => {
|
|
302
|
+
c.component("VirtualHumanPersona", ue), c.component("VirtualHumanEventAdapter", ce);
|
|
303
|
+
}, fe = {
|
|
304
|
+
install: de
|
|
291
305
|
};
|
|
292
306
|
export {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
307
|
+
ce as VirtualHumanEventAdapter,
|
|
308
|
+
ue as VirtualHumanPersona,
|
|
309
|
+
fe as default
|
|
296
310
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(h,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(h=typeof globalThis<"u"?globalThis:h||self,e(h.VirtualHumanCf={},h.Vue))})(this,function(h,e){"use strict";const F=["src","muted"],O={class:"overlay"},j={key:0,class:"status-badge paused"},J={key:1,class:"status-badge playing"},$=((d,A)=>{const n=d.__vccOpts||d;for(const[s,u]of A)n[s]=u;return n})(e.defineComponent({__name:"VirtualHumanPersona",props:{videoSrc:{type:String,required:!0},visible:{type:Boolean,default:!1},isPlaying:{type:Boolean,default:!1},muted:{type:Boolean,default:!0},isDark:{type:Boolean,default:!1},screenClientId:{type:String,required:!1},wsUrl:{type:String,required:!1}},emits:["update:isPlaying","ended","update:visible"],setup(d,{emit:A}){const n=d,s=A,u=e.ref(null),N=e.ref(null),a=e.ref(null),y=e.ref(!1),w=e.ref(400),k=e.ref(500),I=e.ref(16),S=e.ref(16),x=e.ref(!1),b=e.ref(null),H=e.ref(0),z=e.ref(0),l=e.ref(0),r=e.ref(0),i=e.ref(0),f=e.ref(0),g=()=>{if(a.value&&a.value.close(),!(!n.wsUrl||!n.screenClientId))try{const c=new URL(n.wsUrl);c.searchParams.append("sessionId",n.screenClientId+"-persona"),a.value=new WebSocket(c.toString()),a.value.onopen=()=>{y.value=!0,console.log(`[VirtualHumanPersona] Connected to ${n.wsUrl} for session ${n.screenClientId}-persona`)},a.value.onmessage=t=>{try{const o=JSON.parse(t.data);T(o)}catch(o){console.error("[VirtualHumanPersona] Failed to parse message:",t.data,o)}},a.value.onerror=t=>{console.error("[VirtualHumanPersona] WebSocket error:",t)},a.value.onclose=()=>{y.value=!1,console.log("[VirtualHumanPersona] WebSocket disconnected")}}catch(c){console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",c)}},T=c=>{const{type:t,action:o}=c;t==="control"&&(o==="play"||o==="resume"?(s("update:isPlaying",!0),s("update:visible",!0)):o==="pause"?s("update:isPlaying",!1):o==="stop"&&(s("update:isPlaying",!1),s("update:visible",!1),u.value&&(u.value.currentTime=0)))};e.watch(()=>n.screenClientId,()=>{n.screenClientId&&n.wsUrl&&g()}),e.watch(()=>n.wsUrl,()=>{n.screenClientId&&n.wsUrl&&g()}),e.watch(()=>n.isPlaying,c=>{u.value&&(c?u.value.play().catch(t=>console.error("Video play failed:",t)):u.value.pause())});const W=()=>{n.isPlaying||s("update:isPlaying",!0)},B=()=>{n.isPlaying&&s("update:isPlaying",!1)},m=()=>{s("update:isPlaying",!1),s("ended")},C=(c,t)=>{t.preventDefault(),x.value=!0,b.value=c;const o="clientX"in t?t.clientX:t.touches[0].clientX,V="clientY"in t?t.clientY:t.touches[0].clientY;H.value=o,z.value=V,l.value=w.value,r.value=k.value,i.value=I.value,f.value=S.value,document.addEventListener("mousemove",L),document.addEventListener("mouseup",M),document.addEventListener("touchmove",L,{passive:!1}),document.addEventListener("touchend",M)},L=c=>{if(!x.value)return;c.preventDefault();const t="clientX"in c?c.clientX:c.touches[0].clientX,o="clientY"in c?c.clientY:c.touches[0].clientY,V=t-H.value,U=o-z.value,R=200,X=250,Y=800,D=1e3;let p=l.value,v=r.value,E=i.value,P=f.value;b.value==="bottom-right"?(p=l.value+V,P=f.value-U,v=r.value+U):b.value==="bottom-left"?(E=i.value+V,p=l.value-V,P=f.value-U,v=r.value+U):b.value==="top-right"?(p=l.value+V,v=r.value-U):b.value==="top-left"&&(E=i.value+V,p=l.value-V,v=r.value-U),p<R&&(E!==i.value&&(E-=R-p),p=R),v<X&&(P!==f.value&&(P-=X-v),v=X),p>Y&&(E!==i.value&&(E+=p-Y),p=Y),v>D&&(P!==f.value&&(P+=v-D),v=D),w.value=p,k.value=v,I.value=E,S.value=P},M=()=>{x.value=!1,b.value=null,document.removeEventListener("mousemove",L),document.removeEventListener("mouseup",M),document.removeEventListener("touchmove",L),document.removeEventListener("touchend",M)};return e.onMounted(()=>{n.isPlaying&&u.value&&u.value.play().catch(c=>console.error("Video play failed:",c)),n.screenClientId&&n.wsUrl&&g()}),e.onUnmounted(()=>{a.value&&a.value.close()}),(c,t)=>(e.openBlock(),e.createBlock(e.Transition,{name:"fade"},{default:e.withCtx(()=>[d.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:e.normalizeClass(["virtual-human-container",{"is-dark":d.isDark}]),style:e.normalizeStyle({width:w.value+"px",height:k.value+"px",left:I.value+"px",bottom:S.value+"px"})},[e.createElementVNode("div",{class:"video-wrapper",ref_key:"wrapperRef",ref:N},[e.createElementVNode("video",{ref_key:"videoRef",ref:u,src:d.videoSrc,class:"persona-video",muted:d.muted,playsinline:"",loop:"",onPlay:W,onPause:B,onEnded:m},null,40,F),d.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:"resize-handle top-left",onMousedown:t[0]||(t[0]=o=>C("top-left",o)),onTouchstart:t[1]||(t[1]=o=>C("top-left",o))},null,32)):e.createCommentVNode("",!0),d.visible?(e.openBlock(),e.createElementBlock("div",{key:1,class:"resize-handle top-right",onMousedown:t[2]||(t[2]=o=>C("top-right",o)),onTouchstart:t[3]||(t[3]=o=>C("top-right",o))},null,32)):e.createCommentVNode("",!0),d.visible?(e.openBlock(),e.createElementBlock("div",{key:2,class:"resize-handle bottom-left",onMousedown:t[4]||(t[4]=o=>C("bottom-left",o)),onTouchstart:t[5]||(t[5]=o=>C("bottom-left",o))},null,32)):e.createCommentVNode("",!0),d.visible?(e.openBlock(),e.createElementBlock("div",{key:3,class:"resize-handle bottom-right",onMousedown:t[6]||(t[6]=o=>C("bottom-right",o)),onTouchstart:t[7]||(t[7]=o=>C("bottom-right",o))},null,32)):e.createCommentVNode("",!0),e.createElementVNode("div",O,[d.isPlaying?(e.openBlock(),e.createElementBlock("div",J,[...t[9]||(t[9]=[e.createElementVNode("span",{class:"dot animate-pulse"},null,-1),e.createTextVNode(" 播放中 ",-1)])])):(e.openBlock(),e.createElementBlock("div",j,[...t[8]||(t[8]=[e.createElementVNode("span",{class:"dot"},null,-1),e.createTextVNode(" 暂停中 ",-1)])]))])],512)],6)):e.createCommentVNode("",!0)]),_:1}))}}),[["__scopeId","data-v-5d741303"]]),q=e.defineComponent({__name:"VirtualHumanEventAdapter",props:{screenClientId:{type:String,required:!0},wsUrl:{type:String,required:!0}},emits:["highlight","showDialog","end","pause","connected","error","play_complete"],setup(d,{emit:A}){const n=d,s=A,u=e.ref(null),N=e.ref(!1);let a=null,y=0,w=!1,k=0;const I=()=>{a||(a=new(window.AudioContext||window.webkitAudioContext)({sampleRate:24e3})),a.state==="suspended"&&a.resume()},S=()=>{w&&k===0&&(s("play_complete",n.screenClientId),w=!1)},x=l=>{if(I(),!!a)try{const r=window.atob(l),i=r.length,f=new Uint8Array(i);for(let m=0;m<i;m++)f[m]=r.charCodeAt(m);const g=new Int16Array(f.buffer),T=new Float32Array(g.length);for(let m=0;m<g.length;m++)T[m]=g[m]/32768;const W=a.createBuffer(1,T.length,24e3);W.getChannelData(0).set(T);const B=a.createBufferSource();B.buffer=W,B.connect(a.destination),y<a.currentTime&&(y=a.currentTime),B.start(y),y+=W.duration,k++,B.onended=()=>{k--,S()}}catch(r){console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:",r)}},b=l=>{if(!(!a&&l!=="stop"))switch(l){case"play":w=!1,k=0,y=0,a&&a.state==="suspended"&&a.resume();break;case"resume":a&&a.state==="suspended"&&a.resume();break;case"pause":a&&a.state==="running"&&a.suspend();break;case"stop":a&&(a.close(),a=null),y=0,w=!1,k=0;break;case"tts_complete":w=!0,S();break;default:console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${l}`)}},H=()=>{u.value&&u.value.close();try{const l=new URL(n.wsUrl);l.searchParams.append("sessionId",n.screenClientId+"-event"),u.value=new WebSocket(l.toString()),u.value.onopen=()=>{N.value=!0,s("connected"),console.log(`[VirtualHumanEventAdapter] Connected to ${n.wsUrl} for session ${n.screenClientId}-event`)},u.value.onmessage=r=>{try{const i=JSON.parse(r.data);z(i)}catch(i){console.error("[VirtualHumanEventAdapter] Failed to parse message:",r.data,i)}},u.value.onerror=r=>{console.error("[VirtualHumanEventAdapter] WebSocket error:",r),s("error",r)},u.value.onclose=()=>{N.value=!1,console.log("[VirtualHumanEventAdapter] WebSocket disconnected")}}catch(l){console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",l),s("error",l)}},z=l=>{const{type:r,payload:i,action:f}=l;switch(console.log("msgmsg",l),r){case"audio":const g=(i==null?void 0:i.data)||l.data;g&&x(g);break;case"dialog_event":l.event&&s(l.event,l.params);break;case"control":f&&b(f);break;case"highlight":s("highlight",i);break;case"showDialog":s("showDialog",i);break;case"end":s("end",i);break;case"pause":s("pause",i);break;default:console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${r}`)}};return e.watch(()=>n.screenClientId,()=>{n.screenClientId&&n.wsUrl&&H()}),e.watch(()=>n.wsUrl,()=>{n.screenClientId&&n.wsUrl&&H()}),e.onMounted(()=>{n.screenClientId&&n.wsUrl&&H()}),e.onUnmounted(()=>{u.value&&u.value.close(),a&&a.close()}),(l,r)=>e.renderSlot(l.$slots,"default")}}),G={install:d=>{d.component("VirtualHumanPersona",$),d.component("VirtualHumanEventAdapter",q)}};h.VirtualHumanEventAdapter=q,h.VirtualHumanPersona=$,h.default=G,Object.defineProperties(h,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@ const props = defineProps({
|
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
const emit = defineEmits(['highlight', 'showDialog', 'end', 'pause', 'connected', 'error']);
|
|
21
|
+
const emit = defineEmits(['highlight', 'showDialog', 'end', 'pause', 'connected', 'error', 'play_complete']);
|
|
22
22
|
|
|
23
23
|
const ws = ref<WebSocket | null>(null);
|
|
24
24
|
const isConnected = ref(false);
|
|
@@ -26,6 +26,8 @@ const isConnected = ref(false);
|
|
|
26
26
|
// Audio playback variables
|
|
27
27
|
let audioContext: AudioContext | null = null;
|
|
28
28
|
let nextStartTime = 0;
|
|
29
|
+
let isTtsComplete = false;
|
|
30
|
+
let activeSources = 0;
|
|
29
31
|
|
|
30
32
|
const initAudioContext = () => {
|
|
31
33
|
if (!audioContext) {
|
|
@@ -38,6 +40,13 @@ const initAudioContext = () => {
|
|
|
38
40
|
}
|
|
39
41
|
};
|
|
40
42
|
|
|
43
|
+
const checkPlayComplete = () => {
|
|
44
|
+
if (isTtsComplete && activeSources === 0) {
|
|
45
|
+
emit('play_complete', props.screenClientId);
|
|
46
|
+
isTtsComplete = false;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
41
50
|
const handleAudioMessage = (base64Data: string) => {
|
|
42
51
|
initAudioContext();
|
|
43
52
|
if (!audioContext) return;
|
|
@@ -70,29 +79,50 @@ const handleAudioMessage = (base64Data: string) => {
|
|
|
70
79
|
}
|
|
71
80
|
source.start(nextStartTime);
|
|
72
81
|
nextStartTime += audioBuffer.duration;
|
|
82
|
+
|
|
83
|
+
activeSources++;
|
|
84
|
+
source.onended = () => {
|
|
85
|
+
activeSources--;
|
|
86
|
+
checkPlayComplete();
|
|
87
|
+
};
|
|
73
88
|
} catch (error) {
|
|
74
89
|
console.error('[VirtualHumanEventAdapter] Failed to decode and play audio:', error);
|
|
75
90
|
}
|
|
76
91
|
};
|
|
77
92
|
|
|
78
93
|
const handleControlMessage = (action: string) => {
|
|
79
|
-
if (!audioContext) return;
|
|
94
|
+
if (!audioContext && action !== 'stop') return;
|
|
80
95
|
switch (action) {
|
|
81
96
|
case 'play':
|
|
97
|
+
isTtsComplete = false;
|
|
98
|
+
activeSources = 0;
|
|
99
|
+
nextStartTime = 0;
|
|
100
|
+
if (audioContext && audioContext.state === 'suspended') {
|
|
101
|
+
audioContext.resume();
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
82
104
|
case 'resume':
|
|
83
|
-
if (audioContext.state === 'suspended') {
|
|
105
|
+
if (audioContext && audioContext.state === 'suspended') {
|
|
84
106
|
audioContext.resume();
|
|
85
107
|
}
|
|
86
108
|
break;
|
|
87
109
|
case 'pause':
|
|
88
|
-
if (audioContext.state === 'running') {
|
|
110
|
+
if (audioContext && audioContext.state === 'running') {
|
|
89
111
|
audioContext.suspend();
|
|
90
112
|
}
|
|
91
113
|
break;
|
|
92
114
|
case 'stop':
|
|
93
|
-
audioContext
|
|
94
|
-
|
|
115
|
+
if (audioContext) {
|
|
116
|
+
audioContext.close();
|
|
117
|
+
audioContext = null;
|
|
118
|
+
}
|
|
95
119
|
nextStartTime = 0;
|
|
120
|
+
isTtsComplete = false;
|
|
121
|
+
activeSources = 0;
|
|
122
|
+
break;
|
|
123
|
+
case 'tts_complete':
|
|
124
|
+
isTtsComplete = true;
|
|
125
|
+
checkPlayComplete();
|
|
96
126
|
break;
|
|
97
127
|
default:
|
|
98
128
|
console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${action}`);
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
v-if="visible"
|
|
5
5
|
class="virtual-human-container"
|
|
6
6
|
:class="{ 'is-dark': isDark }"
|
|
7
|
+
:style="{
|
|
8
|
+
width: containerWidth + 'px',
|
|
9
|
+
height: containerHeight + 'px',
|
|
10
|
+
left: containerLeft + 'px',
|
|
11
|
+
bottom: containerBottom + 'px'
|
|
12
|
+
}"
|
|
7
13
|
>
|
|
8
|
-
<div
|
|
9
|
-
class="video-wrapper"
|
|
10
|
-
ref="wrapperRef"
|
|
11
|
-
:style="{ transform: `scale(${scale})`, transformOrigin: 'left bottom' }"
|
|
12
|
-
>
|
|
14
|
+
<div class="video-wrapper" ref="wrapperRef">
|
|
13
15
|
<video
|
|
14
16
|
ref="videoRef"
|
|
15
17
|
:src="videoSrc"
|
|
@@ -114,12 +116,19 @@ const ws = ref<WebSocket | null>(null);
|
|
|
114
116
|
const isConnected = ref(false);
|
|
115
117
|
|
|
116
118
|
// 缩放相关状态
|
|
117
|
-
const
|
|
119
|
+
const containerWidth = ref(400);
|
|
120
|
+
const containerHeight = ref(500);
|
|
121
|
+
const containerLeft = ref(16);
|
|
122
|
+
const containerBottom = ref(16);
|
|
123
|
+
|
|
118
124
|
const isResizing = ref(false);
|
|
119
125
|
const resizeHandle = ref<string | null>(null);
|
|
120
126
|
const startX = ref(0);
|
|
121
127
|
const startY = ref(0);
|
|
122
|
-
const
|
|
128
|
+
const startW = ref(0);
|
|
129
|
+
const startH = ref(0);
|
|
130
|
+
const startL = ref(0);
|
|
131
|
+
const startB = ref(0);
|
|
123
132
|
|
|
124
133
|
const connectWebSocket = () => {
|
|
125
134
|
if (ws.value) {
|
|
@@ -220,23 +229,29 @@ const handleEnded = () => {
|
|
|
220
229
|
|
|
221
230
|
// 缩放功能
|
|
222
231
|
const startResize = (handle: string, event: MouseEvent | TouchEvent) => {
|
|
232
|
+
event.preventDefault();
|
|
223
233
|
isResizing.value = true;
|
|
224
234
|
resizeHandle.value = handle;
|
|
225
|
-
startScale.value = scale.value;
|
|
226
235
|
|
|
227
236
|
const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX;
|
|
228
237
|
const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY;
|
|
229
238
|
startX.value = clientX;
|
|
230
239
|
startY.value = clientY;
|
|
231
240
|
|
|
241
|
+
startW.value = containerWidth.value;
|
|
242
|
+
startH.value = containerHeight.value;
|
|
243
|
+
startL.value = containerLeft.value;
|
|
244
|
+
startB.value = containerBottom.value;
|
|
245
|
+
|
|
232
246
|
document.addEventListener('mousemove', handleResize);
|
|
233
247
|
document.addEventListener('mouseup', stopResize);
|
|
234
|
-
document.addEventListener('touchmove', handleResize);
|
|
248
|
+
document.addEventListener('touchmove', handleResize, { passive: false });
|
|
235
249
|
document.addEventListener('touchend', stopResize);
|
|
236
250
|
};
|
|
237
251
|
|
|
238
252
|
const handleResize = (event: MouseEvent | TouchEvent) => {
|
|
239
|
-
if (!isResizing.value
|
|
253
|
+
if (!isResizing.value) return;
|
|
254
|
+
event.preventDefault();
|
|
240
255
|
|
|
241
256
|
const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX;
|
|
242
257
|
const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY;
|
|
@@ -244,25 +259,57 @@ const handleResize = (event: MouseEvent | TouchEvent) => {
|
|
|
244
259
|
const deltaX = clientX - startX.value;
|
|
245
260
|
const deltaY = clientY - startY.value;
|
|
246
261
|
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const
|
|
262
|
+
const minWidth = 200;
|
|
263
|
+
const minHeight = 250;
|
|
264
|
+
const maxWidth = 800;
|
|
265
|
+
const maxHeight = 1000;
|
|
250
266
|
|
|
251
|
-
|
|
252
|
-
let
|
|
253
|
-
|
|
267
|
+
let newW = startW.value;
|
|
268
|
+
let newH = startH.value;
|
|
269
|
+
let newL = startL.value;
|
|
270
|
+
let newB = startB.value;
|
|
254
271
|
|
|
255
272
|
if (resizeHandle.value === 'bottom-right') {
|
|
256
|
-
|
|
273
|
+
newW = startW.value + deltaX;
|
|
274
|
+
newB = startB.value - deltaY;
|
|
275
|
+
newH = startH.value + deltaY;
|
|
257
276
|
} else if (resizeHandle.value === 'bottom-left') {
|
|
258
|
-
|
|
277
|
+
newL = startL.value + deltaX;
|
|
278
|
+
newW = startW.value - deltaX;
|
|
279
|
+
newB = startB.value - deltaY;
|
|
280
|
+
newH = startH.value + deltaY;
|
|
259
281
|
} else if (resizeHandle.value === 'top-right') {
|
|
260
|
-
|
|
282
|
+
newW = startW.value + deltaX;
|
|
283
|
+
newH = startH.value - deltaY;
|
|
261
284
|
} else if (resizeHandle.value === 'top-left') {
|
|
262
|
-
|
|
285
|
+
newL = startL.value + deltaX;
|
|
286
|
+
newW = startW.value - deltaX;
|
|
287
|
+
newH = startH.value - deltaY;
|
|
263
288
|
}
|
|
264
289
|
|
|
265
|
-
|
|
290
|
+
// 约束最小宽高
|
|
291
|
+
if (newW < minWidth) {
|
|
292
|
+
if (newL !== startL.value) newL -= (minWidth - newW);
|
|
293
|
+
newW = minWidth;
|
|
294
|
+
}
|
|
295
|
+
if (newH < minHeight) {
|
|
296
|
+
if (newB !== startB.value) newB -= (minHeight - newH);
|
|
297
|
+
newH = minHeight;
|
|
298
|
+
}
|
|
299
|
+
// 约束最大宽高
|
|
300
|
+
if (newW > maxWidth) {
|
|
301
|
+
if (newL !== startL.value) newL += (newW - maxWidth);
|
|
302
|
+
newW = maxWidth;
|
|
303
|
+
}
|
|
304
|
+
if (newH > maxHeight) {
|
|
305
|
+
if (newB !== startB.value) newB += (newH - maxHeight);
|
|
306
|
+
newH = maxHeight;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
containerWidth.value = newW;
|
|
310
|
+
containerHeight.value = newH;
|
|
311
|
+
containerLeft.value = newL;
|
|
312
|
+
containerBottom.value = newB;
|
|
266
313
|
};
|
|
267
314
|
|
|
268
315
|
const stopResize = () => {
|
|
@@ -304,23 +351,25 @@ onUnmounted(() => {
|
|
|
304
351
|
|
|
305
352
|
.virtual-human-container {
|
|
306
353
|
position: fixed;
|
|
307
|
-
left
|
|
308
|
-
bottom: 16px;
|
|
309
|
-
width: 400px;
|
|
310
|
-
height: 500px;
|
|
354
|
+
/* left, bottom, width, height are set dynamically */
|
|
311
355
|
z-index: 2147483647;
|
|
312
356
|
overflow: visible;
|
|
313
357
|
}
|
|
314
358
|
|
|
315
359
|
.video-wrapper {
|
|
316
360
|
position: relative;
|
|
317
|
-
width:
|
|
318
|
-
height:
|
|
361
|
+
width: 100%;
|
|
362
|
+
height: 100%;
|
|
319
363
|
border-radius: 1rem;
|
|
320
364
|
overflow: hidden;
|
|
321
365
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
|
322
366
|
background: #ffffff;
|
|
323
|
-
|
|
367
|
+
border: 2px solid transparent;
|
|
368
|
+
transition: background-color 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.video-wrapper:hover {
|
|
372
|
+
border-color: #409EFF;
|
|
324
373
|
}
|
|
325
374
|
|
|
326
375
|
.virtual-human-container.is-dark .video-wrapper {
|
|
@@ -346,11 +395,13 @@ onUnmounted(() => {
|
|
|
346
395
|
cursor: pointer;
|
|
347
396
|
z-index: 20;
|
|
348
397
|
opacity: 0;
|
|
398
|
+
pointer-events: none;
|
|
349
399
|
transition: opacity 0.3s ease, background 0.2s, border-color 0.2s;
|
|
350
400
|
}
|
|
351
401
|
|
|
352
402
|
.video-wrapper:hover .resize-handle {
|
|
353
403
|
opacity: 1;
|
|
404
|
+
pointer-events: auto;
|
|
354
405
|
}
|
|
355
406
|
|
|
356
407
|
.resize-handle:hover {
|