virtual-human-cf 1.0.1 → 1.0.3

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/dist/style.css CHANGED
@@ -1 +1 @@
1
- .fade-enter-active[data-v-8d9d61ad],.fade-leave-active[data-v-8d9d61ad]{transition:opacity .5s ease,transform .5s ease}.fade-enter-from[data-v-8d9d61ad],.fade-leave-to[data-v-8d9d61ad]{opacity:0;transform:translateY(10px)}.virtual-human-container[data-v-8d9d61ad]{position:relative;width:100%;max-width:400px;border-radius:1rem;overflow:hidden;box-shadow:0 10px 25px #0000001a;background:#fff;transition:background-color .3s ease}.virtual-human-container.is-dark[data-v-8d9d61ad]{background:#1f2937;box-shadow:0 10px 25px #00000080}.video-wrapper[data-v-8d9d61ad]{position:relative;width:100%;aspect-ratio:9 / 16;background:#000}.persona-video[data-v-8d9d61ad]{width:100%;height:100%;object-fit:cover}.overlay[data-v-8d9d61ad]{position:absolute;top:1rem;right:1rem;z-index:10}.status-badge[data-v-8d9d61ad]{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-8d9d61ad]{background:#ef4444cc}.status-badge.playing[data-v-8d9d61ad]{background:#22c55ecc}.dot[data-v-8d9d61ad]{width:6px;height:6px;border-radius:50%;background-color:#fff}@keyframes pulse-8d9d61ad{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse[data-v-8d9d61ad]{animation:pulse-8d9d61ad 2s cubic-bezier(.4,0,.6,1) infinite}
1
+ .fade-enter-active[data-v-54e42f84],.fade-leave-active[data-v-54e42f84]{transition:opacity .5s ease,transform .5s ease}.fade-enter-from[data-v-54e42f84],.fade-leave-to[data-v-54e42f84]{opacity:0;transform:translateY(10px)}.virtual-human-container[data-v-54e42f84]{position:fixed;left:16px;bottom:16px;width:400px;height:500px;z-index:2147483647;overflow:visible}.video-wrapper[data-v-54e42f84]{position:relative;width:400px;height:500px;border-radius:1rem;overflow:hidden;box-shadow:0 10px 25px #0000001a;background:#fff;transition:background-color .3s ease,box-shadow .3s ease}.virtual-human-container.is-dark .video-wrapper[data-v-54e42f84]{background:#1f2937;box-shadow:0 10px 25px #00000080}.persona-video[data-v-54e42f84]{width:100%;height:100%;object-fit:cover;transform-origin:center;transition:transform .1s ease-out}.resize-handle[data-v-54e42f84]{position:absolute;width:20px;height:20px;background:#00000080;border:2px solid rgba(255,255,255,.8);border-radius:4px;cursor:pointer;z-index:20;transition:background .2s,border-color .2s}.resize-handle[data-v-54e42f84]:hover{background:#000000b3;border-color:#fff}.resize-handle.top-left[data-v-54e42f84]{top:-10px;left:-10px;cursor:nwse-resize}.resize-handle.top-right[data-v-54e42f84]{top:-10px;right:-10px;cursor:nesw-resize}.resize-handle.bottom-left[data-v-54e42f84]{bottom:-10px;left:-10px;cursor:nesw-resize}.resize-handle.bottom-right[data-v-54e42f84]{bottom:-10px;right:-10px;cursor:nwse-resize}.overlay[data-v-54e42f84]{position:absolute;top:1rem;right:1rem;z-index:10}.status-badge[data-v-54e42f84]{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-54e42f84]{background:#ef4444cc}.status-badge.playing[data-v-54e42f84]{background:#22c55ecc}.dot[data-v-54e42f84]{width:6px;height:6px;border-radius:50%;background-color:#fff}@keyframes pulse-54e42f84{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse[data-v-54e42f84]{animation:pulse-54e42f84 2s cubic-bezier(.4,0,.6,1) infinite}
@@ -1,11 +1,11 @@
1
- import { defineComponent as H, ref as g, watch as w, onMounted as I, onUnmounted as _, openBlock as h, createBlock as E, Transition as x, withCtx as B, createElementBlock as A, normalizeClass as W, createElementVNode as y, createTextVNode as U, createCommentVNode as $, renderSlot as D } from "vue";
2
- const F = { class: "video-wrapper" }, T = ["src", "muted"], q = { class: "overlay" }, M = {
1
+ import { defineComponent as T, ref as v, watch as z, onMounted as W, onUnmounted as X, openBlock as h, createBlock as D, Transition as F, withCtx as q, createElementBlock as b, normalizeClass as N, createElementVNode as H, normalizeStyle as O, createCommentVNode as I, createTextVNode as L, renderSlot as J } from "vue";
2
+ const j = ["src", "muted"], G = { class: "overlay" }, K = {
3
3
  key: 0,
4
4
  class: "status-badge paused"
5
- }, N = {
5
+ }, Q = {
6
6
  key: 1,
7
7
  class: "status-badge playing"
8
- }, R = /* @__PURE__ */ H({
8
+ }, Z = /* @__PURE__ */ T({
9
9
  __name: "VirtualHumanPersona",
10
10
  props: {
11
11
  // 视频源URL
@@ -16,7 +16,7 @@ const F = { class: "video-wrapper" }, T = ["src", "muted"], q = { class: "overla
16
16
  // 是否可见
17
17
  visible: {
18
18
  type: Boolean,
19
- default: !0
19
+ default: !1
20
20
  },
21
21
  // 是否自动播放
22
22
  isPlaying: {
@@ -46,90 +46,131 @@ const F = { class: "video-wrapper" }, T = ["src", "muted"], q = { class: "overla
46
46
  }
47
47
  },
48
48
  emits: ["update:isPlaying", "ended", "update:visible"],
49
- setup(r, { emit: p }) {
50
- const e = r, s = p, o = g(null), c = g(null), n = g(!1), u = () => {
51
- if (c.value && c.value.close(), !(!e.wsUrl || !e.screenClientId))
49
+ setup(i, { emit: P }) {
50
+ const t = i, s = P, l = v(null), k = v(null), n = v(null), m = v(!1), V = v(1), E = v(!1), p = v(null), C = v(0), x = v(0), o = v(1), u = () => {
51
+ if (n.value && n.value.close(), !(!t.wsUrl || !t.screenClientId))
52
52
  try {
53
- const i = new URL(e.wsUrl);
54
- i.searchParams.append("sessionId", e.screenClientId + "-persona"), c.value = new WebSocket(i.toString()), c.value.onopen = () => {
55
- n.value = !0, console.log(`[VirtualHumanPersona] Connected to ${e.wsUrl} for session ${e.screenClientId}-persona`);
56
- }, c.value.onmessage = (t) => {
53
+ const r = new URL(t.wsUrl);
54
+ r.searchParams.append("sessionId", t.screenClientId + "-persona"), n.value = new WebSocket(r.toString()), n.value.onopen = () => {
55
+ m.value = !0, console.log(`[VirtualHumanPersona] Connected to ${t.wsUrl} for session ${t.screenClientId}-persona`);
56
+ }, n.value.onmessage = (e) => {
57
57
  try {
58
- const a = JSON.parse(t.data);
59
- b(a);
58
+ const a = JSON.parse(e.data);
59
+ c(a);
60
60
  } catch (a) {
61
- console.error("[VirtualHumanPersona] Failed to parse message:", t.data, a);
61
+ console.error("[VirtualHumanPersona] Failed to parse message:", e.data, a);
62
62
  }
63
- }, c.value.onerror = (t) => {
64
- console.error("[VirtualHumanPersona] WebSocket error:", t);
65
- }, c.value.onclose = () => {
66
- n.value = !1, console.log("[VirtualHumanPersona] WebSocket disconnected");
63
+ }, n.value.onerror = (e) => {
64
+ console.error("[VirtualHumanPersona] WebSocket error:", e);
65
+ }, n.value.onclose = () => {
66
+ m.value = !1, console.log("[VirtualHumanPersona] WebSocket disconnected");
67
67
  };
68
- } catch (i) {
69
- console.error("[VirtualHumanPersona] Failed to initialize WebSocket:", i);
68
+ } catch (r) {
69
+ console.error("[VirtualHumanPersona] Failed to initialize WebSocket:", r);
70
70
  }
71
- }, b = (i) => {
72
- const { type: t, action: a } = i;
73
- t === "control" && (a === "play" ? (s("update:isPlaying", !0), s("update:visible", !0)) : a === "pause" ? (s("update:isPlaying", !1), s("update:visible", !1)) : a === "stop" && (s("update:isPlaying", !1), s("update:visible", !1), o.value && (o.value.currentTime = 0)));
71
+ }, c = (r) => {
72
+ const { type: e, action: a } = r;
73
+ e === "control" && (a === "play" ? (s("update:isPlaying", !0), s("update:visible", !0)) : a === "pause" ? s("update:isPlaying", !1) : a === "stop" && (s("update:isPlaying", !1), s("update:visible", !1), l.value && (l.value.currentTime = 0)));
74
74
  };
75
- w(() => e.screenClientId, () => {
76
- e.screenClientId && e.wsUrl && u();
77
- }), w(() => e.wsUrl, () => {
78
- e.screenClientId && e.wsUrl && u();
79
- }), w(() => e.isPlaying, (i) => {
80
- o.value && (i ? o.value.play().catch((t) => console.error("Video play failed:", t)) : o.value.pause());
75
+ z(() => t.screenClientId, () => {
76
+ t.screenClientId && t.wsUrl && u();
77
+ }), z(() => t.wsUrl, () => {
78
+ t.screenClientId && t.wsUrl && u();
79
+ }), z(() => t.isPlaying, (r) => {
80
+ l.value && (r ? l.value.play().catch((e) => console.error("Video play failed:", e)) : l.value.pause());
81
81
  });
82
- const k = () => {
83
- e.isPlaying || s("update:isPlaying", !0);
84
- }, C = () => {
85
- e.isPlaying && s("update:isPlaying", !1);
86
- }, v = () => {
82
+ const w = () => {
83
+ t.isPlaying || s("update:isPlaying", !0);
84
+ }, g = () => {
85
+ t.isPlaying && s("update:isPlaying", !1);
86
+ }, A = () => {
87
87
  s("update:isPlaying", !1), s("ended");
88
+ }, f = (r, e) => {
89
+ E.value = !0, p.value = r, o.value = V.value;
90
+ const a = "clientX" in e ? e.clientX : e.touches[0].clientX, S = "clientY" in e ? e.clientY : e.touches[0].clientY;
91
+ C.value = a, x.value = S, document.addEventListener("mousemove", y), document.addEventListener("mouseup", d), document.addEventListener("touchmove", y), document.addEventListener("touchend", d);
92
+ }, y = (r) => {
93
+ if (!E.value || !k.value) return;
94
+ const e = "clientX" in r ? r.clientX : r.touches[0].clientX, a = "clientY" in r ? r.clientY : r.touches[0].clientY, S = e - C.value, M = a - x.value, B = k.value.getBoundingClientRect(), Y = 0.5, $ = 2;
95
+ let U = o.value;
96
+ const R = Math.min(B.width, B.height);
97
+ p.value === "bottom-right" ? U = o.value + (S + M) / R : p.value === "bottom-left" ? U = o.value + (-S + M) / R : p.value === "top-right" ? U = o.value + (S - M) / R : p.value === "top-left" && (U = o.value + (-S - M) / R), V.value = Math.max(Y, Math.min($, U));
98
+ }, d = () => {
99
+ E.value = !1, p.value = null, document.removeEventListener("mousemove", y), document.removeEventListener("mouseup", d), document.removeEventListener("touchmove", y), document.removeEventListener("touchend", d);
88
100
  };
89
- return I(() => {
90
- e.isPlaying && o.value && o.value.play().catch((i) => console.error("Video play failed:", i)), e.screenClientId && e.wsUrl && u();
91
- }), _(() => {
92
- c.value && c.value.close();
93
- }), (i, t) => (h(), E(x, { name: "fade" }, {
94
- default: B(() => [
95
- r.visible ? (h(), A("div", {
101
+ return W(() => {
102
+ t.isPlaying && l.value && l.value.play().catch((r) => console.error("Video play failed:", r)), t.screenClientId && t.wsUrl && u();
103
+ }), X(() => {
104
+ n.value && n.value.close();
105
+ }), (r, e) => (h(), D(F, { name: "fade" }, {
106
+ default: q(() => [
107
+ i.visible ? (h(), b("div", {
96
108
  key: 0,
97
- class: W(["virtual-human-container", { "is-dark": r.isDark }])
109
+ class: N(["virtual-human-container", { "is-dark": i.isDark }])
98
110
  }, [
99
- y("div", F, [
100
- y("video", {
111
+ H("div", {
112
+ class: "video-wrapper",
113
+ ref_key: "wrapperRef",
114
+ ref: k,
115
+ style: O({ transform: `scale(${V.value})`, transformOrigin: "left bottom" })
116
+ }, [
117
+ H("video", {
101
118
  ref_key: "videoRef",
102
- ref: o,
103
- src: r.videoSrc,
119
+ ref: l,
120
+ src: i.videoSrc,
104
121
  class: "persona-video",
105
- muted: r.muted,
122
+ muted: i.muted,
106
123
  playsinline: "",
107
124
  loop: "",
108
- onPlay: k,
109
- onPause: C,
110
- onEnded: v
111
- }, null, 40, T),
112
- y("div", q, [
113
- r.isPlaying ? (h(), A("div", N, [...t[1] || (t[1] = [
114
- y("span", { class: "dot animate-pulse" }, null, -1),
115
- U(" 播放中 ", -1)
116
- ])])) : (h(), A("div", M, [...t[0] || (t[0] = [
117
- y("span", { class: "dot" }, null, -1),
118
- U(" 暂停中 ", -1)
125
+ onPlay: w,
126
+ onPause: g,
127
+ onEnded: A
128
+ }, null, 40, j),
129
+ i.visible ? (h(), b("div", {
130
+ key: 0,
131
+ class: "resize-handle top-left",
132
+ onMousedown: e[0] || (e[0] = (a) => f("top-left", a)),
133
+ onTouchstart: e[1] || (e[1] = (a) => f("top-left", a))
134
+ }, null, 32)) : I("", !0),
135
+ i.visible ? (h(), b("div", {
136
+ key: 1,
137
+ class: "resize-handle top-right",
138
+ onMousedown: e[2] || (e[2] = (a) => f("top-right", a)),
139
+ onTouchstart: e[3] || (e[3] = (a) => f("top-right", a))
140
+ }, null, 32)) : I("", !0),
141
+ i.visible ? (h(), b("div", {
142
+ key: 2,
143
+ class: "resize-handle bottom-left",
144
+ onMousedown: e[4] || (e[4] = (a) => f("bottom-left", a)),
145
+ onTouchstart: e[5] || (e[5] = (a) => f("bottom-left", a))
146
+ }, null, 32)) : I("", !0),
147
+ i.visible ? (h(), b("div", {
148
+ key: 3,
149
+ class: "resize-handle bottom-right",
150
+ onMousedown: e[6] || (e[6] = (a) => f("bottom-right", a)),
151
+ onTouchstart: e[7] || (e[7] = (a) => f("bottom-right", a))
152
+ }, null, 32)) : I("", !0),
153
+ H("div", G, [
154
+ i.isPlaying ? (h(), b("div", Q, [...e[9] || (e[9] = [
155
+ H("span", { class: "dot animate-pulse" }, null, -1),
156
+ L(" 播放中 ", -1)
157
+ ])])) : (h(), b("div", K, [...e[8] || (e[8] = [
158
+ H("span", { class: "dot" }, null, -1),
159
+ L(" 暂停中 ", -1)
119
160
  ])]))
120
161
  ])
121
- ])
122
- ], 2)) : $("", !0)
162
+ ], 4)
163
+ ], 2)) : I("", !0)
123
164
  ]),
124
165
  _: 1
125
166
  }));
126
167
  }
127
- }), z = (r, p) => {
128
- const e = r.__vccOpts || r;
129
- for (const [s, o] of p)
130
- e[s] = o;
131
- return e;
132
- }, O = /* @__PURE__ */ z(R, [["__scopeId", "data-v-8d9d61ad"]]), J = /* @__PURE__ */ H({
168
+ }), _ = (i, P) => {
169
+ const t = i.__vccOpts || i;
170
+ for (const [s, l] of P)
171
+ t[s] = l;
172
+ return t;
173
+ }, ee = /* @__PURE__ */ _(Z, [["__scopeId", "data-v-54e42f84"]]), te = /* @__PURE__ */ T({
133
174
  __name: "VirtualHumanEventAdapter",
134
175
  props: {
135
176
  // 屏幕客户端ID
@@ -144,32 +185,32 @@ const F = { class: "video-wrapper" }, T = ["src", "muted"], q = { class: "overla
144
185
  }
145
186
  },
146
187
  emits: ["highlight", "showDialog", "end", "pause", "connected", "error"],
147
- setup(r, { emit: p }) {
148
- const e = r, s = p, o = g(null), c = g(!1);
149
- let n = null, u = 0;
150
- const b = () => {
188
+ setup(i, { emit: P }) {
189
+ const t = i, s = P, l = v(null), k = v(!1);
190
+ let n = null, m = 0;
191
+ const V = () => {
151
192
  n || (n = new (window.AudioContext || window.webkitAudioContext)({
152
193
  sampleRate: 24e3
153
194
  })), n.state === "suspended" && n.resume();
154
- }, k = (t) => {
155
- if (b(), !!n)
195
+ }, E = (o) => {
196
+ if (V(), !!n)
156
197
  try {
157
- const a = window.atob(t), l = a.length, m = new Uint8Array(l);
158
- for (let d = 0; d < l; d++)
159
- m[d] = a.charCodeAt(d);
160
- const f = new Int16Array(m.buffer), P = new Float32Array(f.length);
161
- for (let d = 0; d < f.length; d++)
162
- P[d] = f[d] / 32768;
163
- const S = n.createBuffer(1, P.length, 24e3);
164
- S.getChannelData(0).set(P);
165
- const V = n.createBufferSource();
166
- V.buffer = S, V.connect(n.destination), u < n.currentTime && (u = n.currentTime), V.start(u), u += S.duration;
167
- } catch (a) {
168
- console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:", a);
198
+ const u = window.atob(o), c = u.length, w = new Uint8Array(c);
199
+ for (let d = 0; d < c; d++)
200
+ w[d] = u.charCodeAt(d);
201
+ const g = new Int16Array(w.buffer), A = new Float32Array(g.length);
202
+ for (let d = 0; d < g.length; d++)
203
+ A[d] = g[d] / 32768;
204
+ const f = n.createBuffer(1, A.length, 24e3);
205
+ f.getChannelData(0).set(A);
206
+ const y = n.createBufferSource();
207
+ y.buffer = f, y.connect(n.destination), m < n.currentTime && (m = n.currentTime), y.start(m), m += f.duration;
208
+ } catch (u) {
209
+ console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:", u);
169
210
  }
170
- }, C = (t) => {
211
+ }, p = (o) => {
171
212
  if (n)
172
- switch (t) {
213
+ switch (o) {
173
214
  case "play":
174
215
  n.state === "suspended" && n.resume();
175
216
  break;
@@ -177,78 +218,78 @@ const F = { class: "video-wrapper" }, T = ["src", "muted"], q = { class: "overla
177
218
  n.state === "running" && n.suspend();
178
219
  break;
179
220
  case "stop":
180
- n.close(), n = null, u = 0;
221
+ n.close(), n = null, m = 0;
181
222
  break;
182
223
  default:
183
- console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${t}`);
224
+ console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${o}`);
184
225
  }
185
- }, v = () => {
186
- o.value && o.value.close();
226
+ }, C = () => {
227
+ l.value && l.value.close();
187
228
  try {
188
- const t = new URL(e.wsUrl);
189
- t.searchParams.append("sessionId", e.screenClientId + "-event"), o.value = new WebSocket(t.toString()), o.value.onopen = () => {
190
- c.value = !0, s("connected"), console.log(`[VirtualHumanEventAdapter] Connected to ${e.wsUrl} for session ${e.screenClientId}-event`);
191
- }, o.value.onmessage = (a) => {
229
+ const o = new URL(t.wsUrl);
230
+ o.searchParams.append("sessionId", t.screenClientId + "-event"), l.value = new WebSocket(o.toString()), l.value.onopen = () => {
231
+ k.value = !0, s("connected"), console.log(`[VirtualHumanEventAdapter] Connected to ${t.wsUrl} for session ${t.screenClientId}-event`);
232
+ }, l.value.onmessage = (u) => {
192
233
  try {
193
- const l = JSON.parse(a.data);
194
- i(l);
195
- } catch (l) {
196
- console.error("[VirtualHumanEventAdapter] Failed to parse message:", a.data, l);
234
+ const c = JSON.parse(u.data);
235
+ x(c);
236
+ } catch (c) {
237
+ console.error("[VirtualHumanEventAdapter] Failed to parse message:", u.data, c);
197
238
  }
198
- }, o.value.onerror = (a) => {
199
- console.error("[VirtualHumanEventAdapter] WebSocket error:", a), s("error", a);
200
- }, o.value.onclose = () => {
201
- c.value = !1, console.log("[VirtualHumanEventAdapter] WebSocket disconnected");
239
+ }, l.value.onerror = (u) => {
240
+ console.error("[VirtualHumanEventAdapter] WebSocket error:", u), s("error", u);
241
+ }, l.value.onclose = () => {
242
+ k.value = !1, console.log("[VirtualHumanEventAdapter] WebSocket disconnected");
202
243
  };
203
- } catch (t) {
204
- console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:", t), s("error", t);
244
+ } catch (o) {
245
+ console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:", o), s("error", o);
205
246
  }
206
- }, i = (t) => {
207
- const { type: a, payload: l, action: m } = t;
208
- switch (a) {
247
+ }, x = (o) => {
248
+ const { type: u, payload: c, action: w } = o;
249
+ switch (console.log("msgmsg", o), u) {
209
250
  case "audio":
210
- const f = (l == null ? void 0 : l.data) || t.data;
211
- f && k(f);
251
+ const g = (c == null ? void 0 : c.data) || o.data;
252
+ g && E(g);
212
253
  break;
213
254
  case "dialog_event":
214
- t.event && s(t.event, t.params);
255
+ o.event && s(o.event, o.params);
215
256
  break;
216
257
  case "control":
217
- m && C(m);
258
+ w && p(w);
218
259
  break;
219
260
  case "highlight":
220
- s("highlight", l);
261
+ s("highlight", c);
221
262
  break;
222
263
  case "showDialog":
223
- s("showDialog", l);
264
+ s("showDialog", c);
224
265
  break;
225
266
  case "end":
226
- s("end", l);
267
+ s("end", c);
227
268
  break;
228
269
  case "pause":
229
- s("pause", l);
270
+ s("pause", c);
230
271
  break;
231
272
  default:
232
- console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${a}`);
273
+ console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${u}`);
233
274
  }
234
275
  };
235
- return w(() => e.screenClientId, () => {
236
- e.screenClientId && e.wsUrl && v();
237
- }), w(() => e.wsUrl, () => {
238
- e.screenClientId && e.wsUrl && v();
239
- }), I(() => {
240
- e.screenClientId && e.wsUrl && v();
241
- }), _(() => {
242
- o.value && o.value.close(), n && n.close();
243
- }), (t, a) => D(t.$slots, "default");
276
+ return z(() => t.screenClientId, () => {
277
+ t.screenClientId && t.wsUrl && C();
278
+ }), z(() => t.wsUrl, () => {
279
+ t.screenClientId && t.wsUrl && C();
280
+ }), W(() => {
281
+ t.screenClientId && t.wsUrl && C();
282
+ }), X(() => {
283
+ l.value && l.value.close(), n && n.close();
284
+ }), (o, u) => J(o.$slots, "default");
244
285
  }
245
- }), L = (r) => {
246
- r.component("VirtualHumanPersona", O), r.component("VirtualHumanEventAdapter", J);
247
- }, G = {
248
- install: L
286
+ }), ne = (i) => {
287
+ i.component("VirtualHumanPersona", ee), i.component("VirtualHumanEventAdapter", te);
288
+ }, oe = {
289
+ install: ne
249
290
  };
250
291
  export {
251
- J as VirtualHumanEventAdapter,
252
- O as VirtualHumanPersona,
253
- G as default
292
+ te as VirtualHumanEventAdapter,
293
+ ee as VirtualHumanPersona,
294
+ oe as default
254
295
  };
@@ -1 +1 @@
1
- (function(u,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(u=typeof globalThis<"u"?globalThis:u||self,e(u.VirtualHumanCf={},u.Vue))})(this,function(u,e){"use strict";const U={class:"video-wrapper"},_=["src","muted"],A={class:"overlay"},E={key:0,class:"status-badge paused"},H={key:1,class:"status-badge playing"},P=((l,y)=>{const t=l.__vccOpts||l;for(const[s,r]of y)t[s]=r;return t})(e.defineComponent({__name:"VirtualHumanPersona",props:{videoSrc:{type:String,required:!0},visible:{type:Boolean,default:!0},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(l,{emit:y}){const t=l,s=y,r=e.ref(null),d=e.ref(null),a=e.ref(!1),f=()=>{if(d.value&&d.value.close(),!(!t.wsUrl||!t.screenClientId))try{const c=new URL(t.wsUrl);c.searchParams.append("sessionId",t.screenClientId+"-persona"),d.value=new WebSocket(c.toString()),d.value.onopen=()=>{a.value=!0,console.log(`[VirtualHumanPersona] Connected to ${t.wsUrl} for session ${t.screenClientId}-persona`)},d.value.onmessage=n=>{try{const o=JSON.parse(n.data);w(o)}catch(o){console.error("[VirtualHumanPersona] Failed to parse message:",n.data,o)}},d.value.onerror=n=>{console.error("[VirtualHumanPersona] WebSocket error:",n)},d.value.onclose=()=>{a.value=!1,console.log("[VirtualHumanPersona] WebSocket disconnected")}}catch(c){console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",c)}},w=c=>{const{type:n,action:o}=c;n==="control"&&(o==="play"?(s("update:isPlaying",!0),s("update:visible",!0)):o==="pause"?(s("update:isPlaying",!1),s("update:visible",!1)):o==="stop"&&(s("update:isPlaying",!1),s("update:visible",!1),r.value&&(r.value.currentTime=0)))};e.watch(()=>t.screenClientId,()=>{t.screenClientId&&t.wsUrl&&f()}),e.watch(()=>t.wsUrl,()=>{t.screenClientId&&t.wsUrl&&f()}),e.watch(()=>t.isPlaying,c=>{r.value&&(c?r.value.play().catch(n=>console.error("Video play failed:",n)):r.value.pause())});const k=()=>{t.isPlaying||s("update:isPlaying",!0)},b=()=>{t.isPlaying&&s("update:isPlaying",!1)},g=()=>{s("update:isPlaying",!1),s("ended")};return e.onMounted(()=>{t.isPlaying&&r.value&&r.value.play().catch(c=>console.error("Video play failed:",c)),t.screenClientId&&t.wsUrl&&f()}),e.onUnmounted(()=>{d.value&&d.value.close()}),(c,n)=>(e.openBlock(),e.createBlock(e.Transition,{name:"fade"},{default:e.withCtx(()=>[l.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:e.normalizeClass(["virtual-human-container",{"is-dark":l.isDark}])},[e.createElementVNode("div",U,[e.createElementVNode("video",{ref_key:"videoRef",ref:r,src:l.videoSrc,class:"persona-video",muted:l.muted,playsinline:"",loop:"",onPlay:k,onPause:b,onEnded:g},null,40,_),e.createElementVNode("div",A,[l.isPlaying?(e.openBlock(),e.createElementBlock("div",H,[...n[1]||(n[1]=[e.createElementVNode("span",{class:"dot animate-pulse"},null,-1),e.createTextVNode(" 播放中 ",-1)])])):(e.openBlock(),e.createElementBlock("div",E,[...n[0]||(n[0]=[e.createElementVNode("span",{class:"dot"},null,-1),e.createTextVNode(" 暂停中 ",-1)])]))])])],2)):e.createCommentVNode("",!0)]),_:1}))}}),[["__scopeId","data-v-8d9d61ad"]]),S=e.defineComponent({__name:"VirtualHumanEventAdapter",props:{screenClientId:{type:String,required:!0},wsUrl:{type:String,required:!0}},emits:["highlight","showDialog","end","pause","connected","error"],setup(l,{emit:y}){const t=l,s=y,r=e.ref(null),d=e.ref(!1);let a=null,f=0;const w=()=>{a||(a=new(window.AudioContext||window.webkitAudioContext)({sampleRate:24e3})),a.state==="suspended"&&a.resume()},k=n=>{if(w(),!!a)try{const o=window.atob(n),i=o.length,h=new Uint8Array(i);for(let p=0;p<i;p++)h[p]=o.charCodeAt(p);const m=new Int16Array(h.buffer),v=new Float32Array(m.length);for(let p=0;p<m.length;p++)v[p]=m[p]/32768;const C=a.createBuffer(1,v.length,24e3);C.getChannelData(0).set(v);const V=a.createBufferSource();V.buffer=C,V.connect(a.destination),f<a.currentTime&&(f=a.currentTime),V.start(f),f+=C.duration}catch(o){console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:",o)}},b=n=>{if(a)switch(n){case"play":a.state==="suspended"&&a.resume();break;case"pause":a.state==="running"&&a.suspend();break;case"stop":a.close(),a=null,f=0;break;default:console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${n}`)}},g=()=>{r.value&&r.value.close();try{const n=new URL(t.wsUrl);n.searchParams.append("sessionId",t.screenClientId+"-event"),r.value=new WebSocket(n.toString()),r.value.onopen=()=>{d.value=!0,s("connected"),console.log(`[VirtualHumanEventAdapter] Connected to ${t.wsUrl} for session ${t.screenClientId}-event`)},r.value.onmessage=o=>{try{const i=JSON.parse(o.data);c(i)}catch(i){console.error("[VirtualHumanEventAdapter] Failed to parse message:",o.data,i)}},r.value.onerror=o=>{console.error("[VirtualHumanEventAdapter] WebSocket error:",o),s("error",o)},r.value.onclose=()=>{d.value=!1,console.log("[VirtualHumanEventAdapter] WebSocket disconnected")}}catch(n){console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",n),s("error",n)}},c=n=>{const{type:o,payload:i,action:h}=n;switch(o){case"audio":const m=(i==null?void 0:i.data)||n.data;m&&k(m);break;case"dialog_event":n.event&&s(n.event,n.params);break;case"control":h&&b(h);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: ${o}`)}};return e.watch(()=>t.screenClientId,()=>{t.screenClientId&&t.wsUrl&&g()}),e.watch(()=>t.wsUrl,()=>{t.screenClientId&&t.wsUrl&&g()}),e.onMounted(()=>{t.screenClientId&&t.wsUrl&&g()}),e.onUnmounted(()=>{r.value&&r.value.close(),a&&a.close()}),(n,o)=>e.renderSlot(n.$slots,"default")}}),I={install:l=>{l.component("VirtualHumanPersona",P),l.component("VirtualHumanEventAdapter",S)}};u.VirtualHumanEventAdapter=S,u.VirtualHumanPersona=P,u.default=I,Object.defineProperties(u,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
1
+ (function(p,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(p=typeof globalThis<"u"?globalThis:p||self,e(p.VirtualHumanCf={},p.Vue))})(this,function(p,e){"use strict";const z=["src","muted"],N={class:"overlay"},T={key:0,class:"status-badge paused"},R={key:1,class:"status-badge playing"},I=((c,V)=>{const n=c.__vccOpts||c;for(const[s,r]of V)n[s]=r;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(c,{emit:V}){const n=c,s=V,r=e.ref(null),v=e.ref(null),o=e.ref(null),g=e.ref(!1),E=e.ref(1),S=e.ref(!1),h=e.ref(null),b=e.ref(0),H=e.ref(0),l=e.ref(1),d=()=>{if(o.value&&o.value.close(),!(!n.wsUrl||!n.screenClientId))try{const i=new URL(n.wsUrl);i.searchParams.append("sessionId",n.screenClientId+"-persona"),o.value=new WebSocket(i.toString()),o.value.onopen=()=>{g.value=!0,console.log(`[VirtualHumanPersona] Connected to ${n.wsUrl} for session ${n.screenClientId}-persona`)},o.value.onmessage=t=>{try{const a=JSON.parse(t.data);u(a)}catch(a){console.error("[VirtualHumanPersona] Failed to parse message:",t.data,a)}},o.value.onerror=t=>{console.error("[VirtualHumanPersona] WebSocket error:",t)},o.value.onclose=()=>{g.value=!1,console.log("[VirtualHumanPersona] WebSocket disconnected")}}catch(i){console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",i)}},u=i=>{const{type:t,action:a}=i;t==="control"&&(a==="play"?(s("update:isPlaying",!0),s("update:visible",!0)):a==="pause"?s("update:isPlaying",!1):a==="stop"&&(s("update:isPlaying",!1),s("update:visible",!1),r.value&&(r.value.currentTime=0)))};e.watch(()=>n.screenClientId,()=>{n.screenClientId&&n.wsUrl&&d()}),e.watch(()=>n.wsUrl,()=>{n.screenClientId&&n.wsUrl&&d()}),e.watch(()=>n.isPlaying,i=>{r.value&&(i?r.value.play().catch(t=>console.error("Video play failed:",t)):r.value.pause())});const k=()=>{n.isPlaying||s("update:isPlaying",!0)},y=()=>{n.isPlaying&&s("update:isPlaying",!1)},P=()=>{s("update:isPlaying",!1),s("ended")},m=(i,t)=>{S.value=!0,h.value=i,l.value=E.value;const a="clientX"in t?t.clientX:t.touches[0].clientX,C="clientY"in t?t.clientY:t.touches[0].clientY;b.value=a,H.value=C,document.addEventListener("mousemove",w),document.addEventListener("mouseup",f),document.addEventListener("touchmove",w),document.addEventListener("touchend",f)},w=i=>{if(!S.value||!v.value)return;const t="clientX"in i?i.clientX:i.touches[0].clientX,a="clientY"in i?i.clientY:i.touches[0].clientY,C=t-b.value,U=a-H.value,x=v.value.getBoundingClientRect(),W=.5,X=2;let B=l.value;const A=Math.min(x.width,x.height);h.value==="bottom-right"?B=l.value+(C+U)/A:h.value==="bottom-left"?B=l.value+(-C+U)/A:h.value==="top-right"?B=l.value+(C-U)/A:h.value==="top-left"&&(B=l.value+(-C-U)/A),E.value=Math.max(W,Math.min(X,B))},f=()=>{S.value=!1,h.value=null,document.removeEventListener("mousemove",w),document.removeEventListener("mouseup",f),document.removeEventListener("touchmove",w),document.removeEventListener("touchend",f)};return e.onMounted(()=>{n.isPlaying&&r.value&&r.value.play().catch(i=>console.error("Video play failed:",i)),n.screenClientId&&n.wsUrl&&d()}),e.onUnmounted(()=>{o.value&&o.value.close()}),(i,t)=>(e.openBlock(),e.createBlock(e.Transition,{name:"fade"},{default:e.withCtx(()=>[c.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:e.normalizeClass(["virtual-human-container",{"is-dark":c.isDark}])},[e.createElementVNode("div",{class:"video-wrapper",ref_key:"wrapperRef",ref:v,style:e.normalizeStyle({transform:`scale(${E.value})`,transformOrigin:"left bottom"})},[e.createElementVNode("video",{ref_key:"videoRef",ref:r,src:c.videoSrc,class:"persona-video",muted:c.muted,playsinline:"",loop:"",onPlay:k,onPause:y,onEnded:P},null,40,z),c.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:"resize-handle top-left",onMousedown:t[0]||(t[0]=a=>m("top-left",a)),onTouchstart:t[1]||(t[1]=a=>m("top-left",a))},null,32)):e.createCommentVNode("",!0),c.visible?(e.openBlock(),e.createElementBlock("div",{key:1,class:"resize-handle top-right",onMousedown:t[2]||(t[2]=a=>m("top-right",a)),onTouchstart:t[3]||(t[3]=a=>m("top-right",a))},null,32)):e.createCommentVNode("",!0),c.visible?(e.openBlock(),e.createElementBlock("div",{key:2,class:"resize-handle bottom-left",onMousedown:t[4]||(t[4]=a=>m("bottom-left",a)),onTouchstart:t[5]||(t[5]=a=>m("bottom-left",a))},null,32)):e.createCommentVNode("",!0),c.visible?(e.openBlock(),e.createElementBlock("div",{key:3,class:"resize-handle bottom-right",onMousedown:t[6]||(t[6]=a=>m("bottom-right",a)),onTouchstart:t[7]||(t[7]=a=>m("bottom-right",a))},null,32)):e.createCommentVNode("",!0),e.createElementVNode("div",N,[c.isPlaying?(e.openBlock(),e.createElementBlock("div",R,[...t[9]||(t[9]=[e.createElementVNode("span",{class:"dot animate-pulse"},null,-1),e.createTextVNode(" 播放中 ",-1)])])):(e.openBlock(),e.createElementBlock("div",T,[...t[8]||(t[8]=[e.createElementVNode("span",{class:"dot"},null,-1),e.createTextVNode(" 暂停中 ",-1)])]))])],4)],2)):e.createCommentVNode("",!0)]),_:1}))}}),[["__scopeId","data-v-54e42f84"]]),M=e.defineComponent({__name:"VirtualHumanEventAdapter",props:{screenClientId:{type:String,required:!0},wsUrl:{type:String,required:!0}},emits:["highlight","showDialog","end","pause","connected","error"],setup(c,{emit:V}){const n=c,s=V,r=e.ref(null),v=e.ref(!1);let o=null,g=0;const E=()=>{o||(o=new(window.AudioContext||window.webkitAudioContext)({sampleRate:24e3})),o.state==="suspended"&&o.resume()},S=l=>{if(E(),!!o)try{const d=window.atob(l),u=d.length,k=new Uint8Array(u);for(let f=0;f<u;f++)k[f]=d.charCodeAt(f);const y=new Int16Array(k.buffer),P=new Float32Array(y.length);for(let f=0;f<y.length;f++)P[f]=y[f]/32768;const m=o.createBuffer(1,P.length,24e3);m.getChannelData(0).set(P);const w=o.createBufferSource();w.buffer=m,w.connect(o.destination),g<o.currentTime&&(g=o.currentTime),w.start(g),g+=m.duration}catch(d){console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:",d)}},h=l=>{if(o)switch(l){case"play":o.state==="suspended"&&o.resume();break;case"pause":o.state==="running"&&o.suspend();break;case"stop":o.close(),o=null,g=0;break;default:console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${l}`)}},b=()=>{r.value&&r.value.close();try{const l=new URL(n.wsUrl);l.searchParams.append("sessionId",n.screenClientId+"-event"),r.value=new WebSocket(l.toString()),r.value.onopen=()=>{v.value=!0,s("connected"),console.log(`[VirtualHumanEventAdapter] Connected to ${n.wsUrl} for session ${n.screenClientId}-event`)},r.value.onmessage=d=>{try{const u=JSON.parse(d.data);H(u)}catch(u){console.error("[VirtualHumanEventAdapter] Failed to parse message:",d.data,u)}},r.value.onerror=d=>{console.error("[VirtualHumanEventAdapter] WebSocket error:",d),s("error",d)},r.value.onclose=()=>{v.value=!1,console.log("[VirtualHumanEventAdapter] WebSocket disconnected")}}catch(l){console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",l),s("error",l)}},H=l=>{const{type:d,payload:u,action:k}=l;switch(console.log("msgmsg",l),d){case"audio":const y=(u==null?void 0:u.data)||l.data;y&&S(y);break;case"dialog_event":l.event&&s(l.event,l.params);break;case"control":k&&h(k);break;case"highlight":s("highlight",u);break;case"showDialog":s("showDialog",u);break;case"end":s("end",u);break;case"pause":s("pause",u);break;default:console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${d}`)}};return e.watch(()=>n.screenClientId,()=>{n.screenClientId&&n.wsUrl&&b()}),e.watch(()=>n.wsUrl,()=>{n.screenClientId&&n.wsUrl&&b()}),e.onMounted(()=>{n.screenClientId&&n.wsUrl&&b()}),e.onUnmounted(()=>{r.value&&r.value.close(),o&&o.close()}),(l,d)=>e.renderSlot(l.$slots,"default")}}),L={install:c=>{c.component("VirtualHumanPersona",I),c.component("VirtualHumanEventAdapter",M)}};p.VirtualHumanEventAdapter=M,p.VirtualHumanPersona=I,p.default=L,Object.defineProperties(p,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virtual-human-cf",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Vue3 Digital Human Component Package by cf ",
5
5
  "main": "dist/virtual-human-cf.umd.js",
6
6
  "module": "dist/virtual-human-cf.es.js",
@@ -140,6 +140,7 @@ const connectWebSocket = () => {
140
140
 
141
141
  const handleMessage = (msg: any) => {
142
142
  const { type, payload, action } = msg;
143
+ console.log("msgmsg",msg)
143
144
  switch (type) {
144
145
  case 'audio':
145
146
  const base64Data = payload?.data || msg.data;
@@ -5,7 +5,11 @@
5
5
  class="virtual-human-container"
6
6
  :class="{ 'is-dark': isDark }"
7
7
  >
8
- <div class="video-wrapper">
8
+ <div
9
+ class="video-wrapper"
10
+ ref="wrapperRef"
11
+ :style="{ transform: `scale(${scale})`, transformOrigin: 'left bottom' }"
12
+ >
9
13
  <video
10
14
  ref="videoRef"
11
15
  :src="videoSrc"
@@ -17,7 +21,33 @@
17
21
  @pause="handlePause"
18
22
  @ended="handleEnded"
19
23
  ></video>
20
-
24
+
25
+ <!-- 缩放控制点 - 四个角 -->
26
+ <div
27
+ v-if="visible"
28
+ class="resize-handle top-left"
29
+ @mousedown="startResize('top-left', $event)"
30
+ @touchstart="startResize('top-left', $event)"
31
+ ></div>
32
+ <div
33
+ v-if="visible"
34
+ class="resize-handle top-right"
35
+ @mousedown="startResize('top-right', $event)"
36
+ @touchstart="startResize('top-right', $event)"
37
+ ></div>
38
+ <div
39
+ v-if="visible"
40
+ class="resize-handle bottom-left"
41
+ @mousedown="startResize('bottom-left', $event)"
42
+ @touchstart="startResize('bottom-left', $event)"
43
+ ></div>
44
+ <div
45
+ v-if="visible"
46
+ class="resize-handle bottom-right"
47
+ @mousedown="startResize('bottom-right', $event)"
48
+ @touchstart="startResize('bottom-right', $event)"
49
+ ></div>
50
+
21
51
  <!-- UI Overlay for controls or status -->
22
52
  <div class="overlay">
23
53
  <div v-if="!isPlaying" class="status-badge paused">
@@ -46,7 +76,7 @@ const props = defineProps({
46
76
  // 是否可见
47
77
  visible: {
48
78
  type: Boolean,
49
- default: true,
79
+ default: false,
50
80
  },
51
81
  // 是否自动播放
52
82
  isPlaying: {
@@ -78,10 +108,19 @@ const props = defineProps({
78
108
  const emit = defineEmits(['update:isPlaying', 'ended', 'update:visible']);
79
109
 
80
110
  const videoRef = ref<HTMLVideoElement | null>(null);
111
+ const wrapperRef = ref<HTMLDivElement | null>(null);
81
112
 
82
113
  const ws = ref<WebSocket | null>(null);
83
114
  const isConnected = ref(false);
84
115
 
116
+ // 缩放相关状态
117
+ const scale = ref(1);
118
+ const isResizing = ref(false);
119
+ const resizeHandle = ref<string | null>(null);
120
+ const startX = ref(0);
121
+ const startY = ref(0);
122
+ const startScale = ref(1);
123
+
85
124
  const connectWebSocket = () => {
86
125
  if (ws.value) {
87
126
  ws.value.close();
@@ -129,7 +168,7 @@ const handleMessage = (msg: any) => {
129
168
  emit('update:visible', true);
130
169
  } else if (action === 'pause') {
131
170
  emit('update:isPlaying', false);
132
- emit('update:visible', false);
171
+ // 暂停时不隐藏视频
133
172
  } else if (action === 'stop') {
134
173
  emit('update:isPlaying', false);
135
174
  emit('update:visible', false);
@@ -179,6 +218,62 @@ const handleEnded = () => {
179
218
  emit('ended');
180
219
  };
181
220
 
221
+ // 缩放功能
222
+ const startResize = (handle: string, event: MouseEvent | TouchEvent) => {
223
+ isResizing.value = true;
224
+ resizeHandle.value = handle;
225
+ startScale.value = scale.value;
226
+
227
+ const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX;
228
+ const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY;
229
+ startX.value = clientX;
230
+ startY.value = clientY;
231
+
232
+ document.addEventListener('mousemove', handleResize);
233
+ document.addEventListener('mouseup', stopResize);
234
+ document.addEventListener('touchmove', handleResize);
235
+ document.addEventListener('touchend', stopResize);
236
+ };
237
+
238
+ const handleResize = (event: MouseEvent | TouchEvent) => {
239
+ if (!isResizing.value || !wrapperRef.value) return;
240
+
241
+ const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX;
242
+ const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY;
243
+
244
+ const deltaX = clientX - startX.value;
245
+ const deltaY = clientY - startY.value;
246
+
247
+ const rect = wrapperRef.value.getBoundingClientRect();
248
+ const minScale = 0.5;
249
+ const maxScale = 2;
250
+
251
+ // 根据拖动的角计算缩放比例
252
+ let newScale = startScale.value;
253
+ const baseSize = Math.min(rect.width, rect.height);
254
+
255
+ if (resizeHandle.value === 'bottom-right') {
256
+ newScale = startScale.value + (deltaX + deltaY) / baseSize;
257
+ } else if (resizeHandle.value === 'bottom-left') {
258
+ newScale = startScale.value + (-deltaX + deltaY) / baseSize;
259
+ } else if (resizeHandle.value === 'top-right') {
260
+ newScale = startScale.value + (deltaX - deltaY) / baseSize;
261
+ } else if (resizeHandle.value === 'top-left') {
262
+ newScale = startScale.value + (-deltaX - deltaY) / baseSize;
263
+ }
264
+
265
+ scale.value = Math.max(minScale, Math.min(maxScale, newScale));
266
+ };
267
+
268
+ const stopResize = () => {
269
+ isResizing.value = false;
270
+ resizeHandle.value = null;
271
+ document.removeEventListener('mousemove', handleResize);
272
+ document.removeEventListener('mouseup', stopResize);
273
+ document.removeEventListener('touchmove', handleResize);
274
+ document.removeEventListener('touchend', stopResize);
275
+ };
276
+
182
277
  onMounted(() => {
183
278
  if (props.isPlaying && videoRef.value) {
184
279
  videoRef.value.play().catch(e => console.error('Video play failed:', e));
@@ -208,32 +303,78 @@ onUnmounted(() => {
208
303
  }
209
304
 
210
305
  .virtual-human-container {
306
+ position: fixed;
307
+ left: 16px;
308
+ bottom: 16px;
309
+ width: 400px;
310
+ height: 500px;
311
+ z-index: 2147483647;
312
+ overflow: visible;
313
+ }
314
+
315
+ .video-wrapper {
211
316
  position: relative;
212
- width: 100%;
213
- max-width: 400px;
317
+ width: 400px;
318
+ height: 500px;
214
319
  border-radius: 1rem;
215
320
  overflow: hidden;
216
321
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
217
322
  background: #ffffff;
218
- transition: background-color 0.3s ease;
323
+ transition: background-color 0.3s ease, box-shadow 0.3s ease;
219
324
  }
220
325
 
221
- .virtual-human-container.is-dark {
326
+ .virtual-human-container.is-dark .video-wrapper {
222
327
  background: #1f2937;
223
328
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
224
329
  }
225
330
 
226
- .video-wrapper {
227
- position: relative;
228
- width: 100%;
229
- aspect-ratio: 9 / 16; /* Portrait ratio for digital human */
230
- background: #000;
231
- }
232
-
233
331
  .persona-video {
234
332
  width: 100%;
235
333
  height: 100%;
236
334
  object-fit: cover;
335
+ transform-origin: center;
336
+ transition: transform 0.1s ease-out;
337
+ }
338
+
339
+ .resize-handle {
340
+ position: absolute;
341
+ width: 20px;
342
+ height: 20px;
343
+ background: rgba(0, 0, 0, 0.5);
344
+ border: 2px solid rgba(255, 255, 255, 0.8);
345
+ border-radius: 4px;
346
+ cursor: pointer;
347
+ z-index: 20;
348
+ transition: background 0.2s, border-color 0.2s;
349
+ }
350
+
351
+ .resize-handle:hover {
352
+ background: rgba(0, 0, 0, 0.7);
353
+ border-color: rgba(255, 255, 255, 1);
354
+ }
355
+
356
+ .resize-handle.top-left {
357
+ top: -10px;
358
+ left: -10px;
359
+ cursor: nwse-resize;
360
+ }
361
+
362
+ .resize-handle.top-right {
363
+ top: -10px;
364
+ right: -10px;
365
+ cursor: nesw-resize;
366
+ }
367
+
368
+ .resize-handle.bottom-left {
369
+ bottom: -10px;
370
+ left: -10px;
371
+ cursor: nesw-resize;
372
+ }
373
+
374
+ .resize-handle.bottom-right {
375
+ bottom: -10px;
376
+ right: -10px;
377
+ cursor: nwse-resize;
237
378
  }
238
379
 
239
380
  .overlay {