@zeewain/3d-avatar-sdk 1.2.1 → 1.2.2

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.
Files changed (55) hide show
  1. package/README.md +3 -4
  2. package/dist/assets/Build/webgl.data.unityweb +0 -0
  3. package/dist/assets/Build/webgl.framework.js.unityweb +0 -0
  4. package/dist/assets/Build/webgl.wasm.unityweb +0 -0
  5. package/dist/examples/test-umd/index.html +762 -0
  6. package/dist/examples/test-vue2/.eslintignore +45 -0
  7. package/dist/examples/test-vue2/.eslintrc.js +174 -0
  8. package/dist/examples/test-vue2/.stylelintignore +50 -0
  9. package/dist/examples/test-vue2/.stylelintrc.js +79 -0
  10. package/dist/examples/test-vue2/README.md +139 -0
  11. package/dist/examples/test-vue2/babel.config.js +14 -0
  12. package/dist/examples/test-vue2/package.json +53 -0
  13. package/dist/examples/test-vue2/pnpm-lock.yaml +8776 -0
  14. package/dist/examples/test-vue2/public/index.html +19 -0
  15. package/dist/examples/test-vue2/setup.js +170 -0
  16. package/dist/examples/test-vue2/src/App.vue +943 -0
  17. package/dist/examples/test-vue2/src/components/BroadcastAPI.vue +666 -0
  18. package/dist/examples/test-vue2/src/components/CameraAPI.vue +414 -0
  19. package/dist/examples/test-vue2/src/components/GlobalConfig.vue +200 -0
  20. package/dist/examples/test-vue2/src/components/InfoCards.vue +294 -0
  21. package/dist/examples/test-vue2/src/components/InitAPI.vue +334 -0
  22. package/dist/examples/test-vue2/src/components/LogPanel.vue +249 -0
  23. package/dist/examples/test-vue2/src/components/MotionControlAPI.vue +400 -0
  24. package/dist/examples/test-vue2/src/components/UnityPreview.vue +201 -0
  25. package/dist/examples/test-vue2/src/main.js +16 -0
  26. package/dist/examples/test-vue2/vue.config.js +41 -0
  27. package/dist/examples/test-vue3/.eslintrc +3 -0
  28. package/dist/examples/test-vue3/.stylelintignore +3 -0
  29. package/dist/examples/test-vue3/.stylelintrc +48 -0
  30. package/dist/examples/test-vue3/README.md +236 -0
  31. package/dist/examples/test-vue3/env.d.ts +8 -0
  32. package/dist/examples/test-vue3/index.html +95 -0
  33. package/dist/examples/test-vue3/package.json +55 -0
  34. package/dist/examples/test-vue3/pnpm-lock.yaml +4636 -0
  35. package/dist/examples/test-vue3/setup.js +167 -0
  36. package/dist/examples/test-vue3/src/App.vue +962 -0
  37. package/dist/examples/test-vue3/src/components/BroadcastAPI.vue +636 -0
  38. package/dist/examples/test-vue3/src/components/CameraAPI.vue +376 -0
  39. package/dist/examples/test-vue3/src/components/GlobalConfig.vue +213 -0
  40. package/dist/examples/test-vue3/src/components/InfoCards.vue +288 -0
  41. package/dist/examples/test-vue3/src/components/InitAPI.vue +339 -0
  42. package/dist/examples/test-vue3/src/components/LogPanel.vue +236 -0
  43. package/dist/examples/test-vue3/src/components/MotionControlAPI.vue +373 -0
  44. package/dist/examples/test-vue3/src/components/UnityPreview.vue +189 -0
  45. package/dist/examples/test-vue3/src/main.ts +12 -0
  46. package/dist/examples/test-vue3/src/types.ts +9 -0
  47. package/dist/examples/test-vue3/tsconfig.json +44 -0
  48. package/dist/examples/test-vue3/tsconfig.node.json +14 -0
  49. package/dist/examples/test-vue3/vite.config.ts +75 -0
  50. package/dist/index.d.ts +15 -9
  51. package/dist/index.es5.js +64 -17
  52. package/dist/index.es5.umd.js +64 -17
  53. package/dist/index.esm.js +71 -16
  54. package/dist/index.umd.cjs +71 -16
  55. package/package.json +4 -3
@@ -0,0 +1,236 @@
1
+ <template>
2
+ <div class="log-panel">
3
+ <div class="log-container">
4
+ <div class="log-list" ref="logContainer">
5
+ <el-timeline v-if="props.logs.length > 0">
6
+ <el-timeline-item
7
+ v-for="(log, index) in props.logs"
8
+ :key="index"
9
+ :timestamp="log.time"
10
+ :type="getLogType(log.type)"
11
+ :icon="getLogIcon(log.type)"
12
+ placement="top"
13
+ size="normal">
14
+ <div class="log-item">
15
+ <el-tag
16
+ :type="getTagType(log.type) as any"
17
+ size="small"
18
+ class="log-type-tag">
19
+ {{ getTypeLabel(log.type) }}
20
+ </el-tag>
21
+ <span class="log-message">{{ log.message }}</span>
22
+ </div>
23
+ </el-timeline-item>
24
+ </el-timeline>
25
+
26
+ <div v-else class="empty-logs">
27
+ <el-empty description="暂无日志" :image-size="80">
28
+ <template #description>
29
+ <span>暂无操作日志</span>
30
+ </template>
31
+ </el-empty>
32
+ </div>
33
+ </div>
34
+
35
+ <div class="log-actions">
36
+ <el-button
37
+ type="primary"
38
+ :icon="Bottom"
39
+ @click="scrollToBottom"
40
+ >
41
+ 滚动到底部
42
+ </el-button>
43
+ <el-button
44
+ type="danger"
45
+ :icon="Delete"
46
+ @click="clearLogs"
47
+ >
48
+ 清空日志
49
+ </el-button>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import { ref, nextTick, watch } from 'vue';
57
+ import { ElButton, ElTag, ElTimeline, ElTimelineItem, ElEmpty } from 'element-plus';
58
+ import { Bottom } from '@element-plus/icons-vue';
59
+ import { Delete } from '@element-plus/icons-vue';
60
+
61
+ interface ILogEntry {
62
+ time: string;
63
+ message: string;
64
+ type: 'info' | 'success' | 'warning' | 'error';
65
+ }
66
+
67
+ const props = defineProps<{ logs: ILogEntry[] }>();
68
+ const emit = defineEmits<{ (e: 'clear-logs'): void }>();
69
+
70
+ const logContainer = ref<HTMLElement | null>(null);
71
+
72
+ watch(
73
+ () => props.logs,
74
+ async () => {
75
+ await nextTick();
76
+ if (logContainer.value) {
77
+ logContainer.value.scrollTop = logContainer.value.scrollHeight;
78
+ }
79
+ }
80
+ );
81
+
82
+ function scrollToBottom() {
83
+ if (logContainer.value) {
84
+ logContainer.value.scrollTop = logContainer.value.scrollHeight;
85
+ }
86
+ }
87
+
88
+ function clearLogs() {
89
+ emit('clear-logs');
90
+ }
91
+
92
+ function getLogType(type: ILogEntry['type']) {
93
+ const typeMap = {
94
+ info: 'primary',
95
+ success: 'success',
96
+ warning: 'warning',
97
+ error: 'danger',
98
+ };
99
+ return typeMap[type] || 'primary';
100
+ }
101
+
102
+ function getLogIcon(type: ILogEntry['type']) {
103
+ const iconMap = {
104
+ info: 'el-icon-info',
105
+ success: 'el-icon-success',
106
+ warning: 'el-icon-warning',
107
+ error: 'el-icon-error',
108
+ };
109
+ return iconMap[type] || 'el-icon-info';
110
+ }
111
+
112
+ function getTagType(type: ILogEntry['type']) {
113
+ const tagMap = {
114
+ info: 'info',
115
+ success: 'success',
116
+ warning: 'warning',
117
+ error: 'danger',
118
+ };
119
+ return tagMap[type] || 'info';
120
+ }
121
+
122
+ function getTypeLabel(type: ILogEntry['type']) {
123
+ const labelMap = {
124
+ info: '信息',
125
+ success: '成功',
126
+ warning: '警告',
127
+ error: '错误',
128
+ };
129
+ return labelMap[type] || '信息';
130
+ }
131
+ </script>
132
+
133
+ <style lang="scss" scoped>
134
+ .log-panel {
135
+ height: 100%;
136
+ max-height: 650px;
137
+ display: flex;
138
+ flex-direction: column;
139
+ .log-container {
140
+ height: 100%;
141
+ display: flex;
142
+ flex-direction: column;
143
+ overflow: auto;
144
+ .log-list {
145
+ flex: 1;
146
+ overflow-y: auto;
147
+ min-height: 300px;
148
+ padding: 10px;
149
+ background-color: #fafafa;
150
+ border-radius: 4px;
151
+ border: 1px solid #e4e7ed;
152
+ .el-timeline {
153
+ .el-timeline-item {
154
+ .el-timeline-item__wrapper {
155
+ .el-timeline-item__timestamp {
156
+ font-size: 11px;
157
+ color: #909399;
158
+ font-family: Monaco, Menlo, "Ubuntu Mono", monospace;
159
+ }
160
+ .el-timeline-item__content {
161
+ .log-item {
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 8px;
165
+ margin-bottom: 5px;
166
+ .log-type-tag {
167
+ flex-shrink: 0;
168
+ }
169
+ .log-message {
170
+ font-size: 13px;
171
+ line-height: 1.4;
172
+ color: #2c3e50;
173
+ word-break: break-word;
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ .empty-logs {
181
+ height: 200px;
182
+ display: flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ }
186
+ }
187
+ .log-actions {
188
+ margin-top: 10px;
189
+ display: flex;
190
+ gap: 8px;
191
+ justify-content: center;
192
+ .el-button {
193
+ flex: 1;
194
+ max-width: 120px;
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ // 移动端优化
201
+ @media screen and (max-width: 768px) {
202
+ .log-panel {
203
+ .log-container {
204
+ .log-list {
205
+ min-height: 250px;
206
+ padding: 8px;
207
+ .el-timeline {
208
+ .el-timeline-item {
209
+ .el-timeline-item__wrapper {
210
+ .el-timeline-item__timestamp {
211
+ font-size: 10px;
212
+ }
213
+ .el-timeline-item__content {
214
+ .log-item {
215
+ flex-direction: column;
216
+ align-items: flex-start;
217
+ gap: 4px;
218
+ .log-message {
219
+ font-size: 12px;
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ .log-actions {
228
+ .el-button {
229
+ font-size: 12px;
230
+ padding: 5px 8px;
231
+ }
232
+ }
233
+ }
234
+ }
235
+ }
236
+ </style>
@@ -0,0 +1,373 @@
1
+ <template>
2
+ <div class="motion-control-api">
3
+ <!-- API说明 -->
4
+ <div class="api-description">
5
+ <el-alert
6
+ title="动作管理API"
7
+ type="info"
8
+ :closable="false"
9
+ show-icon
10
+ >
11
+ <template #default>
12
+ <div class="api-signatures">
13
+ <div class="api-signature">
14
+ <code>playMotion(clipCode: string): Promise&lt;IAvatarCallbackResponse&gt;</code>
15
+ </div>
16
+ <div class="api-signature">
17
+ <code>getCurrentMotion(getRemainingTime?: boolean): Promise&lt;any&gt;</code>
18
+ </div>
19
+ </div>
20
+ <p>完整的数字人动作管理功能,支持动作播放和状态查询</p>
21
+ <p class="api-features">
22
+ ✅ 动作播放控制 &nbsp;&nbsp;
23
+ ✅ 当前动作查询 &nbsp;&nbsp;
24
+ ✅ 剩余时间获取 &nbsp;&nbsp;
25
+ ✅ 统一状态管理
26
+ </p>
27
+ </template>
28
+ </el-alert>
29
+ </div>
30
+
31
+ <!-- 动作播放控制 -->
32
+ <div class="api-controls">
33
+ <el-card shadow="hover" class="control-card">
34
+ <template #header>
35
+ <div class="card-header">
36
+ <span>🎭 动作播放</span>
37
+ <el-tag v-if="sdkStatus.canPlayMotion" type="success" size="default">可用</el-tag>
38
+ <el-tag v-else type="info" size="default">不可用</el-tag>
39
+ </div>
40
+ </template>
41
+
42
+ <div class="control-content">
43
+ <el-form :model="motionData" label-width="80px" size="default">
44
+ <el-form-item label="动作编码" required>
45
+ <el-input
46
+ v-model="motionData.clipCode"
47
+ placeholder="请输入动作编码"
48
+ prefix-icon="magic-stick"
49
+ :disabled="!sdkStatus.canPlayMotion"
50
+ @keydown.enter.native="handlePlayMotion"
51
+ />
52
+ <div class="input-help">
53
+ <span class="help-text">输入动作编码</span>
54
+ </div>
55
+ </el-form-item>
56
+
57
+ <el-form-item class="action-buttons">
58
+ <el-button
59
+ type="primary"
60
+ icon="video-play"
61
+ :loading="playLoading"
62
+ :disabled="!sdkStatus.canPlayMotion || !motionData.clipCode || !globalConfig.avatarCode"
63
+ @click="handlePlayMotion"
64
+ >
65
+ {{ playLoading ? '播放中...' : '播放动作' }}
66
+ </el-button>
67
+ </el-form-item>
68
+ </el-form>
69
+
70
+ <div class="status-tips">
71
+ <el-alert
72
+ v-if="!sdkStatus.canPlayMotion"
73
+ title="请先初始化Avatar后再播放动作"
74
+ type="warning"
75
+ :closable="false"
76
+ show-icon
77
+ />
78
+ <el-alert
79
+ v-else-if="!globalConfig.avatarCode"
80
+ title="请先在全局配置中设置Avatar Code"
81
+ type="warning"
82
+ :closable="false"
83
+ show-icon
84
+ />
85
+ </div>
86
+ </div>
87
+ </el-card>
88
+ </div>
89
+
90
+ <!-- 动作查询 -->
91
+ <div class="api-controls" style="margin-top: 20px;">
92
+ <el-card shadow="hover" class="control-card">
93
+ <template #header>
94
+ <div class="card-header">
95
+ <span>🔍 动作查询</span>
96
+ <el-tag v-if="sdkStatus.canGetMotion" type="success" size="default">可用</el-tag>
97
+ <el-tag v-else type="info" size="default">不可用</el-tag>
98
+ </div>
99
+ </template>
100
+
101
+ <div class="control-content">
102
+ <el-form size="default">
103
+ <el-form-item>
104
+ <el-checkbox
105
+ v-model="queryData.getRemainingTime"
106
+ :disabled="!sdkStatus.canGetMotion"
107
+ >
108
+ 获取剩余播放时间
109
+ </el-checkbox>
110
+ </el-form-item>
111
+
112
+ <el-form-item class="action-buttons">
113
+ <el-button
114
+ type="primary"
115
+ icon="search"
116
+ :loading="queryLoading"
117
+ :disabled="!sdkStatus.canGetMotion"
118
+ @click="handleGetCurrentMotion"
119
+ >
120
+ {{ queryLoading ? '查询中...' : '获取当前动作' }}
121
+ </el-button>
122
+ </el-form-item>
123
+ </el-form>
124
+
125
+ <div class="status-tips">
126
+ <el-alert
127
+ v-if="!sdkStatus.canGetMotion"
128
+ title="请先初始化Avatar后再查询动作"
129
+ type="warning"
130
+ :closable="false"
131
+ show-icon
132
+ />
133
+ </div>
134
+ </div>
135
+ </el-card>
136
+ </div>
137
+
138
+ <!-- 执行结果 -->
139
+ <div class="api-result" style="margin-top: 20px;">
140
+ <el-card shadow="hover" class="result-card">
141
+ <template #header>
142
+ <div class="card-header">
143
+ <span>执行结果</span>
144
+ <el-link
145
+ size="default"
146
+ icon="refresh"
147
+ @click="clearResult"
148
+ >
149
+ 清空
150
+ </el-link>
151
+ </div>
152
+ </template>
153
+
154
+ <div class="result-content">
155
+ <pre class="result-text">{{ result }}</pre>
156
+ </div>
157
+ </el-card>
158
+ </div>
159
+ </div>
160
+ </template>
161
+
162
+ <script setup lang="ts">
163
+ import { ref, reactive } from 'vue';
164
+ import { ElMessage } from 'element-plus';
165
+
166
+ interface ISDKStatus {
167
+ canPlayMotion: boolean;
168
+ canGetMotion: boolean;
169
+ }
170
+ interface IGlobalConfig {
171
+ avatarCode: string;
172
+ }
173
+
174
+ const props = defineProps<{
175
+ sdkStatus: ISDKStatus;
176
+ globalConfig: IGlobalConfig;
177
+ }>();
178
+ const emit = defineEmits<{
179
+ (e: 'play-motion', params: { clipCode: string }): void;
180
+ (e: 'get-current-motion', params: { getRemainingTime: boolean }): void;
181
+ }>();
182
+
183
+ const motionData = reactive({
184
+ clipCode: 'DH_ACTION_MODEL103_000023',
185
+ });
186
+ const queryData = reactive({
187
+ getRemainingTime: true,
188
+ });
189
+ const result = ref(
190
+ '等待动作操作...\n\n📋 功能说明:\n- 播放动作:输入动作编码来播放指定动作\n- 查询动作:获取当前正在播放的动作信息\n- 剩余时间:可选择查询动作剩余播放时间\n\n'
191
+ );
192
+ const playLoading = ref(false);
193
+ const queryLoading = ref(false);
194
+
195
+ async function handlePlayMotion() {
196
+ const { avatarCode } = props.globalConfig;
197
+ const { clipCode } = motionData;
198
+ if (!avatarCode) {
199
+ result.value = '❌ 请先在全局配置中设置Avatar Code';
200
+ ElMessage.warning('请先设置Avatar Code');
201
+ return;
202
+ }
203
+ if (!clipCode) {
204
+ result.value = '❌ 请输入动作编码';
205
+ ElMessage.warning('请输入动作编码');
206
+ return;
207
+ }
208
+ playLoading.value = true;
209
+ result.value = `🔄 正在播放动作...\n🎭 动作编码: ${clipCode}\n🤖 数字人: ${avatarCode}\n\n📡 发送动作播放指令...`;
210
+ try {
211
+ emit('play-motion', { clipCode });
212
+ setTimeout(() => {
213
+ playLoading.value = false;
214
+ result.value = '✅ 动作播放请求已发送\n📋 请查看右侧日志面板获取详细信息\n\n📝 操作说明:\n- 动作播放成功会返回动作ID\n- 不同动作有不同的播放时长\n- 播放过程中可以播放新动作覆盖\n- 可以使用查询功能获取当前动作状态';
215
+ }, 800);
216
+ } catch (error: any) {
217
+ playLoading.value = false;
218
+ result.value = `❌ 动作播放失败: ${error.message}\n\n🔍 可能的原因:\n- 动作编码不存在\n- Avatar未正确初始化\n- Unity通信异常\n- 动作文件损坏`;
219
+ ElMessage.error(`动作播放失败: ${error.message}`);
220
+ }
221
+ }
222
+
223
+ async function handleGetCurrentMotion() {
224
+ if (!props.sdkStatus.canGetMotion) {
225
+ ElMessage.warning('请先初始化Avatar');
226
+ return;
227
+ }
228
+ queryLoading.value = true;
229
+ result.value = `🔄 正在获取当前动作信息...\n📊 查询参数: ${queryData.getRemainingTime ? '包含剩余时间' : '仅获取动作编码'}\n\n📡 发送查询请求...`;
230
+ try {
231
+ emit('get-current-motion', { getRemainingTime: queryData.getRemainingTime });
232
+ setTimeout(() => {
233
+ queryLoading.value = false;
234
+ result.value = '✅ 动作查询请求已发送\n📋 请查看右侧日志面板获取详细信息\n\n📝 查询结果说明:\n- 如果没有播放动作,返回值为空\n- 动作ID格式通常为 MOTION_XXX\n- 剩余时间以毫秒为单位\n- 查询不会影响当前播放状态';
235
+ }, 500);
236
+ } catch (error: any) {
237
+ queryLoading.value = false;
238
+ result.value = `❌ 动作查询失败: ${error.message}\n\n🔍 可能的原因:\n- Avatar未正确初始化\n- Unity通信异常\n- 内部状态错误`;
239
+ ElMessage.error(`动作查询失败: ${error.message}`);
240
+ }
241
+ }
242
+
243
+ function clearResult() {
244
+ result.value = '等待动作操作...\n\n📋 功能说明:\n- 播放动作:输入动作编码来播放指定动作\n- 查询动作:获取当前正在播放的动作信息\n- 剩余时间:可选择查询动作剩余播放时间\n\n🎭 ';
245
+ }
246
+ </script>
247
+
248
+ <style lang="scss" scoped>
249
+ .motion-control-api {
250
+ .api-description {
251
+ margin-bottom: 20px;
252
+ .api-signatures {
253
+ .api-signature {
254
+ background-color: #f5f5f5;
255
+ padding: 8px 12px;
256
+ border-radius: 4px;
257
+ margin-bottom: 8px;
258
+ font-family: Monaco, Menlo, "Ubuntu Mono", monospace;
259
+ font-size: 12px;
260
+ border-left: 4px solid #f56c6c;
261
+ &:last-child {
262
+ margin-bottom: 10px;
263
+ }
264
+ code {
265
+ color: #2c3e50;
266
+ font-weight: 600;
267
+ }
268
+ }
269
+ }
270
+ p {
271
+ margin: 5px 0;
272
+ color: #606266;
273
+ font-size: 14px;
274
+ &.api-features {
275
+ color: #f56c6c;
276
+ font-weight: 500;
277
+ margin-top: 8px;
278
+ }
279
+ }
280
+ }
281
+ .api-controls {
282
+ .control-card {
283
+ .card-header {
284
+ display: flex;
285
+ justify-content: space-between;
286
+ align-items: center;
287
+ font-weight: 600;
288
+ }
289
+ .control-content {
290
+ .el-form-item {
291
+ margin-bottom: 18px;
292
+ :deep(.el-form-item__label) {
293
+ font-weight: 500;
294
+ color: #303133;
295
+ }
296
+ &.action-buttons {
297
+ margin-bottom: 0;
298
+ margin-top: 25px;
299
+ .el-button {
300
+ width: 100%;
301
+ padding: 10px 0;
302
+ font-weight: 500;
303
+ }
304
+ }
305
+ .input-help {
306
+ margin-top: 5px;
307
+ .help-text {
308
+ font-size: 12px;
309
+ color: #909399;
310
+ line-height: 1.4;
311
+ }
312
+ }
313
+ }
314
+ .status-tips {
315
+ margin-top: 15px;
316
+ .el-alert {
317
+ margin-bottom: 10px;
318
+ &:last-child {
319
+ margin-bottom: 0;
320
+ }
321
+ }
322
+ }
323
+ }
324
+ }
325
+ }
326
+ .api-result {
327
+ .result-card {
328
+ .card-header {
329
+ display: flex;
330
+ justify-content: space-between;
331
+ align-items: center;
332
+ font-weight: 600;
333
+ }
334
+ .result-content {
335
+ max-height: 300px;
336
+ overflow-y: auto;
337
+ .result-text {
338
+ margin: 0;
339
+ padding: 15px;
340
+ background-color: #f8f9fa;
341
+ border-radius: 4px;
342
+ border: 1px solid #e9ecef;
343
+ font-family: Monaco, Menlo, "Ubuntu Mono", monospace;
344
+ font-size: 13px;
345
+ line-height: 1.6;
346
+ color: #2c3e50;
347
+ white-space: pre-wrap;
348
+ word-wrap: break-word;
349
+ }
350
+ }
351
+ }
352
+ }
353
+ }
354
+
355
+ // 移动端优化
356
+ @media screen and (width <= 768px) {
357
+ .motion-control-api {
358
+ .api-controls .control-card .control-content {
359
+ .el-form-item {
360
+ .input-help {
361
+ .help-text {
362
+ font-size: 11px;
363
+ }
364
+ }
365
+ &.action-buttons .el-button {
366
+ padding: 8px 0;
367
+ font-size: 13px;
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+ </style>