From 39fff8c63cf2aa77e1904599a830269de1f820d6 Mon Sep 17 00:00:00 2001
From: wuxichen <17301714657@163.com>
Date: Wed, 3 Sep 2025 15:06:16 +0800
Subject: [PATCH] init
---
.env | 1 +
.gitignore | 1 +
.vscode/settings.json | 3 +
LICENSE | 201 ++
README.md | 170 +
index.html | 39 +
package.json | 50 +
pnpm-lock.yaml | 2906 +++++++++++++++++
postcss.config.js | 13 +
public/null.svg | 0
src/api/mock.ts | 37 +
src/assets/translate/cat.svg | 64 +
src/assets/translate/dog.svg | 42 +
src/assets/translate/microphone.svg | 6 +
.../translate/microphoneDisabledSvg.svg | 4 +
src/assets/translate/pig.svg | 49 +
src/assets/translate/playing.svg | 24 +
src/component/audioRecorder/archives.tsx | 5 +
.../audioRecorder/deviceCompatibility.tsx | 139 +
src/component/audioRecorder/index copy.tsx | 278 ++
src/component/audioRecorder/index.less | 774 +++++
src/component/audioRecorder/index.tsx | 275 ++
.../audioRecorder/recordingStatusBar.tsx | 54 +
src/component/audioRecorder/translateItem.tsx | 11 +
src/component/audioRecorder/voiceMessage.tsx | 322 ++
.../audioRecorder/voiceRecordButton.less | 350 ++
.../audioRecorder/voiceRecordButton.tsx | 248 ++
src/component/carousel/index.less | 7 +
src/component/carousel/index.tsx | 49 +
src/component/floatingMenu/index.less | 42 +
src/component/floatingMenu/index.tsx | 196 ++
.../petVoiceTranslator copy/index.less | 662 ++++
.../petVoiceTranslator copy/index.tsx | 916 ++++++
src/component/petVoiceTranslator/index.less | 660 ++++
src/component/petVoiceTranslator/index.tsx | 1104 +++++++
src/component/qr-scanner/index.tsx | 88 +
src/component/voiceIcon/index.less | 69 +
src/component/voiceIcon/index.tsx | 22 +
src/component/xpopup/index.less | 11 +
src/component/xpopup/index.tsx | 28 +
src/composables/authorization.ts | 42 +
src/composables/language.ts | 21 +
src/enum/http-enum.ts | 21 +
src/hooks/i18n.ts | 12 +
src/hooks/location.ts | 60 +
src/hooks/session.ts | 28 +
src/hooks/useAudioControl.ts | 55 +
src/hooks/useFileUpload.ts | 198 ++
src/hooks/usePetTranslator.ts | 153 +
src/hooks/useVoiceRecorder.ts | 125 +
src/http/axios-instance.ts | 49 +
src/index.css | 103 +
src/layout/main/index.less | 20 +
src/layout/main/mainLayout.tsx | 89 +
src/locales/en_US.json | 3 +
src/locales/zh_CN.json | 3 +
src/main.tsx | 13 +
src/route/auth.tsx | 29 +
src/route/render-routes.tsx | 27 +
src/route/routes.tsx | 19 +
src/store/i18n.ts | 21 +
src/store/user.ts | 22 +
src/types/chat.ts | 41 +
src/types/http.d.ts | 14 +
src/types/store.d.ts | 19 +
src/types/upload.ts | 34 +
src/utils/amount.ts | 3 +
src/utils/audioManager.ts | 208 ++
src/utils/audioPlayer.ts | 246 ++
src/utils/audioRecorder.ts | 362 ++
src/utils/index.ts | 2 +
src/utils/js-audio-recorder.d.ts | 22 +
src/utils/location.ts | 3 +
src/utils/voice.ts | 186 ++
src/view/app/App.css | 0
src/view/app/App.tsx | 24 +
src/view/archives/index.less | 0
src/view/archives/index.tsx | 14 +
src/view/error/page404.tsx | 18 +
src/view/home/component/index.less | 0
src/view/home/component/message/index.less | 46 +
src/view/home/component/message/index.tsx | 176 +
src/view/home/component/search/index.less | 22 +
src/view/home/component/search/index.tsx | 87 +
src/view/home/component/voice/index.less | 54 +
src/view/home/component/voice/index.tsx | 277 ++
src/view/home/detail.tsx | 5 +
src/view/home/index.less | 39 +
src/view/home/index.tsx | 31 +
src/view/home/translate.tsx | 154 +
src/view/home/types.ts | 12 +
src/view/setting/index.tsx | 13 +
src/vite-env.d.ts | 9 +
tsconfig.json | 30 +
tsconfig.node.json | 10 +
vite.config.ts | 21 +
96 files changed, 13215 insertions(+)
create mode 100644 .env
create mode 100644 .gitignore
create mode 100644 .vscode/settings.json
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 index.html
create mode 100644 package.json
create mode 100644 pnpm-lock.yaml
create mode 100644 postcss.config.js
create mode 100644 public/null.svg
create mode 100644 src/api/mock.ts
create mode 100644 src/assets/translate/cat.svg
create mode 100644 src/assets/translate/dog.svg
create mode 100644 src/assets/translate/microphone.svg
create mode 100644 src/assets/translate/microphoneDisabledSvg.svg
create mode 100644 src/assets/translate/pig.svg
create mode 100644 src/assets/translate/playing.svg
create mode 100644 src/component/audioRecorder/archives.tsx
create mode 100644 src/component/audioRecorder/deviceCompatibility.tsx
create mode 100644 src/component/audioRecorder/index copy.tsx
create mode 100644 src/component/audioRecorder/index.less
create mode 100644 src/component/audioRecorder/index.tsx
create mode 100644 src/component/audioRecorder/recordingStatusBar.tsx
create mode 100644 src/component/audioRecorder/translateItem.tsx
create mode 100644 src/component/audioRecorder/voiceMessage.tsx
create mode 100644 src/component/audioRecorder/voiceRecordButton.less
create mode 100644 src/component/audioRecorder/voiceRecordButton.tsx
create mode 100644 src/component/carousel/index.less
create mode 100644 src/component/carousel/index.tsx
create mode 100644 src/component/floatingMenu/index.less
create mode 100644 src/component/floatingMenu/index.tsx
create mode 100644 src/component/petVoiceTranslator copy/index.less
create mode 100644 src/component/petVoiceTranslator copy/index.tsx
create mode 100644 src/component/petVoiceTranslator/index.less
create mode 100644 src/component/petVoiceTranslator/index.tsx
create mode 100644 src/component/qr-scanner/index.tsx
create mode 100644 src/component/voiceIcon/index.less
create mode 100644 src/component/voiceIcon/index.tsx
create mode 100644 src/component/xpopup/index.less
create mode 100644 src/component/xpopup/index.tsx
create mode 100644 src/composables/authorization.ts
create mode 100644 src/composables/language.ts
create mode 100644 src/enum/http-enum.ts
create mode 100644 src/hooks/i18n.ts
create mode 100644 src/hooks/location.ts
create mode 100644 src/hooks/session.ts
create mode 100644 src/hooks/useAudioControl.ts
create mode 100644 src/hooks/useFileUpload.ts
create mode 100644 src/hooks/usePetTranslator.ts
create mode 100644 src/hooks/useVoiceRecorder.ts
create mode 100644 src/http/axios-instance.ts
create mode 100644 src/index.css
create mode 100644 src/layout/main/index.less
create mode 100644 src/layout/main/mainLayout.tsx
create mode 100644 src/locales/en_US.json
create mode 100644 src/locales/zh_CN.json
create mode 100644 src/main.tsx
create mode 100644 src/route/auth.tsx
create mode 100644 src/route/render-routes.tsx
create mode 100644 src/route/routes.tsx
create mode 100644 src/store/i18n.ts
create mode 100644 src/store/user.ts
create mode 100644 src/types/chat.ts
create mode 100644 src/types/http.d.ts
create mode 100644 src/types/store.d.ts
create mode 100644 src/types/upload.ts
create mode 100644 src/utils/amount.ts
create mode 100644 src/utils/audioManager.ts
create mode 100644 src/utils/audioPlayer.ts
create mode 100644 src/utils/audioRecorder.ts
create mode 100644 src/utils/index.ts
create mode 100644 src/utils/js-audio-recorder.d.ts
create mode 100644 src/utils/location.ts
create mode 100644 src/utils/voice.ts
create mode 100644 src/view/app/App.css
create mode 100644 src/view/app/App.tsx
create mode 100644 src/view/archives/index.less
create mode 100644 src/view/archives/index.tsx
create mode 100644 src/view/error/page404.tsx
create mode 100644 src/view/home/component/index.less
create mode 100644 src/view/home/component/message/index.less
create mode 100644 src/view/home/component/message/index.tsx
create mode 100644 src/view/home/component/search/index.less
create mode 100644 src/view/home/component/search/index.tsx
create mode 100644 src/view/home/component/voice/index.less
create mode 100644 src/view/home/component/voice/index.tsx
create mode 100644 src/view/home/detail.tsx
create mode 100644 src/view/home/index.less
create mode 100644 src/view/home/index.tsx
create mode 100644 src/view/home/translate.tsx
create mode 100644 src/view/home/types.ts
create mode 100644 src/view/setting/index.tsx
create mode 100644 src/vite-env.d.ts
create mode 100644 tsconfig.json
create mode 100644 tsconfig.node.json
create mode 100644 vite.config.ts
diff --git a/.env b/.env
new file mode 100644
index 0000000..38aee11
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+HTTPS=true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..44aeb40
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "typescript.tsdk": "node_modules\\typescript\\lib"
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..be30227
--- /dev/null
+++ b/README.md
@@ -0,0 +1,170 @@
+
+
+
Antd Mobile H5 Template
+
+
+ Vite + React + Antd Mobile 组件库的H5初始化模版(脚手架)
+
+
+
+
+## 功能
+1. 使用了`postcss`实现`px to rem`
+2. 使用`zustand`作为全局缓存库,并依赖其功能实现了基本的`i18n`国际化
+3. 配置了`icon-park`,即字节旗下`ico`库
+4. 对`react-router-dom`进行了功能封装
+5. 使用`Ant Design Mobile`与`Layout`实现了带有`Tabbar`的公共模版
+6. 使用`axios-hooks`库作为请求库,并对`axios`进行少量封装
+7. 自定义`hooks`: `useLocation`、`useI18n`、`useSessionStorage`
+8. 基于`js-qr`实现了浏览器扫码功能组件`QRScanner`
+
+## 说明
+1. 配置`axios`的基地址直接修改`env`配置字段`VITE_BASE_URL`
+2. 全局根字体大小断点(`src/index.css`)
+
+```html
+ html {
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ font-size: 100%;
+ }
+
+ /* 小屏幕设备(如手机)*/
+ @media (max-width: 600px) {
+ html {
+ font-size: 90%; /* 字体稍微小一点 */
+ }
+ }
+
+ /* 中等屏幕设备(如平板)*/
+ @media (min-width: 601px) and (max-width: 1024px) {
+ html {
+ font-size: 100%; /* 标准大小 */
+ }
+ }
+
+ /* 大屏幕设备(如桌面)*/
+ @media (min-width: 1025px) {
+ html {
+ font-size: 110%; /* 字体稍微大一点 */
+ }
+ }
+```
+
+3. 组件库全局配色(`src/index.css`)
+
+```html
+ :root {
+ --primary-color: #FFC300;
+ }
+
+ :root:root {
+ --adm-color-primary: #FFC300;
+ --adm-color-success: #00b578;
+ --adm-color-warning: #ff8f1f;
+ --adm-color-danger: #ff3141;
+
+ --adm-color-white: #ffffff;
+ --adm-color-text: #333333;
+ --adm-color-text-secondary: #666666;
+ --adm-color-weak: #999999;
+ --adm-color-light: #cccccc;
+ --adm-color-border: #eeeeee;
+ --adm-color-box: #f5f5f5;
+ --adm-color-background: #ffffff;
+
+ --adm-font-size-main: var(--adm-font-size-5);
+
+ --adm-font-family: -apple-system, blinkmacsystemfont, 'Helvetica Neue',
+ helvetica, segoe ui, arial, roboto, 'PingFang SC', 'miui',
+ 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
+ }
+```
+4. 修改语言
+
+```js
+const i18nStore = useI18nStore()
+i18nStore.changeLanguage('en_US')
+i18nStore.changeLanguage('zh_CN')
+```
+其他语言可自行扩展,在`App.tsx`中,使用`useI18nStore`hooks同步修改了组件库的国际化配置
+```jsx
+const i18nStore = useI18nStore()
+ return (
+ <>
+
+
+
+ >
+ )
+```
+5. 使用i18n
+
+```js
+const t = useI18n();
+
+t('index.title"')
+```
+国际化文字映射在`src/locales`文件夹下
+
+6. 发送请求
+
+首先在`src/api`中新增请求文件`xxx.ts`,并定义返回值与参数的`ts interface`,利用 `axios-hooks`发送对应的`http`请求
+```js
+import useAxios from 'axios-hooks';
+import {Page, Result} from "@/types/http";
+
+export interface MockResult {
+ id: number;
+}
+
+export interface MockPage {
+ id: number;
+}
+
+/**
+ * fetch the data
+ * 详细使用可以查看 useAxios 的文档
+ */
+export const useFetchXXX = () => {
+ // set the url
+ const url = `/xxx/xxx`;
+ // fetch the data
+ const [{data, loading, error}, refetch] = useAxios>(url);
+ // to do something
+ return {data, loading, error, refetch};
+}
+
+
+/**
+ * fetch the data with page
+ * 详细使用可以查看 useAxios 的文档
+ */
+export const useFetchPageXXX = (page: number, size: number) => {
+ // set the url
+ const url = `/xxx/xxx?page=${page}&size=${size}`;
+ // fetch the data
+ const [{data, loading, error}, refetch] = useAxios>(url);
+ // to do something
+ return {data, loading, error, refetch};
+}
+```
+`useAxios`返回值分别为`数据`、`状态`、`错误`、`操作对象`(`refetch`用于中断请求)
+
+7. 路由与缓存配置可直接参考代码
+
+## 补充
+简单的封装,方便构建新项目时直接复用,没有复杂的操作,如果你想使用`react`构建一个`h5`或者是`响应式`的`web`项目,可以直接使用这个模版。
+
+
+```shell
+git clone git@github.com:JanYork/react-h5-template.git
+
+cd react-h5-template
+
+pnpm i
+
+pnpm run dev
+```
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..237e845
--- /dev/null
+++ b/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ Antd Mobile H5 Templet
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8bf8314
--- /dev/null
+++ b/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "ec-web-h5",
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "dev:https": "vite --https",
+ "build": "tsc && vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@icon-park/react": "^1.4.2",
+ "@types/node": "^20.10.0",
+ "@types/react-router-dom": "^5.3.3",
+ "@uidotdev/usehooks": "^2.4.1",
+ "@vitejs/plugin-basic-ssl": "^2.1.0",
+ "antd-mobile": "^5.33.0",
+ "antd-mobile-icons": "^0.3.0",
+ "axios": "^1.6.2",
+ "axios-hooks": "^5.0.2",
+ "js-audio-recorder": "^1.0.7",
+ "jsqr": "^1.4.0",
+ "less": "^4.2.0",
+ "query-string": "^8.1.0",
+ "react": "^18.2.0",
+ "react-audio-voice-recorder": "^2.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.20.0",
+ "react-slick": "^0.29.0",
+ "slick-carousel": "^1.8.1",
+ "weixin-js-sdk": "^1.6.5",
+ "zustand": "^4.4.6"
+ },
+ "devDependencies": {
+ "@types/lodash.isequal": "^4.5.8",
+ "@types/react": "^18.3.24",
+ "@types/react-dom": "^18.3.7",
+ "@types/react-slick": "^0.23.12",
+ "@typescript-eslint/eslint-plugin": "^6.10.0",
+ "@typescript-eslint/parser": "^6.10.0",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "eslint": "^9.34.0",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.20",
+ "postcss-pxtorem": "^6.0.0",
+ "typescript": "^5.2.2",
+ "vite": "^5.0.0"
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..9a8419a
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,2906 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@icon-park/react':
+ specifier: ^1.4.2
+ version: 1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@types/node':
+ specifier: ^20.10.0
+ version: 20.19.11
+ '@types/react-router-dom':
+ specifier: ^5.3.3
+ version: 5.3.3
+ '@uidotdev/usehooks':
+ specifier: ^2.4.1
+ version: 2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@vitejs/plugin-basic-ssl':
+ specifier: ^2.1.0
+ version: 2.1.0(vite@5.4.19(@types/node@20.19.11)(less@4.4.1))
+ antd-mobile:
+ specifier: ^5.33.0
+ version: 5.39.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ antd-mobile-icons:
+ specifier: ^0.3.0
+ version: 0.3.0
+ axios:
+ specifier: ^1.6.2
+ version: 1.11.0
+ axios-hooks:
+ specifier: ^5.0.2
+ version: 5.1.1(axios@1.11.0)(react@18.3.1)
+ js-audio-recorder:
+ specifier: ^1.0.7
+ version: 1.0.7
+ jsqr:
+ specifier: ^1.4.0
+ version: 1.4.0
+ less:
+ specifier: ^4.2.0
+ version: 4.4.1
+ query-string:
+ specifier: ^8.1.0
+ version: 8.2.0
+ react:
+ specifier: ^18.2.0
+ version: 18.3.1
+ react-audio-voice-recorder:
+ specifier: ^2.2.0
+ version: 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.3.1(react@18.3.1)
+ react-router-dom:
+ specifier: ^6.20.0
+ version: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react-slick:
+ specifier: ^0.29.0
+ version: 0.29.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ slick-carousel:
+ specifier: ^1.8.1
+ version: 1.8.1(jquery@3.7.1)
+ weixin-js-sdk:
+ specifier: ^1.6.5
+ version: 1.6.5
+ zustand:
+ specifier: ^4.4.6
+ version: 4.5.7(@types/react@18.3.24)(react@18.3.1)
+ devDependencies:
+ '@types/lodash.isequal':
+ specifier: ^4.5.8
+ version: 4.5.8
+ '@types/react':
+ specifier: ^18.3.24
+ version: 18.3.24
+ '@types/react-dom':
+ specifier: ^18.3.7
+ version: 18.3.7(@types/react@18.3.24)
+ '@types/react-slick':
+ specifier: ^0.23.12
+ version: 0.23.13
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^6.10.0
+ version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.34.0)(typescript@5.9.2))(eslint@9.34.0)(typescript@5.9.2)
+ '@typescript-eslint/parser':
+ specifier: ^6.10.0
+ version: 6.21.0(eslint@9.34.0)(typescript@5.9.2)
+ '@vitejs/plugin-react-swc':
+ specifier: ^3.5.0
+ version: 3.11.0(vite@5.4.19(@types/node@20.19.11)(less@4.4.1))
+ eslint:
+ specifier: ^9.34.0
+ version: 9.34.0
+ eslint-plugin-react-hooks:
+ specifier: ^5.2.0
+ version: 5.2.0(eslint@9.34.0)
+ eslint-plugin-react-refresh:
+ specifier: ^0.4.20
+ version: 0.4.20(eslint@9.34.0)
+ postcss-pxtorem:
+ specifier: ^6.0.0
+ version: 6.1.0(postcss@8.5.6)
+ typescript:
+ specifier: ^5.2.2
+ version: 5.9.2
+ vite:
+ specifier: ^5.0.0
+ version: 5.4.19(@types/node@20.19.11)(less@4.4.1)
+
+packages:
+
+ '@babel/runtime@7.26.10':
+ resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/runtime@7.28.3':
+ resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
+ engines: {node: '>=6.9.0'}
+
+ '@esbuild/aix-ppc64@0.21.5':
+ resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.21.5':
+ resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.21.5':
+ resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.21.5':
+ resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.21.5':
+ resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.21.5':
+ resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.21.5':
+ resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.21.5':
+ resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.21.5':
+ resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.21.5':
+ resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.21.5':
+ resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.21.5':
+ resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.21.5':
+ resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.21.5':
+ resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.21.5':
+ resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.21.5':
+ resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.21.5':
+ resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-x64@0.21.5':
+ resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-x64@0.21.5':
+ resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/sunos-x64@0.21.5':
+ resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.21.5':
+ resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.21.5':
+ resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.21.5':
+ resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.7.0':
+ resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.1':
+ resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.21.0':
+ resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/config-helpers@0.3.1':
+ resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.15.2':
+ resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/eslintrc@3.3.1':
+ resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/js@9.34.0':
+ resolution: {integrity: sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/object-schema@2.1.6':
+ resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/plugin-kit@0.3.5':
+ resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@ffmpeg/ffmpeg@0.11.6':
+ resolution: {integrity: sha512-uN8J8KDjADEavPhNva6tYO9Fj0lWs9z82swF3YXnTxWMBoFLGq3LZ6FLlIldRKEzhOBKnkVfA8UnFJuvGvNxcA==}
+ engines: {node: '>=12.16.1'}
+
+ '@floating-ui/core@1.7.3':
+ resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
+
+ '@floating-ui/dom@1.7.4':
+ resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
+
+ '@floating-ui/utils@0.2.10':
+ resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+
+ '@humanfs/core@0.19.1':
+ resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.6':
+ resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.3.1':
+ resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
+ engines: {node: '>=18.18'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@icon-park/react@1.4.2':
+ resolution: {integrity: sha512-+MtQLjNiRuia3fC/NfpSCTIy5KH5b+NkMB9zYd7p3R4aAIK61AjK0OSraaICJdkKooU9jpzk8m0fY4g9A3JqhQ==}
+ engines: {node: '>= 8.0.0', npm: '>= 5.0.0'}
+ peerDependencies:
+ react: '>=16.9'
+ react-dom: '>=16.9'
+
+ '@nodelib/fs.scandir@2.1.5':
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.stat@2.0.5':
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.walk@1.2.8':
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+
+ '@rc-component/mini-decimal@1.1.0':
+ resolution: {integrity: sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==}
+ engines: {node: '>=8.x'}
+
+ '@react-spring/animated@9.6.1':
+ resolution: {integrity: sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+
+ '@react-spring/core@9.6.1':
+ resolution: {integrity: sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+
+ '@react-spring/rafz@9.6.1':
+ resolution: {integrity: sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==}
+
+ '@react-spring/shared@9.6.1':
+ resolution: {integrity: sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+
+ '@react-spring/types@9.6.1':
+ resolution: {integrity: sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==}
+
+ '@react-spring/web@9.6.1':
+ resolution: {integrity: sha512-X2zR6q2Z+FjsWfGAmAXlQaoUHbPmfuCaXpuM6TcwXPpLE1ZD4A1eys/wpXboFQmDkjnrlTmKvpVna1MjWpZ5Hw==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+
+ '@remix-run/router@1.23.0':
+ resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==}
+ engines: {node: '>=14.0.0'}
+
+ '@rolldown/pluginutils@1.0.0-beta.27':
+ resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
+
+ '@rollup/rollup-android-arm-eabi@4.50.0':
+ resolution: {integrity: sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.50.0':
+ resolution: {integrity: sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.50.0':
+ resolution: {integrity: sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.50.0':
+ resolution: {integrity: sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.50.0':
+ resolution: {integrity: sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.50.0':
+ resolution: {integrity: sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.50.0':
+ resolution: {integrity: sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.50.0':
+ resolution: {integrity: sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-arm64-gnu@4.50.0':
+ resolution: {integrity: sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm64-musl@4.50.0':
+ resolution: {integrity: sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.50.0':
+ resolution: {integrity: sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.50.0':
+ resolution: {integrity: sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.50.0':
+ resolution: {integrity: sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-riscv64-musl@4.50.0':
+ resolution: {integrity: sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-s390x-gnu@4.50.0':
+ resolution: {integrity: sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-gnu@4.50.0':
+ resolution: {integrity: sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-musl@4.50.0':
+ resolution: {integrity: sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-openharmony-arm64@4.50.0':
+ resolution: {integrity: sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.50.0':
+ resolution: {integrity: sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.50.0':
+ resolution: {integrity: sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.50.0':
+ resolution: {integrity: sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@swc/core-darwin-arm64@1.13.5':
+ resolution: {integrity: sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@swc/core-darwin-x64@1.13.5':
+ resolution: {integrity: sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@swc/core-linux-arm-gnueabihf@1.13.5':
+ resolution: {integrity: sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==}
+ engines: {node: '>=10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@swc/core-linux-arm64-gnu@1.13.5':
+ resolution: {integrity: sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@swc/core-linux-arm64-musl@1.13.5':
+ resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@swc/core-linux-x64-gnu@1.13.5':
+ resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@swc/core-linux-x64-musl@1.13.5':
+ resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@swc/core-win32-arm64-msvc@1.13.5':
+ resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@swc/core-win32-ia32-msvc@1.13.5':
+ resolution: {integrity: sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==}
+ engines: {node: '>=10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@swc/core-win32-x64-msvc@1.13.5':
+ resolution: {integrity: sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@swc/core@1.13.5':
+ resolution: {integrity: sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@swc/helpers': '>=0.5.17'
+ peerDependenciesMeta:
+ '@swc/helpers':
+ optional: true
+
+ '@swc/counter@0.1.3':
+ resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
+
+ '@swc/types@0.1.24':
+ resolution: {integrity: sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@types/history@4.7.11':
+ resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==}
+
+ '@types/js-cookie@3.0.6':
+ resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/lodash.isequal@4.5.8':
+ resolution: {integrity: sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==}
+
+ '@types/lodash@4.17.20':
+ resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
+
+ '@types/node@20.19.11':
+ resolution: {integrity: sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==}
+
+ '@types/prop-types@15.7.15':
+ resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
+
+ '@types/react-dom@18.3.7':
+ resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
+ peerDependencies:
+ '@types/react': ^18.0.0
+
+ '@types/react-router-dom@5.3.3':
+ resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==}
+
+ '@types/react-router@5.1.20':
+ resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
+
+ '@types/react-slick@0.23.13':
+ resolution: {integrity: sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==}
+
+ '@types/react@18.3.24':
+ resolution: {integrity: sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==}
+
+ '@types/semver@7.7.0':
+ resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==}
+
+ '@typescript-eslint/eslint-plugin@6.21.0':
+ resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
+ eslint: ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@typescript-eslint/parser@6.21.0':
+ resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@typescript-eslint/scope-manager@6.21.0':
+ resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+
+ '@typescript-eslint/type-utils@6.21.0':
+ resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@typescript-eslint/types@6.21.0':
+ resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+
+ '@typescript-eslint/typescript-estree@6.21.0':
+ resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@typescript-eslint/utils@6.21.0':
+ resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+
+ '@typescript-eslint/visitor-keys@6.21.0':
+ resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+
+ '@uidotdev/usehooks@2.4.1':
+ resolution: {integrity: sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==}
+ engines: {node: '>=16'}
+ peerDependencies:
+ react: '>=18.0.0'
+ react-dom: '>=18.0.0'
+
+ '@use-gesture/core@10.3.0':
+ resolution: {integrity: sha512-rh+6MND31zfHcy9VU3dOZCqGY511lvGcfyJenN4cWZe0u1BH6brBpBddLVXhF2r4BMqWbvxfsbL7D287thJU2A==}
+
+ '@use-gesture/react@10.3.0':
+ resolution: {integrity: sha512-3zc+Ve99z4usVP6l9knYVbVnZgfqhKah7sIG+PS2w+vpig2v2OLct05vs+ZXMzwxdNCMka8B+8WlOo0z6Pn6DA==}
+ peerDependencies:
+ react: '>= 16.8.0'
+
+ '@vitejs/plugin-basic-ssl@2.1.0':
+ resolution: {integrity: sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ peerDependencies:
+ vite: ^6.0.0 || ^7.0.0
+
+ '@vitejs/plugin-react-swc@3.11.0':
+ resolution: {integrity: sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==}
+ peerDependencies:
+ vite: ^4 || ^5 || ^6 || ^7
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.15.0:
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ahooks@3.9.5:
+ resolution: {integrity: sha512-TrjXie49Q8HuHKTa84Fm9A+famMDAG1+7a9S9Gq6RQ0h90Jgqmiq3CkObuRjWT/C4d6nRZCw35Y2k2fmybb5eA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ antd-mobile-icons@0.3.0:
+ resolution: {integrity: sha512-rqINQpJWZWrva9moCd1Ye695MZYWmqLPE+bY8d2xLRy7iSQwPsinCdZYjpUPp2zL/LnKYSyXxP2ut2A+DC+whQ==}
+
+ antd-mobile-v5-count@1.0.1:
+ resolution: {integrity: sha512-YGsiEDCPUDz3SzfXi6gLZn/HpeSMW+jgPc4qiYUr1fSopg3hkUie2TnooJdExgfiETHefH3Ggs58He0OVfegLA==}
+
+ antd-mobile@5.39.0:
+ resolution: {integrity: sha512-x0cr1KYcYEOzLzD8r5S3NYtViTxTkHSh8krjM5q6RxphjabvEFQTZuf3i7gJzICprirJ4GO/F7K3m8qldCiEjw==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ array-union@2.1.0:
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+ engines: {node: '>=8'}
+
+ async-validator@4.2.5:
+ resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
+
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ axios-hooks@5.1.1:
+ resolution: {integrity: sha512-ti27vL2ttZUdOoBSwLzR63sW5zu0cC12jgOOw3PPhG6D8wajt4wfi9t9xUVpvK+6zHawcS2rHo9fKDTiOzOlgg==}
+ peerDependencies:
+ axios: '>=1.0.0'
+ react: ^16.8.0-0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ axios@1.11.0:
+ resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ brace-expansion@1.1.12:
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+ brace-expansion@2.0.2:
+ resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ classnames@2.5.1:
+ resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ copy-anything@2.0.6:
+ resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+ dayjs@1.11.18:
+ resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==}
+
+ debug@4.4.1:
+ resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ decode-uri-component@0.4.1:
+ resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==}
+ engines: {node: '>=14.16'}
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ dir-glob@3.0.1:
+ resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
+ engines: {node: '>=8'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ enquire.js@2.1.6:
+ resolution: {integrity: sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==}
+
+ errno@0.1.8:
+ resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
+ hasBin: true
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
+ esbuild@0.21.5:
+ resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
+ engines: {node: '>=12'}
+ hasBin: true
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-plugin-react-hooks@5.2.0:
+ resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+
+ eslint-plugin-react-refresh@0.4.20:
+ resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==}
+ peerDependencies:
+ eslint: '>=8.40'
+
+ eslint-scope@8.4.0:
+ resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.1:
+ resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint@9.34.0:
+ resolution: {integrity: sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.4.0:
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ esquery@1.6.0:
+ resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fastq@1.19.1:
+ resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ filter-obj@5.1.0:
+ resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==}
+ engines: {node: '>=14.16'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.3.3:
+ resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
+ follow-redirects@1.15.11:
+ resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
+ form-data@4.0.4:
+ resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
+ engines: {node: '>= 6'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+
+ globby@11.1.0:
+ resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
+ engines: {node: '>=10'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ graphemer@1.4.0:
+ resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ image-size@0.5.5:
+ resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==}
+ engines: {node: '>=0.10.0'}
+ hasBin: true
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ intersection-observer@0.12.2:
+ resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ is-url@1.2.4:
+ resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
+
+ is-what@3.14.1:
+ resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ jquery@3.7.1:
+ resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==}
+
+ js-audio-recorder@1.0.7:
+ resolution: {integrity: sha512-JiDODCElVHGrFyjGYwYyNi7zCbKk9va9C77w+zCPMmi4C6ix7zsX2h3ddHugmo4dOTOTCym9++b/wVW9nC0IaA==}
+
+ js-cookie@3.0.5:
+ resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
+ engines: {node: '>=14'}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ json2mq@0.2.0:
+ resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==}
+
+ jsqr@1.4.0:
+ resolution: {integrity: sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==}
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ less@4.4.1:
+ resolution: {integrity: sha512-X9HKyiXPi0f/ed0XhgUlBeFfxrlDP3xR4M7768Zl+WXLUViuL9AOPPJP4nCV0tgRWvTYvpNmN0SFhZOQzy16PA==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash.debounce@4.0.8:
+ resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ lodash@4.17.21:
+ resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+
+ loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+
+ lru-cache@11.1.0:
+ resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
+ engines: {node: 20 || >=22}
+
+ make-dir@2.1.0:
+ resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
+ engines: {node: '>=6'}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ mime@1.6.0:
+ resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+ minimatch@9.0.3:
+ resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nano-memoize@3.0.16:
+ resolution: {integrity: sha512-JyK96AKVGAwVeMj3MoMhaSXaUNqgMbCRSQB3trUV8tYZfWEzqUBKdK1qJpfuNXgKeHOx1jv/IEYTM659ly7zUA==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ needle@3.3.1:
+ resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==}
+ engines: {node: '>= 4.4.x'}
+ hasBin: true
+
+ node-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ parse-node-version@1.0.1:
+ resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==}
+ engines: {node: '>= 0.10'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ pify@4.0.1:
+ resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
+ engines: {node: '>=6'}
+
+ postcss-pxtorem@6.1.0:
+ resolution: {integrity: sha512-ROODSNci9ADal3zUcPHOF/K83TiCgNSPXQFSbwyPHNV8ioHIE4SaC+FPOufd8jsr5jV2uIz29v1Uqy1c4ov42g==}
+ peerDependencies:
+ postcss: ^8.0.0
+
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+ prr@1.0.1:
+ resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ query-string@8.2.0:
+ resolution: {integrity: sha512-tUZIw8J0CawM5wyGBiDOAp7ObdRQh4uBor/fUR9ZjmbZVvw95OD9If4w3MQxr99rg0DJZ/9CIORcpEqU5hQG7g==}
+ engines: {node: '>=14.16'}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ rc-field-form@1.44.0:
+ resolution: {integrity: sha512-el7w87fyDUsca63Y/s8qJcq9kNkf/J5h+iTdqG5WsSHLH0e6Usl7QuYSmSVzJMgtp40mOVZIY/W/QP9zwrp1FA==}
+ engines: {node: '>=8.x'}
+ peerDependencies:
+ react: '>=16.9.0'
+ react-dom: '>=16.9.0'
+
+ rc-motion@2.9.5:
+ resolution: {integrity: sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==}
+ peerDependencies:
+ react: '>=16.9.0'
+ react-dom: '>=16.9.0'
+
+ rc-segmented@2.4.1:
+ resolution: {integrity: sha512-KUi+JJFdKnumV9iXlm+BJ00O4NdVBp2TEexLCk6bK1x/RH83TvYKQMzIz/7m3UTRPD08RM/8VG/JNjWgWbd4cw==}
+ peerDependencies:
+ react: '>=16.0.0'
+ react-dom: '>=16.0.0'
+
+ rc-util@5.44.4:
+ resolution: {integrity: sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==}
+ peerDependencies:
+ react: '>=16.9.0'
+ react-dom: '>=16.9.0'
+
+ react-audio-visualize@1.2.0:
+ resolution: {integrity: sha512-rfO5nmT0fp23gjU0y2WQT6+ZOq2ZsuPTMphchwX1PCz1Di4oaIr6x7JZII8MLrbHdG7UB0OHfGONTIsWdh67kQ==}
+ peerDependencies:
+ react: '>=16.2.0'
+ react-dom: '>=16.2.0'
+
+ react-audio-voice-recorder@2.2.0:
+ resolution: {integrity: sha512-Hq+143Zs99vJojT/uFvtpxUuiIKoLbMhxhA7qgxe5v8hNXrh5/qTnvYP92hFaE5V+GyoCXlESONa0ufk7t5kHQ==}
+ peerDependencies:
+ react: '>=16.2.0'
+ react-dom: '>=16.2.0'
+
+ react-dom@18.3.1:
+ resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
+ peerDependencies:
+ react: ^18.3.1
+
+ react-fast-compare@3.2.2:
+ resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
+
+ react-is@18.3.1:
+ resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+
+ react-router-dom@6.30.1:
+ resolution: {integrity: sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ react: '>=16.8'
+ react-dom: '>=16.8'
+
+ react-router@6.30.1:
+ resolution: {integrity: sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ react: '>=16.8'
+
+ react-slick@0.29.0:
+ resolution: {integrity: sha512-TGdOKE+ZkJHHeC4aaoH85m8RnFyWqdqRfAGkhd6dirmATXMZWAxOpTLmw2Ll/jPTQ3eEG7ercFr/sbzdeYCJXA==}
+ peerDependencies:
+ react: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0
+
+ react@18.3.1:
+ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
+ engines: {node: '>=0.10.0'}
+
+ regenerator-runtime@0.13.11:
+ resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
+
+ regenerator-runtime@0.14.1:
+ resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+
+ resize-observer-polyfill@1.5.1:
+ resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ resolve-url@0.2.1:
+ resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==}
+ deprecated: https://github.com/lydell/resolve-url#deprecated
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rollup@4.50.0:
+ resolution: {integrity: sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ runes2@1.1.4:
+ resolution: {integrity: sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g==}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ sax@1.4.1:
+ resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
+
+ scheduler@0.23.2:
+ resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
+
+ screenfull@5.2.0:
+ resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
+ engines: {node: '>=0.10.0'}
+
+ semver@5.7.2:
+ resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
+ hasBin: true
+
+ semver@7.7.2:
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ slash@3.0.0:
+ resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
+ engines: {node: '>=8'}
+
+ slick-carousel@1.8.1:
+ resolution: {integrity: sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==}
+ peerDependencies:
+ jquery: '>=1.8.0'
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ split-on-first@3.0.0:
+ resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==}
+ engines: {node: '>=12'}
+
+ staged-components@1.1.3:
+ resolution: {integrity: sha512-9EIswzDqjwlEu+ymkV09TTlJfzSbKgEnNteUnZSTxkpMgr5Wx2CzzA9WcMFWBNCldqVPsHVnRGGrApduq2Se5A==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+
+ string-convert@0.2.1:
+ resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
+ ts-api-utils@1.4.3:
+ resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
+ engines: {node: '>=16'}
+ peerDependencies:
+ typescript: '>=4.2.0'
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typescript@5.9.2:
+ resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@6.21.0:
+ resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ use-sync-external-store@1.5.0:
+ resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ vite@5.4.19:
+ resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || >=20.0.0
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+
+ webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
+ weixin-js-sdk@1.6.5:
+ resolution: {integrity: sha512-Gph1WAWB2YN/lMOFB/ymb+hbU/wYazzJgu6PMMktCy9cSCeW5wA6Zwt0dpahJbJ+RJEwtTv2x9iIu0U4enuVSQ==}
+
+ whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ zustand@4.5.7:
+ resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
+ engines: {node: '>=12.7.0'}
+ peerDependencies:
+ '@types/react': '>=16.8'
+ immer: '>=9.0.6'
+ react: '>=16.8'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+
+snapshots:
+
+ '@babel/runtime@7.26.10':
+ dependencies:
+ regenerator-runtime: 0.14.1
+
+ '@babel/runtime@7.28.3': {}
+
+ '@esbuild/aix-ppc64@0.21.5':
+ optional: true
+
+ '@esbuild/android-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/android-arm@0.21.5':
+ optional: true
+
+ '@esbuild/android-x64@0.21.5':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/darwin-x64@0.21.5':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-arm@0.21.5':
+ optional: true
+
+ '@esbuild/linux-ia32@0.21.5':
+ optional: true
+
+ '@esbuild/linux-loong64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.21.5':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-s390x@0.21.5':
+ optional: true
+
+ '@esbuild/linux-x64@0.21.5':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.21.5':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.21.5':
+ optional: true
+
+ '@esbuild/sunos-x64@0.21.5':
+ optional: true
+
+ '@esbuild/win32-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/win32-ia32@0.21.5':
+ optional: true
+
+ '@esbuild/win32-x64@0.21.5':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.7.0(eslint@9.34.0)':
+ dependencies:
+ eslint: 9.34.0
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.1': {}
+
+ '@eslint/config-array@0.21.0':
+ dependencies:
+ '@eslint/object-schema': 2.1.6
+ debug: 4.4.1
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.3.1': {}
+
+ '@eslint/core@0.15.2':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/eslintrc@3.3.1':
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.4.1
+ espree: 10.4.0
+ globals: 14.0.0
+ ignore: 5.3.2
+ import-fresh: 3.3.1
+ js-yaml: 4.1.0
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@9.34.0': {}
+
+ '@eslint/object-schema@2.1.6': {}
+
+ '@eslint/plugin-kit@0.3.5':
+ dependencies:
+ '@eslint/core': 0.15.2
+ levn: 0.4.1
+
+ '@ffmpeg/ffmpeg@0.11.6':
+ dependencies:
+ is-url: 1.2.4
+ node-fetch: 2.7.0
+ regenerator-runtime: 0.13.11
+ resolve-url: 0.2.1
+ transitivePeerDependencies:
+ - encoding
+
+ '@floating-ui/core@1.7.3':
+ dependencies:
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/dom@1.7.4':
+ dependencies:
+ '@floating-ui/core': 1.7.3
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/utils@0.2.10': {}
+
+ '@humanfs/core@0.19.1': {}
+
+ '@humanfs/node@0.16.6':
+ dependencies:
+ '@humanfs/core': 0.19.1
+ '@humanwhocodes/retry': 0.3.1
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.3.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@icon-park/react@1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.19.1
+
+ '@rc-component/mini-decimal@1.1.0':
+ dependencies:
+ '@babel/runtime': 7.28.3
+
+ '@react-spring/animated@9.6.1(react@18.3.1)':
+ dependencies:
+ '@react-spring/shared': 9.6.1(react@18.3.1)
+ '@react-spring/types': 9.6.1
+ react: 18.3.1
+
+ '@react-spring/core@9.6.1(react@18.3.1)':
+ dependencies:
+ '@react-spring/animated': 9.6.1(react@18.3.1)
+ '@react-spring/rafz': 9.6.1
+ '@react-spring/shared': 9.6.1(react@18.3.1)
+ '@react-spring/types': 9.6.1
+ react: 18.3.1
+
+ '@react-spring/rafz@9.6.1': {}
+
+ '@react-spring/shared@9.6.1(react@18.3.1)':
+ dependencies:
+ '@react-spring/rafz': 9.6.1
+ '@react-spring/types': 9.6.1
+ react: 18.3.1
+
+ '@react-spring/types@9.6.1': {}
+
+ '@react-spring/web@9.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@react-spring/animated': 9.6.1(react@18.3.1)
+ '@react-spring/core': 9.6.1(react@18.3.1)
+ '@react-spring/shared': 9.6.1(react@18.3.1)
+ '@react-spring/types': 9.6.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ '@remix-run/router@1.23.0': {}
+
+ '@rolldown/pluginutils@1.0.0-beta.27': {}
+
+ '@rollup/rollup-android-arm-eabi@4.50.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.50.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.50.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.50.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.50.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.50.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.50.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.50.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.50.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.50.0':
+ optional: true
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.50.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.50.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.50.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.50.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.50.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.50.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.50.0':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.50.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.50.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.50.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.50.0':
+ optional: true
+
+ '@swc/core-darwin-arm64@1.13.5':
+ optional: true
+
+ '@swc/core-darwin-x64@1.13.5':
+ optional: true
+
+ '@swc/core-linux-arm-gnueabihf@1.13.5':
+ optional: true
+
+ '@swc/core-linux-arm64-gnu@1.13.5':
+ optional: true
+
+ '@swc/core-linux-arm64-musl@1.13.5':
+ optional: true
+
+ '@swc/core-linux-x64-gnu@1.13.5':
+ optional: true
+
+ '@swc/core-linux-x64-musl@1.13.5':
+ optional: true
+
+ '@swc/core-win32-arm64-msvc@1.13.5':
+ optional: true
+
+ '@swc/core-win32-ia32-msvc@1.13.5':
+ optional: true
+
+ '@swc/core-win32-x64-msvc@1.13.5':
+ optional: true
+
+ '@swc/core@1.13.5':
+ dependencies:
+ '@swc/counter': 0.1.3
+ '@swc/types': 0.1.24
+ optionalDependencies:
+ '@swc/core-darwin-arm64': 1.13.5
+ '@swc/core-darwin-x64': 1.13.5
+ '@swc/core-linux-arm-gnueabihf': 1.13.5
+ '@swc/core-linux-arm64-gnu': 1.13.5
+ '@swc/core-linux-arm64-musl': 1.13.5
+ '@swc/core-linux-x64-gnu': 1.13.5
+ '@swc/core-linux-x64-musl': 1.13.5
+ '@swc/core-win32-arm64-msvc': 1.13.5
+ '@swc/core-win32-ia32-msvc': 1.13.5
+ '@swc/core-win32-x64-msvc': 1.13.5
+
+ '@swc/counter@0.1.3': {}
+
+ '@swc/types@0.1.24':
+ dependencies:
+ '@swc/counter': 0.1.3
+
+ '@types/estree@1.0.8': {}
+
+ '@types/history@4.7.11': {}
+
+ '@types/js-cookie@3.0.6': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/lodash.isequal@4.5.8':
+ dependencies:
+ '@types/lodash': 4.17.20
+
+ '@types/lodash@4.17.20': {}
+
+ '@types/node@20.19.11':
+ dependencies:
+ undici-types: 6.21.0
+
+ '@types/prop-types@15.7.15': {}
+
+ '@types/react-dom@18.3.7(@types/react@18.3.24)':
+ dependencies:
+ '@types/react': 18.3.24
+
+ '@types/react-router-dom@5.3.3':
+ dependencies:
+ '@types/history': 4.7.11
+ '@types/react': 18.3.24
+ '@types/react-router': 5.1.20
+
+ '@types/react-router@5.1.20':
+ dependencies:
+ '@types/history': 4.7.11
+ '@types/react': 18.3.24
+
+ '@types/react-slick@0.23.13':
+ dependencies:
+ '@types/react': 18.3.24
+
+ '@types/react@18.3.24':
+ dependencies:
+ '@types/prop-types': 15.7.15
+ csstype: 3.1.3
+
+ '@types/semver@7.7.0': {}
+
+ '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.34.0)(typescript@5.9.2))(eslint@9.34.0)(typescript@5.9.2)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.1
+ '@typescript-eslint/parser': 6.21.0(eslint@9.34.0)(typescript@5.9.2)
+ '@typescript-eslint/scope-manager': 6.21.0
+ '@typescript-eslint/type-utils': 6.21.0(eslint@9.34.0)(typescript@5.9.2)
+ '@typescript-eslint/utils': 6.21.0(eslint@9.34.0)(typescript@5.9.2)
+ '@typescript-eslint/visitor-keys': 6.21.0
+ debug: 4.4.1
+ eslint: 9.34.0
+ graphemer: 1.4.0
+ ignore: 5.3.2
+ natural-compare: 1.4.0
+ semver: 7.7.2
+ ts-api-utils: 1.4.3(typescript@5.9.2)
+ optionalDependencies:
+ typescript: 5.9.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@6.21.0(eslint@9.34.0)(typescript@5.9.2)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 6.21.0
+ '@typescript-eslint/types': 6.21.0
+ '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.2)
+ '@typescript-eslint/visitor-keys': 6.21.0
+ debug: 4.4.1
+ eslint: 9.34.0
+ optionalDependencies:
+ typescript: 5.9.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@6.21.0':
+ dependencies:
+ '@typescript-eslint/types': 6.21.0
+ '@typescript-eslint/visitor-keys': 6.21.0
+
+ '@typescript-eslint/type-utils@6.21.0(eslint@9.34.0)(typescript@5.9.2)':
+ dependencies:
+ '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.2)
+ '@typescript-eslint/utils': 6.21.0(eslint@9.34.0)(typescript@5.9.2)
+ debug: 4.4.1
+ eslint: 9.34.0
+ ts-api-utils: 1.4.3(typescript@5.9.2)
+ optionalDependencies:
+ typescript: 5.9.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@6.21.0': {}
+
+ '@typescript-eslint/typescript-estree@6.21.0(typescript@5.9.2)':
+ dependencies:
+ '@typescript-eslint/types': 6.21.0
+ '@typescript-eslint/visitor-keys': 6.21.0
+ debug: 4.4.1
+ globby: 11.1.0
+ is-glob: 4.0.3
+ minimatch: 9.0.3
+ semver: 7.7.2
+ ts-api-utils: 1.4.3(typescript@5.9.2)
+ optionalDependencies:
+ typescript: 5.9.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@6.21.0(eslint@9.34.0)(typescript@5.9.2)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0)
+ '@types/json-schema': 7.0.15
+ '@types/semver': 7.7.0
+ '@typescript-eslint/scope-manager': 6.21.0
+ '@typescript-eslint/types': 6.21.0
+ '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.2)
+ eslint: 9.34.0
+ semver: 7.7.2
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@typescript-eslint/visitor-keys@6.21.0':
+ dependencies:
+ '@typescript-eslint/types': 6.21.0
+ eslint-visitor-keys: 3.4.3
+
+ '@uidotdev/usehooks@2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ '@use-gesture/core@10.3.0': {}
+
+ '@use-gesture/react@10.3.0(react@18.3.1)':
+ dependencies:
+ '@use-gesture/core': 10.3.0
+ react: 18.3.1
+
+ '@vitejs/plugin-basic-ssl@2.1.0(vite@5.4.19(@types/node@20.19.11)(less@4.4.1))':
+ dependencies:
+ vite: 5.4.19(@types/node@20.19.11)(less@4.4.1)
+
+ '@vitejs/plugin-react-swc@3.11.0(vite@5.4.19(@types/node@20.19.11)(less@4.4.1))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.0-beta.27
+ '@swc/core': 1.13.5
+ vite: 5.4.19(@types/node@20.19.11)(less@4.4.1)
+ transitivePeerDependencies:
+ - '@swc/helpers'
+
+ acorn-jsx@5.3.2(acorn@8.15.0):
+ dependencies:
+ acorn: 8.15.0
+
+ acorn@8.15.0: {}
+
+ ahooks@3.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@babel/runtime': 7.28.3
+ '@types/js-cookie': 3.0.6
+ dayjs: 1.11.18
+ intersection-observer: 0.12.2
+ js-cookie: 3.0.5
+ lodash: 4.17.21
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-fast-compare: 3.2.2
+ resize-observer-polyfill: 1.5.1
+ screenfull: 5.2.0
+ tslib: 2.8.1
+
+ ajv@6.12.6:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ antd-mobile-icons@0.3.0: {}
+
+ antd-mobile-v5-count@1.0.1: {}
+
+ antd-mobile@5.39.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@floating-ui/dom': 1.7.4
+ '@rc-component/mini-decimal': 1.1.0
+ '@react-spring/web': 9.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@use-gesture/react': 10.3.0(react@18.3.1)
+ ahooks: 3.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ antd-mobile-icons: 0.3.0
+ antd-mobile-v5-count: 1.0.1
+ classnames: 2.5.1
+ dayjs: 1.11.18
+ deepmerge: 4.3.1
+ nano-memoize: 3.0.16
+ rc-field-form: 1.44.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ rc-segmented: 2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-fast-compare: 3.2.2
+ react-is: 18.3.1
+ runes2: 1.1.4
+ staged-components: 1.1.3(react@18.3.1)
+ tslib: 2.8.1
+ use-sync-external-store: 1.5.0(react@18.3.1)
+
+ argparse@2.0.1: {}
+
+ array-union@2.1.0: {}
+
+ async-validator@4.2.5: {}
+
+ asynckit@0.4.0: {}
+
+ axios-hooks@5.1.1(axios@1.11.0)(react@18.3.1):
+ dependencies:
+ '@babel/runtime': 7.26.10
+ axios: 1.11.0
+ dequal: 2.0.3
+ lru-cache: 11.1.0
+ react: 18.3.1
+
+ axios@1.11.0:
+ dependencies:
+ follow-redirects: 1.15.11
+ form-data: 4.0.4
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
+ balanced-match@1.0.2: {}
+
+ brace-expansion@1.1.12:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@2.0.2:
+ dependencies:
+ balanced-match: 1.0.2
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ callsites@3.1.0: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ classnames@2.5.1: {}
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ concat-map@0.0.1: {}
+
+ copy-anything@2.0.6:
+ dependencies:
+ is-what: 3.14.1
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ csstype@3.1.3: {}
+
+ dayjs@1.11.18: {}
+
+ debug@4.4.1:
+ dependencies:
+ ms: 2.1.3
+
+ decode-uri-component@0.4.1: {}
+
+ deep-is@0.1.4: {}
+
+ deepmerge@4.3.1: {}
+
+ delayed-stream@1.0.0: {}
+
+ dequal@2.0.3: {}
+
+ dir-glob@3.0.1:
+ dependencies:
+ path-type: 4.0.0
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ enquire.js@2.1.6: {}
+
+ errno@0.1.8:
+ dependencies:
+ prr: 1.0.1
+ optional: true
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ esbuild@0.21.5:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.21.5
+ '@esbuild/android-arm': 0.21.5
+ '@esbuild/android-arm64': 0.21.5
+ '@esbuild/android-x64': 0.21.5
+ '@esbuild/darwin-arm64': 0.21.5
+ '@esbuild/darwin-x64': 0.21.5
+ '@esbuild/freebsd-arm64': 0.21.5
+ '@esbuild/freebsd-x64': 0.21.5
+ '@esbuild/linux-arm': 0.21.5
+ '@esbuild/linux-arm64': 0.21.5
+ '@esbuild/linux-ia32': 0.21.5
+ '@esbuild/linux-loong64': 0.21.5
+ '@esbuild/linux-mips64el': 0.21.5
+ '@esbuild/linux-ppc64': 0.21.5
+ '@esbuild/linux-riscv64': 0.21.5
+ '@esbuild/linux-s390x': 0.21.5
+ '@esbuild/linux-x64': 0.21.5
+ '@esbuild/netbsd-x64': 0.21.5
+ '@esbuild/openbsd-x64': 0.21.5
+ '@esbuild/sunos-x64': 0.21.5
+ '@esbuild/win32-arm64': 0.21.5
+ '@esbuild/win32-ia32': 0.21.5
+ '@esbuild/win32-x64': 0.21.5
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react-hooks@5.2.0(eslint@9.34.0):
+ dependencies:
+ eslint: 9.34.0
+
+ eslint-plugin-react-refresh@0.4.20(eslint@9.34.0):
+ dependencies:
+ eslint: 9.34.0
+
+ eslint-scope@8.4.0:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.1: {}
+
+ eslint@9.34.0:
+ dependencies:
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0)
+ '@eslint-community/regexpp': 4.12.1
+ '@eslint/config-array': 0.21.0
+ '@eslint/config-helpers': 0.3.1
+ '@eslint/core': 0.15.2
+ '@eslint/eslintrc': 3.3.1
+ '@eslint/js': 9.34.0
+ '@eslint/plugin-kit': 0.3.5
+ '@humanfs/node': 0.16.6
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.8
+ '@types/json-schema': 7.0.15
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.1
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.4.0
+ eslint-visitor-keys: 4.2.1
+ espree: 10.4.0
+ esquery: 1.6.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.4.0:
+ dependencies:
+ acorn: 8.15.0
+ acorn-jsx: 5.3.2(acorn@8.15.0)
+ eslint-visitor-keys: 4.2.1
+
+ esquery@1.6.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ esutils@2.0.3: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fastq@1.19.1:
+ dependencies:
+ reusify: 1.1.0
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ filter-obj@5.1.0: {}
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.3.3
+ keyv: 4.5.4
+
+ flatted@3.3.3: {}
+
+ follow-redirects@1.15.11: {}
+
+ form-data@4.0.4:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.2
+ mime-types: 2.1.35
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@14.0.0: {}
+
+ globby@11.1.0:
+ dependencies:
+ array-union: 2.1.0
+ dir-glob: 3.0.1
+ fast-glob: 3.3.3
+ ignore: 5.3.2
+ merge2: 1.4.1
+ slash: 3.0.0
+
+ gopd@1.2.0: {}
+
+ graceful-fs@4.2.11:
+ optional: true
+
+ graphemer@1.4.0: {}
+
+ has-flag@4.0.0: {}
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ hasown@2.0.2:
+ dependencies:
+ function-bind: 1.1.2
+
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+ optional: true
+
+ ignore@5.3.2: {}
+
+ image-size@0.5.5:
+ optional: true
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
+ intersection-observer@0.12.2: {}
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-number@7.0.0: {}
+
+ is-url@1.2.4: {}
+
+ is-what@3.14.1: {}
+
+ isexe@2.0.0: {}
+
+ jquery@3.7.1: {}
+
+ js-audio-recorder@1.0.7: {}
+
+ js-cookie@3.0.5: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.1.0:
+ dependencies:
+ argparse: 2.0.1
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json2mq@0.2.0:
+ dependencies:
+ string-convert: 0.2.1
+
+ jsqr@1.4.0: {}
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ less@4.4.1:
+ dependencies:
+ copy-anything: 2.0.6
+ parse-node-version: 1.0.1
+ tslib: 2.8.1
+ optionalDependencies:
+ errno: 0.1.8
+ graceful-fs: 4.2.11
+ image-size: 0.5.5
+ make-dir: 2.1.0
+ mime: 1.6.0
+ needle: 3.3.1
+ source-map: 0.6.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash.debounce@4.0.8: {}
+
+ lodash.merge@4.6.2: {}
+
+ lodash@4.17.21: {}
+
+ loose-envify@1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
+
+ lru-cache@11.1.0: {}
+
+ make-dir@2.1.0:
+ dependencies:
+ pify: 4.0.1
+ semver: 5.7.2
+ optional: true
+
+ math-intrinsics@1.1.0: {}
+
+ merge2@1.4.1: {}
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ mime@1.6.0:
+ optional: true
+
+ minimatch@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.12
+
+ minimatch@9.0.3:
+ dependencies:
+ brace-expansion: 2.0.2
+
+ ms@2.1.3: {}
+
+ nano-memoize@3.0.16: {}
+
+ nanoid@3.3.11: {}
+
+ natural-compare@1.4.0: {}
+
+ needle@3.3.1:
+ dependencies:
+ iconv-lite: 0.6.3
+ sax: 1.4.1
+ optional: true
+
+ node-fetch@2.7.0:
+ dependencies:
+ whatwg-url: 5.0.0
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ parse-node-version@1.0.1: {}
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ path-type@4.0.0: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1: {}
+
+ pify@4.0.1:
+ optional: true
+
+ postcss-pxtorem@6.1.0(postcss@8.5.6):
+ dependencies:
+ postcss: 8.5.6
+
+ postcss@8.5.6:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ proxy-from-env@1.1.0: {}
+
+ prr@1.0.1:
+ optional: true
+
+ punycode@2.3.1: {}
+
+ query-string@8.2.0:
+ dependencies:
+ decode-uri-component: 0.4.1
+ filter-obj: 5.1.0
+ split-on-first: 3.0.0
+
+ queue-microtask@1.2.3: {}
+
+ rc-field-form@1.44.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@babel/runtime': 7.28.3
+ async-validator: 4.2.5
+ rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ rc-motion@2.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@babel/runtime': 7.28.3
+ classnames: 2.5.1
+ rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ rc-segmented@2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@babel/runtime': 7.28.3
+ classnames: 2.5.1
+ rc-motion: 2.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ rc-util@5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@babel/runtime': 7.28.3
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-is: 18.3.1
+
+ react-audio-visualize@1.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ react-audio-voice-recorder@2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@ffmpeg/ffmpeg': 0.11.6
+ react: 18.3.1
+ react-audio-visualize: 1.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react-dom: 18.3.1(react@18.3.1)
+ transitivePeerDependencies:
+ - encoding
+
+ react-dom@18.3.1(react@18.3.1):
+ dependencies:
+ loose-envify: 1.4.0
+ react: 18.3.1
+ scheduler: 0.23.2
+
+ react-fast-compare@3.2.2: {}
+
+ react-is@18.3.1: {}
+
+ react-router-dom@6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@remix-run/router': 1.23.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-router: 6.30.1(react@18.3.1)
+
+ react-router@6.30.1(react@18.3.1):
+ dependencies:
+ '@remix-run/router': 1.23.0
+ react: 18.3.1
+
+ react-slick@0.29.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ classnames: 2.5.1
+ enquire.js: 2.1.6
+ json2mq: 0.2.0
+ lodash.debounce: 4.0.8
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ resize-observer-polyfill: 1.5.1
+
+ react@18.3.1:
+ dependencies:
+ loose-envify: 1.4.0
+
+ regenerator-runtime@0.13.11: {}
+
+ regenerator-runtime@0.14.1: {}
+
+ resize-observer-polyfill@1.5.1: {}
+
+ resolve-from@4.0.0: {}
+
+ resolve-url@0.2.1: {}
+
+ reusify@1.1.0: {}
+
+ rollup@4.50.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.50.0
+ '@rollup/rollup-android-arm64': 4.50.0
+ '@rollup/rollup-darwin-arm64': 4.50.0
+ '@rollup/rollup-darwin-x64': 4.50.0
+ '@rollup/rollup-freebsd-arm64': 4.50.0
+ '@rollup/rollup-freebsd-x64': 4.50.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.50.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.50.0
+ '@rollup/rollup-linux-arm64-gnu': 4.50.0
+ '@rollup/rollup-linux-arm64-musl': 4.50.0
+ '@rollup/rollup-linux-loongarch64-gnu': 4.50.0
+ '@rollup/rollup-linux-ppc64-gnu': 4.50.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.50.0
+ '@rollup/rollup-linux-riscv64-musl': 4.50.0
+ '@rollup/rollup-linux-s390x-gnu': 4.50.0
+ '@rollup/rollup-linux-x64-gnu': 4.50.0
+ '@rollup/rollup-linux-x64-musl': 4.50.0
+ '@rollup/rollup-openharmony-arm64': 4.50.0
+ '@rollup/rollup-win32-arm64-msvc': 4.50.0
+ '@rollup/rollup-win32-ia32-msvc': 4.50.0
+ '@rollup/rollup-win32-x64-msvc': 4.50.0
+ fsevents: 2.3.3
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ runes2@1.1.4: {}
+
+ safer-buffer@2.1.2:
+ optional: true
+
+ sax@1.4.1:
+ optional: true
+
+ scheduler@0.23.2:
+ dependencies:
+ loose-envify: 1.4.0
+
+ screenfull@5.2.0: {}
+
+ semver@5.7.2:
+ optional: true
+
+ semver@7.7.2: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ slash@3.0.0: {}
+
+ slick-carousel@1.8.1(jquery@3.7.1):
+ dependencies:
+ jquery: 3.7.1
+
+ source-map-js@1.2.1: {}
+
+ source-map@0.6.1:
+ optional: true
+
+ split-on-first@3.0.0: {}
+
+ staged-components@1.1.3(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+
+ string-convert@0.2.1: {}
+
+ strip-json-comments@3.1.1: {}
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ tr46@0.0.3: {}
+
+ ts-api-utils@1.4.3(typescript@5.9.2):
+ dependencies:
+ typescript: 5.9.2
+
+ tslib@2.8.1: {}
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typescript@5.9.2: {}
+
+ undici-types@6.21.0: {}
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ use-sync-external-store@1.5.0(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+
+ vite@5.4.19(@types/node@20.19.11)(less@4.4.1):
+ dependencies:
+ esbuild: 0.21.5
+ postcss: 8.5.6
+ rollup: 4.50.0
+ optionalDependencies:
+ '@types/node': 20.19.11
+ fsevents: 2.3.3
+ less: 4.4.1
+
+ webidl-conversions@3.0.1: {}
+
+ weixin-js-sdk@1.6.5: {}
+
+ whatwg-url@5.0.0:
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ word-wrap@1.2.5: {}
+
+ yocto-queue@0.1.0: {}
+
+ zustand@4.5.7(@types/react@18.3.24)(react@18.3.1):
+ dependencies:
+ use-sync-external-store: 1.5.0(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.24
+ react: 18.3.1
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..fa5e842
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,13 @@
+export default {
+ plugins: {
+ 'postcss-pxtorem': {
+ rootValue: 16,
+ unitPrecision: 5,
+ propList: ['*'],
+ selectorBlackList: [],
+ replace: true,
+ mediaQuery: false,
+ minPixelValue: 0,
+ },
+ },
+};
diff --git a/public/null.svg b/public/null.svg
new file mode 100644
index 0000000..e69de29
diff --git a/src/api/mock.ts b/src/api/mock.ts
new file mode 100644
index 0000000..0e65ac0
--- /dev/null
+++ b/src/api/mock.ts
@@ -0,0 +1,37 @@
+import useAxios from 'axios-hooks';
+import {Page, Result} from "@/types/http";
+
+export interface MockResult {
+ id: number;
+}
+
+export interface MockPage {
+ id: number;
+}
+
+/**
+ * fetch the data
+ * 详细使用可以查看 useAxios 的文档
+ */
+export const useFetchXXX = () => {
+ // set the url
+ const url = `/xxx/xxx`;
+ // fetch the data
+ const [{data, loading, error}, refetch] = useAxios>(url);
+ // to do something
+ return {data, loading, error, refetch};
+}
+
+
+/**
+ * fetch the data with page
+ * 详细使用可以查看 useAxios 的文档
+ */
+export const useFetchPageXXX = (page: number, size: number) => {
+ // set the url
+ const url = `/xxx/xxx?page=${page}&size=${size}`;
+ // fetch the data
+ const [{data, loading, error}, refetch] = useAxios>(url);
+ // to do something
+ return {data, loading, error, refetch};
+}
\ No newline at end of file
diff --git a/src/assets/translate/cat.svg b/src/assets/translate/cat.svg
new file mode 100644
index 0000000..3457da1
--- /dev/null
+++ b/src/assets/translate/cat.svg
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/translate/dog.svg b/src/assets/translate/dog.svg
new file mode 100644
index 0000000..aa4b8b8
--- /dev/null
+++ b/src/assets/translate/dog.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/translate/microphone.svg b/src/assets/translate/microphone.svg
new file mode 100644
index 0000000..6499bd9
--- /dev/null
+++ b/src/assets/translate/microphone.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/assets/translate/microphoneDisabledSvg.svg b/src/assets/translate/microphoneDisabledSvg.svg
new file mode 100644
index 0000000..463ff15
--- /dev/null
+++ b/src/assets/translate/microphoneDisabledSvg.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/assets/translate/pig.svg b/src/assets/translate/pig.svg
new file mode 100644
index 0000000..b1ece1b
--- /dev/null
+++ b/src/assets/translate/pig.svg
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/translate/playing.svg b/src/assets/translate/playing.svg
new file mode 100644
index 0000000..191166a
--- /dev/null
+++ b/src/assets/translate/playing.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/component/audioRecorder/archives.tsx b/src/component/audioRecorder/archives.tsx
new file mode 100644
index 0000000..1a09fbe
--- /dev/null
+++ b/src/component/audioRecorder/archives.tsx
@@ -0,0 +1,5 @@
+function Index() {
+ return <>档案>;
+}
+
+export default Index;
diff --git a/src/component/audioRecorder/deviceCompatibility.tsx b/src/component/audioRecorder/deviceCompatibility.tsx
new file mode 100644
index 0000000..a599eb7
--- /dev/null
+++ b/src/component/audioRecorder/deviceCompatibility.tsx
@@ -0,0 +1,139 @@
+// components/DeviceCompatibility.tsx
+import React, { useEffect, useState } from "react";
+import { UniversalAudioRecorder } from "@/utils/audioRecorder";
+
+interface DeviceInfo {
+ isIOS: boolean;
+ isSafari: boolean;
+ supportedFormats: string[];
+ hasMediaRecorder: boolean;
+ hasGetUserMedia: boolean;
+}
+
+const DeviceCompatibility: React.FC = () => {
+ const [deviceInfo, setDeviceInfo] = useState(null);
+ const [showDetails, setShowDetails] = useState(false);
+
+ useEffect(() => {
+ const checkCompatibility = () => {
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
+ const isSafari = /^((?!chrome|android).)*safari/i.test(
+ navigator.userAgent
+ );
+ const hasMediaRecorder = typeof MediaRecorder !== "undefined";
+ const hasGetUserMedia = !!(
+ navigator.mediaDevices && navigator.mediaDevices.getUserMedia
+ );
+
+ let supportedFormats: string[] = [];
+ if (hasMediaRecorder) {
+ supportedFormats = UniversalAudioRecorder.getSupportedFormats();
+ }
+
+ setDeviceInfo({
+ isIOS,
+ isSafari,
+ supportedFormats,
+ hasMediaRecorder,
+ hasGetUserMedia,
+ });
+ };
+
+ checkCompatibility();
+ }, []);
+
+ if (!deviceInfo) return null;
+
+ const getCompatibilityStatus = (): "good" | "warning" | "error" => {
+ if (!deviceInfo.hasMediaRecorder || !deviceInfo.hasGetUserMedia) {
+ return "error";
+ }
+ if (deviceInfo.supportedFormats.length === 0) {
+ return "warning";
+ }
+ return "good";
+ };
+
+ const status = getCompatibilityStatus();
+
+ return (
+
+
setShowDetails(!showDetails)}
+ >
+
+ {status === "good" && "✅"}
+ {status === "warning" && "⚠️"}
+ {status === "error" && "❌"}
+
+
+ {status === "good" && "设备兼容"}
+ {status === "warning" && "部分兼容"}
+ {status === "error" && "不兼容"}
+
+ {showDetails ? "▼" : "▶"}
+
+
+ {showDetails && (
+
+
+
+ 设备信息:
+
+
+ iOS设备: {deviceInfo.isIOS ? "是" : "否"}
+ Safari浏览器: {deviceInfo.isSafari ? "是" : "否"}
+
+ 支持MediaRecorder: {deviceInfo.hasMediaRecorder ? "是" : "否"}
+
+
+ 支持getUserMedia: {deviceInfo.hasGetUserMedia ? "是" : "否"}
+
+
+
+
+
+
+ 支持的音频格式:
+
+ {deviceInfo.supportedFormats.length > 0 ? (
+
+ {deviceInfo.supportedFormats.map((format, index) => (
+ {format}
+ ))}
+
+ ) : (
+
未检测到支持的格式
+ )}
+
+
+ {status === "error" && (
+
+
+ 错误: 您的设备不支持录音功能
+
+
请尝试:
+
+ 使用最新版本的浏览器
+ 确保在HTTPS环境下访问
+ 检查浏览器权限设置
+
+
+ )}
+
+ {status === "warning" && (
+
+
+ 警告: 录音功能可能不稳定
+
+
建议使用Chrome、Safari或Firefox最新版本
+
+ )}
+
+ )}
+
+ );
+};
+
+export default DeviceCompatibility;
diff --git a/src/component/audioRecorder/index copy.tsx b/src/component/audioRecorder/index copy.tsx
new file mode 100644
index 0000000..ad1c8ff
--- /dev/null
+++ b/src/component/audioRecorder/index copy.tsx
@@ -0,0 +1,278 @@
+// components/PetTranslatorChat.tsx (添加音频控制)
+import React, { useRef, useEffect } from "react";
+import { usePetTranslator } from "@/hooks/usePetTranslator";
+import { useFileUpload } from "@/hooks/useFileUpload";
+import { useAudioControl } from "@/hooks/useAudioControl";
+import { VoiceMessage, ChatMessage } from "@/types/chat";
+import { UploadConfig } from "@/types/upload";
+import VoiceMessageComponent from "./voiceMessage";
+import VoiceRecordButton from "./voiceRecordButton";
+import RecordingStatusBar from "./recordingStatusBar";
+import AudioManager from "@/utils/audioManager";
+import { useVoiceRecorder } from "@/hooks/useVoiceRecorder";
+import "./index.less";
+import { CapsuleTabs } from "antd-mobile";
+
+const PetTranslatorChat: React.FC = () => {
+ const {
+ messages,
+ currentPet,
+ translateVoice,
+ addMessage,
+ updateMessage,
+ clearMessages,
+ } = usePetTranslator();
+
+ const { isRecording, isPaused, recordingTime } = useVoiceRecorder();
+ const { uploadFile } = useFileUpload();
+ const { currentPlayingId, stopAllAudio, pauseAllAudio } = useAudioControl();
+ const messagesEndRef = useRef(null);
+
+ // 上传配置
+ const uploadConfig: UploadConfig = {
+ url: "/api/upload/voice",
+ method: "POST",
+ fieldName: "voiceFile",
+ maxFileSize: 10 * 1024 * 1024, // 10MB
+ allowedTypes: [
+ "audio/webm",
+ "audio/mp4",
+ "audio/aac",
+ "audio/wav",
+ "audio/ogg",
+ "audio/mpeg",
+ "audio/x-m4a",
+ ],
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem("token")}`,
+ },
+ };
+
+ // 组件卸载时清理所有音频
+ useEffect(() => {
+ return () => {
+ AudioManager.getInstance().cleanup();
+ };
+ }, []);
+
+ // 开始录音时暂停所有音频播放
+ useEffect(() => {
+ if (isRecording) {
+ pauseAllAudio();
+ }
+ }, [isRecording, pauseAllAudio]);
+
+ const handleRecordComplete = (
+ audioBlob: Blob,
+ duration: number,
+ uploadResponse?: any
+ ) => {
+ const audioUrl = URL.createObjectURL(audioBlob);
+
+ const voiceMessage: VoiceMessage = {
+ id: `voice_${Date.now()}`,
+ type: "voice",
+ content: {
+ duration,
+ url: audioUrl,
+ blob: audioBlob,
+ uploadStatus: uploadResponse ? "success" : undefined,
+ fileId: uploadResponse?.data?.fileId,
+ fileName: uploadResponse?.data?.fileName,
+ serverUrl: uploadResponse?.data?.fileUrl,
+ },
+ sender: "user",
+ timestamp: Date.now(),
+ };
+
+ addMessage(voiceMessage);
+
+ // 自动开始翻译
+ setTimeout(() => {
+ translateVoice(voiceMessage);
+ }, 500);
+ };
+
+ const handleRetryUpload = async (messageId: string) => {
+ const message = messages.find(
+ (msg) => msg.id === messageId
+ ) as VoiceMessage;
+ if (!message || !message.content.blob) return;
+
+ try {
+ // 更新上传状态
+ updateMessage(messageId, {
+ ...message,
+ content: {
+ ...message.content,
+ uploadStatus: "uploading",
+ uploadProgress: 0,
+ },
+ });
+
+ const fileName = `voice_${Date.now()}.wav`;
+ const uploadResponse = await uploadFile(
+ message.content.blob,
+ fileName,
+ uploadConfig
+ );
+
+ // 更新成功状态
+ updateMessage(messageId, {
+ ...message,
+ content: {
+ ...message.content,
+ uploadStatus: "success",
+ fileId: uploadResponse.data?.fileId,
+ fileName: uploadResponse.data?.fileName,
+ serverUrl: uploadResponse.data?.fileUrl,
+ },
+ });
+ } catch (error) {
+ // 更新失败状态
+ updateMessage(messageId, {
+ ...message,
+ content: {
+ ...message.content,
+ uploadStatus: "error",
+ },
+ });
+
+ console.error("重新上传失败:", error);
+ }
+ };
+
+ const handleRecordError = (error: Error) => {
+ alert(error.message);
+ };
+
+ const formatTime = (timestamp: number): string => {
+ return new Date(timestamp).toLocaleTimeString("zh-CN", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ };
+
+ return (
+ <>
+ {/* 录音状态栏 */}
+
+
+ {/* 头部 */}
+ {/*
+
+
{currentPet.avatar}
+
+
{currentPet.name}
+
+ {isRecording
+ ? "正在听你说话..."
+ : currentPlayingId
+ ? "正在播放语音..."
+ : "在线 · 等待翻译"}
+
+
+
*/}
+
+ {/*
+
+ {currentPlayingId && (
+
+ ⏹️
+
+ )}
+
+ {
+ stopAllAudio();
+ clearMessages();
+ }}
+ disabled={isRecording}
+ >
+ 🗑️
+ */}
+ {/*
+
*/}
+
+
+
+ 宠物翻译
+
+
+ 宠物档案
+
+
+
+
+ {/* 消息列表 */}
+
+ {messages.length === 0 ? (
+
+
🐾
+
开始和{currentPet.name}对话吧!
+
+ 点击下方按钮开始录音,我会帮你翻译
+
+
+ ) : (
+ messages.map((message) => (
+
+
+ {message.sender === "pet" && (
+
{currentPet.avatar}
+ )}
+
+
+ {message.type === "voice" ? (
+
translateVoice(message)}
+ onRetryUpload={() => handleRetryUpload(message.id)}
+ />
+ ) : (
+ {message.content}
+ )}
+
+
+ {formatTime(message.timestamp)}
+
+
+
+ {message.sender === "user" && (
+
👤
+ )}
+
+
+ ))
+ )}
+
+
+
+ {/* 输入区域 */}
+
+ >
+ );
+};
+
+export default PetTranslatorChat;
diff --git a/src/component/audioRecorder/index.less b/src/component/audioRecorder/index.less
new file mode 100644
index 0000000..d8a03a5
--- /dev/null
+++ b/src/component/audioRecorder/index.less
@@ -0,0 +1,774 @@
+/* PetTranslatorChat.css */
+.pet-translator-chat {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ max-width: 400px;
+ margin: 0 auto;
+ background: #f5f5f5;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+/* 头部样式 */
+.chat-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ background: #fff;
+ color: rgba(0, 0, 0, 0.25);
+ .lef {
+ display: flex;
+ gap: 12px;
+
+ h2 {
+ font-size: 20px;
+ &.active {
+ color: #000;
+ }
+ }
+ }
+}
+
+.pet-info {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.pet-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background: #f0f0f0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+}
+
+.pet-name {
+ font-weight: 600;
+ color: #333;
+ font-size: 16px;
+}
+
+.pet-status {
+ font-size: 12px;
+ color: #999;
+}
+
+.clear-button {
+ background: none;
+ border: none;
+ font-size: 18px;
+ cursor: pointer;
+ padding: 8px;
+ border-radius: 50%;
+ transition: background 0.3s;
+}
+
+.clear-button:hover {
+ background: #f0f0f0;
+}
+
+/* 消息容器 */
+.messages-container {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+/* 空状态 */
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ text-align: center;
+ color: #999;
+}
+
+.empty-icon {
+ font-size: 48px;
+ margin-bottom: 16px;
+}
+
+.empty-title {
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 8px;
+ color: #666;
+}
+
+.empty-subtitle {
+ font-size: 14px;
+ line-height: 1.4;
+}
+
+/* 消息样式 */
+.message {
+ display: flex;
+ gap: 8px;
+ max-width: 80%;
+}
+
+.message.own {
+ align-self: flex-end;
+ flex-direction: row-reverse;
+}
+
+.message.other {
+ align-self: flex-start;
+}
+
+.avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background: #f0f0f0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ flex-shrink: 0;
+}
+
+.user-avatar {
+ background: #007bff;
+ color: white;
+}
+
+.message-content {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.message-time {
+ font-size: 11px;
+ color: #999;
+ text-align: center;
+}
+
+/* 语音消息样式 */
+.voice-message {
+ background: #fff;
+ border-radius: 18px;
+ padding: 12px 16px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ min-width: 120px;
+}
+
+.voice-message.own {
+ background: #007bff;
+ color: white;
+}
+
+.voice-content {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.play-button {
+ width: 24px;
+ height: 24px;
+ border: none;
+ background: none;
+ cursor: pointer;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: background 0.3s;
+}
+
+.play-button:hover {
+ background: rgba(0, 0, 0, 0.1);
+}
+
+.voice-message.own .play-button:hover {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.waveform {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ height: 20px;
+ flex: 1;
+}
+
+.waveform-bar {
+ width: 3px;
+ background: #ddd;
+ border-radius: 2px;
+ transition: all 0.3s;
+}
+
+.voice-message.own .waveform-bar {
+ background: rgba(255, 255, 255, 0.6);
+}
+
+.waveform-bar.active {
+ background: #007bff;
+}
+
+.voice-message.own .waveform-bar.active {
+ background: white;
+}
+
+.duration {
+ font-size: 12px;
+ color: #666;
+ min-width: 30px;
+ text-align: right;
+}
+
+.voice-message.own .duration {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+/* 翻译部分 */
+.translation-section {
+ margin-top: 8px;
+ padding-top: 8px;
+ border-top: 1px solid #eee;
+}
+
+.translate-button {
+ background: #28a745;
+ color: white;
+ border: none;
+ border-radius: 12px;
+ padding: 6px 12px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: background 0.3s;
+}
+
+.translate-button:hover {
+ background: #218838;
+}
+
+.translating {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #666;
+ font-size: 12px;
+}
+
+.loading-dots {
+ display: flex;
+ gap: 2px;
+}
+
+.loading-dots span {
+ width: 4px;
+ height: 4px;
+ background: #666;
+ border-radius: 50%;
+ animation: loading 1.4s infinite ease-in-out;
+}
+
+.loading-dots span:nth-child(1) {
+ animation-delay: -0.32s;
+}
+.loading-dots span:nth-child(2) {
+ animation-delay: -0.16s;
+}
+
+@keyframes loading {
+ 0%,
+ 80%,
+ 100% {
+ transform: scale(0);
+ }
+ 40% {
+ transform: scale(1);
+ }
+}
+
+.translation-result {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ background: rgba(40, 167, 69, 0.1);
+ padding: 8px;
+ border-radius: 8px;
+ border-left: 3px solid #28a745;
+}
+
+.translation-icon {
+ font-size: 14px;
+}
+
+.translation-text {
+ font-size: 13px;
+ line-height: 1.4;
+ color: #333;
+}
+
+/* 文本消息 */
+.text-message {
+ background: #fff;
+ border-radius: 18px;
+ padding: 12px 16px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ font-size: 14px;
+ line-height: 1.4;
+}
+
+.message.other .text-message {
+ background: #e8f5e8;
+}
+
+/* 输入区域 */
+.input-area {
+ padding: 16px;
+ background: #fff;
+ border-top: 1px solid #e5e5e5;
+}
+
+/* 录音按钮 */
+.voice-record-container {
+ position: relative;
+}
+
+.voice-record-button {
+ width: 100%;
+ height: 50px;
+ background: #007bff;
+ color: white;
+ border: none;
+ border-radius: 25px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: all 0.3s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ user-select: none;
+}
+
+.voice-record-button:hover {
+ background: #0056b3;
+}
+
+.voice-record-button.pressed {
+ background: #dc3545;
+ transform: scale(0.95);
+}
+
+.microphone-icon {
+ font-size: 18px;
+}
+
+.button-text {
+ font-weight: 500;
+}
+
+/* 录音覆盖层 */
+.recording-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.recording-modal {
+ background: white;
+ border-radius: 20px;
+ padding: 40px;
+ text-align: center;
+ max-width: 300px;
+ width: 90%;
+}
+
+.recording-animation {
+ margin-bottom: 20px;
+}
+
+.sound-wave {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 4px;
+ height: 40px;
+}
+
+.wave-bar {
+ width: 4px;
+ background: #007bff;
+ border-radius: 2px;
+ animation: wave 1s infinite ease-in-out;
+}
+
+.wave-bar:nth-child(1) {
+ animation-delay: -0.4s;
+}
+.wave-bar:nth-child(2) {
+ animation-delay: -0.3s;
+}
+.wave-bar:nth-child(3) {
+ animation-delay: -0.2s;
+}
+.wave-bar:nth-child(4) {
+ animation-delay: -0.1s;
+}
+.wave-bar:nth-child(5) {
+ animation-delay: 0s;
+}
+
+@keyframes wave {
+ 0%,
+ 40%,
+ 100% {
+ height: 10px;
+ }
+ 20% {
+ height: 30px;
+ }
+}
+
+.recording-time {
+ font-size: 24px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 16px;
+}
+
+.recording-hint {
+ font-size: 14px;
+ color: #666;
+}
+
+.cancel-hint {
+ color: #dc3545;
+ font-weight: 600;
+}
+
+.normal-hint {
+ color: #666;
+}
+
+/* 响应式 */
+@media (max-width: 480px) {
+ .pet-translator-chat {
+ height: 100vh;
+ }
+
+ .message {
+ max-width: 85%;
+ }
+
+ .recording-modal {
+ padding: 30px 20px;
+ }
+}
+
+/* 上传进度样式 */
+.upload-progress {
+ margin: 12px 0;
+ padding: 8px;
+ background: rgba(0, 123, 255, 0.1);
+ border-radius: 8px;
+ border: 1px solid rgba(0, 123, 255, 0.2);
+}
+
+.upload-status {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+ font-size: 13px;
+ color: #007bff;
+}
+
+.upload-icon {
+ font-size: 14px;
+}
+
+.upload-progress-bar {
+ width: 100%;
+ height: 4px;
+ background: rgba(0, 123, 255, 0.2);
+ border-radius: 2px;
+ overflow: hidden;
+}
+
+.upload-progress-fill {
+ height: 100%;
+ background: #007bff;
+ border-radius: 2px;
+ transition: width 0.3s ease;
+}
+
+/* 语音消息上传状态 */
+.upload-status-section {
+ margin-top: 8px;
+ padding-top: 8px;
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.voice-message.own .upload-status-section {
+ border-top-color: rgba(255, 255, 255, 0.3);
+}
+
+.upload-status-indicator {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ margin-bottom: 4px;
+}
+
+.upload-status-indicator.uploading {
+ color: #007bff;
+}
+
+.upload-status-indicator.success {
+ color: #28a745;
+}
+
+.upload-status-indicator.error {
+ color: #dc3545;
+}
+
+.retry-upload-button {
+ background: #dc3545;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 2px 6px;
+ font-size: 10px;
+ cursor: pointer;
+ margin-left: 4px;
+ transition: background 0.3s;
+}
+
+.retry-upload-button:hover {
+ background: #c82333;
+}
+
+.file-info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ font-size: 10px;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.voice-message:not(.own) .file-info {
+ color: #666;
+}
+
+.file-name,
+.file-size {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+/* 处理中状态 */
+.processing-hint {
+ color: #007bff;
+ font-weight: 600;
+}
+
+.upload-hint {
+ color: #28a745;
+ font-weight: 600;
+}
+
+/* 按钮禁用状态 */
+.control-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none !important;
+}
+
+.voice-start-button:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ transform: none !important;
+}
+
+/* 上传动画 */
+@keyframes uploadPulse {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.6;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+.upload-status-indicator.uploading .upload-icon {
+ animation: uploadPulse 1s infinite;
+}
+
+/* DeviceCompatibility.css */
+.device-compatibility {
+ margin: 10px 0;
+ border-radius: 8px;
+ overflow: hidden;
+ font-size: 12px;
+}
+
+.device-compatibility.good {
+ background: #d4edda;
+ border: 1px solid #c3e6cb;
+}
+
+.device-compatibility.warning {
+ background: #fff3cd;
+ border: 1px solid #ffeaa7;
+}
+
+.device-compatibility.error {
+ background: #f8d7da;
+ border: 1px solid #f5c6cb;
+}
+
+.compatibility-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ cursor: pointer;
+ user-select: none;
+}
+
+.status-icon {
+ font-size: 14px;
+}
+
+.status-text {
+ flex: 1;
+ font-weight: 600;
+}
+
+.toggle-icon {
+ font-size: 10px;
+ transition: transform 0.3s;
+}
+
+.compatibility-details {
+ padding: 12px;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ background: rgba(255, 255, 255, 0.5);
+}
+
+.device-info ul,
+.supported-formats ul {
+ margin: 8px 0;
+ padding-left: 20px;
+}
+
+.device-info li,
+.supported-formats li {
+ margin: 4px 0;
+}
+
+.no-formats {
+ color: #dc3545;
+ font-style: italic;
+}
+
+.error-message,
+.warning-message {
+ margin-top: 12px;
+ padding: 8px;
+ border-radius: 4px;
+}
+
+.error-message {
+ background: rgba(220, 53, 69, 0.1);
+ border-left: 3px solid #dc3545;
+}
+
+.warning-message {
+ background: rgba(255, 193, 7, 0.1);
+ border-left: 3px solid #ffc107;
+}
+
+/* 播放错误样式 */
+.play-error {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 8px;
+ padding: 6px 8px;
+ background: rgba(220, 53, 69, 0.1);
+ border: 1px solid rgba(220, 53, 69, 0.3);
+ border-radius: 6px;
+ font-size: 11px;
+}
+
+.error-icon {
+ color: #dc3545;
+}
+
+.error-text {
+ flex: 1;
+ color: #dc3545;
+}
+
+.retry-play-button {
+ background: #dc3545;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 2px 6px;
+ font-size: 10px;
+ cursor: pointer;
+ transition: background 0.3s;
+}
+
+.retry-play-button:hover {
+ background: #c82333;
+}
+
+.play-button.error {
+ background: rgba(220, 53, 69, 0.1);
+ color: #dc3545;
+}
+
+.play-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* 安卓特定样式优化 */
+@media screen and (max-width: 768px) {
+ .voice-content {
+ min-height: 40px;
+ }
+
+ .play-button {
+ min-width: 32px;
+ min-height: 32px;
+ }
+
+ .waveform {
+ min-height: 24px;
+ }
+}
diff --git a/src/component/audioRecorder/index.tsx b/src/component/audioRecorder/index.tsx
new file mode 100644
index 0000000..cd86b08
--- /dev/null
+++ b/src/component/audioRecorder/index.tsx
@@ -0,0 +1,275 @@
+// components/PetTranslatorChat.tsx (添加音频控制)
+import React, { useRef, useEffect, useState } from "react";
+import { usePetTranslator } from "@/hooks/usePetTranslator";
+import { useFileUpload } from "@/hooks/useFileUpload";
+import { useAudioControl } from "@/hooks/useAudioControl";
+import { VoiceMessage, ChatMessage } from "@/types/chat";
+import { UploadConfig } from "@/types/upload";
+import VoiceMessageComponent from "./voiceMessage";
+import VoiceRecordButton from "./voiceRecordButton";
+import RecordingStatusBar from "./recordingStatusBar";
+import AudioManager from "@/utils/audioManager";
+import { useVoiceRecorder } from "@/hooks/useVoiceRecorder";
+import TranslateItem from "./translateItem";
+import ArchivesItem from "./archives";
+import "./index.less";
+import { CapsuleTabs } from "antd-mobile";
+
+const PetTranslatorChat: React.FC = () => {
+ const {
+ messages,
+ currentPet,
+ translateVoice,
+ addMessage,
+ updateMessage,
+ clearMessages,
+ } = usePetTranslator();
+
+ const { isRecording, isPaused, recordingTime } = useVoiceRecorder();
+ const { uploadFile } = useFileUpload();
+ const { currentPlayingId, stopAllAudio, pauseAllAudio } = useAudioControl();
+ const messagesEndRef = useRef(null);
+ const [tabValue, setTabValue] = useState("translate");
+
+ // 上传配置
+ const uploadConfig: UploadConfig = {
+ url: "/api/upload/voice",
+ method: "POST",
+ fieldName: "voiceFile",
+ maxFileSize: 10 * 1024 * 1024, // 10MB
+ allowedTypes: [
+ "audio/webm",
+ "audio/mp4",
+ "audio/aac",
+ "audio/wav",
+ "audio/ogg",
+ "audio/mpeg",
+ "audio/x-m4a",
+ ],
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem("token")}`,
+ },
+ };
+
+ // 组件卸载时清理所有音频
+ useEffect(() => {
+ return () => {
+ AudioManager.getInstance().cleanup();
+ };
+ }, []);
+
+ // 开始录音时暂停所有音频播放
+ useEffect(() => {
+ if (isRecording) {
+ pauseAllAudio();
+ }
+ }, [isRecording, pauseAllAudio]);
+
+ const handleRecordComplete = (
+ audioBlob: Blob,
+ duration: number,
+ uploadResponse?: any
+ ) => {
+ const audioUrl = URL.createObjectURL(audioBlob);
+
+ const voiceMessage: VoiceMessage = {
+ id: `voice_${Date.now()}`,
+ type: "voice",
+ content: {
+ duration,
+ url: audioUrl,
+ blob: audioBlob,
+ uploadStatus: uploadResponse ? "success" : undefined,
+ fileId: uploadResponse?.data?.fileId,
+ fileName: uploadResponse?.data?.fileName,
+ serverUrl: uploadResponse?.data?.fileUrl,
+ },
+ sender: "user",
+ timestamp: Date.now(),
+ };
+
+ addMessage(voiceMessage);
+
+ // 自动开始翻译
+ setTimeout(() => {
+ translateVoice(voiceMessage);
+ }, 500);
+ };
+
+ const handleRetryUpload = async (messageId: string) => {
+ const message = messages.find(
+ (msg) => msg.id === messageId
+ ) as VoiceMessage;
+ if (!message || !message.content.blob) return;
+
+ try {
+ // 更新上传状态
+ updateMessage(messageId, {
+ ...message,
+ content: {
+ ...message.content,
+ uploadStatus: "uploading",
+ uploadProgress: 0,
+ },
+ });
+
+ const fileName = `voice_${Date.now()}.wav`;
+ const uploadResponse = await uploadFile(
+ message.content.blob,
+ fileName,
+ uploadConfig
+ );
+
+ // 更新成功状态
+ updateMessage(messageId, {
+ ...message,
+ content: {
+ ...message.content,
+ uploadStatus: "success",
+ fileId: uploadResponse.data?.fileId,
+ fileName: uploadResponse.data?.fileName,
+ serverUrl: uploadResponse.data?.fileUrl,
+ },
+ });
+ } catch (error) {
+ // 更新失败状态
+ updateMessage(messageId, {
+ ...message,
+ content: {
+ ...message.content,
+ uploadStatus: "error",
+ },
+ });
+
+ console.error("重新上传失败:", error);
+ }
+ };
+
+ const handleRecordError = (error: Error) => {
+ alert(error.message);
+ };
+
+ const formatTime = (timestamp: number): string => {
+ return new Date(timestamp).toLocaleTimeString("zh-CN", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ };
+
+ return (
+ <>
+ {/* 录音状态栏 */}
+
+
+ {/* 头部 */}
+
+
+
setTabValue("translate")}
+ className={`${tabValue === "translate" ? "active" : ""}`}
+ >
+ 宠物翻译
+
+ setTabValue("archives")}
+ className={`${tabValue === "archives" ? "active" : ""}`}
+ >
+ 宠物档案
+
+
+
+ {/*
+
{currentPet.avatar}
*/}
+ {/*
*/}
+ {/*
{currentPet.name}
*/}
+ {/*
+ {isRecording
+ ? "正在听你说话..."
+ : currentPlayingId
+ ? "正在播放语音..."
+ : "在线 · 等待翻译"}
+
*/}
+ {/*
*/}
+ {/*
*/}
+
+ {/*
+ {currentPlayingId && (
+
+ ⏹️
+
+ )}
+
+ {
+ stopAllAudio();
+ clearMessages();
+ }}
+ disabled={isRecording}
+ >
+ 🗑️
+
+
*/}
+
+ {tabValue == "translate" ? : }
+ {/* 消息列表 */}
+
+ {messages.map((message) => (
+
+
+ {message.sender === "pet" && (
+
{currentPet.avatar}
+ )}
+
+
+ {message.type === "voice" ? (
+
translateVoice(message)}
+ onRetryUpload={() => handleRetryUpload(message.id)}
+ />
+ ) : (
+ {message.content}
+ )}
+
+
+ {formatTime(message.timestamp)}
+
+
+
+ {message.sender === "user" && (
+
👤
+ )}
+
+
+ ))}
+
+
+
+ {/* 输入区域 */}
+
+ >
+ );
+};
+
+export default PetTranslatorChat;
diff --git a/src/component/audioRecorder/recordingStatusBar.tsx b/src/component/audioRecorder/recordingStatusBar.tsx
new file mode 100644
index 0000000..e7d6df0
--- /dev/null
+++ b/src/component/audioRecorder/recordingStatusBar.tsx
@@ -0,0 +1,54 @@
+// components/RecordingStatusBar.tsx
+import React from "react";
+
+interface RecordingStatusBarProps {
+ isRecording: boolean;
+ isPaused: boolean;
+ duration: number;
+ maxDuration: number;
+}
+
+const RecordingStatusBar: React.FC = ({
+ isRecording,
+ isPaused,
+ duration,
+ maxDuration,
+}) => {
+ if (!isRecording) return null;
+
+ const formatTime = (seconds: number): string => {
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins.toString().padStart(2, "0")}:${secs
+ .toString()
+ .padStart(2, "0")}`;
+ };
+
+ const getStatusColor = (): string => {
+ if (isPaused) return "#ffc107";
+ if (duration >= maxDuration * 0.9) return "#dc3545";
+ return "#28a745";
+ };
+
+ return (
+
+
+
+
+
+ {isPaused ? "录音暂停中" : "正在录音"}
+
+
+
+
+ {formatTime(duration)} / {formatTime(maxDuration)}
+
+
+
+ );
+};
+
+export default RecordingStatusBar;
diff --git a/src/component/audioRecorder/translateItem.tsx b/src/component/audioRecorder/translateItem.tsx
new file mode 100644
index 0000000..ec6390b
--- /dev/null
+++ b/src/component/audioRecorder/translateItem.tsx
@@ -0,0 +1,11 @@
+function Index() {
+ return <>
+
+
+
+
+
+ >;
+}
+
+export default Index;
diff --git a/src/component/audioRecorder/voiceMessage.tsx b/src/component/audioRecorder/voiceMessage.tsx
new file mode 100644
index 0000000..3b684d3
--- /dev/null
+++ b/src/component/audioRecorder/voiceMessage.tsx
@@ -0,0 +1,322 @@
+// 消息列表
+import React, { useState, useRef, useEffect } from "react";
+import { VoiceMessage as VoiceMessageType } from "@/types/chat";
+import { UniversalAudioPlayer } from "@/utils/audioPlayer";
+import AudioManager from "@/utils/audioManager";
+
+interface VoiceMessageProps {
+ message: VoiceMessageType;
+ isOwn: boolean;
+ onTranslate?: () => void;
+ onRetryUpload?: () => void;
+}
+
+const VoiceMessage: React.FC = ({
+ message,
+ isOwn,
+ onTranslate,
+ onRetryUpload,
+}) => {
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [isLoading, setIsLoading] = useState(false);
+ const [playError, setPlayError] = useState(null);
+
+ const playerRef = useRef(null);
+ const timerRef = useRef(null);
+ const audioManager = AudioManager.getInstance();
+
+ // 使用消息ID作为音频实例的唯一标识
+ const audioId = `voice_${message.id}`;
+
+ useEffect(() => {
+ return () => {
+ // 组件卸载时清理
+ stopTimer();
+ audioManager.unregisterAudio(audioId);
+ };
+ }, [audioId]);
+
+ const startTimer = () => {
+ timerRef.current = setInterval(() => {
+ if (audioManager.isPlaying(audioId)) {
+ const current = audioManager.getCurrentTime(audioId);
+ setCurrentTime(current);
+ } else {
+ // 播放结束
+ stopTimer();
+ setIsPlaying(false);
+ setCurrentTime(0);
+ }
+ }, 100);
+ };
+
+ const stopTimer = () => {
+ if (timerRef.current) {
+ clearInterval(timerRef.current);
+ timerRef.current = null;
+ }
+ };
+
+ const loadAudio = async (): Promise => {
+ if (playerRef.current) {
+ return; // 已经加载过了
+ }
+
+ setIsLoading(true);
+ setPlayError(null);
+
+ try {
+ const player = new UniversalAudioPlayer();
+
+ // 优先使用blob,其次使用URL
+ let audioBlob: Blob | null = null;
+
+ if (message.content.blob) {
+ audioBlob = message.content.blob;
+ } else if (message.content.url || message.content.serverUrl) {
+ // 从URL获取blob
+ const audioUrl = message.content.serverUrl || message.content.url;
+ const response = await fetch(audioUrl!);
+ audioBlob = await response.blob();
+ }
+
+ if (!audioBlob) {
+ throw new Error("无法获取音频数据");
+ }
+
+ await player.loadAudio(audioBlob);
+ playerRef.current = player;
+
+ // 注册到全局音频管理器
+ audioManager.registerAudio(audioId, player, {
+ onPlay: () => {
+ setIsPlaying(true);
+ startTimer();
+ },
+ onPause: () => {
+ setIsPlaying(false);
+ stopTimer();
+ },
+ onStop: () => {
+ setIsPlaying(false);
+ setCurrentTime(0);
+ stopTimer();
+ },
+ });
+
+ console.log("音频加载成功:", {
+ id: audioId,
+ size: audioBlob.size,
+ type: audioBlob.type,
+ duration: player.getDuration(),
+ });
+ } catch (error) {
+ console.error("音频加载失败:", error);
+ setPlayError(error instanceof Error ? error.message : "音频加载失败");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const togglePlay = async () => {
+ try {
+ if (!playerRef.current) {
+ await loadAudio();
+ if (!playerRef.current) {
+ return;
+ }
+ }
+
+ if (isPlaying) {
+ // 暂停当前音频
+ audioManager.pauseAudio(audioId);
+ } else {
+ // 播放音频(会自动停止其他正在播放的音频)
+ await audioManager.playAudio(audioId);
+ setPlayError(null);
+ }
+ } catch (error) {
+ console.error("播放控制失败:", error);
+ setPlayError(error instanceof Error ? error.message : "播放失败");
+ setIsPlaying(false);
+ stopTimer();
+ }
+ };
+
+ const formatTime = (seconds: number): string => {
+ return `${Math.floor(seconds)}''`;
+ };
+
+ const formatFileSize = (bytes: number): string => {
+ if (bytes === 0) return "0 B";
+ const k = 1024;
+ const sizes = ["B", "KB", "MB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
+ };
+
+ const getWaveformBars = (): JSX.Element[] => {
+ const bars = [];
+ const barCount = Math.min(
+ Math.max(Math.floor(message.content.duration), 3),
+ 20
+ );
+ const duration =
+ audioManager.getDuration(audioId) || message.content.duration;
+
+ for (let i = 0; i < barCount; i++) {
+ const height = Math.random() * 20 + 10;
+ const isActive =
+ isPlaying && duration > 0 && (currentTime / duration) * barCount > i;
+
+ bars.push(
+
+ );
+ }
+
+ return bars;
+ };
+
+ const getUploadStatusIcon = () => {
+ switch (message.content.uploadStatus) {
+ case "uploading":
+ return "📤";
+ case "success":
+ return "✅";
+ case "error":
+ return "❌";
+ default:
+ return null;
+ }
+ };
+
+ const getPlayButtonContent = () => {
+ if (isLoading) return "⏳";
+ if (playError) return "❌";
+ if (isPlaying) return "⏸️";
+ return "▶️";
+ };
+
+ const isPlayDisabled =
+ isLoading ||
+ (!message.content.url &&
+ !message.content.serverUrl &&
+ !message.content.blob);
+
+ return (
+
+
+
+ {getPlayButtonContent()}
+
+
+
{getWaveformBars()}
+
+
+ {formatTime(isPlaying ? currentTime : message.content.duration)}
+
+
+
+ {/* 播放错误提示 */}
+ {playError && (
+
+ ⚠️
+ {playError}
+ {
+ setPlayError(null);
+ audioManager.unregisterAudio(audioId);
+ if (playerRef.current) {
+ playerRef.current.destroy();
+ playerRef.current = null;
+ }
+ }}
+ >
+ 重试
+
+
+ )}
+
+ {/* 上传状态显示 */}
+ {isOwn && message.content.uploadStatus && (
+
+
+ {getUploadStatusIcon()}
+
+ {message.content.uploadStatus === "uploading" &&
+ `上传中 ${message.content.uploadProgress || 0}%`}
+ {message.content.uploadStatus === "success" && "已上传"}
+ {message.content.uploadStatus === "error" && "上传失败"}
+
+
+ {message.content.uploadStatus === "error" && onRetryUpload && (
+
+ 重试
+
+ )}
+
+
+ {/* 文件信息 */}
+ {message.content.uploadStatus === "success" && (
+
+ {message.content.fileName && (
+ 📁 {message.content.fileName}
+ )}
+ {message.content.blob && (
+
+ 📊 {formatFileSize(message.content.blob.size)}
+
+ )}
+
+ )}
+
+ )}
+
+ {/* 翻译按钮和结果 */}
+ {isOwn && (
+
+ {!message.translation && !message.translating && (
+
+ 🐾 翻译
+
+ )}
+
+ {message.translating && (
+
+ )}
+
+ {message.translation && (
+
+
🗣️
+
{message.translation}
+
+ )}
+
+ )}
+
+ );
+};
+
+export default VoiceMessage;
diff --git a/src/component/audioRecorder/voiceRecordButton.less b/src/component/audioRecorder/voiceRecordButton.less
new file mode 100644
index 0000000..46917f2
--- /dev/null
+++ b/src/component/audioRecorder/voiceRecordButton.less
@@ -0,0 +1,350 @@
+/* VoiceRecordButton.css */
+.voice-input-container {
+ padding: 16px;
+ background: #fff;
+ border-top: 1px solid #e5e5e5;
+}
+
+.voice-start-button {
+ width: 100%;
+ height: 50px;
+ background: #007bff;
+ color: white;
+ border: none;
+ border-radius: 25px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ user-select: none;
+ box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
+}
+
+.voice-start-button:hover {
+ background: #0056b3;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 123, 255, 0.4);
+}
+
+.voice-start-button:active {
+ transform: translateY(0);
+}
+
+.microphone-icon {
+ font-size: 20px;
+}
+
+.button-text {
+ font-weight: 600;
+}
+
+/* 录音中的容器 */
+.voice-recording-container {
+ padding: 20px;
+ background: #fff;
+ border-top: 1px solid #e5e5e5;
+ border-radius: 16px 16px 0 0;
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
+}
+
+/* 录音状态 */
+.recording-status {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+}
+
+.recording-indicator {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.recording-dot {
+ width: 12px;
+ height: 12px;
+ background: #dc3545;
+ border-radius: 50%;
+ animation: pulse-recording 1s infinite;
+}
+
+.recording-dot.paused {
+ background: #ffc107;
+ animation: none;
+}
+
+@keyframes pulse-recording {
+ 0% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.2);
+ opacity: 0.7;
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+.recording-text {
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+}
+
+.recording-time {
+ font-size: 18px;
+ font-weight: 700;
+ color: #007bff;
+}
+
+.max-time {
+ font-size: 12px;
+ color: #999;
+ font-weight: 400;
+}
+
+/* 进度条 */
+.recording-progress {
+ width: 100%;
+ height: 4px;
+ background: #e9ecef;
+ border-radius: 2px;
+ margin-bottom: 20px;
+ overflow: hidden;
+}
+
+.progress-bar {
+ height: 100%;
+ background: linear-gradient(90deg, #007bff, #0056b3);
+ border-radius: 2px;
+ transition: width 0.3s ease;
+}
+
+/* 波形动画 */
+.sound-wave-container {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 24px;
+}
+
+.sound-wave {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ height: 40px;
+}
+
+.wave-bar {
+ width: 4px;
+ background: #007bff;
+ border-radius: 2px;
+ animation: wave-animation 1.2s infinite ease-in-out;
+}
+
+.sound-wave.paused .wave-bar {
+ animation-play-state: paused;
+}
+
+.wave-bar:nth-child(1) {
+ animation-delay: -0.4s;
+}
+.wave-bar:nth-child(2) {
+ animation-delay: -0.3s;
+}
+.wave-bar:nth-child(3) {
+ animation-delay: -0.2s;
+}
+.wave-bar:nth-child(4) {
+ animation-delay: -0.1s;
+}
+.wave-bar:nth-child(5) {
+ animation-delay: 0s;
+}
+
+@keyframes wave-animation {
+ 0%,
+ 40%,
+ 100% {
+ height: 8px;
+ opacity: 0.5;
+ }
+ 20% {
+ height: 32px;
+ opacity: 1;
+ }
+}
+
+/* 控制按钮 */
+.recording-controls {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.control-button {
+ flex: 1;
+ height: 48px;
+ border: none;
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ font-size: 12px;
+ font-weight: 600;
+ min-width: 0;
+}
+
+.control-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.control-button:not(:disabled):hover {
+ transform: translateY(-2px);
+}
+
+.control-button:not(:disabled):active {
+ transform: translateY(0);
+}
+
+.cancel-button {
+ background: #f8f9fa;
+ color: #dc3545;
+ border: 2px solid #dc3545;
+}
+
+.cancel-button:not(:disabled):hover {
+ background: #dc3545;
+ color: white;
+}
+
+.pause-button {
+ background: #f8f9fa;
+ color: #ffc107;
+ border: 2px solid #ffc107;
+}
+
+.pause-button:not(:disabled):hover {
+ background: #ffc107;
+ color: white;
+}
+
+.send-button {
+ background: #28a745;
+ color: white;
+ border: 2px solid #28a745;
+}
+
+.send-button:not(:disabled):hover {
+ background: #218838;
+ border-color: #218838;
+}
+
+.button-icon {
+ font-size: 16px;
+}
+
+.button-label {
+ font-size: 11px;
+ line-height: 1;
+}
+
+/* 提示文字 */
+.recording-hint {
+ text-align: center;
+ font-size: 13px;
+ color: #666;
+}
+
+.warning-hint {
+ color: #dc3545;
+ font-weight: 600;
+}
+
+.normal-hint {
+ color: #28a745;
+}
+
+/* 响应式设计 */
+@media (max-width: 480px) {
+ .voice-recording-container {
+ padding: 16px;
+ }
+
+ .recording-controls {
+ gap: 8px;
+ }
+
+ .control-button {
+ height: 44px;
+ font-size: 11px;
+ }
+
+ .button-icon {
+ font-size: 14px;
+ }
+
+ .button-label {
+ font-size: 10px;
+ }
+
+ .recording-time {
+ font-size: 16px;
+ }
+}
+
+/* 动画效果 */
+.voice-recording-container {
+ animation: slideUp 0.3s ease-out;
+}
+
+@keyframes slideUp {
+ from {
+ transform: translateY(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* 深色模式支持 */
+@media (prefers-color-scheme: dark) {
+ .voice-recording-container {
+ background: #2c2c2e;
+ border-top-color: #3a3a3c;
+ }
+
+ .recording-text {
+ color: #fff;
+ }
+
+ .recording-progress {
+ background: #3a3a3c;
+ }
+
+ .wave-bar {
+ background: #0a84ff;
+ }
+
+ .control-button {
+ background: #3a3a3c;
+ }
+
+ .recording-hint {
+ color: #8e8e93;
+ }
+}
diff --git a/src/component/audioRecorder/voiceRecordButton.tsx b/src/component/audioRecorder/voiceRecordButton.tsx
new file mode 100644
index 0000000..451c1d6
--- /dev/null
+++ b/src/component/audioRecorder/voiceRecordButton.tsx
@@ -0,0 +1,248 @@
+// components/VoiceRecordButton.tsx (更新)
+import React, { useState, useEffect } from "react";
+import { useVoiceRecorder } from "@/hooks/useVoiceRecorder";
+import { useFileUpload } from "@/hooks/useFileUpload";
+import { UploadConfig } from "@/types/upload";
+import "./index.less";
+interface VoiceRecordButtonProps {
+ onRecordComplete: (
+ audioBlob: Blob,
+ duration: number,
+ uploadResponse?: any
+ ) => void;
+ onError?: (error: Error) => void;
+ maxDuration?: number;
+ uploadConfig?: UploadConfig;
+ autoUpload?: boolean;
+}
+
+const VoiceRecordButton: React.FC = ({
+ onRecordComplete,
+ onError,
+ maxDuration = 60,
+ uploadConfig,
+ autoUpload = true,
+}) => {
+ const {
+ isRecording,
+ recordingTime,
+ isPaused,
+ startRecording,
+ stopRecording,
+ pauseRecording,
+ resumeRecording,
+ cancelRecording,
+ } = useVoiceRecorder();
+
+ const { uploadStatus, uploadFile, resetUpload } = useFileUpload();
+ const [showControls, setShowControls] = useState(false);
+ const [isProcessing, setIsProcessing] = useState(false);
+
+ // 自动停止录音当达到最大时长
+ useEffect(() => {
+ if (recordingTime >= maxDuration && isRecording) {
+ handleSendRecording();
+ }
+ }, [recordingTime, maxDuration, isRecording]);
+
+ const handleStartRecording = async () => {
+ try {
+ resetUpload();
+ await startRecording();
+ setShowControls(true);
+ } catch (error) {
+ onError?.(error as Error);
+ }
+ };
+
+ const handleSendRecording = async () => {
+ if (recordingTime < 1) {
+ onError?.(new Error("录音时间太短,至少需要1秒"));
+ cancelRecording();
+ setShowControls(false);
+ return;
+ }
+
+ setIsProcessing(true);
+
+ try {
+ const audioBlob = await stopRecording();
+ if (!audioBlob) {
+ throw new Error("录音数据获取失败");
+ }
+
+ let uploadResponse;
+
+ // 如果配置了上传且启用自动上传
+ if (uploadConfig && autoUpload) {
+ const fileName = `voice_${Date.now()}.wav`;
+ uploadResponse = await uploadFile(audioBlob, fileName, uploadConfig);
+ }
+
+ onRecordComplete(audioBlob, recordingTime, uploadResponse);
+ setShowControls(false);
+ } catch (error) {
+ onError?.(error as Error);
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ const handleCancelRecording = () => {
+ cancelRecording();
+ resetUpload();
+ setShowControls(false);
+ };
+
+ const handlePauseResume = () => {
+ if (isPaused) {
+ resumeRecording();
+ } else {
+ pauseRecording();
+ }
+ };
+
+ const formatTime = (seconds: number): string => {
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins.toString().padStart(2, "0")}:${secs
+ .toString()
+ .padStart(2, "0")}`;
+ };
+
+ const getProgressPercentage = (): number => {
+ return Math.min((recordingTime / maxDuration) * 100, 100);
+ };
+
+ const isUploading = uploadStatus.status === "uploading";
+ const uploadProgress = uploadStatus.progress?.percentage || 0;
+
+ if (!showControls) {
+ return (
+
+
+ 🎤
+
+ {isProcessing ? "处理中..." : "点击录音"}
+
+
+
+ );
+ }
+
+ return (
+
+ {/* 录音状态指示器 */}
+
+
+
+
+ {isProcessing
+ ? "处理中..."
+ : isPaused
+ ? "录音已暂停"
+ : "正在录音..."}
+
+
+
+
+ {formatTime(recordingTime)}
+ /{formatTime(maxDuration)}
+
+
+
+ {/* 进度条 */}
+
+
+ {/* 上传进度 */}
+ {isUploading && (
+
+
+ 📤
+ 上传中... {uploadProgress}%
+
+
+
+ )}
+
+ {/* 波形动画 */}
+
+
+ {[...Array(5)].map((_, index) => (
+
+ ))}
+
+
+
+ {/* 控制按钮 */}
+
+
+ 🗑️
+ 取消
+
+
+
+ {isPaused ? "▶️" : "⏸️"}
+ {isPaused ? "继续" : "暂停"}
+
+
+
+ {isProcessing ? "⏳" : "📤"}
+
+ {isProcessing ? "处理中" : "发送"}
+
+
+
+
+ {/* 提示文字 */}
+ {/*
+ {isProcessing ? (
+ 正在处理录音...
+ ) : isUploading ? (
+ 正在上传到服务器...
+ ) : recordingTime < 1 ? (
+ 录音时间至少需要1秒
+ ) : (
+ 点击发送按钮完成录音
+ )}
+
*/}
+
+ );
+};
+
+export default VoiceRecordButton;
diff --git a/src/component/carousel/index.less b/src/component/carousel/index.less
new file mode 100644
index 0000000..f1685d9
--- /dev/null
+++ b/src/component/carousel/index.less
@@ -0,0 +1,7 @@
+.carousel {
+ .carousel-item {
+ .carousel-image {
+ border-radius: 15px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/component/carousel/index.tsx b/src/component/carousel/index.tsx
new file mode 100644
index 0000000..932a50e
--- /dev/null
+++ b/src/component/carousel/index.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import Slider, {Settings} from 'react-slick';
+import 'slick-carousel/slick/slick.css';
+import 'slick-carousel/slick/slick-theme.css';
+import './index.less';
+
+export interface CarouselComponentProps {
+ images: string[];
+ height?: number;
+}
+
+/**
+ * 轮播图组件
+ * @param images 图片地址数组
+ * @param height 图片高度
+ * @constructor Carousel
+ */
+const Carousel: React.FC = ({images, height = 180}) => {
+ const settings: Settings = {
+ dots: false,
+ infinite: true,
+ speed: 3000,
+ slidesToShow: 1,
+ slidesToScroll: 1,
+ autoplay: true,
+ autoplaySpeed: 2000,
+ responsive: [
+ {
+ breakpoint: 768,
+ settings: {
+ arrows: false,
+ }
+ }
+ ]
+ };
+
+ return (
+
+ {images.map((image, index) => (
+
+
+
+ ))}
+
+ );
+};
+
+export default Carousel;
\ No newline at end of file
diff --git a/src/component/floatingMenu/index.less b/src/component/floatingMenu/index.less
new file mode 100644
index 0000000..be92e5f
--- /dev/null
+++ b/src/component/floatingMenu/index.less
@@ -0,0 +1,42 @@
+/* FloatingFanMenu.css */
+
+@keyframes menuItemPop {
+ 0% {
+ opacity: 0;
+ transform: scale(0) rotate(-180deg);
+ }
+ 70% {
+ opacity: 1;
+ transform: scale(1.1) rotate(-10deg);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1) rotate(0deg);
+ }
+}
+
+/* 悬停效果 */
+.menu-item:hover {
+ transform: scale(1.1) !important;
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3) !important;
+ transition: all 0.2s ease !important;
+}
+.adm-floating-bubble-button {
+ z-index: 999;
+ width: 72px;
+ height: 72px;
+ overflow: visible;
+ .cat {
+ position: absolute;
+ width: 70px;
+ font-size: 12px;
+
+ bottom: -10px;
+ background: rgba(255, 204, 199, 1);
+ padding: 4px 0px;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+}
diff --git a/src/component/floatingMenu/index.tsx b/src/component/floatingMenu/index.tsx
new file mode 100644
index 0000000..9fdb2b9
--- /dev/null
+++ b/src/component/floatingMenu/index.tsx
@@ -0,0 +1,196 @@
+import React, { useState, useRef } from "react";
+import { FloatingBubble, Image } from "antd-mobile";
+import {
+ AddOutline,
+ MessageOutline,
+ UserOutline,
+ SetOutline,
+ HeartOutline,
+ CheckOutline,
+} from "antd-mobile-icons";
+import { createPortal } from "react-dom";
+import dogSvg from "@/assets/translate/dog.svg";
+import catSvg from "@/assets/translate/cat.svg";
+import pigSvg from "@/assets/translate/pig.svg";
+import "./index.less";
+import { MoreTwo } from "@icon-park/react";
+
+export interface FloatMenuItemConfig {
+ icon: React.ReactNode;
+ type?: string;
+}
+
+const FloatingFanMenu: React.FC<{
+ menuItems: FloatMenuItemConfig[];
+ value?: FloatMenuItemConfig;
+ onChange?: (item: FloatMenuItemConfig) => void;
+}> = (props) => {
+ const { menuItems = [] } = props;
+ const [visible, setVisible] = useState(false);
+ const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
+ const bubbleRef = useRef(null);
+
+ // 点击时获取FloatingBubble的位置
+ const handleMainClick = () => {
+ if (!visible) {
+ // 显示菜单时,获取当前FloatingBubble的位置
+ if (bubbleRef.current) {
+ const bubble = bubbleRef.current.querySelector(
+ ".adm-floating-bubble-button"
+ );
+ if (bubble) {
+ const rect = bubble.getBoundingClientRect();
+ setMenuPosition({
+ x: rect.left + rect.width / 2,
+ y: rect.top + rect.height / 2,
+ });
+ }
+ }
+ }
+ setVisible(!visible);
+ };
+
+ const handleItemClick = (item: FloatMenuItemConfig) => {
+ props.onChange?.(item);
+ setVisible(false);
+ };
+
+ // 计算菜单项位置
+ const getMenuItemPosition = (index: number) => {
+ const positions = [
+ { x: 0, y: -80 }, // 上方
+ { x: -60, y: -60 }, // 左上
+ { x: -80, y: 0 }, // 左方
+ { x: -60, y: 60 }, // 左下
+ ];
+
+ const pos = positions[index] || { x: 0, y: 0 };
+ let x = menuPosition.x + pos.x;
+ let y = menuPosition.y + pos.y;
+
+ // // 边界检测
+ // const itemSize = 48;
+ // const margin = 20;
+ // 边界检测
+ const itemSize = 48;
+ // const margin = 0;
+
+ // x = Math.max(
+ // // margin + itemSize / 2,
+ // Math.min(window.innerWidth - margin - itemSize / 2, x)
+ // );
+ // y = Math.max(
+ // // margin + itemSize / 2,
+ // Math.min(window.innerHeight - margin - itemSize / 2, y)
+ // );
+
+ return {
+ left: x - itemSize / 2,
+ top: y - itemSize / 2,
+ };
+ };
+
+ // 菜单组件
+ const MenuComponent = () => (
+ <>
+ {/* 背景遮罩 */}
+ setVisible(false)}
+ />
+
+ {/* 菜单项 */}
+ {menuItems.map((item, index) => {
+ const position = getMenuItemPosition(index);
+ return (
+
handleItemClick(item)}
+ // title={item.label}
+ >
+ {item.icon}
+
+ );
+ })}
+ >
+ );
+
+ return (
+ <>
+ {/* 主按钮 */}
+
+
+
+ {props.value?.icon}
+
+ {/* {!visible && } */}
+ 切换语言
+
+
+
+
+
+ {/* 菜单 - 只在有位置信息时渲染 */}
+ {visible &&
+ menuPosition.x > 0 &&
+ menuPosition.y > 0 &&
+ createPortal(
, document.body)}
+ >
+ );
+};
+
+export default FloatingFanMenu;
diff --git a/src/component/petVoiceTranslator copy/index.less b/src/component/petVoiceTranslator copy/index.less
new file mode 100644
index 0000000..aa64c44
--- /dev/null
+++ b/src/component/petVoiceTranslator copy/index.less
@@ -0,0 +1,662 @@
+// PetVoiceTranslator.less
+.pet-voice-translator {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ background: #ededed;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
+ "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial,
+ sans-serif;
+ position: relative;
+ overflow: hidden;
+
+ // 处理iOS安全区域
+ padding-top: var(--safe-area-inset-top, 0);
+ padding-bottom: var(--safe-area-inset-bottom, 0);
+
+ // Safari 特殊处理
+ -webkit-overflow-scrolling: touch;
+ -webkit-user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-tap-highlight-color: transparent;
+
+ .chat-header {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ background: #f7f7f7;
+ border-bottom: 1px solid #e5e5e5;
+ flex-shrink: 0;
+ position: relative;
+ z-index: 10;
+
+ // 确保头部不被NavBar遮挡
+ min-height: 64px;
+
+ .pet-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 20px;
+ background: linear-gradient(45deg, #ff9a9e, #fecfef);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ margin-right: 12px;
+ }
+
+ .chat-title {
+ flex: 1;
+
+ .pet-name {
+ font-size: 16px;
+ font-weight: 500;
+ color: #333;
+ margin-bottom: 2px;
+ }
+
+ .chat-subtitle {
+ font-size: 12px;
+ color: #999;
+ }
+ }
+ }
+
+ .messages-container {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: 16px 12px;
+ position: relative;
+
+ // Safari 滚动优化
+ -webkit-overflow-scrolling: touch;
+ -webkit-transform: translateZ(0);
+ transform: translateZ(0);
+
+ // 确保可以滚动
+ overscroll-behavior: contain;
+ scroll-behavior: smooth;
+
+ // 滚动条样式
+ &::-webkit-scrollbar {
+ width: 0;
+ background: transparent;
+ }
+
+ .empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: #999;
+ min-height: 200px;
+
+ .empty-icon {
+ font-size: 64px;
+ margin-bottom: 16px;
+ opacity: 0.6;
+ }
+
+ .empty-text {
+ font-size: 16px;
+ margin-bottom: 8px;
+ }
+
+ .empty-subtitle {
+ font-size: 14px;
+ opacity: 0.8;
+ }
+ }
+
+ .message-wrapper {
+ margin-bottom: 16px;
+ animation: messageSlideIn 0.3s ease-out;
+
+ -webkit-user-select: none;
+ user-select: none;
+
+ .message-item {
+ display: flex;
+ align-items: flex-start;
+
+ .avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 6px;
+ background: linear-gradient(45deg, #ff9a9e, #fecfef);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 18px;
+ margin-right: 8px;
+ flex-shrink: 0;
+ }
+
+ .message-content {
+ flex: 1;
+ max-width: calc(100% - 80px);
+
+ .voice-bubble {
+ background: #95ec69;
+ border-radius: 8px;
+ padding: 8px 12px;
+ margin-bottom: 6px;
+ position: relative;
+ display: inline-block;
+ min-width: 120px;
+ cursor: pointer;
+ user-select: none;
+
+ // 防止长按选择
+ -webkit-user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-tap-highlight-color: transparent;
+
+ // 优化点击响应
+ touch-action: manipulation;
+
+ &::before {
+ content: "";
+ position: absolute;
+ left: -8px;
+ top: 12px;
+ width: 0;
+ height: 0;
+ border-top: 6px solid transparent;
+ border-bottom: 6px solid transparent;
+ border-right: 8px solid #95ec69;
+ }
+
+ &:active {
+ background: #8de354;
+ transform: scale(0.98);
+ transition: all 0.1s ease;
+ }
+
+ .voice-content {
+ display: flex;
+ align-items: center;
+
+ .voice-icon {
+ margin-right: 8px;
+ font-size: 16px;
+ color: #333;
+
+ .playing-animation {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+
+ span {
+ width: 3px;
+ height: 12px;
+ background: #333;
+ border-radius: 1px;
+ animation: voiceWave 1s infinite ease-in-out;
+
+ &:nth-child(2) {
+ animation-delay: 0.1s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.2s;
+ }
+ }
+ }
+ }
+
+ .voice-duration {
+ font-size: 14px;
+ color: #333;
+ font-weight: 500;
+ }
+ }
+ }
+
+ .translation-text {
+ font-size: 13px;
+ color: #666;
+ margin-left: 4px;
+ margin-bottom: 4px;
+ line-height: 1.4;
+ word-wrap: break-word;
+ word-break: break-word;
+
+ &.translating {
+ .translating-content {
+ display: flex;
+ align-items: center;
+
+ .loading-dots {
+ margin-right: 8px;
+
+ &::after {
+ content: "...";
+ animation: loadingDots 1.5s infinite;
+ }
+ }
+
+ .typing-indicator {
+ display: flex;
+ gap: 3px;
+
+ span {
+ width: 4px;
+ height: 4px;
+ background: #999;
+ border-radius: 50%;
+ animation: typingBounce 1.4s infinite ease-in-out;
+
+ &:nth-child(1) {
+ animation-delay: 0s;
+ }
+
+ &:nth-child(2) {
+ animation-delay: 0.2s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.4s;
+ }
+ }
+ }
+ }
+ }
+
+ .translation-label {
+ font-size: 11px;
+ color: #999;
+ margin-bottom: 2px;
+ }
+
+ .translation-content {
+ color: #333;
+ word-wrap: break-word;
+ word-break: break-word;
+ }
+ }
+
+ .message-time {
+ font-size: 11px;
+ color: #999;
+ margin-left: 4px;
+ }
+ }
+ }
+ }
+ }
+
+ .recording-controls {
+ background: #f7f7f7;
+ border-top: 1px solid #e5e5e5;
+ padding: 12px 16px 20px;
+ flex-shrink: 0;
+ position: relative;
+ z-index: 10;
+
+ // 确保控制区域不被底部安全区域影响
+ min-height: 100px;
+
+ // 防止在Safari中被下拉刷新影响
+ -webkit-user-select: none;
+ user-select: none;
+
+ .recorder-wrapper {
+ display: none;
+ }
+
+ .control-area {
+ .recording-info {
+ text-align: center;
+ margin-bottom: 12px;
+
+ .recording-indicator {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #ff4d4f;
+ font-size: 14px;
+ margin-bottom: 4px;
+
+ .recording-dot {
+ width: 8px;
+ height: 8px;
+ background: #ff4d4f;
+ border-radius: 50%;
+ margin-right: 8px;
+ animation: recordingPulse 1s infinite;
+ }
+ }
+
+ .recording-tip {
+ font-size: 12px;
+ color: #999;
+ }
+ }
+
+ .input-area {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .button-group {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ margin-bottom: 8px;
+
+ .cancel-button {
+ width: 40px;
+ height: 40px;
+ border-radius: 20px;
+ border: none;
+ background: #ff4d4f;
+ color: white;
+ cursor: pointer;
+ transition: all 0.1s ease;
+ outline: none;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-appearance: none;
+ -webkit-user-select: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: slideInLeft 0.3s ease-out;
+
+ // 优化点击响应
+ touch-action: manipulation;
+
+ &:active {
+ transform: scale(0.9);
+ background: #ff7875;
+ }
+
+ .cancel-icon {
+ font-size: 18px;
+ }
+ }
+
+ .record-button {
+ width: 60px;
+ height: 60px;
+ border-radius: 30px;
+ border: none;
+ background: #1890ff;
+ color: white;
+ cursor: pointer;
+ transition: all 0.1s ease;
+ outline: none;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-appearance: none;
+ -webkit-user-select: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ // 优化点击响应
+ touch-action: manipulation;
+
+ &:active {
+ transform: scale(0.9);
+ }
+
+ &:disabled {
+ background: #d9d9d9;
+ color: #999;
+ cursor: not-allowed;
+ }
+
+ &.processing {
+ background: #faad14;
+
+ cursor: wait;
+
+ .processing-animation {
+ display: flex;
+
+ align-items: center;
+
+ gap: 3 px;
+
+ span {
+ width: 3 px;
+
+ height: 16 px;
+
+ background: white;
+
+ border-radius: 1 px;
+
+ animation: processingWave 1.5 s infinite ease - in - out;
+
+ &:nth-child(2) {
+ animation-delay: 0.2 s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.4 s;
+ }
+ }
+ }
+ }
+
+ &.recording {
+ background: #ff4d4f;
+ animation: recordingButtonPulse 1s infinite;
+
+ .recording-animation {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+
+ span {
+ width: 3px;
+ height: 16px;
+ background: white;
+ border-radius: 1px;
+ animation: recordingWave 1s infinite ease-in-out;
+
+ &:nth-child(2) {
+ animation-delay: 0.1s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.2s;
+ }
+ }
+ }
+ }
+
+ .mic-icon {
+ font-size: 24px;
+ }
+ }
+ }
+
+ .record-hint {
+ font-size: 12px;
+ color: #999;
+ }
+ }
+ }
+ }
+}
+
+// 全局样式,确保页面不会滚动
+html,
+body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ overflow: hidden;
+ -webkit-overflow-scrolling: touch;
+ -webkit-user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-tap-highlight-color: transparent;
+
+ // 防止iOS Safari的橡皮筋效果
+ position: fixed;
+ width: 100%;
+}
+
+// 动画定义
+@keyframes messageSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@keyframes slideInLeft {
+ from {
+ opacity: 0;
+ transform: translateX(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@keyframes voiceWave {
+ 0%,
+ 40%,
+ 100% {
+ transform: scaleY(0.4);
+ }
+ 20% {
+ transform: scaleY(1);
+ }
+}
+
+@keyframes loadingDots {
+ 0%,
+ 20% {
+ content: ".";
+ }
+ 40% {
+ content: "..";
+ }
+ 60%,
+ 100% {
+ content: "...";
+ }
+}
+
+@keyframes typingBounce {
+ 0%,
+ 60%,
+ 100% {
+ transform: translateY(0);
+ }
+ 30% {
+ transform: translateY(-6px);
+ }
+}
+
+@keyframes recordingPulse {
+ 0% {
+ opacity: 1;
+ transform: scale(1);
+ }
+ 50% {
+ opacity: 0.5;
+ transform: scale(1.2);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+@keyframes recordingButtonPulse {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(255, 77, 79, 0.7);
+ }
+ 70% {
+ box-shadow: 0 0 0 10px rgba(255, 77, 79, 0);
+ }
+ 100% {
+ box-shadow: 0 0 0 0 rgba(255, 77, 79, 0);
+ }
+}
+
+@keyframes recordingWave {
+ 0%,
+ 40%,
+ 100% {
+ transform: scaleY(0.4);
+ }
+ 20% {
+ transform: scaleY(1);
+ }
+}
+
+// 移动端适配
+// @media (max-width: 768px) {
+// .pet-voice-translator {
+// .messages-container {
+// padding: 12px 8px;
+
+// .message-wrapper .message-item .message-content {
+// max-width: calc(100% - 60px);
+// }
+// }
+
+// .recording-controls {
+// padding: 10px 12px 16px;
+// }
+// }
+// }
+
+// iOS Safari 特殊处理
+@supports (-webkit-touch-callout: none) {
+ .pet-voice-translator {
+ // 确保在iOS Safari中正确显示
+ // height: 100vh;
+ height: -webkit-fill-available;
+
+ .messages-container {
+ -webkit-overflow-scrolling: touch;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+ }
+}
+
+// 深色模式适配
+@media (prefers-color-scheme: dark) {
+ .pet-voice-translator {
+ background: #1e1e1e;
+
+ .chat-header {
+ background: #2a2a2a;
+ border-bottom-color: #3a3a3a;
+
+ .chat-title .pet-name {
+ color: #fff;
+ }
+ }
+
+ .messages-container {
+ .message-wrapper .message-item .message-content .translation-text {
+ color: #ccc;
+
+ .translation-content {
+ color: #fff;
+ }
+ }
+ }
+
+ .recording-controls {
+ background: #2a2a2a;
+ border-top-color: #3a3a3a;
+ }
+ }
+}
diff --git a/src/component/petVoiceTranslator copy/index.tsx b/src/component/petVoiceTranslator copy/index.tsx
new file mode 100644
index 0000000..0b99e4a
--- /dev/null
+++ b/src/component/petVoiceTranslator copy/index.tsx
@@ -0,0 +1,916 @@
+// PetVoiceTranslator.tsx
+import React, { useState, useRef, useEffect, useCallback } from "react";
+import { AudioRecorder, useAudioRecorder } from "react-audio-voice-recorder";
+import { Button, Toast, Dialog } from "antd-mobile";
+import { SoundOutline, CloseOutline } from "antd-mobile-icons";
+import "./index.less";
+
+interface Message {
+ id: string;
+ type: "voice";
+ audioUrl: string;
+ duration: number;
+ timestamp: number;
+ translatedText?: string;
+ isTranslating?: boolean;
+ isPlaying?: boolean;
+}
+
+const PetVoiceTranslator: React.FC = () => {
+ const [messages, setMessages] = useState
([]);
+ const [isRecording, setIsRecording] = useState(false);
+ const [hasPermission, setHasPermission] = useState(null);
+ const [currentPlayingId, setCurrentPlayingId] = useState(null);
+ const [recordingDuration, setRecordingDuration] = useState(0);
+ const [isInitialized, setIsInitialized] = useState(false);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [permissionChecking, setPermissionChecking] = useState(false);
+ const [dialogShowing, setDialogShowing] = useState(false);
+
+ const messagesEndRef = useRef(null);
+ const messagesContainerRef = useRef(null);
+ const audioRefs = useRef<{ [key: string]: HTMLAudioElement }>({});
+ const recordingTimerRef = useRef();
+ const isRecordingRef = useRef(false);
+ const processingTimeoutRef = useRef();
+ const permissionCheckTimeoutRef = useRef();
+
+ // 新增:防止重复初始化的标志
+ const initializingRef = useRef(false);
+ const initializedRef = useRef(false);
+
+ const recorderControls = useAudioRecorder(
+ {
+ noiseSuppression: true,
+ echoCancellation: true,
+ autoGainControl: true,
+ },
+ (err) => {
+ console.error("录音错误:", err);
+ Toast.show("录音失败,请重试");
+ resetRecordingState();
+ }
+ );
+
+ // 使用useEffect的依赖数组为空,确保只执行一次
+ useEffect(() => {
+ console.log("useEffect执行,初始化状态:", {
+ initializing: initializingRef.current,
+ initialized: initializedRef.current,
+ });
+
+ // 防止重复初始化
+ if (initializingRef.current || initializedRef.current) {
+ console.log("已经初始化或正在初始化,跳过");
+ return;
+ }
+
+ initializeApp();
+
+ // 清理函数
+ return () => {
+ console.log("组件卸载,执行清理");
+ cleanup();
+ };
+ }, []); // 空依赖数组确保只执行一次
+
+ useEffect(() => {
+ isRecordingRef.current = isRecording;
+ }, [isRecording]);
+
+ // 优化的初始化函数
+ const initializeApp = useCallback(async () => {
+ // 防止重复执行
+ if (initializingRef.current || initializedRef.current) {
+ console.log("初始化已执行或正在执行,跳过重复调用");
+ return;
+ }
+
+ initializingRef.current = true;
+ console.log("=== 开始初始化应用 ===");
+
+ try {
+ console.log("1. 检查麦克风权限...");
+ await checkMicrophonePermission();
+
+ // console.log("2. 设置Safari兼容性...");
+ // setupSafariCompatibility();
+
+ console.log("3. 标记初始化完成...");
+ setIsInitialized(true);
+ initializedRef.current = true;
+
+ console.log("=== 应用初始化完成 ===");
+ } catch (error) {
+ console.error("应用初始化失败:", error);
+ Toast.show("应用初始化失败");
+ // 即使失败也标记为已初始化,避免无限重试
+ setIsInitialized(true);
+ initializedRef.current = true;
+ } finally {
+ initializingRef.current = false;
+ }
+ }, []); // 空依赖数组,函数只创建一次
+
+ const cleanup = useCallback(() => {
+ console.log("执行清理操作...");
+
+ // 重置初始化标志
+ initializingRef.current = false;
+ initializedRef.current = false;
+
+ if (recordingTimerRef.current) {
+ clearInterval(recordingTimerRef.current);
+ }
+ if (processingTimeoutRef.current) {
+ clearTimeout(processingTimeoutRef.current);
+ }
+ if (permissionCheckTimeoutRef.current) {
+ clearTimeout(permissionCheckTimeoutRef.current);
+ }
+
+ Object.values(audioRefs.current).forEach((audio) => {
+ audio.pause();
+ audio.src = "";
+ });
+
+ cleanupSafariCompatibility();
+ }, []);
+
+ const resetRecordingState = useCallback(() => {
+ console.log("重置录音状态");
+ setIsRecording(false);
+ setIsProcessing(false);
+ setRecordingDuration(0);
+ // isRecordingRef.current = false;
+
+ if (recordingTimerRef.current) {
+ clearInterval(recordingTimerRef.current);
+ recordingTimerRef.current = undefined;
+ }
+
+ if (processingTimeoutRef.current) {
+ clearTimeout(processingTimeoutRef.current);
+ processingTimeoutRef.current = undefined;
+ }
+ }, []);
+
+ // 优化的权限检查函数
+ const checkMicrophonePermission = useCallback(
+ async (showToast = false) => {
+ console.log("检查麦克风权限...", {
+ permissionChecking,
+ dialogShowing,
+ hasPermission,
+ });
+
+ // 防止重复检查
+ if (permissionChecking) {
+ console.log("权限检查正在进行中,跳过");
+ return hasPermission;
+ }
+
+ setPermissionChecking(true);
+
+ try {
+ // 检查浏览器支持
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
+ console.error("浏览器不支持录音功能");
+ setHasPermission(false);
+ if (showToast) {
+ Toast.show("浏览器不支持录音功能");
+ }
+ return false;
+ }
+
+ // 先检查权限状态(如果浏览器支持)
+ if (navigator.permissions && navigator.permissions.query) {
+ try {
+ const permissionStatus = await navigator.permissions.query({
+ name: "microphone" as PermissionName,
+ });
+
+ // alert(permissionStatus.state);
+ // console.log("权限状态:", permissionStatus.state);
+
+ if (permissionStatus.state === "denied") {
+ console.log("权限被拒绝");
+ setHasPermission(false);
+
+ if (showToast && !dialogShowing) {
+ showPermissionDialog();
+ }
+ return false;
+ }
+
+ if (permissionStatus.state === "granted") {
+ console.log("权限已授予");
+ setHasPermission(true);
+ return true;
+ }
+ } catch (permError) {
+ console.log("权限查询不支持,继续使用getUserMedia检查");
+ }
+ }
+
+ // 尝试获取媒体流
+ const stream = await navigator.mediaDevices.getUserMedia({
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ });
+
+ console.log("麦克风权限获取成功");
+ stream.getTracks().forEach((track) => track.stop());
+ setHasPermission(true);
+
+ if (showToast) {
+ Toast.show("麦克风权限已获取");
+ }
+ return true;
+ } catch (error) {
+ console.error("权限检查失败:", error);
+ setHasPermission(false);
+ showPermissionDialog();
+ return false;
+ } finally {
+ setPermissionChecking(false);
+ }
+ },
+ [permissionChecking, dialogShowing, hasPermission]
+ );
+
+ const showPermissionDialog = useCallback(() => {
+ if (dialogShowing) {
+ console.log("弹窗已显示,跳过");
+ return;
+ }
+
+ console.log("显示权限弹窗");
+ setDialogShowing(true);
+
+ Dialog.confirm({
+ title: "需要录音权限",
+ content:
+ '为了使用录音翻译功能,需要您授权麦克风权限。请在浏览器设置中允许访问麦克风,然后点击"重新获取权限"。',
+ confirmText: "重新获取权限",
+ cancelText: "取消",
+ onConfirm: async () => {
+ console.log("用户点击重新获取权限");
+ setDialogShowing(false);
+
+ setTimeout(async () => {
+ await checkMicrophonePermission(true);
+ }, 500);
+ },
+ onCancel: () => {
+ console.log("用户取消权限获取");
+ setDialogShowing(false);
+ Toast.show("需要麦克风权限才能使用录音功能");
+ },
+ onClose: () => {
+ console.log("弹窗关闭");
+ setDialogShowing(false);
+ },
+ });
+ }, [dialogShowing, checkMicrophonePermission]);
+
+ const setupSafariCompatibility = useCallback(() => {
+ console.log("设置Safari兼容性...");
+
+ const root = document.documentElement;
+ const isIPhoneX =
+ /iPhone/.test(navigator.userAgent) && window.screen.height >= 812;
+
+ if (isIPhoneX) {
+ root.style.setProperty(
+ "--safe-area-inset-top",
+ "env(safe-area-inset-top, 44px)"
+ );
+ root.style.setProperty(
+ "--safe-area-inset-bottom",
+ "env(safe-area-inset-bottom, 34px)"
+ );
+ } else {
+ root.style.setProperty(
+ "--safe-area-inset-top",
+ "env(safe-area-inset-top, 20px)"
+ );
+ root.style.setProperty(
+ "--safe-area-inset-bottom",
+ "env(safe-area-inset-bottom, 0px)"
+ );
+ }
+
+ document.body.style.overflow = "hidden";
+ document.body.style.position = "fixed";
+ document.body.style.width = "100%";
+ document.body.style.height = "100%";
+ document.body.style.top = "0";
+ document.body.style.left = "0";
+
+ document.addEventListener("touchstart", preventPullToRefresh, {
+ passive: false,
+ });
+ document.addEventListener("touchmove", preventPullToRefresh, {
+ passive: false,
+ });
+ document.addEventListener("touchend", preventDoubleTapZoom, {
+ passive: false,
+ });
+
+ let viewport = document.querySelector("meta[name=viewport]");
+ if (!viewport) {
+ viewport = document.createElement("meta");
+ viewport.setAttribute("name", "viewport");
+ document.head.appendChild(viewport);
+ }
+ viewport.setAttribute(
+ "content",
+ "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
+ );
+ }, []);
+
+ const cleanupSafariCompatibility = useCallback(() => {
+ console.log("清理Safari兼容性设置...");
+
+ document.body.style.overflow = "";
+ document.body.style.position = "";
+ document.body.style.width = "";
+ document.body.style.height = "";
+ document.body.style.top = "";
+ document.body.style.left = "";
+
+ document.removeEventListener("touchstart", preventPullToRefresh);
+ document.removeEventListener("touchmove", preventPullToRefresh);
+ document.removeEventListener("touchend", preventDoubleTapZoom);
+ }, []);
+
+ const preventPullToRefresh = (e: TouchEvent) => {
+ const target = e.target as HTMLElement;
+ const messagesContainer = messagesContainerRef.current;
+
+ if (messagesContainer && messagesContainer.contains(target)) {
+ return;
+ }
+
+ if (e.touches.length > 1) {
+ e.preventDefault();
+ return;
+ }
+
+ const touch = e.touches[0];
+ const startY = touch.clientY;
+
+ if (startY < 100 && e.type === "touchstart") {
+ e.preventDefault();
+ }
+ };
+
+ let lastTouchEnd = 0;
+ const preventDoubleTapZoom = (e: TouchEvent) => {
+ const now = Date.now();
+ if (now - lastTouchEnd <= 300) {
+ e.preventDefault();
+ }
+ lastTouchEnd = now;
+ };
+
+ const stopAllAudio = () => {
+ if (currentPlayingId && audioRefs.current[currentPlayingId]) {
+ audioRefs.current[currentPlayingId].pause();
+ audioRefs.current[currentPlayingId].currentTime = 0;
+
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === currentPlayingId ? { ...msg, isPlaying: false } : msg
+ )
+ );
+ setCurrentPlayingId(null);
+ }
+
+ Object.values(audioRefs.current).forEach((audio) => {
+ if (!audio.paused) {
+ audio.pause();
+ audio.currentTime = 0;
+ }
+ });
+ };
+
+ const startRecording = useCallback(async () => {
+ console.log("=== 开始录音函数调用 ===");
+
+ if (isProcessing || isRecordingRef.current) {
+ console.log("正在处理中或已在录音,忽略此次调用");
+ return;
+ }
+
+ if (!isInitialized || !initializedRef.current) {
+ console.log("应用未初始化完成");
+ Toast.show("应用正在初始化,请稍后重试");
+ return;
+ }
+
+ if (!hasPermission) {
+ console.log("没有录音权限,尝试获取权限");
+ const granted = await checkMicrophonePermission(true);
+ if (!granted) {
+ console.log("权限获取失败");
+ return;
+ }
+ }
+
+ try {
+ setIsProcessing(true);
+ setIsRecording(true);
+ setRecordingDuration(0);
+
+ stopAllAudio();
+
+ await recorderControls.startRecording();
+
+ recordingTimerRef.current = setInterval(() => {
+ setRecordingDuration((prev) => prev + 1);
+ }, 1000);
+
+ Toast.show("开始录音...");
+
+ processingTimeoutRef.current = setTimeout(() => {
+ setIsProcessing(false);
+ }, 1000);
+ } catch (error) {
+ console.error("开始录音失败:", error);
+ resetRecordingState();
+
+ if (
+ error.name === "NotAllowedError" ||
+ error.name === "PermissionDeniedError"
+ ) {
+ setHasPermission(false);
+ if (!dialogShowing) {
+ showPermissionDialog();
+ }
+ } else {
+ Toast.show(`录音失败: ${error.message || "未知错误"}`);
+ }
+ }
+ }, [
+ isProcessing,
+ hasPermission,
+ isInitialized,
+ recorderControls,
+ checkMicrophonePermission,
+ dialogShowing,
+ showPermissionDialog,
+ ]);
+
+ const stopRecording = useCallback(() => {
+ console.log("=== 停止录音函数调用 ===");
+
+ if (isProcessing || !isRecordingRef.current) {
+ console.log("正在处理中或未在录音,忽略此次调用");
+ return;
+ }
+
+ try {
+ setIsProcessing(true);
+ setIsRecording(false);
+
+ if (recordingTimerRef.current) {
+ clearInterval(recordingTimerRef.current);
+ recordingTimerRef.current = undefined;
+ }
+
+ recorderControls.stopRecording();
+
+ processingTimeoutRef.current = setTimeout(() => {
+ setIsProcessing(false);
+ }, 2000);
+ } catch (error) {
+ console.error("停止录音失败:", error);
+ resetRecordingState();
+ Toast.show("停止录音失败");
+ }
+ }, [isProcessing, recorderControls]);
+
+ const onRecordingComplete = useCallback(
+ (blob: Blob) => {
+ console.log("=== 录音完成回调 ===");
+
+ setIsProcessing(false);
+ if (processingTimeoutRef.current) {
+ clearTimeout(processingTimeoutRef.current);
+ }
+
+ if (recordingDuration < 1) {
+ Toast.show("录音时间太短,请重新录音");
+ setRecordingDuration(0);
+ return;
+ }
+
+ const audioUrl = URL.createObjectURL(blob);
+ const newMessage: Message = {
+ id: Date.now().toString(),
+ type: "voice",
+ audioUrl,
+ duration: recordingDuration,
+ timestamp: Date.now(),
+ isTranslating: true,
+ };
+
+ setMessages((prev) => [...prev, newMessage]);
+ setRecordingDuration(0);
+
+ setTimeout(() => {
+ translateAudio(newMessage.id, blob);
+ }, 1000);
+
+ Toast.show("语音已发送");
+ },
+ [recordingDuration]
+ );
+
+ const cancelRecording = useCallback(() => {
+ console.log("=== 取消录音 ===");
+
+ try {
+ if (isRecordingRef.current) {
+ recorderControls.stopRecording();
+ }
+ } catch (error) {
+ console.error("取消录音失败:", error);
+ }
+
+ resetRecordingState();
+ Toast.show("已取消录音");
+ }, [recorderControls, resetRecordingState]);
+
+ const handleRecordButtonClick = useCallback(
+ (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ console.log("=== 录音按钮点击 ===");
+
+ if (isProcessing || permissionChecking) {
+ console.log("正在处理中,忽略点击");
+ return;
+ }
+
+ if (isRecordingRef.current) {
+ stopRecording();
+ } else {
+ startRecording();
+ }
+ },
+ [isProcessing, permissionChecking, startRecording, stopRecording]
+ );
+
+ const translateAudio = async (messageId: string, audioBlob: Blob) => {
+ try {
+ const translatedText = await mockTranslateAudio(audioBlob);
+
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === messageId
+ ? { ...msg, translatedText, isTranslating: false }
+ : msg
+ )
+ );
+ } catch (error) {
+ console.error("翻译失败:", error);
+ Toast.show("翻译失败,请重试");
+
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === messageId
+ ? {
+ ...msg,
+ isTranslating: false,
+ translatedText: "翻译失败,请重试",
+ }
+ : msg
+ )
+ );
+ }
+ };
+
+ const mockTranslateAudio = async (audioBlob: Blob): Promise => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ const mockTranslations = [
+ "汪汪汪!我饿了,想吃东西!🍖",
+ "喵喵~我想要抱抱!🤗",
+ "我想出去玩耍!🎾",
+ "我很开心!😊",
+ "我有点害怕...😰",
+ "我想睡觉了~😴",
+ "主人,陪我玩一会儿吧!🎮",
+ "我想喝水了💧",
+ "外面有什么声音?👂",
+ "我爱你,主人!❤️",
+ ];
+ const randomTranslation =
+ mockTranslations[Math.floor(Math.random() * mockTranslations.length)];
+ resolve(randomTranslation);
+ }, 2000 + Math.random() * 2000);
+ });
+ };
+
+ const playAudio = (messageId: string, audioUrl: string) => {
+ if (isRecording) {
+ Toast.show("录音中,无法播放音频");
+ return;
+ }
+
+ if (currentPlayingId === messageId) {
+ if (audioRefs.current[messageId]) {
+ audioRefs.current[messageId].pause();
+ audioRefs.current[messageId].currentTime = 0;
+ }
+ setCurrentPlayingId(null);
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === messageId ? { ...msg, isPlaying: false } : msg
+ )
+ );
+ return;
+ }
+
+ stopAllAudio();
+
+ if (!audioRefs.current[messageId]) {
+ audioRefs.current[messageId] = new Audio(audioUrl);
+ }
+
+ const audio = audioRefs.current[messageId];
+ audio.currentTime = 0;
+
+ audio.onended = () => {
+ setCurrentPlayingId(null);
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === messageId ? { ...msg, isPlaying: false } : msg
+ )
+ );
+ };
+
+ audio.onerror = (error) => {
+ console.error("音频播放错误:", error);
+ Toast.show("音频播放失败");
+ setCurrentPlayingId(null);
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === messageId ? { ...msg, isPlaying: false } : msg
+ )
+ );
+ };
+
+ audio
+ .play()
+ .then(() => {
+ setCurrentPlayingId(messageId);
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === messageId
+ ? { ...msg, isPlaying: true }
+ : { ...msg, isPlaying: false }
+ )
+ );
+ })
+ .catch((error) => {
+ console.error("音频播放失败:", error);
+ Toast.show("音频播放失败");
+ });
+ };
+
+ const formatDuration = (seconds: number) => {
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
+ };
+
+ const formatTime = (timestamp: number) => {
+ return new Date(timestamp).toLocaleTimeString("zh-CN", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ };
+
+ return (
+
+ {/* 调试信息 */}
+ {process.env.NODE_ENV === "development" && (
+
+
初始化中: {initializingRef.current ? "⏳" : "✅"}
+
已初始化: {initializedRef.current ? "✅" : "❌"}
+
状态初始化: {isInitialized ? "✅" : "❌"}
+
+ 权限: {hasPermission === null ? "⏳" : hasPermission ? "✅" : "❌"}
+
+
录音: {isRecording ? "🔴" : "⚪"}
+
处理中: {isProcessing ? "⏳" : "✅"}
+
权限检查: {permissionChecking ? "⏳" : "✅"}
+
弹窗: {dialogShowing ? "📱" : "❌"}
+
+ )}
+
+ {/* 头部 */}
+
+
🐾
+
+
我的宠物
+
+ {initializingRef.current
+ ? "初始化中..."
+ : !isInitialized
+ ? "等待初始化..."
+ : permissionChecking
+ ? "检查权限中..."
+ : hasPermission === null
+ ? "权限状态未知"
+ : hasPermission
+ ? "宠物语音翻译器"
+ : "需要麦克风权限"}
+
+
+
+
+ {/* 消息列表 */}
+
+ {messages.length === 0 ? (
+
+
🎤
+
+ {initializingRef.current
+ ? "正在初始化..."
+ : !isInitialized
+ ? "等待初始化..."
+ : permissionChecking
+ ? "正在检查权限..."
+ : hasPermission === null
+ ? "权限状态未知"
+ : !hasPermission
+ ? "请授权麦克风权限"
+ : "点击下方按钮开始录音"}
+
+
听听你的宠物想说什么~
+
+ ) : (
+ messages.map((message) => (
+
+
+
🐾
+
+
+
playAudio(message.id, message.audioUrl)}
+ >
+
+ {message.isPlaying ? (
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+ {formatDuration(message.duration)}
+
+
+
+
+ {message.isTranslating ? (
+
+ ) : message.translatedText ? (
+
+
🐾 宠物说:
+
+ {message.translatedText}
+
+
+ ) : null}
+
+
+ {formatTime(message.timestamp)}
+
+
+
+
+ ))
+ )}
+
+
+
+ {/* 录音控制区域 */}
+
+
+
+
+ {isRecording && (
+
+
+
+
录音中 {formatDuration(recordingDuration)}
+
+
再次点击完成录音并发送
+
+ )}
+
+
+
+ {/* {isRecording && (
+
+
+
+ )} */}
+
+
+ {isProcessing || permissionChecking ? (
+
+
+
+
+
+ ) : isRecording ? (
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+ {!isRecording && (
+
+ {initializingRef.current
+ ? "初始化中..."
+ : !isInitialized
+ ? "等待初始化..."
+ : permissionChecking
+ ? "检查权限中..."
+ : !hasPermission
+ ? "需要麦克风权限"
+ : isProcessing
+ ? "处理中..."
+ : "点击录音"}
+
+ )}
+
+
+
+
+ );
+};
+
+export default PetVoiceTranslator;
diff --git a/src/component/petVoiceTranslator/index.less b/src/component/petVoiceTranslator/index.less
new file mode 100644
index 0000000..a9ce8d4
--- /dev/null
+++ b/src/component/petVoiceTranslator/index.less
@@ -0,0 +1,660 @@
+// PetVoiceTranslator.less
+.pet-voice-translator {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background: #ededed;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
+ "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial,
+ sans-serif;
+ position: relative;
+ overflow: hidden;
+ // 处理iOS安全区域
+ padding-bottom: env(safe-area-inset-bottom);
+
+ // Safari 特殊处理
+ -webkit-overflow-scrolling: touch;
+ -webkit-user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-tap-highlight-color: transparent;
+
+ .chat-header {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ background: #f7f7f7;
+ border-bottom: 1px solid #e5e5e5;
+ flex-shrink: 0;
+ position: relative;
+ z-index: 10;
+
+ // 确保头部不被NavBar遮挡
+ min-height: 64px;
+
+ .pet-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 20px;
+ background: linear-gradient(45deg, #ff9a9e, #fecfef);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ margin-right: 12px;
+ }
+
+ .chat-title {
+ flex: 1;
+
+ .pet-name {
+ font-size: 16px;
+ font-weight: 500;
+ color: #333;
+ margin-bottom: 2px;
+ }
+
+ .chat-subtitle {
+ font-size: 12px;
+ color: #999;
+ }
+ }
+ }
+
+ .messages-container {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: 16px 12px;
+ position: relative;
+
+ // Safari 滚动优化
+ -webkit-overflow-scrolling: touch;
+ -webkit-transform: translateZ(0);
+ transform: translateZ(0);
+
+ // 确保可以滚动
+ overscroll-behavior: contain;
+ scroll-behavior: smooth;
+
+ // 滚动条样式
+ &::-webkit-scrollbar {
+ width: 0;
+ background: transparent;
+ }
+
+ .empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: #999;
+ min-height: 200px;
+
+ .empty-icon {
+ font-size: 64px;
+ margin-bottom: 16px;
+ opacity: 0.6;
+ }
+
+ .empty-text {
+ font-size: 16px;
+ margin-bottom: 8px;
+ }
+
+ .empty-subtitle {
+ font-size: 14px;
+ opacity: 0.8;
+ }
+ }
+
+ .message-wrapper {
+ margin-bottom: 16px;
+ animation: messageSlideIn 0.3s ease-out;
+
+ -webkit-user-select: none;
+ user-select: none;
+
+ .message-item {
+ display: flex;
+ align-items: flex-start;
+
+ .avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 6px;
+ background: linear-gradient(45deg, #ff9a9e, #fecfef);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 18px;
+ margin-right: 8px;
+ flex-shrink: 0;
+ }
+
+ .message-content {
+ flex: 1;
+ max-width: calc(100% - 80px);
+
+ .voice-bubble {
+ background: #95ec69;
+ border-radius: 8px;
+ padding: 8px 12px;
+ margin-bottom: 6px;
+ position: relative;
+ display: inline-block;
+ min-width: 120px;
+ cursor: pointer;
+ user-select: none;
+
+ // 防止长按选择
+ -webkit-user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-tap-highlight-color: transparent;
+
+ // 优化点击响应
+ touch-action: manipulation;
+
+ &::before {
+ content: "";
+ position: absolute;
+ left: -8px;
+ top: 12px;
+ width: 0;
+ height: 0;
+ border-top: 6px solid transparent;
+ border-bottom: 6px solid transparent;
+ border-right: 8px solid #95ec69;
+ }
+
+ &:active {
+ background: #8de354;
+ transform: scale(0.98);
+ transition: all 0.1s ease;
+ }
+
+ .voice-content {
+ display: flex;
+ align-items: center;
+
+ .voice-icon {
+ margin-right: 8px;
+ font-size: 16px;
+ color: #333;
+
+ .playing-animation {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+
+ span {
+ width: 3px;
+ height: 12px;
+ background: #333;
+ border-radius: 1px;
+ animation: voiceWave 1s infinite ease-in-out;
+
+ &:nth-child(2) {
+ animation-delay: 0.1s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.2s;
+ }
+ }
+ }
+ }
+
+ .voice-duration {
+ font-size: 14px;
+ color: #333;
+ font-weight: 500;
+ }
+ }
+ }
+
+ .translation-text {
+ font-size: 13px;
+ color: #666;
+ margin-left: 4px;
+ margin-bottom: 4px;
+ line-height: 1.4;
+ word-wrap: break-word;
+ word-break: break-word;
+
+ &.translating {
+ .translating-content {
+ display: flex;
+ align-items: center;
+
+ .loading-dots {
+ margin-right: 8px;
+
+ &::after {
+ content: "...";
+ animation: loadingDots 1.5s infinite;
+ }
+ }
+
+ .typing-indicator {
+ display: flex;
+ gap: 3px;
+
+ span {
+ width: 4px;
+ height: 4px;
+ background: #999;
+ border-radius: 50%;
+ animation: typingBounce 1.4s infinite ease-in-out;
+
+ &:nth-child(1) {
+ animation-delay: 0s;
+ }
+
+ &:nth-child(2) {
+ animation-delay: 0.2s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.4s;
+ }
+ }
+ }
+ }
+ }
+
+ .translation-label {
+ font-size: 11px;
+ color: #999;
+ margin-bottom: 2px;
+ }
+
+ .translation-content {
+ color: #333;
+ word-wrap: break-word;
+ word-break: break-word;
+ }
+ }
+
+ .message-time {
+ font-size: 11px;
+ color: #999;
+ margin-left: 4px;
+ }
+ }
+ }
+ }
+ }
+
+ .recording-controls {
+ background: #f7f7f7;
+ border-top: 1px solid #e5e5e5;
+ padding: 12px 16px 20px;
+ flex-shrink: 0;
+ position: relative;
+ z-index: 10;
+
+ // 确保控制区域不被底部安全区域影响
+ min-height: 100px;
+
+ // 防止在Safari中被下拉刷新影响
+ -webkit-user-select: none;
+ user-select: none;
+
+ .recorder-wrapper {
+ display: none;
+ }
+
+ .control-area {
+ .recording-info {
+ text-align: center;
+ margin-bottom: 12px;
+
+ .recording-indicator {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #ff4d4f;
+ font-size: 14px;
+ margin-bottom: 4px;
+
+ .recording-dot {
+ width: 8px;
+ height: 8px;
+ background: #ff4d4f;
+ border-radius: 50%;
+ margin-right: 8px;
+ animation: recordingPulse 1s infinite;
+ }
+ }
+
+ .recording-tip {
+ font-size: 12px;
+ color: #999;
+ }
+ }
+
+ .input-area {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .button-group {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ margin-bottom: 8px;
+
+ .cancel-button {
+ width: 40px;
+ height: 40px;
+ border-radius: 20px;
+ border: none;
+ background: #ff4d4f;
+ color: white;
+ cursor: pointer;
+ transition: all 0.1s ease;
+ outline: none;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-appearance: none;
+ -webkit-user-select: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: slideInLeft 0.3s ease-out;
+
+ // 优化点击响应
+ touch-action: manipulation;
+
+ &:active {
+ transform: scale(0.9);
+ background: #ff7875;
+ }
+
+ .cancel-icon {
+ font-size: 18px;
+ }
+ }
+
+ .record-button {
+ width: 60px;
+ height: 60px;
+ border-radius: 30px;
+ border: none;
+ background: #1890ff;
+ color: white;
+ cursor: pointer;
+ transition: all 0.1s ease;
+ outline: none;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-appearance: none;
+ -webkit-user-select: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ // 优化点击响应
+ touch-action: manipulation;
+
+ &:active {
+ transform: scale(0.9);
+ }
+
+ &:disabled {
+ background: #d9d9d9;
+ color: #999;
+ cursor: not-allowed;
+ }
+
+ &.processing {
+ background: #faad14;
+
+ cursor: wait;
+
+ .processing-animation {
+ display: flex;
+
+ align-items: center;
+
+ gap: 3 px;
+
+ span {
+ width: 3 px;
+
+ height: 16 px;
+
+ background: white;
+
+ border-radius: 1 px;
+
+ animation: processingWave 1.5 s infinite ease - in - out;
+
+ &:nth-child(2) {
+ animation-delay: 0.2 s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.4 s;
+ }
+ }
+ }
+ }
+
+ &.recording {
+ background: #ff4d4f;
+ animation: recordingButtonPulse 1s infinite;
+
+ .recording-animation {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+
+ span {
+ width: 3px;
+ height: 16px;
+ background: white;
+ border-radius: 1px;
+ animation: recordingWave 1s infinite ease-in-out;
+
+ &:nth-child(2) {
+ animation-delay: 0.1s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.2s;
+ }
+ }
+ }
+ }
+
+ .mic-icon {
+ font-size: 24px;
+ }
+ }
+ }
+
+ .record-hint {
+ font-size: 12px;
+ color: #999;
+ }
+ }
+ }
+ }
+}
+
+// 全局样式,确保页面不会滚动
+html,
+body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ overflow: hidden;
+ -webkit-overflow-scrolling: touch;
+ -webkit-user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-tap-highlight-color: transparent;
+
+ // 防止iOS Safari的橡皮筋效果
+ position: fixed;
+ width: 100%;
+}
+
+// 动画定义
+@keyframes messageSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@keyframes slideInLeft {
+ from {
+ opacity: 0;
+ transform: translateX(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@keyframes voiceWave {
+ 0%,
+ 40%,
+ 100% {
+ transform: scaleY(0.4);
+ }
+ 20% {
+ transform: scaleY(1);
+ }
+}
+
+@keyframes loadingDots {
+ 0%,
+ 20% {
+ content: ".";
+ }
+ 40% {
+ content: "..";
+ }
+ 60%,
+ 100% {
+ content: "...";
+ }
+}
+
+@keyframes typingBounce {
+ 0%,
+ 60%,
+ 100% {
+ transform: translateY(0);
+ }
+ 30% {
+ transform: translateY(-6px);
+ }
+}
+
+@keyframes recordingPulse {
+ 0% {
+ opacity: 1;
+ transform: scale(1);
+ }
+ 50% {
+ opacity: 0.5;
+ transform: scale(1.2);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+@keyframes recordingButtonPulse {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(255, 77, 79, 0.7);
+ }
+ 70% {
+ box-shadow: 0 0 0 10px rgba(255, 77, 79, 0);
+ }
+ 100% {
+ box-shadow: 0 0 0 0 rgba(255, 77, 79, 0);
+ }
+}
+
+@keyframes recordingWave {
+ 0%,
+ 40%,
+ 100% {
+ transform: scaleY(0.4);
+ }
+ 20% {
+ transform: scaleY(1);
+ }
+}
+
+// 移动端适配
+// @media (max-width: 768px) {
+// .pet-voice-translator {
+// .messages-container {
+// padding: 12px 8px;
+
+// .message-wrapper .message-item .message-content {
+// max-width: calc(100% - 60px);
+// }
+// }
+
+// .recording-controls {
+// padding: 10px 12px 16px;
+// }
+// }
+// }
+
+// iOS Safari 特殊处理
+@supports (-webkit-touch-callout: none) {
+ .pet-voice-translator {
+ // 确保在iOS Safari中正确显示
+ // height: 100vh;
+ height: -webkit-fill-available;
+
+ .messages-container {
+ -webkit-overflow-scrolling: touch;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+ }
+}
+
+// 深色模式适配
+@media (prefers-color-scheme: dark) {
+ .pet-voice-translator {
+ background: #1e1e1e;
+
+ .chat-header {
+ background: #2a2a2a;
+ border-bottom-color: #3a3a3a;
+
+ .chat-title .pet-name {
+ color: #fff;
+ }
+ }
+
+ .messages-container {
+ .message-wrapper .message-item .message-content .translation-text {
+ color: #ccc;
+
+ .translation-content {
+ color: #fff;
+ }
+ }
+ }
+
+ .recording-controls {
+ background: #2a2a2a;
+ border-top-color: #3a3a3a;
+ }
+ }
+}
diff --git a/src/component/petVoiceTranslator/index.tsx b/src/component/petVoiceTranslator/index.tsx
new file mode 100644
index 0000000..a0a4949
--- /dev/null
+++ b/src/component/petVoiceTranslator/index.tsx
@@ -0,0 +1,1104 @@
+// // PetVoiceTranslator.tsx
+// import React, { useState, useRef, useEffect, useCallback } from "react";
+// import { AudioRecorder, useAudioRecorder } from "react-audio-voice-recorder";
+// import { Button, Toast, Dialog } from "antd-mobile";
+// import { SoundOutline, CloseOutline } from "antd-mobile-icons";
+// import "./index.less";
+
+// interface Message {
+// id: string;
+// type: "voice";
+// audioUrl: string;
+// duration: number;
+// timestamp: number;
+// translatedText?: string;
+// isTranslating?: boolean;
+// isPlaying?: boolean;
+// }
+
+// const PetVoiceTranslator: React.FC = () => {
+// const [messages, setMessages] = useState([]);
+// const [isRecording, setIsRecording] = useState(false);
+// const [hasPermission, setHasPermission] = useState(null);
+// const [currentPlayingId, setCurrentPlayingId] = useState(null);
+// const [recordingDuration, setRecordingDuration] = useState(0);
+// const [isInitialized, setIsInitialized] = useState(false);
+// const [permissionChecking, setPermissionChecking] = useState(false);
+// const [dialogShowing, setDialogShowing] = useState(false);
+
+// const messagesEndRef = useRef(null);
+// const messagesContainerRef = useRef(null);
+// const audioRefs = useRef<{ [key: string]: HTMLAudioElement }>({});
+// const recordingTimerRef = useRef();
+// const isRecordingRef = useRef(false);
+// const initializingRef = useRef(false);
+// const initializedRef = useRef(false);
+// const isCancelledRef = useRef(false);
+// const recordingStartTimeRef = useRef(0);
+
+// // 音效相关
+// const sendSoundRef = useRef(null);
+// const startRecordSoundRef = useRef(null);
+
+// const recorderControls = useAudioRecorder(
+// {
+// noiseSuppression: true,
+// echoCancellation: true,
+// autoGainControl: true,
+// },
+// (err) => {
+// console.error("录音错误:", err);
+// Toast.show("录音失败,请重试");
+// resetRecordingState();
+// }
+// );
+
+// useEffect(() => {
+// if (initializingRef.current || initializedRef.current) {
+// return;
+// }
+// initializeApp();
+
+// return () => {
+// cleanup();
+// };
+// }, []);
+
+// useEffect(() => {
+// scrollToBottom();
+// }, [messages]);
+
+// useEffect(() => {
+// isRecordingRef.current = isRecording;
+// }, [isRecording]);
+
+// // 初始化音效
+// useEffect(() => {
+// initializeSounds();
+// return () => {
+// cleanupSounds();
+// };
+// }, []);
+
+// const initializeSounds = () => {
+// try {
+// // 发送音效 - 使用Web Audio API生成
+// sendSoundRef.current = createSendSound();
+
+// // 开始录音音效
+// startRecordSoundRef.current = createStartRecordSound();
+
+// console.log("音效初始化完成");
+// } catch (error) {
+// console.error("音效初始化失败:", error);
+// }
+// };
+
+// const cleanupSounds = () => {
+// if (sendSoundRef.current) {
+// sendSoundRef.current.pause();
+// sendSoundRef.current = null;
+// }
+// if (startRecordSoundRef.current) {
+// startRecordSoundRef.current.pause();
+// startRecordSoundRef.current = null;
+// }
+// };
+
+// // 创建发送音效 - 清脆的"叮"声
+// const createSendSound = () => {
+// try {
+// const audioContext = new (window.AudioContext ||
+// (window as any).webkitAudioContext)();
+// const oscillator = audioContext.createOscillator();
+// const gainNode = audioContext.createGain();
+
+// oscillator.connect(gainNode);
+// gainNode.connect(audioContext.destination);
+
+// // 设置音调 - 清脆的高音
+// oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
+// oscillator.frequency.exponentialRampToValueAtTime(
+// 1200,
+// audioContext.currentTime + 0.1
+// );
+
+// // 设置音量包络
+// gainNode.gain.setValueAtTime(0, audioContext.currentTime);
+// gainNode.gain.linearRampToValueAtTime(
+// 0.3,
+// audioContext.currentTime + 0.01
+// );
+// gainNode.gain.exponentialRampToValueAtTime(
+// 0.01,
+// audioContext.currentTime + 0.3
+// );
+
+// oscillator.type = "sine";
+
+// // 创建音频元素
+// const audio = new Audio();
+
+// // 重写play方法来播放合成音效
+// audio.play = () => {
+// return new Promise((resolve) => {
+// try {
+// const newOscillator = audioContext.createOscillator();
+// const newGainNode = audioContext.createGain();
+
+// newOscillator.connect(newGainNode);
+// newGainNode.connect(audioContext.destination);
+
+// newOscillator.frequency.setValueAtTime(
+// 800,
+// audioContext.currentTime
+// );
+// newOscillator.frequency.exponentialRampToValueAtTime(
+// 1200,
+// audioContext.currentTime + 0.1
+// );
+
+// newGainNode.gain.setValueAtTime(0, audioContext.currentTime);
+// newGainNode.gain.linearRampToValueAtTime(
+// 0.3,
+// audioContext.currentTime + 0.01
+// );
+// newGainNode.gain.exponentialRampToValueAtTime(
+// 0.01,
+// audioContext.currentTime + 0.3
+// );
+
+// newOscillator.type = "sine";
+// newOscillator.start(audioContext.currentTime);
+// newOscillator.stop(audioContext.currentTime + 0.3);
+
+// setTimeout(() => resolve(undefined), 300);
+// } catch (error) {
+// console.error("播放发送音效失败:", error);
+// resolve(undefined);
+// }
+// });
+// };
+
+// return audio;
+// } catch (error) {
+// console.error("创建发送音效失败:", error);
+// return new Audio(); // 返回空音频对象
+// }
+// };
+
+// // 创建开始录音音效 - 低沉的"嘟"声
+// const createStartRecordSound = () => {
+// try {
+// const audio = new Audio();
+
+// audio.play = () => {
+// return new Promise((resolve) => {
+// try {
+// const audioContext = new (window.AudioContext ||
+// (window as any).webkitAudioContext)();
+// const oscillator = audioContext.createOscillator();
+// const gainNode = audioContext.createGain();
+
+// oscillator.connect(gainNode);
+// gainNode.connect(audioContext.destination);
+
+// // 设置音调 - 低沉的音
+// oscillator.frequency.setValueAtTime(400, audioContext.currentTime);
+
+// // 设置音量包络
+// gainNode.gain.setValueAtTime(0, audioContext.currentTime);
+// gainNode.gain.linearRampToValueAtTime(
+// 0.2,
+// audioContext.currentTime + 0.05
+// );
+// gainNode.gain.linearRampToValueAtTime(
+// 0,
+// audioContext.currentTime + 0.2
+// );
+
+// oscillator.type = "sine";
+// oscillator.start(audioContext.currentTime);
+// oscillator.stop(audioContext.currentTime + 0.2);
+
+// setTimeout(() => resolve(undefined), 200);
+// } catch (error) {
+// console.error("播放录音音效失败:", error);
+// resolve(undefined);
+// }
+// });
+// };
+
+// return audio;
+// } catch (error) {
+// console.error("创建录音音效失败:", error);
+// return new Audio();
+// }
+// };
+
+// // 震动函数
+// const vibrate = (pattern: number | number[]) => {
+// try {
+// if ("vibrate" in navigator) {
+// navigator.vibrate(pattern);
+// console.log("震动:", pattern);
+// } else {
+// console.log("设备不支持震动");
+// }
+// } catch (error) {
+// console.error("震动失败:", error);
+// }
+// };
+
+// // 播放音效
+// const playSound = async (soundRef: React.RefObject) => {
+// try {
+// if (soundRef.current) {
+// await soundRef.current.play();
+// }
+// } catch (error) {
+// console.error("播放音效失败:", error);
+// }
+// };
+
+// const initializeApp = useCallback(async () => {
+// if (initializingRef.current || initializedRef.current) {
+// return;
+// }
+
+// initializingRef.current = true;
+// console.log("=== 开始初始化应用 ===");
+
+// try {
+// await checkMicrophonePermission();
+// setupSafariCompatibility();
+// setIsInitialized(true);
+// initializedRef.current = true;
+// console.log("=== 应用初始化完成 ===");
+// } catch (error) {
+// console.error("应用初始化失败:", error);
+// Toast.show("应用初始化失败");
+// setIsInitialized(true);
+// initializedRef.current = true;
+// } finally {
+// initializingRef.current = false;
+// }
+// }, []);
+
+// const cleanup = useCallback(() => {
+// console.log("执行清理操作...");
+
+// initializingRef.current = false;
+// initializedRef.current = false;
+// isCancelledRef.current = false;
+// recordingStartTimeRef.current = 0;
+
+// if (recordingTimerRef.current) {
+// clearInterval(recordingTimerRef.current);
+// }
+
+// Object.values(audioRefs.current).forEach((audio) => {
+// audio.pause();
+// audio.src = "";
+// });
+
+// cleanupSounds();
+// cleanupSafariCompatibility();
+// }, []);
+
+// const resetRecordingState = useCallback(() => {
+// console.log("重置录音状态");
+// setIsRecording(false);
+// setRecordingDuration(0);
+// isRecordingRef.current = false;
+// isCancelledRef.current = false;
+// recordingStartTimeRef.current = 0;
+
+// if (recordingTimerRef.current) {
+// clearInterval(recordingTimerRef.current);
+// recordingTimerRef.current = undefined;
+// }
+// }, []);
+
+// const checkMicrophonePermission = useCallback(
+// async (showToast = false) => {
+// if (permissionChecking) {
+// return hasPermission;
+// }
+
+// setPermissionChecking(true);
+
+// try {
+// if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
+// setHasPermission(false);
+// if (showToast) {
+// Toast.show("浏览器不支持录音功能");
+// }
+// return false;
+// }
+
+// if (navigator.permissions && navigator.permissions.query) {
+// try {
+// const permissionStatus = await navigator.permissions.query({
+// name: "microphone" as PermissionName,
+// });
+
+// if (permissionStatus.state === "denied") {
+// setHasPermission(false);
+// if (showToast && !dialogShowing) {
+// showPermissionDialog();
+// }
+// return false;
+// }
+
+// if (permissionStatus.state === "granted") {
+// setHasPermission(true);
+// return true;
+// }
+// } catch (permError) {
+// console.log("权限查询不支持,继续使用getUserMedia检查");
+// }
+// }
+
+// const stream = await navigator.mediaDevices.getUserMedia({
+// audio: {
+// echoCancellation: true,
+// noiseSuppression: true,
+// autoGainControl: true,
+// },
+// });
+
+// stream.getTracks().forEach((track) => track.stop());
+// setHasPermission(true);
+
+// if (showToast) {
+// Toast.show("麦克风权限已获取");
+// }
+// return true;
+// } catch (error) {
+// console.error("权限检查失败:", error);
+// setHasPermission(false);
+
+// if (showToast && !dialogShowing) {
+// showPermissionDialog();
+// }
+// return false;
+// } finally {
+// setPermissionChecking(false);
+// }
+// },
+// [permissionChecking, dialogShowing, hasPermission]
+// );
+
+// const showPermissionDialog = useCallback(() => {
+// if (dialogShowing) {
+// return;
+// }
+
+// setDialogShowing(true);
+
+// Dialog.confirm({
+// title: "需要录音权限",
+// content:
+// '为了使用录音翻译功能,需要您授权麦克风权限。请在浏览器设置中允许访问麦克风,然后点击"重新获取权限"。',
+// confirmText: "重新获取权限",
+// cancelText: "取消",
+// onConfirm: async () => {
+// setDialogShowing(false);
+// setTimeout(async () => {
+// await checkMicrophonePermission(true);
+// }, 500);
+// },
+// onCancel: () => {
+// setDialogShowing(false);
+// Toast.show("需要麦克风权限才能使用录音功能");
+// },
+// onClose: () => {
+// setDialogShowing(false);
+// },
+// });
+// }, [dialogShowing, checkMicrophonePermission]);
+
+// const setupSafariCompatibility = useCallback(() => {
+// const root = document.documentElement;
+// const isIPhoneX =
+// /iPhone/.test(navigator.userAgent) && window.screen.height >= 812;
+
+// if (isIPhoneX) {
+// root.style.setProperty(
+// "--safe-area-inset-top",
+// "env(safe-area-inset-top, 44px)"
+// );
+// root.style.setProperty(
+// "--safe-area-inset-bottom",
+// "env(safe-area-inset-bottom, 34px)"
+// );
+// } else {
+// root.style.setProperty(
+// "--safe-area-inset-top",
+// "env(safe-area-inset-top, 20px)"
+// );
+// root.style.setProperty(
+// "--safe-area-inset-bottom",
+// "env(safe-area-inset-bottom, 0px)"
+// );
+// }
+
+// document.body.style.overflow = "hidden";
+// document.body.style.position = "fixed";
+// document.body.style.width = "100%";
+// document.body.style.height = "100%";
+// document.body.style.top = "0";
+// document.body.style.left = "0";
+
+// document.addEventListener("touchstart", preventPullToRefresh, {
+// passive: false,
+// });
+// document.addEventListener("touchmove", preventPullToRefresh, {
+// passive: false,
+// });
+// document.addEventListener("touchend", preventDoubleTapZoom, {
+// passive: false,
+// });
+
+// let viewport = document.querySelector("meta[name=viewport]");
+// if (!viewport) {
+// viewport = document.createElement("meta");
+// viewport.setAttribute("name", "viewport");
+// document.head.appendChild(viewport);
+// }
+// viewport.setAttribute(
+// "content",
+// "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
+// );
+// }, []);
+
+// const cleanupSafariCompatibility = useCallback(() => {
+// document.body.style.overflow = "";
+// document.body.style.position = "";
+// document.body.style.width = "";
+// document.body.style.height = "";
+// document.body.style.top = "";
+// document.body.style.left = "";
+
+// document.removeEventListener("touchstart", preventPullToRefresh);
+// document.removeEventListener("touchmove", preventPullToRefresh);
+// document.removeEventListener("touchend", preventDoubleTapZoom);
+// }, []);
+
+// const preventPullToRefresh = (e: TouchEvent) => {
+// const target = e.target as HTMLElement;
+// const messagesContainer = messagesContainerRef.current;
+
+// if (messagesContainer && messagesContainer.contains(target)) {
+// return;
+// }
+
+// if (e.touches.length > 1) {
+// e.preventDefault();
+// return;
+// }
+
+// const touch = e.touches[0];
+// const startY = touch.clientY;
+
+// if (startY < 100 && e.type === "touchstart") {
+// e.preventDefault();
+// }
+// };
+
+// let lastTouchEnd = 0;
+// const preventDoubleTapZoom = (e: TouchEvent) => {
+// const now = Date.now();
+// if (now - lastTouchEnd <= 300) {
+// e.preventDefault();
+// }
+// lastTouchEnd = now;
+// };
+
+// const scrollToBottom = () => {
+// if (messagesContainerRef.current) {
+// const container = messagesContainerRef.current;
+// requestAnimationFrame(() => {
+// container.scrollTop = container.scrollHeight;
+// });
+// }
+// };
+
+// const stopAllAudio = () => {
+// if (currentPlayingId && audioRefs.current[currentPlayingId]) {
+// audioRefs.current[currentPlayingId].pause();
+// audioRefs.current[currentPlayingId].currentTime = 0;
+
+// setMessages((prev) =>
+// prev.map((msg) =>
+// msg.id === currentPlayingId ? { ...msg, isPlaying: false } : msg
+// )
+// );
+// setCurrentPlayingId(null);
+// }
+
+// Object.values(audioRefs.current).forEach((audio) => {
+// if (!audio.paused) {
+// audio.pause();
+// audio.currentTime = 0;
+// }
+// });
+// };
+
+// const startRecording = useCallback(async () => {
+// console.log("=== 开始录音 ===");
+
+// if (isRecordingRef.current) {
+// console.log("已在录音,忽略");
+// return;
+// }
+
+// if (!isInitialized || !initializedRef.current) {
+// Toast.show("应用正在初始化,请稍后重试");
+// return;
+// }
+
+// if (!hasPermission) {
+// const granted = await checkMicrophonePermission(true);
+// if (!granted) {
+// return;
+// }
+// }
+
+// try {
+// // 震动反馈 - 开始录音时轻微震动
+// vibrate(50);
+
+// // 播放开始录音音效
+// playSound(startRecordSoundRef);
+
+// // 立即更新UI状态
+// setIsRecording(true);
+// setRecordingDuration(0);
+// isCancelledRef.current = false;
+// recordingStartTimeRef.current = Date.now();
+
+// stopAllAudio();
+
+// // 使用react-audio-voice-recorder开始录音
+// recorderControls.startRecording();
+
+// // 立即开始计时
+// recordingTimerRef.current = setInterval(() => {
+// setRecordingDuration((prev) => prev + 1);
+// }, 1000);
+
+// Toast.show("开始录音...");
+// } catch (error) {
+// console.error("开始录音失败:", error);
+// resetRecordingState();
+
+// if (
+// error.name === "NotAllowedError" ||
+// error.name === "PermissionDeniedError"
+// ) {
+// setHasPermission(false);
+// if (!dialogShowing) {
+// showPermissionDialog();
+// }
+// } else {
+// Toast.show(`录音失败: ${error.message || "未知错误"}`);
+// }
+// }
+// }, [
+// hasPermission,
+// isInitialized,
+// recorderControls,
+// checkMicrophonePermission,
+// dialogShowing,
+// showPermissionDialog,
+// ]);
+
+// const stopRecording = useCallback(() => {
+// console.log("=== 停止录音并发送 ===");
+
+// if (!isRecordingRef.current) {
+// console.log("未在录音,忽略");
+// return;
+// }
+
+// try {
+// // 震动反馈 - 发送时的震动模式:短-停-短
+// vibrate([100, 50, 100]);
+
+// // 立即更新UI状态
+// setIsRecording(false);
+
+// if (recordingTimerRef.current) {
+// clearInterval(recordingTimerRef.current);
+// recordingTimerRef.current = undefined;
+// }
+
+// // 停止录音并触发onRecordingComplete
+// recorderControls.stopRecording();
+// } catch (error) {
+// console.error("停止录音失败:", error);
+// resetRecordingState();
+// Toast.show("停止录音失败");
+// }
+// }, [recorderControls]);
+
+// // 在发送时检查录音时长
+// const onRecordingComplete = useCallback(
+// (blob: Blob) => {
+// console.log("=== 录音完成回调 ===");
+// console.log("录音完成状态:", {
+// blobSize: blob.size,
+// isCancelled: isCancelledRef.current,
+// recordingDuration,
+// });
+
+// // 如果被取消,直接忽略,不做任何处理
+// if (isCancelledRef.current) {
+// console.log("录音被取消,完全忽略结果");
+// return;
+// }
+
+// // 计算实际录音时长
+// const actualDuration = Math.floor(
+// (Date.now() - recordingStartTimeRef.current) / 1000
+// );
+// const finalDuration = Math.max(recordingDuration, actualDuration);
+
+// console.log("录音完成,准备发送:", {
+// counterDuration: recordingDuration,
+// actualDuration,
+// finalDuration,
+// });
+
+// // 在这里检查录音时长 - 发送时拦截
+// if (finalDuration < 1) {
+// console.log("录音时间太短,拒绝发送");
+// Toast.show("录音时间太短,请重新录音");
+// setRecordingDuration(0);
+// return;
+// }
+
+// // 检查blob有效性
+// if (!blob || blob.size === 0) {
+// console.log("录音数据无效,拒绝发送");
+// Toast.show("录音数据无效,请重新录音");
+// setRecordingDuration(0);
+// return;
+// }
+
+// // 通过所有检查,发送消息
+// // 播放发送成功音效
+// playSound(sendSoundRef);
+
+// // 发送成功的震动反馈 - 长震动表示成功
+// vibrate(200);
+
+// const audioUrl = URL.createObjectURL(blob);
+// const newMessage: Message = {
+// id: Date.now().toString(),
+// type: "voice",
+// audioUrl,
+// duration: finalDuration,
+// timestamp: Date.now(),
+// isTranslating: true,
+// };
+
+// console.log("发送语音消息:", newMessage);
+// setMessages((prev) => [...prev, newMessage]);
+// setRecordingDuration(0);
+
+// setTimeout(() => {
+// translateAudio(newMessage.id, blob);
+// }, 1000);
+
+// Toast.show("语音已发送");
+// },
+// [recordingDuration]
+// );
+
+// const cancelRecording = useCallback(() => {
+// console.log("=== 取消录音 ===");
+
+// // 取消时的震动反馈 - 三次短震动表示取消
+// vibrate([50, 50, 50, 50, 50]);
+
+// // 立即设置取消标志 - 这是最重要的
+// isCancelledRef.current = true;
+
+// // 立即显示取消提示
+// Toast.show("已取消录音");
+
+// // 直接重置状态,不调用recorderControls.stopRecording()
+// setIsRecording(false);
+// setRecordingDuration(0);
+// isRecordingRef.current = false;
+
+// if (recordingTimerRef.current) {
+// clearInterval(recordingTimerRef.current);
+// recordingTimerRef.current = undefined;
+// }
+// }, []);
+
+// const handleRecordButtonClick = useCallback(
+// (e: React.MouseEvent) => {
+// e.preventDefault();
+// e.stopPropagation();
+
+// console.log("=== 录音按钮点击 ===");
+
+// if (permissionChecking) {
+// console.log("权限检查中,忽略点击");
+// return;
+// }
+
+// if (isRecordingRef.current) {
+// stopRecording();
+// } else {
+// startRecording();
+// }
+// },
+// [permissionChecking, startRecording, stopRecording]
+// );
+
+// const translateAudio = async (messageId: string, audioBlob: Blob) => {
+// try {
+// const translatedText = await mockTranslateAudio(audioBlob);
+
+// setMessages((prev) =>
+// prev.map((msg) =>
+// msg.id === messageId
+// ? { ...msg, translatedText, isTranslating: false }
+// : msg
+// )
+// );
+// } catch (error) {
+// console.error("翻译失败:", error);
+// Toast.show("翻译失败,请重试");
+
+// setMessages((prev) =>
+// prev.map((msg) =>
+// msg.id === messageId
+// ? {
+// ...msg,
+// isTranslating: false,
+// translatedText: "翻译失败,请重试",
+// }
+// : msg
+// )
+// );
+// }
+// };
+
+// const mockTranslateAudio = async (audioBlob: Blob): Promise => {
+// return new Promise((resolve) => {
+// setTimeout(() => {
+// const mockTranslations = [
+// "汪汪汪!我饿了,想吃东西!🍖",
+// "喵喵~我想要抱抱!🤗",
+// "我想出去玩耍!🎾",
+// "我很开心!😊",
+// "我有点害怕...😰",
+// "我想睡觉了~😴",
+// "主人,陪我玩一会儿吧!🎮",
+// "我想喝水了💧",
+// "外面有什么声音?👂",
+// "我爱你,主人!❤️",
+// ];
+// const randomTranslation =
+// mockTranslations[Math.floor(Math.random() * mockTranslations.length)];
+// resolve(randomTranslation);
+// }, 2000 + Math.random() * 2000);
+// });
+// };
+
+// const playAudio = (messageId: string, audioUrl: string) => {
+// if (isRecording) {
+// Toast.show("录音中,无法播放音频");
+// return;
+// }
+
+// if (currentPlayingId === messageId) {
+// if (audioRefs.current[messageId]) {
+// audioRefs.current[messageId].pause();
+// audioRefs.current[messageId].currentTime = 0;
+// }
+// setCurrentPlayingId(null);
+// setMessages((prev) =>
+// prev.map((msg) =>
+// msg.id === messageId ? { ...msg, isPlaying: false } : msg
+// )
+// );
+// return;
+// }
+
+// stopAllAudio();
+
+// if (!audioRefs.current[messageId]) {
+// audioRefs.current[messageId] = new Audio(audioUrl);
+// }
+
+// const audio = audioRefs.current[messageId];
+// audio.currentTime = 0;
+
+// audio.onended = () => {
+// setCurrentPlayingId(null);
+// setMessages((prev) =>
+// prev.map((msg) =>
+// msg.id === messageId ? { ...msg, isPlaying: false } : msg
+// )
+// );
+// };
+
+// audio.onerror = (error) => {
+// console.error("音频播放错误:", error);
+// Toast.show("音频播放失败");
+// setCurrentPlayingId(null);
+// setMessages((prev) =>
+// prev.map((msg) =>
+// msg.id === messageId ? { ...msg, isPlaying: false } : msg
+// )
+// );
+// };
+
+// audio
+// .play()
+// .then(() => {
+// setCurrentPlayingId(messageId);
+// setMessages((prev) =>
+// prev.map((msg) =>
+// msg.id === messageId
+// ? { ...msg, isPlaying: true }
+// : { ...msg, isPlaying: false }
+// )
+// );
+// })
+// .catch((error) => {
+// console.error("音频播放失败:", error);
+// Toast.show("音频播放失败");
+// });
+// };
+
+// const formatDuration = (seconds: number) => {
+// const mins = Math.floor(seconds / 60);
+// const secs = seconds % 60;
+// return `${mins}:${secs.toString().padStart(2, "0")}`;
+// };
+
+// const formatTime = (timestamp: number) => {
+// return new Date(timestamp).toLocaleTimeString("zh-CN", {
+// hour: "2-digit",
+// minute: "2-digit",
+// });
+// };
+
+// return (
+//
+// {/* 调试信息 */}
+// {process.env.NODE_ENV === "development" && (
+//
+//
初始化中: {initializingRef.current ? "⏳" : "✅"}
+//
已初始化: {initializedRef.current ? "✅" : "❌"}
+//
状态初始化: {isInitialized ? "✅" : "❌"}
+//
+// 权限: {hasPermission === null ? "⏳" : hasPermission ? "✅" : "❌"}
+//
+//
录音: {isRecording ? "🔴" : "⚪"}
+//
权限检查: {permissionChecking ? "⏳" : "✅"}
+//
弹窗: {dialogShowing ? "📱" : "❌"}
+//
已取消: {isCancelledRef.current ? "✅" : "❌"}
+//
录音时长: {recordingDuration}s
+//
震动支持: {"vibrate" in navigator ? "✅" : "❌"}
+//
+// )}
+
+// {/* 头部 */}
+//
+//
🐾
+//
+//
我的宠物
+//
+// {initializingRef.current
+// ? "初始化中..."
+// : !isInitialized
+// ? "等待初始化..."
+// : permissionChecking
+// ? "检查权限中..."
+// : hasPermission === null
+// ? "权限状态未知"
+// : hasPermission
+// ? "宠物语音翻译器"
+// : "需要麦克风权限"}
+//
+//
+//
+
+// {/* 消息列表 */}
+//
+// {messages.length === 0 ? (
+//
+//
🎤
+//
+// {initializingRef.current
+// ? "正在初始化..."
+// : !isInitialized
+// ? "等待初始化..."
+// : permissionChecking
+// ? "正在检查权限..."
+// : hasPermission === null
+// ? "权限状态未知"
+// : !hasPermission
+// ? "请授权麦克风权限"
+// : "点击下方按钮开始录音"}
+//
+//
听听你的宠物想说什么~
+//
+// ) : (
+// messages.map((message) => (
+//
+//
+//
🐾
+//
+//
+//
playAudio(message.id, message.audioUrl)}
+// >
+//
+// {message.isPlaying ? (
+//
+//
+//
+//
+//
+// ) : (
+//
+// )}
+//
+//
+// {formatDuration(message.duration)}
+//
+//
+//
+
+// {message.isTranslating ? (
+//
+//
+//
翻译中
+//
+//
+//
+//
+//
+//
+//
+// ) : message.translatedText ? (
+//
+//
🐾 宠物说:
+//
+// {message.translatedText}
+//
+//
+// ) : null}
+
+//
+// {formatTime(message.timestamp)}
+//
+//
+//
+//
+// ))
+// )}
+//
+//
+
+// {/* 录音控制区域 */}
+//
+//
+
+//
+// {isRecording && (
+//
+//
+//
+//
录音中 {formatDuration(recordingDuration)}
+//
+//
再次点击完成录音并发送
+//
+// )}
+
+//
+//
+// {isRecording && (
+//
+//
+//
+// )}
+
+//
+// {permissionChecking ? (
+//
+//
+//
+//
+//
+// ) : isRecording ? (
+//
+//
+//
+//
+//
+// ) : (
+//
+// )}
+//
+//
+
+// {!isRecording && (
+//
+// {initializingRef.current
+// ? "初始化中..."
+// : !isInitialized
+// ? "等待初始化..."
+// : permissionChecking
+// ? "检查权限中..."
+// : !hasPermission
+// ? "需要麦克风权限"
+// : "点击录音"}
+//
+// )}
+//
+//
+//
+//
+// );
+// };
+
+// export default PetVoiceTranslator;
diff --git a/src/component/qr-scanner/index.tsx b/src/component/qr-scanner/index.tsx
new file mode 100644
index 0000000..3aebcde
--- /dev/null
+++ b/src/component/qr-scanner/index.tsx
@@ -0,0 +1,88 @@
+import React, {useRef, useEffect, useState} from 'react';
+import jsQR from 'jsqr';
+
+export interface QRScannerProps {
+ width?: number;
+ height?: number;
+ onScan: (data: string) => void;
+}
+
+/**
+ * 二维码扫描组件
+ * @param width 画布宽度
+ * @param height 画布高度
+ * @param onScan 扫描成功的回调函数
+ * @constructor QRScanner
+ */
+const QRScanner: React.FC = ({width = 300, height = 300, onScan}) => {
+ const videoRef = useRef(null);
+ const canvasRef = useRef(null);
+ const [isScanning, setIsScanning] = useState(true);
+ const [scanSuccess, setScanSuccess] = useState(false); // 新的状态变量
+ let streamRef: MediaStream | null = null; // 存储摄像头流的引用
+
+ useEffect(() => {
+ navigator.mediaDevices.getUserMedia({video: {facingMode: "environment"}})
+ .then(stream => {
+ streamRef = stream; // 存储对摄像头流的引用
+ if (videoRef.current) {
+ videoRef.current.srcObject = stream;
+ videoRef.current.addEventListener('loadedmetadata', () => {
+ videoRef.current?.play().then(() => {
+ requestAnimationFrame(tick);
+ }).catch(err => console.error("Error playing video: ", err));
+ });
+ }
+ }).catch(err => {
+ console.error("Error accessing media devices: ", err);
+ setIsScanning(false);
+ });
+ }, []);
+ const tick = () => {
+ // 如果已经成功扫描,就不再继续执行tick
+ if (!isScanning) return;
+
+ if (videoRef.current && canvasRef.current) {
+ if (videoRef.current.readyState === videoRef.current.HAVE_ENOUGH_DATA) {
+ let video = videoRef.current;
+ let canvas = canvasRef.current;
+ let ctx = canvas.getContext('2d');
+
+ if (ctx) {
+ canvas.height = height;
+ canvas.width = width;
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
+
+ let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ let code = jsQR(imageData.data, imageData.width, imageData.height);
+
+ if (code) {
+ onScan(code.data); // 扫码成功
+ setIsScanning(false); // 更新状态为不再扫描
+ setScanSuccess(true); // 显示扫描成功的蒙层
+ if (streamRef) {
+ let tracks = streamRef.getTracks();
+ tracks.forEach(track => track.stop()); // 关闭摄像头
+ }
+ return; // 直接返回,避免再次调用 requestAnimationFrame
+ }
+ }
+ }
+ requestAnimationFrame(tick);
+ }
+ };
+ return (
+
+
+
+ {scanSuccess &&
识别成功!
} {/* 显示识别成功的蒙层 */}
+
+ );
+}
+
+export default QRScanner;
diff --git a/src/component/voiceIcon/index.less b/src/component/voiceIcon/index.less
new file mode 100644
index 0000000..3e40484
--- /dev/null
+++ b/src/component/voiceIcon/index.less
@@ -0,0 +1,69 @@
+/* VoiceIcon.css */
+.voice-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ cursor: pointer;
+ gap: 2px;
+}
+.audio-recorder {
+ display: none !important;
+}
+
+.wave {
+ width: 1px;
+ height: 13px;
+ background-color: rgba(0, 0, 0, 0.6);
+ border-radius: 2px;
+ transition: all 0.3s ease;
+}
+
+.wave1 {
+ height: 4px;
+}
+
+.wave2 {
+ height: 13px;
+}
+
+.wave3 {
+ height: 4px;
+}
+
+.wave3 {
+ height: 8px;
+}
+.wave4 {
+ height: 4px;
+}
+
+/* 播放动画 */
+.voice-icon.playing .wave {
+ animation: voice-wave 1.2s ease-in-out infinite;
+}
+
+.voice-icon.playing .wave1 {
+ animation-delay: 0s;
+}
+
+.voice-icon.playing .wave2 {
+ animation-delay: 0.2s;
+}
+
+.voice-icon.playing .wave3 {
+ animation-delay: 0.4s;
+}
+
+@keyframes voice-wave {
+ 0%,
+ 100% {
+ transform: scaleY(0.3);
+ opacity: 0.5;
+ }
+ 50% {
+ transform: scaleY(1);
+ opacity: 1;
+ }
+}
diff --git a/src/component/voiceIcon/index.tsx b/src/component/voiceIcon/index.tsx
new file mode 100644
index 0000000..09fb7a4
--- /dev/null
+++ b/src/component/voiceIcon/index.tsx
@@ -0,0 +1,22 @@
+import React, { useCallback, useState } from "react";
+import "./index.less";
+
+const VoiceIcon = (props: { isPlaying: boolean; onChange?: () => void }) => {
+ const { isPlaying = false } = props;
+ const onChange = useCallback(() => {
+ props.onChange?.();
+ }, [isPlaying]);
+ return (
+
+ );
+};
+
+export default React.memo(VoiceIcon);
diff --git a/src/component/xpopup/index.less b/src/component/xpopup/index.less
new file mode 100644
index 0000000..b5b92ec
--- /dev/null
+++ b/src/component/xpopup/index.less
@@ -0,0 +1,11 @@
+.xpopup {
+ .adm-popup-body {
+ background-color: rgba(245, 245, 245, 1);
+ padding: 16px;
+ .header {
+ font-size: 16px;
+ display: flex;
+ justify-content: space-between;
+ }
+ }
+}
diff --git a/src/component/xpopup/index.tsx b/src/component/xpopup/index.tsx
new file mode 100644
index 0000000..723cc11
--- /dev/null
+++ b/src/component/xpopup/index.tsx
@@ -0,0 +1,28 @@
+import { Divider, Popup } from "antd-mobile";
+import { CloseOutline } from "antd-mobile-icons";
+import "./index.less";
+
+interface DefinedProps {
+ visible: boolean;
+ title: string;
+ children: React.ReactNode;
+ onClose: () => void;
+}
+function XPopup(props: DefinedProps) {
+ const { visible, title, children, onClose } = props;
+
+ return (
+
+
+
{title}
+
+
+
+
+
+ {children}
+
+ );
+}
+
+export default XPopup;
diff --git a/src/composables/authorization.ts b/src/composables/authorization.ts
new file mode 100644
index 0000000..b622afa
--- /dev/null
+++ b/src/composables/authorization.ts
@@ -0,0 +1,42 @@
+import {useState, useEffect} from 'react';
+
+export const STORAGE_AUTHORIZE_KEY = 'token';
+
+export const useAuthorization = () => {
+ const [currentToken, setCurrentToken] = useState(localStorage.getItem(STORAGE_AUTHORIZE_KEY));
+
+ // 同步 localStorage 变更
+ useEffect(() => {
+ const handleStorageChange = (event: StorageEvent) => {
+ if (event.key === STORAGE_AUTHORIZE_KEY) {
+ setCurrentToken(localStorage.getItem(STORAGE_AUTHORIZE_KEY));
+ }
+ };
+
+ window.addEventListener('storage', handleStorageChange);
+ return () => {
+ window.removeEventListener('storage', handleStorageChange);
+ };
+ }, []);
+
+ const login = (token: string) => {
+ localStorage.setItem(STORAGE_AUTHORIZE_KEY, token);
+ setCurrentToken(token);
+ };
+
+ const logout = () => {
+ localStorage.removeItem(STORAGE_AUTHORIZE_KEY);
+ setCurrentToken(null);
+ };
+
+ const isLogin = () => {
+ return !!currentToken;
+ };
+
+ return {
+ token: currentToken,
+ login,
+ logout,
+ isLogin
+ };
+};
diff --git a/src/composables/language.ts b/src/composables/language.ts
new file mode 100644
index 0000000..9c808aa
--- /dev/null
+++ b/src/composables/language.ts
@@ -0,0 +1,21 @@
+import {setDefaultConfig} from 'antd-mobile';
+import enUS from 'antd-mobile/es/locales/en-US';
+import zhCN from 'antd-mobile/es/locales/zh-CN';
+
+function changeLanguage(language: string) {
+ let locale;
+ switch (language) {
+ case 'zh_CN':
+ locale = enUS;
+ break;
+ case 'en_US':
+ locale = zhCN;
+ break;
+ default:
+ locale = enUS; // 或者是你的默认语言
+ }
+
+ setDefaultConfig({locale: locale});
+}
+
+export default changeLanguage;
diff --git a/src/enum/http-enum.ts b/src/enum/http-enum.ts
new file mode 100644
index 0000000..03938d1
--- /dev/null
+++ b/src/enum/http-enum.ts
@@ -0,0 +1,21 @@
+/**
+ * @description: request method
+ */
+export enum RequestEnum {
+ GET = 'GET',
+ POST = 'POST',
+ PUT = 'PUT',
+ DELETE = 'DELETE',
+}
+
+/**
+ * @description: contentType
+ */
+export enum ContentTypeEnum {
+ // json
+ JSON = 'application/json;charset=UTF-8',
+ // form-data qs
+ FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
+ // form-data upload
+ FORM_DATA = 'multipart/form-data;charset=UTF-8',
+}
diff --git a/src/hooks/i18n.ts b/src/hooks/i18n.ts
new file mode 100644
index 0000000..ac3cd8c
--- /dev/null
+++ b/src/hooks/i18n.ts
@@ -0,0 +1,12 @@
+import zhCN from '@/locales/zh_CN.json'
+import enUS from '@/locales/en_US.json'
+import {useI18nStore} from '@/store/i18n';
+
+export default function useI18n() {
+ const {lang} = useI18nStore();
+ const locales = lang === 'en_US' ? enUS : zhCN;
+
+ return (name: string) => {
+ return locales[name as keyof typeof locales] || name;
+ }
+}
diff --git a/src/hooks/location.ts b/src/hooks/location.ts
new file mode 100644
index 0000000..f549fc2
--- /dev/null
+++ b/src/hooks/location.ts
@@ -0,0 +1,60 @@
+import {useState, useEffect} from 'react';
+import useI18n from "@/hooks/i18n";
+
+// 定义位置坐标的类型
+export interface Coordinates {
+ lat: number;
+ lng: number;
+}
+
+// 定义返回的位置状态的类型
+export interface LocationState {
+ loaded: boolean;
+ coordinates: Coordinates | null;
+}
+
+// 定义错误状态的类型
+export type ErrorState = string | null;
+
+export const useLocation = (): { location: LocationState; error: ErrorState } => {
+ const t = useI18n();
+
+ // 用于存储位置信息和加载状态的状态
+ const [location, setLocation] = useState({
+ loaded: false,
+ coordinates: null,
+ });
+
+ // 用于存储任何错误消息的状态
+ const [error, setError] = useState(null);
+
+ // 地理位置成功处理函数
+ const onSuccess = (location: GeolocationPosition) => {
+ setLocation({
+ loaded: true,
+ coordinates: {
+ lat: location.coords.latitude,
+ lng: location.coords.longitude,
+ },
+ });
+ };
+
+ // 地理位置错误处理函数
+ const onError = (error: GeolocationPositionError) => {
+ setError(error.message);
+ };
+
+ // 使用 useEffect 在组件挂载时执行地理位置请求
+ useEffect(() => {
+ // 检查浏览器是否支持地理位置
+ if (!navigator.geolocation) {
+ setError(t('hooks.location.unsupported'));
+ return;
+ }
+
+ // 请求用户的当前位置
+ navigator.geolocation.getCurrentPosition(onSuccess, onError);
+ }, []);
+
+ return {location, error};
+};
diff --git a/src/hooks/session.ts b/src/hooks/session.ts
new file mode 100644
index 0000000..5595df5
--- /dev/null
+++ b/src/hooks/session.ts
@@ -0,0 +1,28 @@
+import {useState, useEffect} from 'react';
+import isEqual from 'lodash.isequal';
+
+function useSessionStorage(key: string, initialValue: T): [T, (value: T) => void] {
+ // 初始化状态
+ const [storedValue, setStoredValue] = useState(() => {
+ const item = sessionStorage.getItem(key);
+ if (item !== null) {
+ // 如果 sessionStorage 中有数据,则使用现有数据
+ return JSON.parse(item);
+ } else {
+ // 当 sessionStorage 中没有相应的键时,使用 initialValue 初始化,并写入 sessionStorage
+ sessionStorage.setItem(key, JSON.stringify(initialValue));
+ return initialValue;
+ }
+ });
+
+ // 监听并保存变化到 sessionStorage
+ useEffect(() => {
+ if (!isEqual(JSON.parse(sessionStorage.getItem(key) || 'null'), storedValue)) {
+ sessionStorage.setItem(key, JSON.stringify(storedValue));
+ }
+ }, [key, storedValue]);
+
+ return [storedValue, setStoredValue];
+}
+
+export default useSessionStorage;
diff --git a/src/hooks/useAudioControl.ts b/src/hooks/useAudioControl.ts
new file mode 100644
index 0000000..15245dd
--- /dev/null
+++ b/src/hooks/useAudioControl.ts
@@ -0,0 +1,55 @@
+// hooks/useAudioControl.ts
+import { useEffect, useState } from "react";
+import AudioManager from "../utils/audioManager";
+
+interface UseAudioControlReturn {
+ currentPlayingId: string | null;
+ stopAllAudio: () => void;
+ pauseAllAudio: () => void;
+ getAudioStates: () => Record<
+ string,
+ {
+ isPlaying: boolean;
+ duration: number;
+ currentTime: number;
+ }
+ >;
+}
+
+export const useAudioControl = (): UseAudioControlReturn => {
+ const [currentPlayingId, setCurrentPlayingId] = useState(null);
+ const audioManager = AudioManager.getInstance();
+
+ useEffect(() => {
+ // 定期检查当前播放状态
+ const interval = setInterval(() => {
+ const currentId = audioManager.getCurrentAudioId();
+ setCurrentPlayingId(currentId);
+ }, 500);
+
+ return () => {
+ clearInterval(interval);
+ };
+ }, [audioManager]);
+
+ const stopAllAudio = () => {
+ audioManager.stopCurrent();
+ setCurrentPlayingId(null);
+ };
+
+ const pauseAllAudio = () => {
+ audioManager.pauseCurrent();
+ setCurrentPlayingId(null);
+ };
+
+ const getAudioStates = () => {
+ return audioManager.getAudioStates();
+ };
+
+ return {
+ currentPlayingId,
+ stopAllAudio,
+ pauseAllAudio,
+ getAudioStates,
+ };
+};
diff --git a/src/hooks/useFileUpload.ts b/src/hooks/useFileUpload.ts
new file mode 100644
index 0000000..a2f61f8
--- /dev/null
+++ b/src/hooks/useFileUpload.ts
@@ -0,0 +1,198 @@
+// hooks/useFileUpload.ts (更新)
+import { useState, useCallback } from "react";
+import {
+ UploadConfig,
+ UploadProgress,
+ UploadResponse,
+ VoiceUploadStatus,
+} from "../types/upload";
+
+interface UseFileUploadReturn {
+ uploadStatus: VoiceUploadStatus;
+ uploadFile: (
+ file: Blob,
+ fileName: string,
+ config: UploadConfig
+ ) => Promise;
+ resetUpload: () => void;
+}
+
+export const useFileUpload = (): UseFileUploadReturn => {
+ const [uploadStatus, setUploadStatus] = useState({
+ status: "idle",
+ });
+
+ // 检测文件类型并转换文件名
+ const getFileExtension = (mimeType: string): string => {
+ const mimeToExt: Record = {
+ "audio/webm": ".webm",
+ "audio/mp4": ".m4a",
+ "audio/aac": ".aac",
+ "audio/wav": ".wav",
+ "audio/ogg": ".ogg",
+ "audio/mpeg": ".mp3",
+ };
+
+ // 处理带codecs的MIME类型
+ const baseMimeType = mimeType.split(";")[0];
+ return mimeToExt[baseMimeType] || ".webm";
+ };
+
+ const uploadFile = useCallback(
+ async (
+ file: Blob,
+ fileName: string,
+ config: UploadConfig
+ ): Promise => {
+ // 检查文件大小
+ if (config.maxFileSize && file.size > config.maxFileSize) {
+ const error = `文件大小超过限制 (${Math.round(
+ config.maxFileSize / 1024 / 1024
+ )}MB)`;
+ setUploadStatus({
+ status: "error",
+ error,
+ });
+ throw new Error(error);
+ }
+
+ // 更宽松的文件类型检查,支持iOS格式
+ const allowedTypes = config.allowedTypes || [
+ "audio/webm",
+ "audio/mp4",
+ "audio/aac",
+ "audio/wav",
+ "audio/ogg",
+ "audio/mpeg",
+ ];
+
+ const baseMimeType = file.type.split(";")[0];
+ const isTypeAllowed = allowedTypes.some(
+ (type) => baseMimeType === type || baseMimeType === type.split(";")[0]
+ );
+
+ if (!isTypeAllowed) {
+ console.warn(`文件类型 ${file.type} 不在允许列表中,但继续上传`);
+ }
+
+ // 根据实际MIME类型调整文件名
+ const extension = getFileExtension(file.type);
+ const adjustedFileName = fileName.replace(/\.[^/.]+$/, "") + extension;
+
+ setUploadStatus({
+ status: "uploading",
+ progress: { loaded: 0, total: file.size, percentage: 0 },
+ });
+
+ return new Promise((resolve, reject) => {
+ const formData = new FormData();
+ formData.append(config.fieldName || "file", file, adjustedFileName);
+
+ // 添加额外的元数据
+ formData.append("fileName", adjustedFileName);
+ formData.append("originalFileName", fileName);
+ formData.append("fileSize", file.size.toString());
+ formData.append("fileType", file.type);
+ formData.append("uploadTime", new Date().toISOString());
+ formData.append("userAgent", navigator.userAgent);
+
+ const xhr = new XMLHttpRequest();
+
+ // 上传进度监听
+ xhr.upload.addEventListener("progress", (event) => {
+ if (event.lengthComputable) {
+ const progress: UploadProgress = {
+ loaded: event.loaded,
+ total: event.total,
+ percentage: Math.round((event.loaded / event.total) * 100),
+ };
+
+ setUploadStatus({
+ status: "uploading",
+ progress,
+ });
+ }
+ });
+
+ // 上传完成监听
+ xhr.addEventListener("load", () => {
+ try {
+ const response: UploadResponse = JSON.parse(xhr.responseText);
+
+ if (xhr.status >= 200 && xhr.status < 300 && response.success) {
+ setUploadStatus({
+ status: "success",
+ response,
+ progress: {
+ loaded: file.size,
+ total: file.size,
+ percentage: 100,
+ },
+ });
+ resolve(response);
+ } else {
+ const error = response.error || `上传失败: ${xhr.status}`;
+ setUploadStatus({
+ status: "error",
+ error,
+ });
+ reject(new Error(error));
+ }
+ } catch (parseError) {
+ const error = "服务器响应格式错误";
+ setUploadStatus({
+ status: "error",
+ error,
+ });
+ reject(new Error(error));
+ }
+ });
+
+ // 上传错误监听
+ xhr.addEventListener("error", () => {
+ const error = "网络错误,上传失败";
+ setUploadStatus({
+ status: "error",
+ error,
+ });
+ reject(new Error(error));
+ });
+
+ // 上传中断监听
+ xhr.addEventListener("abort", () => {
+ const error = "上传已取消";
+ setUploadStatus({
+ status: "error",
+ error,
+ });
+ reject(new Error(error));
+ });
+
+ // 设置请求头
+ const headers = {
+ "X-Requested-With": "XMLHttpRequest",
+ ...config.headers,
+ };
+
+ Object.entries(headers).forEach(([key, value]) => {
+ xhr.setRequestHeader(key, value);
+ });
+
+ // 发送请求
+ xhr.open(config.method || "POST", config.url);
+ xhr.send(formData);
+ });
+ },
+ []
+ );
+
+ const resetUpload = useCallback(() => {
+ setUploadStatus({ status: "idle" });
+ }, []);
+
+ return {
+ uploadStatus,
+ uploadFile,
+ resetUpload,
+ };
+};
diff --git a/src/hooks/usePetTranslator.ts b/src/hooks/usePetTranslator.ts
new file mode 100644
index 0000000..71438f1
--- /dev/null
+++ b/src/hooks/usePetTranslator.ts
@@ -0,0 +1,153 @@
+// hooks/usePetTranslator.ts
+import { useState, useCallback } from "react";
+import { VoiceMessage, ChatMessage, PetProfile } from "../types/chat";
+
+interface UsePetTranslatorReturn {
+ messages: ChatMessage[];
+ currentPet: PetProfile;
+ translateVoice: (voiceMessage: VoiceMessage) => Promise;
+ addMessage: (message: ChatMessage) => void;
+ updateMessage: (messageId: string, updatedMessage: ChatMessage) => void; // 新增
+ clearMessages: () => void;
+ setPet: (pet: PetProfile) => void;
+}
+
+export const usePetTranslator = (): UsePetTranslatorReturn => {
+ const [messages, setMessages] = useState([]);
+ const [currentPet, setCurrentPet] = useState({
+ name: "小汪",
+ avatar: "🐕",
+ species: "dog",
+ personality: "活泼可爱",
+ });
+
+ // 模拟翻译API调用
+ const translateVoice = useCallback(
+ async (voiceMessage: VoiceMessage): Promise => {
+ // 更新消息状态为翻译中
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === voiceMessage.id ? { ...msg, translating: true } : msg
+ )
+ );
+
+ try {
+ // 模拟API调用延迟
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+
+ // 模拟翻译结果
+ const translations = {
+ dog: [
+ "主人,我饿了!给我点好吃的吧!🍖",
+ "我想出去玩耍!带我去公园吧!🏃♂️",
+ "我爱你,主人!你是最好的!❤️",
+ "我想睡觉了,陪我一起休息吧!😴",
+ "有陌生人!我要保护你!🛡️",
+ "今天天气真好,我们去散步吧!🌞",
+ "我想和其他狗狗玩耍!🐕🦺",
+ "主人回来了!我好开心!🎉",
+ ],
+ cat: [
+ "铲屎官,快来伺候本喵!😼",
+ "我要晒太阳,别打扰我!☀️",
+ "给我准备小鱼干!现在就要!🐟",
+ "我心情不好,离我远点!😾",
+ "勉强让你摸摸我的头吧!😸",
+ "这个位置是我的,你不能坐!🛋️",
+ "我饿了,但我不会告诉你的!🙄",
+ "今天我心情好,可以陪你玩一会!😺",
+ ],
+ bird: [
+ "早上好!新的一天开始了!🌅",
+ "我想唱歌给你听!🎵",
+ "给我一些种子吧!我饿了!🌱",
+ "外面的世界真美好!🌳",
+ "我想和你一起飞翔!🕊️",
+ "今天的阳光真温暖!☀️",
+ "我学会了新的歌曲!🎶",
+ "陪我聊聊天吧!💬",
+ ],
+ other: [
+ "我想和你交流!👋",
+ "照顾好我哦!💕",
+ "我很开心!😊",
+ "陪我玩一会儿吧!🎾",
+ "我需要你的关爱!🤗",
+ "今天过得真愉快!😄",
+ "我想要你的注意!👀",
+ "我们是最好的朋友!🤝",
+ ],
+ };
+
+ const speciesTranslations =
+ translations[currentPet.species] || translations.other;
+ const randomTranslation =
+ speciesTranslations[
+ Math.floor(Math.random() * speciesTranslations.length)
+ ];
+
+ // 更新翻译结果
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === voiceMessage.id
+ ? { ...msg, translation: randomTranslation, translating: false }
+ : msg
+ )
+ );
+
+ // 添加宠物回复
+ const petReply: ChatMessage = {
+ id: `pet_${Date.now()}`,
+ type: "text",
+ content: `${currentPet.name}说:${randomTranslation}`,
+ sender: "pet",
+ timestamp: Date.now(),
+ };
+
+ setMessages((prev) => [...prev, petReply]);
+ } catch (error) {
+ console.error("翻译失败:", error);
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === voiceMessage.id
+ ? { ...msg, translation: "翻译失败,请重试", translating: false }
+ : msg
+ )
+ );
+ }
+ },
+ [currentPet]
+ );
+
+ const addMessage = useCallback((message: ChatMessage): void => {
+ setMessages((prev) => [...prev, message]);
+ }, []);
+
+ // 新增:更新消息方法
+ const updateMessage = useCallback(
+ (messageId: string, updatedMessage: ChatMessage): void => {
+ setMessages((prev) =>
+ prev.map((msg) => (msg.id === messageId ? updatedMessage : msg))
+ );
+ },
+ []
+ );
+
+ const clearMessages = useCallback((): void => {
+ setMessages([]);
+ }, []);
+
+ const setPet = useCallback((pet: PetProfile): void => {
+ setCurrentPet(pet);
+ }, []);
+
+ return {
+ messages,
+ currentPet,
+ translateVoice,
+ addMessage,
+ updateMessage, // 导出新方法
+ clearMessages,
+ setPet,
+ };
+};
diff --git a/src/hooks/useVoiceRecorder.ts b/src/hooks/useVoiceRecorder.ts
new file mode 100644
index 0000000..400df19
--- /dev/null
+++ b/src/hooks/useVoiceRecorder.ts
@@ -0,0 +1,125 @@
+// hooks/useVoiceRecorder.ts (使用新的录音工具类)
+import { useState, useRef, useCallback } from "react";
+import { UniversalAudioRecorder } from "../utils/audioRecorder";
+
+interface UseVoiceRecorderReturn {
+ isRecording: boolean;
+ recordingTime: number;
+ isPaused: boolean;
+ startRecording: () => Promise;
+ stopRecording: () => Promise;
+ pauseRecording: () => void;
+ resumeRecording: () => void;
+ cancelRecording: () => void;
+}
+
+export const useVoiceRecorder = (): UseVoiceRecorderReturn => {
+ const [isRecording, setIsRecording] = useState(false);
+ const [isPaused, setIsPaused] = useState(false);
+ const [recordingTime, setRecordingTime] = useState(0);
+
+ const recorderRef = useRef(null);
+ const timerRef = useRef(null);
+
+ const startTimer = useCallback(() => {
+ timerRef.current = setInterval(() => {
+ if (recorderRef.current) {
+ setRecordingTime(recorderRef.current.getDuration());
+ }
+ }, 100);
+ }, []);
+
+ const stopTimer = useCallback(() => {
+ if (timerRef.current) {
+ clearInterval(timerRef.current);
+ timerRef.current = null;
+ }
+ }, []);
+
+ const startRecording = useCallback(async (): Promise => {
+ try {
+ const recorder = new UniversalAudioRecorder({
+ sampleRate: 16000,
+ channels: 1,
+ bitDepth: 16,
+ });
+
+ await recorder.start();
+ recorderRef.current = recorder;
+
+ setIsRecording(true);
+ setIsPaused(false);
+ setRecordingTime(0);
+ startTimer();
+ } catch (error) {
+ console.error("录音启动失败:", error);
+ throw error;
+ }
+ }, [startTimer]);
+
+ const stopRecording = useCallback(async (): Promise => {
+ if (!recorderRef.current || !isRecording) return null;
+
+ try {
+ const audioBlob = await recorderRef.current.stop();
+
+ setIsRecording(false);
+ setIsPaused(false);
+ setRecordingTime(0);
+ stopTimer();
+
+ recorderRef.current = null;
+ return audioBlob;
+ } catch (error) {
+ console.error("录音停止失败:", error);
+ return null;
+ }
+ }, [isRecording, stopTimer]);
+
+ const pauseRecording = useCallback((): void => {
+ if (!recorderRef.current || !isRecording || isPaused) return;
+
+ try {
+ recorderRef.current.pause();
+ setIsPaused(true);
+ stopTimer();
+ } catch (error) {
+ console.error("录音暂停失败:", error);
+ }
+ }, [isRecording, isPaused, stopTimer]);
+
+ const resumeRecording = useCallback((): void => {
+ if (!recorderRef.current || !isRecording || !isPaused) return;
+
+ try {
+ recorderRef.current.resume();
+ setIsPaused(false);
+ startTimer();
+ } catch (error) {
+ console.error("录音恢复失败:", error);
+ }
+ }, [isRecording, isPaused, startTimer]);
+
+ const cancelRecording = useCallback((): void => {
+ if (recorderRef.current) {
+ recorderRef.current.cancel();
+ recorderRef.current = null;
+ }
+
+ setIsRecording(false);
+ setIsPaused(false);
+ setRecordingTime(0);
+ stopTimer();
+ }, [stopTimer]);
+
+ return {
+ isRecording,
+ recordingTime,
+ isPaused,
+ startRecording,
+ stopRecording,
+ pauseRecording,
+ resumeRecording,
+ cancelRecording,
+ };
+};
diff --git a/src/http/axios-instance.ts b/src/http/axios-instance.ts
new file mode 100644
index 0000000..2bef525
--- /dev/null
+++ b/src/http/axios-instance.ts
@@ -0,0 +1,49 @@
+import Axios, {
+ AxiosError,
+ AxiosInstance as AxiosType,
+ AxiosResponse,
+ InternalAxiosRequestConfig
+} from 'axios';
+import {STORAGE_AUTHORIZE_KEY} from "@/composables/authorization.ts";
+
+export interface ResponseBody {
+ code: number;
+ data?: T;
+ msg: string;
+}
+
+async function requestHandler(config: InternalAxiosRequestConfig): Promise {
+ const token = localStorage.getItem(STORAGE_AUTHORIZE_KEY);
+ if (token) {
+ config.headers[STORAGE_AUTHORIZE_KEY] = token;
+ }
+ return config;
+}
+
+function responseHandler(response: AxiosResponse): AxiosResponse {
+ // 响应拦截器逻辑...
+ return response;
+}
+
+function errorHandler(error: AxiosError): Promise> {
+ // 错误处理逻辑...
+ return Promise.reject(error);
+}
+
+class AxiosInstance {
+ private readonly instance: AxiosType;
+
+ constructor(baseURL: string) {
+ this.instance = Axios.create({baseURL});
+
+ this.instance.interceptors.request.use(requestHandler, errorHandler);
+ this.instance.interceptors.response.use(responseHandler, errorHandler);
+ }
+
+ public getInstance(): AxiosType {
+ return this.instance;
+ }
+}
+
+const baseURL = import.meta.env.VITE_BASE_URL || 'http://127.0.0.1:8080';
+export const axiosInstance = new AxiosInstance(baseURL).getInstance();
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..06b9df1
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,103 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: "Roboto", sans-serif;
+}
+
+a,
+button,
+input,
+textarea {
+ -webkit-tap-highlight-color: transparent;
+}
+
+html {
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ font-size: 100%;
+}
+
+/* 小屏幕设备(如手机)*/
+@media (max-width: 600px) {
+ html {
+ font-size: 90%; /* 字体稍微小一点 */
+ }
+}
+
+/* 中等屏幕设备(如平板)*/
+@media (min-width: 601px) and (max-width: 1024px) {
+ html {
+ font-size: 100%; /* 标准大小 */
+ }
+}
+
+/* 大屏幕设备(如桌面)*/
+@media (min-width: 1025px) {
+ html {
+ font-size: 110%; /* 字体稍微大一点 */
+ }
+}
+
+* {
+ -webkit-backface-visibility: hidden;
+ -moz-backface-visibility: hidden;
+ -ms-backface-visibility: hidden;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
+
+img {
+ max-width: 100%;
+ height: auto;
+}
+
+.ec-navbar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ background-color: #fff;
+ border-bottom: 1px solid #e5e5e5;
+ -webkit-transition: all 0.3s;
+ transition: all 0.3s;
+}
+
+:root {
+ --primary-color: #ffc300;
+}
+
+:root:root {
+ /* --adm-color-primary: #FFC300;
+ --adm-color-success: #00b578;
+ --adm-color-warning: #ff8f1f;
+ --adm-color-danger: #ff3141;
+
+ --adm-color-white: #ffffff;
+ --adm-color-text: #333333;
+ --adm-color-text-secondary: #666666;
+ --adm-color-weak: #999999;
+ --adm-color-light: #cccccc;
+ --adm-color-border: #eeeeee;
+ --adm-color-box: #f5f5f5;
+ --adm-color-background: #ffffff;
+
+ --adm-font-size-main: var(--adm-font-size-5); */
+
+ --adm-font-family: -apple-system, blinkmacsystemfont, "Helvetica Neue",
+ helvetica, segoe ui, arial, roboto, "PingFang SC", "miui",
+ "Hiragino Sans GB", "Microsoft Yahei", sans-serif;
+}
+.i-icon {
+ height: 100%;
+}
+svg {
+ height: 100%;
+}
diff --git a/src/layout/main/index.less b/src/layout/main/index.less
new file mode 100644
index 0000000..fe18e79
--- /dev/null
+++ b/src/layout/main/index.less
@@ -0,0 +1,20 @@
+.main-layout {
+ height: 100%;
+ .layout-content {
+ height: 100%;
+ overflow-y: auto;
+ // padding-bottom: 150px;
+ padding-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/
+ padding-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/
+ }
+
+ .layout-tab {
+ flex: 0 0 auto;
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ z-index: 1000;
+ background-color: #fff;
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
+ }
+}
diff --git a/src/layout/main/mainLayout.tsx b/src/layout/main/mainLayout.tsx
new file mode 100644
index 0000000..9c18d94
--- /dev/null
+++ b/src/layout/main/mainLayout.tsx
@@ -0,0 +1,89 @@
+import React from "react";
+import { NavBar, SafeArea, TabBar, Toast } from "antd-mobile";
+import { useNavigate, useLocation } from "react-router-dom";
+import {
+ ShoppingBag,
+ TransactionOrder,
+ User,
+ CattleZodiac,
+} from "@icon-park/react";
+import useI18n from "@/hooks/i18n.ts";
+import "./index.less";
+
+interface MainLayoutProps {
+ children: React.ReactNode;
+ isShowNavBar?: boolean;
+ title?: string;
+ onLink?: () => void;
+}
+
+const MainLayout: React.FC = ({
+ isShowNavBar,
+ children,
+ onLink,
+ title,
+}) => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { pathname } = location;
+ const [activeKey, setActiveKey] = React.useState(pathname);
+
+ const setRouteActive = (value: string) => {
+ if (value !== "/") {
+ Toast.show("待开发");
+ }
+ };
+
+ const tabs = [
+ {
+ key: "/",
+ title: "宠物翻译",
+ icon: ,
+ },
+ {
+ key: "/set",
+ title: "待办",
+ icon: ,
+ },
+ {
+ key: "/message",
+ title: "消息",
+ icon: ,
+ },
+ {
+ key: "/me",
+ title: "我的",
+
+ icon: ,
+ },
+ ];
+
+ const goBack = () => {
+ if (onLink) {
+ onLink?.();
+ } else {
+ navigate(-1);
+ }
+ };
+ return (
+
+
+ {isShowNavBar ?
{title} : ""}
+
{children}
+
+
+ {/* setRouteActive(value)}
+ safeArea={true}
+ >
+ {tabs.map((item) => (
+
+ ))}
+ */}
+
+
+ );
+};
+
+export default MainLayout;
diff --git a/src/locales/en_US.json b/src/locales/en_US.json
new file mode 100644
index 0000000..373a607
--- /dev/null
+++ b/src/locales/en_US.json
@@ -0,0 +1,3 @@
+{
+ "index.title": "Hello World!"
+}
diff --git a/src/locales/zh_CN.json b/src/locales/zh_CN.json
new file mode 100644
index 0000000..fb48ba5
--- /dev/null
+++ b/src/locales/zh_CN.json
@@ -0,0 +1,3 @@
+{
+ "index.title": "你好,世界!"
+}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..638931a
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,13 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { BrowserRouter } from "react-router-dom";
+import App from "@/view/app/App.tsx";
+import "./index.css";
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+ //
+
+
+
+ //
+);
diff --git a/src/route/auth.tsx b/src/route/auth.tsx
new file mode 100644
index 0000000..1406e73
--- /dev/null
+++ b/src/route/auth.tsx
@@ -0,0 +1,29 @@
+import React, {useEffect} from 'react';
+import {useNavigate} from 'react-router-dom';
+
+interface AuthRouteProps {
+ children: React.ReactNode;
+ auth?: boolean;
+}
+
+/**
+ * 认证路由
+ * @param children 子组件
+ * @param auth 是否需要认证
+ * @constructor 认证路由组件
+ */
+const AuthRoute: React.FC = ({children, auth}) => {
+ const navigate = useNavigate();
+ const token = localStorage.getItem('token'); // 或者其他认证令牌的获取方式
+ const isAuthenticated = Boolean(token); // 认证逻辑
+
+ useEffect(() => {
+ if (auth && !isAuthenticated) {
+ navigate('/login'); // 如果未认证且路由需要认证,则重定向到登录
+ }
+ }, [auth, isAuthenticated, navigate]);
+
+ return <>{children}>;
+};
+
+export default AuthRoute;
diff --git a/src/route/render-routes.tsx b/src/route/render-routes.tsx
new file mode 100644
index 0000000..12d6537
--- /dev/null
+++ b/src/route/render-routes.tsx
@@ -0,0 +1,27 @@
+import {Route, Routes} from 'react-router-dom';
+import {routes, AppRoute} from './routes';
+import AuthRoute from './auth.tsx';
+
+/**
+ * 渲染路由
+ * @constructor RenderRoutes
+ */
+export const RenderRoutes = () => {
+ const renderRoutes = (routes: AppRoute[]) => {
+ return routes.map(route => (
+
+ {route.element}
+
+ }
+ >
+ {route.children && renderRoutes(route.children)}
+
+ ));
+ };
+
+ return {renderRoutes(routes)} ;
+};
diff --git a/src/route/routes.tsx b/src/route/routes.tsx
new file mode 100644
index 0000000..44b59ee
--- /dev/null
+++ b/src/route/routes.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+import Home from "@/view/home";
+import TranslateDetail from "@/view/home/detail";
+import Setting from "@/view/setting";
+import Page404 from "@/view/error/page404";
+export interface AppRoute {
+ path: string;
+ element: React.ReactNode;
+ auth?: boolean;
+ children?: AppRoute[];
+}
+
+export const routes: AppRoute[] = [
+ { path: "/", element: , auth: false },
+ { path: "/set", element: , auth: false },
+ { path: "/detail", element: , auth: false },
+ { path: "/mood", element: , auth: false },
+ { path: "*", element: , auth: false },
+];
diff --git a/src/store/i18n.ts b/src/store/i18n.ts
new file mode 100644
index 0000000..c7b7aee
--- /dev/null
+++ b/src/store/i18n.ts
@@ -0,0 +1,21 @@
+import {create} from 'zustand'
+import {persist, createJSONStorage} from 'zustand/middleware'
+
+export const LANG_KEY = 'lang'
+export const useI18nStore = create()(
+ persist(
+ (set, get) => ({
+ lang: 'en_US',
+ changeLanguage: (lang) => set({lang}),
+ toggleLanguage: () => set(() => {
+ return {
+ lang: get().lang.includes('zh') ? 'en_US' : 'zh_CN'
+ }
+ })
+ }),
+ {
+ name: 'i18n-storage',
+ storage: createJSONStorage(() => localStorage)
+ },
+ ),
+)
\ No newline at end of file
diff --git a/src/store/user.ts b/src/store/user.ts
new file mode 100644
index 0000000..f87b10e
--- /dev/null
+++ b/src/store/user.ts
@@ -0,0 +1,22 @@
+import {create} from 'zustand'
+import {persist, createJSONStorage} from 'zustand/middleware'
+
+export const useUserStore = create()(
+ persist(
+ (set) => ({
+ user: {
+ name: '',
+ avatar: '',
+ email: ''
+ },
+ setUser: (user: User) => set({user}),
+ clearUser: () => set({
+ user: null
+ }),
+ }),
+ {
+ name: 'user-storage',
+ storage: createJSONStorage(() => localStorage)
+ },
+ ),
+)
\ No newline at end of file
diff --git a/src/types/chat.ts b/src/types/chat.ts
new file mode 100644
index 0000000..22ec78a
--- /dev/null
+++ b/src/types/chat.ts
@@ -0,0 +1,41 @@
+// types/chat.ts
+// types/chat.ts (更新)
+export interface VoiceMessage {
+ id: string;
+ type: "voice";
+ content: {
+ duration: number;
+ url?: string;
+ localId?: string;
+ blob?: Blob;
+ waveform?: number[];
+ // 新增上传相关字段
+ fileId?: string;
+ fileName?: string;
+ serverUrl?: string;
+ uploadStatus?: "uploading" | "success" | "error";
+ uploadProgress?: number;
+ };
+ sender: "user" | "pet";
+ timestamp: number;
+ isPlaying?: boolean;
+ translation?: string;
+ translating?: boolean;
+}
+
+export interface TextMessage {
+ id: string;
+ type: "text";
+ content: string;
+ sender: "user" | "pet";
+ timestamp: number;
+}
+
+export type ChatMessage = VoiceMessage | TextMessage;
+
+export interface PetProfile {
+ name: string;
+ avatar: string;
+ species: "dog" | "cat" | "bird" | "other";
+ personality: string;
+}
diff --git a/src/types/http.d.ts b/src/types/http.d.ts
new file mode 100644
index 0000000..019239c
--- /dev/null
+++ b/src/types/http.d.ts
@@ -0,0 +1,14 @@
+interface Page {
+ total: number;
+ size: number;
+ current: number;
+ pages: number;
+ records: T[];
+}
+
+export interface Result {
+ success: boolean;
+ code: number;
+ message: string;
+ data: T;
+}
\ No newline at end of file
diff --git a/src/types/store.d.ts b/src/types/store.d.ts
new file mode 100644
index 0000000..c238023
--- /dev/null
+++ b/src/types/store.d.ts
@@ -0,0 +1,19 @@
+interface User {
+ name: string;
+ avatar: string;
+ email: string;
+}
+
+interface UserState {
+ user: User | null;
+ setUser: (user: User) => void;
+ clearUser: () => void;
+}
+
+type lang = 'zh_CN' | 'en_US'
+
+interface LangStore {
+ lang: lang;
+ changeLanguage: (lang: lang) => void
+ toggleLanguage: () => void
+}
\ No newline at end of file
diff --git a/src/types/upload.ts b/src/types/upload.ts
new file mode 100644
index 0000000..64e0400
--- /dev/null
+++ b/src/types/upload.ts
@@ -0,0 +1,34 @@
+// types/upload.ts
+export interface UploadConfig {
+ url: string;
+ method?: "POST" | "PUT";
+ headers?: Record;
+ fieldName?: string;
+ maxFileSize?: number;
+ allowedTypes?: string[];
+}
+
+export interface UploadProgress {
+ loaded: number;
+ total: number;
+ percentage: number;
+}
+
+export interface UploadResponse {
+ success: boolean;
+ data?: {
+ fileId: string;
+ fileName: string;
+ fileUrl: string;
+ duration: number;
+ size: number;
+ };
+ error?: string;
+}
+
+export interface VoiceUploadStatus {
+ status: "idle" | "uploading" | "success" | "error";
+ progress?: UploadProgress;
+ response?: UploadResponse;
+ error?: string;
+}
diff --git a/src/utils/amount.ts b/src/utils/amount.ts
new file mode 100644
index 0000000..6db3cae
--- /dev/null
+++ b/src/utils/amount.ts
@@ -0,0 +1,3 @@
+export function toYuan(num: number) {
+ return (num / 100).toFixed(2);
+}
\ No newline at end of file
diff --git a/src/utils/audioManager.ts b/src/utils/audioManager.ts
new file mode 100644
index 0000000..044ba5d
--- /dev/null
+++ b/src/utils/audioManager.ts
@@ -0,0 +1,208 @@
+// utils/audioManager.ts
+import { UniversalAudioPlayer } from "./audioPlayer";
+
+export interface AudioInstance {
+ id: string;
+ player: UniversalAudioPlayer;
+ onStop?: () => void;
+ onPlay?: () => void;
+ onPause?: () => void;
+}
+
+class AudioManager {
+ private static instance: AudioManager;
+ private currentAudio: AudioInstance | null = null;
+ private audioInstances: Map = new Map();
+
+ private constructor() {}
+
+ static getInstance(): AudioManager {
+ if (!AudioManager.instance) {
+ AudioManager.instance = new AudioManager();
+ }
+ return AudioManager.instance;
+ }
+
+ // 注册音频实例
+ registerAudio(
+ id: string,
+ player: UniversalAudioPlayer,
+ callbacks?: {
+ onStop?: () => void;
+ onPlay?: () => void;
+ onPause?: () => void;
+ }
+ ): void {
+ const audioInstance: AudioInstance = {
+ id,
+ player,
+ onStop: callbacks?.onStop,
+ onPlay: callbacks?.onPlay,
+ onPause: callbacks?.onPause,
+ };
+
+ this.audioInstances.set(id, audioInstance);
+ }
+
+ // 注销音频实例
+ unregisterAudio(id: string): void {
+ const audioInstance = this.audioInstances.get(id);
+ if (audioInstance) {
+ // 如果是当前播放的音频,先停止
+ if (this.currentAudio?.id === id) {
+ this.stopCurrent();
+ }
+
+ // 销毁播放器
+ audioInstance.player.destroy();
+ this.audioInstances.delete(id);
+ }
+ }
+
+ // 播放指定音频(会自动停止其他音频)
+ async playAudio(id: string): Promise {
+ const audioInstance = this.audioInstances.get(id);
+ if (!audioInstance) {
+ throw new Error(`音频实例 ${id} 不存在`);
+ }
+
+ // 如果当前有其他音频在播放,先停止
+ if (this.currentAudio && this.currentAudio.id !== id) {
+ this.stopCurrent();
+ }
+
+ try {
+ await audioInstance.player.play();
+ this.currentAudio = audioInstance;
+
+ // 触发播放回调
+ audioInstance.onPlay?.();
+
+ console.log(`开始播放音频: ${id}`);
+ } catch (error) {
+ console.error(`播放音频 ${id} 失败:`, error);
+ throw error;
+ }
+ }
+
+ // 暂停指定音频
+ pauseAudio(id: string): void {
+ const audioInstance = this.audioInstances.get(id);
+ if (!audioInstance) {
+ console.warn(`音频实例 ${id} 不存在`);
+ return;
+ }
+
+ audioInstance.player.pause();
+
+ // 如果是当前播放的音频,清除当前音频引用
+ if (this.currentAudio?.id === id) {
+ this.currentAudio = null;
+ }
+
+ // 触发暂停回调
+ audioInstance.onPause?.();
+
+ console.log(`暂停音频: ${id}`);
+ }
+
+ // 停止指定音频
+ stopAudio(id: string): void {
+ const audioInstance = this.audioInstances.get(id);
+ if (!audioInstance) {
+ console.warn(`音频实例 ${id} 不存在`);
+ return;
+ }
+
+ audioInstance.player.stop();
+
+ // 如果是当前播放的音频,清除当前音频引用
+ if (this.currentAudio?.id === id) {
+ this.currentAudio = null;
+ }
+
+ // 触发停止回调
+ audioInstance.onStop?.();
+
+ console.log(`停止音频: ${id}`);
+ }
+
+ // 停止当前播放的音频
+ stopCurrent(): void {
+ if (this.currentAudio) {
+ this.stopAudio(this.currentAudio.id);
+ }
+ }
+
+ // 暂停当前播放的音频
+ pauseCurrent(): void {
+ if (this.currentAudio) {
+ this.pauseAudio(this.currentAudio.id);
+ }
+ }
+
+ // 获取当前播放的音频ID
+ getCurrentAudioId(): string | null {
+ return this.currentAudio?.id || null;
+ }
+
+ // 检查指定音频是否正在播放
+ isPlaying(id: string): boolean {
+ const audioInstance = this.audioInstances.get(id);
+ if (!audioInstance) {
+ return false;
+ }
+ return audioInstance.player.getIsPlaying();
+ }
+
+ // 获取指定音频的播放时长
+ getDuration(id: string): number {
+ const audioInstance = this.audioInstances.get(id);
+ if (!audioInstance) {
+ return 0;
+ }
+ return audioInstance.player.getDuration();
+ }
+
+ // 获取指定音频的当前播放时间
+ getCurrentTime(id: string): number {
+ const audioInstance = this.audioInstances.get(id);
+ if (!audioInstance) {
+ return 0;
+ }
+ return audioInstance.player.getCurrentTime();
+ }
+
+ // 清理所有音频实例
+ cleanup(): void {
+ this.audioInstances.forEach((audioInstance) => {
+ audioInstance.player.destroy();
+ });
+ this.audioInstances.clear();
+ this.currentAudio = null;
+ }
+
+ // 获取所有音频实例的状态
+ getAudioStates(): Record<
+ string,
+ {
+ isPlaying: boolean;
+ duration: number;
+ currentTime: number;
+ }
+ > {
+ const states: Record = {};
+
+ this.audioInstances.forEach((audioInstance, id) => {
+ states[id] = {
+ isPlaying: audioInstance.player.getIsPlaying(),
+ duration: audioInstance.player.getDuration(),
+ currentTime: audioInstance.player.getCurrentTime(),
+ };
+ });
+
+ return states;
+ }
+}
+
+export default AudioManager;
diff --git a/src/utils/audioPlayer.ts b/src/utils/audioPlayer.ts
new file mode 100644
index 0000000..e6c3315
--- /dev/null
+++ b/src/utils/audioPlayer.ts
@@ -0,0 +1,246 @@
+// utils/audioPlayer.ts
+export class UniversalAudioPlayer {
+ private audio: HTMLAudioElement | null = null;
+ private audioContext: AudioContext | null = null;
+ private source: AudioBufferSourceNode | null = null;
+ private audioBuffer: AudioBuffer | null = null;
+ private isPlaying: boolean = false;
+ private startTime: number = 0;
+ private pauseTime: number = 0;
+
+ constructor() {
+ this.initAudioContext();
+ }
+
+ private async initAudioContext(): Promise {
+ try {
+ if (this.isAndroid()) {
+ this.audioContext = new (window.AudioContext ||
+ (window as any).webkitAudioContext)();
+
+ // 安卓需要用户交互来启动AudioContext
+ if (this.audioContext.state === "suspended") {
+ // 等待用户交互后再启动
+ document.addEventListener(
+ "touchstart",
+ this.resumeAudioContext.bind(this),
+ { once: true }
+ );
+ document.addEventListener(
+ "click",
+ this.resumeAudioContext.bind(this),
+ { once: true }
+ );
+ }
+ }
+ } catch (error) {
+ console.warn("AudioContext初始化失败:", error);
+ }
+ }
+
+ private async resumeAudioContext(): Promise {
+ if (this.audioContext && this.audioContext.state === "suspended") {
+ try {
+ await this.audioContext.resume();
+ console.log("AudioContext已启动");
+ } catch (error) {
+ console.error("AudioContext启动失败:", error);
+ }
+ }
+ }
+
+ private isAndroid(): boolean {
+ return /Android/.test(navigator.userAgent);
+ }
+
+ private isIOS(): boolean {
+ return /iPad|iPhone|iPod/.test(navigator.userAgent);
+ }
+
+ async loadAudio(audioBlob: Blob): Promise {
+ try {
+ const audioUrl = URL.createObjectURL(audioBlob);
+
+ if (this.isAndroid() && this.audioContext) {
+ // 安卓使用AudioContext播放
+ await this.loadWithAudioContext(audioBlob);
+ } else {
+ // iOS和桌面使用HTMLAudioElement
+ await this.loadWithAudioElement(audioUrl);
+ }
+ } catch (error) {
+ console.error("音频加载失败:", error);
+ throw error;
+ }
+ }
+
+ private async loadWithAudioContext(audioBlob: Blob): Promise {
+ if (!this.audioContext) {
+ throw new Error("AudioContext未初始化");
+ }
+
+ try {
+ const arrayBuffer = await audioBlob.arrayBuffer();
+ this.audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
+ console.log("AudioContext音频加载成功");
+ } catch (error) {
+ console.error("AudioContext音频解码失败:", error);
+ // 降级到HTMLAudioElement
+ const audioUrl = URL.createObjectURL(audioBlob);
+ await this.loadWithAudioElement(audioUrl);
+ }
+ }
+
+ private async loadWithAudioElement(audioUrl: string): Promise {
+ return new Promise((resolve, reject) => {
+ this.audio = new Audio(audioUrl);
+
+ // 安卓需要设置这些属性
+ this.audio.preload = "auto";
+ this.audio.controls = false;
+
+ this.audio.addEventListener(
+ "canplaythrough",
+ () => {
+ console.log("HTMLAudioElement音频加载成功");
+ resolve();
+ },
+ { once: true }
+ );
+
+ this.audio.addEventListener(
+ "error",
+ (e) => {
+ console.error("HTMLAudioElement音频加载失败:", e);
+ reject(new Error("音频加载失败"));
+ },
+ { once: true }
+ );
+
+ // 安卓需要手动触发加载
+ this.audio.load();
+ });
+ }
+
+ async play(): Promise {
+ try {
+ if (this.audioBuffer && this.audioContext) {
+ // 使用AudioContext播放
+ await this.playWithAudioContext();
+ } else if (this.audio) {
+ // 使用HTMLAudioElement播放
+ await this.playWithAudioElement();
+ } else {
+ throw new Error("音频未加载");
+ }
+ this.isPlaying = true;
+ } catch (error) {
+ console.error("音频播放失败:", error);
+ throw error;
+ }
+ }
+
+ private async playWithAudioContext(): Promise {
+ if (!this.audioContext || !this.audioBuffer) {
+ throw new Error("AudioContext或AudioBuffer未准备好");
+ }
+
+ // 确保AudioContext已启动
+ if (this.audioContext.state === "suspended") {
+ await this.audioContext.resume();
+ }
+
+ this.source = this.audioContext.createBufferSource();
+ this.source.buffer = this.audioBuffer;
+ this.source.connect(this.audioContext.destination);
+
+ this.source.onended = () => {
+ this.isPlaying = false;
+ };
+
+ this.startTime = this.audioContext.currentTime;
+ this.source.start(0);
+ }
+
+ private async playWithAudioElement(): Promise {
+ if (!this.audio) {
+ throw new Error("Audio元素未准备好");
+ }
+
+ // 安卓需要重置音频位置
+ this.audio.currentTime = 0;
+
+ try {
+ await this.audio.play();
+ } catch (error) {
+ // 如果自动播放失败,可能需要用户交互
+ console.warn("自动播放失败,需要用户交互:", error);
+ throw new Error("播放失败,请点击播放按钮");
+ }
+ }
+
+ pause(): void {
+ if (this.source) {
+ this.source.stop();
+ this.source = null;
+ }
+
+ if (this.audio) {
+ this.audio.pause();
+ }
+
+ this.isPlaying = false;
+ }
+
+ stop(): void {
+ this.pause();
+
+ if (this.audio) {
+ this.audio.currentTime = 0;
+ }
+ }
+
+ getIsPlaying(): boolean {
+ return this.isPlaying;
+ }
+
+ getDuration(): number {
+ if (this.audioBuffer) {
+ return this.audioBuffer.duration;
+ }
+
+ if (this.audio) {
+ return this.audio.duration || 0;
+ }
+
+ return 0;
+ }
+
+ getCurrentTime(): number {
+ if (this.audioContext && this.startTime > 0) {
+ return this.audioContext.currentTime - this.startTime;
+ }
+
+ if (this.audio) {
+ return this.audio.currentTime;
+ }
+
+ return 0;
+ }
+
+ destroy(): void {
+ this.stop();
+
+ if (this.audio) {
+ this.audio.src = "";
+ this.audio = null;
+ }
+
+ if (this.audioContext && this.audioContext.state !== "closed") {
+ this.audioContext.close().catch(console.error);
+ this.audioContext = null;
+ }
+
+ this.audioBuffer = null;
+ }
+}
diff --git a/src/utils/audioRecorder.ts b/src/utils/audioRecorder.ts
new file mode 100644
index 0000000..25d2027
--- /dev/null
+++ b/src/utils/audioRecorder.ts
@@ -0,0 +1,362 @@
+// utils/audioRecorder.ts
+export interface AudioRecorderConfig {
+ sampleRate?: number;
+ channels?: number;
+ bitDepth?: number;
+}
+
+export class UniversalAudioRecorder {
+ private mediaRecorder: MediaRecorder | null = null;
+ private audioChunks: BlobPart[] = [];
+ private stream: MediaStream | null = null;
+ private startTime: number = 0;
+ private pausedDuration: number = 0;
+ private pauseStartTime: number = 0;
+ private audioContext: AudioContext | null = null;
+
+ constructor(private config: AudioRecorderConfig = {}) {
+ this.config = {
+ sampleRate: 44100,
+ channels: 1,
+ bitDepth: 16,
+ ...config,
+ };
+ }
+
+ // 设备检测
+ private isIOS(): boolean {
+ return /iPad|iPhone|iPod/.test(navigator.userAgent);
+ }
+
+ private isAndroid(): boolean {
+ return /Android/.test(navigator.userAgent);
+ }
+
+ private isSafari(): boolean {
+ return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
+ }
+
+ private isChrome(): boolean {
+ return (
+ /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor)
+ );
+ }
+
+ // 获取设备支持的最佳MIME类型
+ private getSupportedMimeType(): string {
+ const isIOSDevice = this.isIOS();
+ const isAndroidDevice = this.isAndroid();
+ const isSafariBrowser = this.isSafari();
+ const isChromeBrowser = this.isChrome();
+
+ let mimeTypes: string[] = [];
+
+ if (isAndroidDevice) {
+ // 安卓设备优先使用这些格式
+ mimeTypes = [
+ "audio/webm;codecs=opus",
+ "audio/webm",
+ "audio/mp4;codecs=aac",
+ "audio/mp4",
+ "audio/aac",
+ "audio/ogg;codecs=opus",
+ "audio/wav",
+ ];
+ } else if (isIOSDevice || isSafariBrowser) {
+ // iOS和Safari优先使用这些格式
+ mimeTypes = [
+ "audio/mp4;codecs=aac",
+ "audio/mp4",
+ "audio/aac",
+ "audio/wav",
+ "audio/webm;codecs=opus",
+ "audio/webm",
+ ];
+ } else {
+ // 其他浏览器(主要是桌面Chrome/Firefox)
+ mimeTypes = [
+ "audio/webm;codecs=opus",
+ "audio/webm",
+ "audio/mp4;codecs=aac",
+ "audio/mp4",
+ "audio/ogg;codecs=opus",
+ "audio/wav",
+ ];
+ }
+
+ for (const mimeType of mimeTypes) {
+ if (MediaRecorder.isTypeSupported(mimeType)) {
+ console.log(
+ `选择的音频格式: ${mimeType} (设备: ${this.getDeviceInfo()})`
+ );
+ return mimeType;
+ }
+ }
+
+ console.warn("没有找到明确支持的音频格式,使用浏览器默认格式");
+ return "";
+ }
+
+ private getDeviceInfo(): string {
+ if (this.isAndroid()) return "Android";
+ if (this.isIOS()) return "iOS";
+ return "Desktop";
+ }
+
+ // 初始化AudioContext(安卓设备需要)
+ private async initAudioContext(): Promise {
+ try {
+ // 安卓设备需要AudioContext来确保音频正常工作
+ if (this.isAndroid() && !this.audioContext) {
+ this.audioContext = new (window.AudioContext ||
+ (window as any).webkitAudioContext)();
+
+ // 安卓需要用户交互来启动AudioContext
+ if (this.audioContext.state === "suspended") {
+ await this.audioContext.resume();
+ }
+ }
+ } catch (error) {
+ console.warn("AudioContext初始化失败:", error);
+ }
+ }
+
+ async start(): Promise {
+ try {
+ // 初始化AudioContext
+ await this.initAudioContext();
+
+ // 获取音频约束
+ const audioConstraints = this.getAudioConstraints();
+
+ this.stream = await navigator.mediaDevices.getUserMedia(audioConstraints);
+
+ // 安卓设备需要特殊处理
+ if (this.isAndroid() && this.audioContext && this.stream) {
+ // 创建音频源节点以确保音频流正常
+ const source = this.audioContext.createMediaStreamSource(this.stream);
+ const analyser = this.audioContext.createAnalyser();
+ source.connect(analyser);
+ }
+
+ const selectedMimeType = this.getSupportedMimeType();
+
+ // 创建MediaRecorder
+ const mediaRecorderOptions: MediaRecorderOptions = {};
+ if (selectedMimeType) {
+ mediaRecorderOptions.mimeType = selectedMimeType;
+ }
+
+ // 安卓使用较大的时间片,iOS使用中等时间片
+ const timeslice = this.isAndroid() ? 1000 : this.isIOS() ? 1000 : 100;
+
+ this.mediaRecorder = new MediaRecorder(this.stream, mediaRecorderOptions);
+
+ this.audioChunks = [];
+ this.startTime = Date.now();
+ this.pausedDuration = 0;
+
+ this.mediaRecorder.ondataavailable = (event) => {
+ if (event.data.size > 0) {
+ this.audioChunks.push(event.data);
+ console.log(`收到音频数据: ${event.data.size} bytes`);
+ }
+ };
+
+ this.mediaRecorder.onerror = (event) => {
+ console.error("MediaRecorder错误:", event);
+ };
+
+ this.mediaRecorder.onstart = () => {
+ console.log("录音开始");
+ };
+
+ this.mediaRecorder.onstop = () => {
+ console.log("录音停止");
+ };
+
+ this.mediaRecorder.start(timeslice);
+ console.log("录音器启动成功:", {
+ mimeType: this.mediaRecorder.mimeType,
+ state: this.mediaRecorder.state,
+ device: this.getDeviceInfo(),
+ });
+ } catch (error) {
+ this.cleanup();
+ console.error("录音启动失败:", error);
+ throw new Error(`录音启动失败: ${error}`);
+ }
+ }
+
+ private getAudioConstraints(): MediaStreamConstraints {
+ const isAndroidDevice = this.isAndroid();
+ const isIOSDevice = this.isIOS();
+
+ if (isAndroidDevice) {
+ // 安卓设备使用更宽松的约束
+ return {
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ // 安卓不强制指定采样率和声道数
+ },
+ };
+ } else if (isIOSDevice) {
+ // iOS设备约束
+ return {
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ };
+ } else {
+ // 桌面设备可以使用更详细的约束
+ return {
+ audio: {
+ sampleRate: this.config.sampleRate,
+ channelCount: this.config.channels,
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ };
+ }
+ }
+
+ stop(): Promise {
+ return new Promise((resolve, reject) => {
+ if (!this.mediaRecorder) {
+ reject(new Error("录音器未初始化"));
+ return;
+ }
+
+ const timeout = setTimeout(() => {
+ reject(new Error("录音停止超时"));
+ }, 10000); // 增加超时时间
+
+ this.mediaRecorder.onstop = () => {
+ clearTimeout(timeout);
+ try {
+ console.log(`收集到 ${this.audioChunks.length} 个音频片段`);
+
+ if (this.audioChunks.length === 0) {
+ reject(new Error("没有录制到音频数据"));
+ return;
+ }
+
+ const mimeType = this.mediaRecorder?.mimeType || "audio/webm";
+ const audioBlob = new Blob(this.audioChunks, { type: mimeType });
+
+ console.log("录音完成:", {
+ size: audioBlob.size,
+ type: audioBlob.type,
+ duration: this.getDuration(),
+ chunks: this.audioChunks.length,
+ });
+
+ // 验证blob是否有效
+ if (audioBlob.size === 0) {
+ reject(new Error("录制的音频文件为空"));
+ return;
+ }
+
+ this.cleanup();
+ resolve(audioBlob);
+ } catch (error) {
+ this.cleanup();
+ reject(error);
+ }
+ };
+
+ this.mediaRecorder.onerror = (event) => {
+ clearTimeout(timeout);
+ this.cleanup();
+ reject(new Error("录音停止时发生错误"));
+ };
+
+ try {
+ if (
+ this.mediaRecorder.state === "recording" ||
+ this.mediaRecorder.state === "paused"
+ ) {
+ this.mediaRecorder.stop();
+ } else {
+ clearTimeout(timeout);
+ reject(new Error("录音器状态异常"));
+ }
+ } catch (error) {
+ clearTimeout(timeout);
+ this.cleanup();
+ reject(error);
+ }
+ });
+ }
+
+ pause(): void {
+ if (this.mediaRecorder && this.mediaRecorder.state === "recording") {
+ this.pauseStartTime = Date.now();
+ this.mediaRecorder.pause();
+ }
+ }
+
+ resume(): void {
+ if (this.mediaRecorder && this.mediaRecorder.state === "paused") {
+ this.pausedDuration += Date.now() - this.pauseStartTime;
+ this.mediaRecorder.resume();
+ }
+ }
+
+ cancel(): void {
+ try {
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
+ this.mediaRecorder.stop();
+ }
+ } catch (error) {
+ console.error("取消录音时出错:", error);
+ }
+ this.cleanup();
+ }
+
+ getDuration(): number {
+ const currentTime = Date.now();
+ const totalTime = currentTime - this.startTime - this.pausedDuration;
+ return Math.max(0, Math.floor(totalTime / 1000));
+ }
+
+ getState(): string {
+ return this.mediaRecorder?.state || "inactive";
+ }
+
+ // 获取支持的音频格式信息
+ static getSupportedFormats(): string[] {
+ const formats = [
+ "audio/webm;codecs=opus",
+ "audio/webm",
+ "audio/mp4;codecs=aac",
+ "audio/mp4",
+ "audio/aac",
+ "audio/wav",
+ "audio/ogg;codecs=opus",
+ "audio/ogg",
+ ];
+
+ return formats.filter((format) => MediaRecorder.isTypeSupported(format));
+ }
+
+ private cleanup(): void {
+ if (this.stream) {
+ this.stream.getTracks().forEach((track) => track.stop());
+ this.stream = null;
+ }
+
+ if (this.audioContext && this.audioContext.state !== "closed") {
+ this.audioContext.close().catch(console.error);
+ this.audioContext = null;
+ }
+
+ this.mediaRecorder = null;
+ this.audioChunks = [];
+ }
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 0000000..5a21bf8
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,2 @@
+export * from './amount';
+export * from './location';
\ No newline at end of file
diff --git a/src/utils/js-audio-recorder.d.ts b/src/utils/js-audio-recorder.d.ts
new file mode 100644
index 0000000..a8b6c3c
--- /dev/null
+++ b/src/utils/js-audio-recorder.d.ts
@@ -0,0 +1,22 @@
+declare module "js-audio-recorder" {
+ export interface RecorderOptions {
+ sampleBits?: number;
+ sampleRate?: number;
+ numChannels?: number;
+ compiling?: boolean;
+ }
+
+ export default class Recorder {
+ constructor(options?: RecorderOptions);
+ start(): Promise;
+ pause(): void;
+ resume(): void;
+ stop(): void;
+ getBlob(): Blob;
+ getWAVBlob(): Blob;
+ destroy(): void;
+ duration: number;
+ fileSize: number;
+ isrecording: boolean;
+ }
+}
diff --git a/src/utils/location.ts b/src/utils/location.ts
new file mode 100644
index 0000000..5a997c7
--- /dev/null
+++ b/src/utils/location.ts
@@ -0,0 +1,3 @@
+export function toKm(m: number): number {
+ return m / 1000;
+}
\ No newline at end of file
diff --git a/src/utils/voice.ts b/src/utils/voice.ts
new file mode 100644
index 0000000..f67276e
--- /dev/null
+++ b/src/utils/voice.ts
@@ -0,0 +1,186 @@
+// export interface VoiceMessage {
+// id: string;
+// audioUrl: string;
+// audioBlob: Blob;
+// duration: number;
+// isPlaying: boolean;
+// timestamp: number;
+// type: "sent" | "received";
+// }
+
+// export interface WeChatVoiceRecorderProps {
+// onSendVoice?: (voiceData: VoiceMessage) => void;
+// maxDuration?: number;
+// className?: string;
+// }
+
+// export interface RecordingState {
+// isRecording: boolean;
+// isPaused: boolean;
+// duration: number;
+// hasPermission: boolean;
+// }
+export const mockTranslateAudio = async (): Promise => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ const mockTranslations = [
+ "汪汪汪!我饿了,想吃东西!🍖",
+ "喵喵~我想要抱抱!🤗",
+ "我想出去玩耍!🎾",
+ "我很开心!😊",
+ "我有点害怕...😰",
+ "我想睡觉了~😴",
+ "主人,陪我玩一会儿吧!🎮",
+ "我想喝水了💧",
+ "外面有什么声音?👂",
+ "我爱你,主人!❤️",
+ ];
+ const randomTranslation =
+ mockTranslations[Math.floor(Math.random() * mockTranslations.length)];
+ resolve(randomTranslation);
+ }, 2000 + Math.random() * 2000);
+ });
+};
+// 创建发送音效 - 清脆的"叮"声
+export const createSendSound = () => {
+ try {
+ const audioContext = new (window.AudioContext ||
+ (window as any).webkitAudioContext)();
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ // 设置音调 - 清脆的高音
+ oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
+ oscillator.frequency.exponentialRampToValueAtTime(
+ 1200,
+ audioContext.currentTime + 0.1
+ );
+
+ // 设置音量包络
+ gainNode.gain.setValueAtTime(0, audioContext.currentTime);
+ gainNode.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.01);
+ gainNode.gain.exponentialRampToValueAtTime(
+ 0.01,
+ audioContext.currentTime + 0.3
+ );
+
+ oscillator.type = "sine";
+
+ // 创建音频元素
+ const audio = new Audio();
+
+ // 重写play方法来播放合成音效
+ audio.play = () => {
+ return new Promise((resolve) => {
+ try {
+ const newOscillator = audioContext.createOscillator();
+ const newGainNode = audioContext.createGain();
+
+ newOscillator.connect(newGainNode);
+ newGainNode.connect(audioContext.destination);
+
+ newOscillator.frequency.setValueAtTime(800, audioContext.currentTime);
+ newOscillator.frequency.exponentialRampToValueAtTime(
+ 1200,
+ audioContext.currentTime + 0.1
+ );
+
+ newGainNode.gain.setValueAtTime(0, audioContext.currentTime);
+ newGainNode.gain.linearRampToValueAtTime(
+ 0.3,
+ audioContext.currentTime + 0.01
+ );
+ newGainNode.gain.exponentialRampToValueAtTime(
+ 0.01,
+ audioContext.currentTime + 0.3
+ );
+
+ newOscillator.type = "sine";
+ newOscillator.start(audioContext.currentTime);
+ newOscillator.stop(audioContext.currentTime + 0.3);
+
+ setTimeout(() => resolve(undefined), 300);
+ } catch (error) {
+ console.error("播放发送音效失败:", error);
+ resolve(undefined);
+ }
+ });
+ };
+
+ return audio;
+ } catch (error) {
+ console.error("创建发送音效失败:", error);
+ return new Audio(); // 返回空音频对象
+ }
+};
+export const createStartRecordSound = () => {
+ try {
+ const audio = new Audio();
+
+ audio.play = () => {
+ return new Promise((resolve) => {
+ try {
+ const audioContext = new (window.AudioContext ||
+ (window as any).webkitAudioContext)();
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ // 设置音调 - 低沉的音
+ oscillator.frequency.setValueAtTime(400, audioContext.currentTime);
+
+ // 设置音量包络
+ gainNode.gain.setValueAtTime(0, audioContext.currentTime);
+ gainNode.gain.linearRampToValueAtTime(
+ 0.2,
+ audioContext.currentTime + 0.05
+ );
+ gainNode.gain.linearRampToValueAtTime(
+ 0,
+ audioContext.currentTime + 0.2
+ );
+
+ oscillator.type = "sine";
+ oscillator.start(audioContext.currentTime);
+ oscillator.stop(audioContext.currentTime + 0.2);
+
+ setTimeout(() => resolve(undefined), 200);
+ } catch (error) {
+ console.error("播放录音音效失败:", error);
+ resolve(undefined);
+ }
+ });
+ };
+
+ return audio;
+ } catch (error) {
+ console.error("创建录音音效失败:", error);
+ return new Audio();
+ }
+};
+
+export const getAudioDuration = async (audioBlob: Blob): Promise => {
+ return new Promise((resolve, reject) => {
+ const AudioContextClass =
+ window.AudioContext || (window as any).webkitAudioContext;
+ const audioContext = new AudioContextClass();
+
+ const fileReader = new FileReader();
+ fileReader.onload = async (e) => {
+ try {
+ const arrayBuffer = e.target?.result as ArrayBuffer;
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
+ const duration = audioBuffer.duration;
+ resolve(duration);
+ } catch (error) {
+ reject(error);
+ }
+ };
+ fileReader.readAsArrayBuffer(audioBlob);
+ });
+};
diff --git a/src/view/app/App.css b/src/view/app/App.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/view/app/App.tsx b/src/view/app/App.tsx
new file mode 100644
index 0000000..58c3075
--- /dev/null
+++ b/src/view/app/App.tsx
@@ -0,0 +1,24 @@
+import "./App.css";
+import { RenderRoutes } from "@/route/render-routes.tsx";
+import { axiosInstance } from "@/http/axios-instance.ts";
+import { configure } from "axios-hooks";
+import enUS from "antd-mobile/es/locales/en-US";
+import { ConfigProvider } from "antd-mobile";
+import { useI18nStore } from "@/store/i18n.ts";
+import zhCN from "antd-mobile/es/locales/zh-CN";
+
+function App() {
+ configure({
+ axios: axiosInstance,
+ });
+ const i18nStore = useI18nStore();
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+export default App;
diff --git a/src/view/archives/index.less b/src/view/archives/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/src/view/archives/index.tsx b/src/view/archives/index.tsx
new file mode 100644
index 0000000..bbfd98e
--- /dev/null
+++ b/src/view/archives/index.tsx
@@ -0,0 +1,14 @@
+// 档案
+import MainLayout from "@/layout/main/mainLayout";
+import { Button, Tabs } from "antd-mobile";
+import "./index.less";
+
+function Index() {
+ return (
+
+
+
+ );
+}
+
+export default Index;
diff --git a/src/view/error/page404.tsx b/src/view/error/page404.tsx
new file mode 100644
index 0000000..c9032f8
--- /dev/null
+++ b/src/view/error/page404.tsx
@@ -0,0 +1,18 @@
+import MainLayout from "@/layout/main/mainLayout";
+import { Button } from "antd-mobile";
+import { useNavigate } from "react-router-dom";
+function Index() {
+ const navigate = useNavigate();
+ const goHome = () => {
+ navigate("/");
+ };
+ return (
+
+
+ 404
+
+
+ );
+}
+
+export default Index;
diff --git a/src/view/home/component/index.less b/src/view/home/component/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/src/view/home/component/message/index.less b/src/view/home/component/message/index.less
new file mode 100644
index 0000000..3d6e700
--- /dev/null
+++ b/src/view/home/component/message/index.less
@@ -0,0 +1,46 @@
+.message {
+ flex: 1 auto;
+ overflow-y: auto;
+ padding: 12px;
+ .item {
+ color: rgba(0, 0, 0, 0.45);
+ font-size: 12px;
+ display: flex;
+ gap: 12px;
+ margin-bottom: 20px;
+ }
+ .avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 40px;
+ background: #eee;
+ }
+ .voice-container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: rgba(0, 0, 0, 0.02);
+ border: 1px solid rgba(0, 0, 0, 0.06);
+ border-radius: 8px;
+ margin-top: 8px;
+ padding-right: 12px;
+ .time {
+ color: rgba(0, 0, 0, 0.88);
+ }
+ .tips {
+ // align-self: stretch;
+ // display: grid;
+ // align-items: center;
+ color: rgba(0, 0, 0, 0.45);
+ }
+ }
+ .translate {
+ display: flex;
+ gap: 12px;
+ padding: 12px;
+ align-items: center;
+ background: rgba(0, 0, 0, 0.02);
+ border-radius: 8px;
+ margin-top: 12px;
+ }
+}
diff --git a/src/view/home/component/message/index.tsx b/src/view/home/component/message/index.tsx
new file mode 100644
index 0000000..aae218e
--- /dev/null
+++ b/src/view/home/component/message/index.tsx
@@ -0,0 +1,176 @@
+import { Divider, Image, SpinLoading, Toast } from "antd-mobile";
+import VoiceIcon from "@/component/voiceIcon";
+import { Message } from "../../types";
+import dogSvg from "@/assets/translate/dog.svg";
+import catSvg from "@/assets/translate/cat.svg";
+import pigSvg from "@/assets/translate/pig.svg";
+import "./index.less";
+import { useEffect, useRef, useState } from "react";
+
+interface DefinedProps {
+ data: Message[];
+ isRecording: boolean;
+}
+
+function Index(props: DefinedProps) {
+ const { data, isRecording } = props;
+ const audioRefs = useRef<{ [key: string]: HTMLAudioElement }>({});
+ const [isPlaying, setIsPlating] = useState(false);
+ const [currentPlayingId, setCurrentPlayingId] = useState();
+
+ useEffect(() => {
+ if (isRecording) {
+ stopAllAudio();
+ }
+ }, [isRecording]);
+ const onVoiceChange = () => {
+ setIsPlating(!isPlaying);
+ };
+ const playAudio = (messageId: number, audioUrl: string) => {
+ if (isRecording) {
+ Toast.show("录音中,无法播放音频");
+ return;
+ }
+
+ if (currentPlayingId === messageId) {
+ if (audioRefs.current[messageId]) {
+ audioRefs.current[messageId].pause();
+ audioRefs.current[messageId].currentTime = 0;
+ }
+ setCurrentPlayingId(undefined);
+ setIsPlating(false);
+ return;
+ }
+
+ stopAllAudio();
+ if (!audioRefs.current[messageId]) {
+ audioRefs.current[messageId] = new Audio(audioUrl);
+ }
+
+ const audio = audioRefs.current[messageId];
+ audio.currentTime = 0;
+
+ audio.onended = () => {
+ setCurrentPlayingId(undefined);
+ setIsPlating(false);
+ };
+
+ audio.onerror = (error) => {
+ console.error("音频播放错误:", error);
+ Toast.show("音频播放失败");
+ setIsPlating(false);
+ };
+
+ audio
+ .play()
+ .then(() => {
+ setCurrentPlayingId(messageId);
+ setIsPlating(true);
+ })
+ .catch((error) => {
+ console.error("音频播放失败:", error);
+ Toast.show("音频播放失败");
+ });
+ };
+ const stopAllAudio = () => {
+ if (currentPlayingId && audioRefs.current[currentPlayingId]) {
+ audioRefs.current[currentPlayingId].pause();
+ audioRefs.current[currentPlayingId].currentTime = 0;
+ setIsPlating(false);
+ setCurrentPlayingId(undefined);
+ }
+
+ Object.values(audioRefs.current).forEach((audio) => {
+ if (!audio.paused) {
+ audio.pause();
+ audio.currentTime = 0;
+ }
+ });
+ };
+ const renderAvatar = (type?: "pig" | "cat" | "dog") => {
+ if (type === "pig") {
+ ;
+ }
+ if (type === "cat") {
+ return (
+
+ );
+ }
+ return (
+
+ );
+ };
+
+ return (
+
+ {data.map((item, index) => (
+
playAudio(item.id, item.audioUrl)}
+ >
+ {renderAvatar(item.type)}
+
+
+
{item.name}
+
+
{item.timestamp}
+
+
+ {item.isTranslating ? (
+
+
+ 翻译中...
+
+ ) : (
+
{item.translatedText}
+ )}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ {isRecording ? "录制中..." : "轻点麦克风录制"}
+
+
+
+
+
+ );
+}
+
+export default Index;
diff --git a/src/view/home/component/search/index.less b/src/view/home/component/search/index.less
new file mode 100644
index 0000000..7289ace
--- /dev/null
+++ b/src/view/home/component/search/index.less
@@ -0,0 +1,22 @@
+.search {
+ display: flex;
+ gap: 12px;
+ width: 100%;
+ .adm-search-bar-input-box-icon {
+ display: flex;
+ align-items: center;
+ }
+ .adm-search-bar {
+ flex: 1 auto;
+ }
+ .all {
+ height: 34px !important;
+ background: var(--adm-color-fill-content);
+ border-radius: 6px;
+ padding: 0px 12px;
+ display: flex;
+ align-items: center;
+ color: rgba(0, 0, 0, 0.88);
+ gap: 6px;
+ }
+}
diff --git a/src/view/home/component/search/index.tsx b/src/view/home/component/search/index.tsx
new file mode 100644
index 0000000..d371724
--- /dev/null
+++ b/src/view/home/component/search/index.tsx
@@ -0,0 +1,87 @@
+import { DownOne } from "@icon-park/react";
+import {
+ ActionSheet,
+ Dropdown,
+ type DropdownRef,
+ Popup,
+ Radio,
+ SearchBar,
+ Space,
+} from "antd-mobile";
+import { RadioValue } from "antd-mobile/es/components/radio";
+import { useRef, useState } from "react";
+
+interface PropsConfig {
+ handleAllAni: () => void;
+}
+const allAni = ["全部宠物", "丑丑", "胖胖", "可可"];
+function Index(props: PropsConfig) {
+ const [aniName, setAniName] = useState("全部宠物");
+ const animenuRef = useRef(null);
+ const handleAniSelect = (val: RadioValue) => {
+ setAniName(allAni[val as number]);
+ animenuRef.current?.close();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ 全部宠物
+
+
+ 丑丑
+
+
+ 胖胖
+
+
+ 可可
+
+
+
+
+
+
+ {/*
+ 全部宠物
+
+
+
setVisible(false)}
+ /> */}
+ {/* {
+ setVisible(false);
+ }}
+ >
+
+ 宠物
+
+ 11111
+ */}
+
+ );
+}
+
+export default Index;
diff --git a/src/view/home/component/voice/index.less b/src/view/home/component/voice/index.less
new file mode 100644
index 0000000..2cfcb68
--- /dev/null
+++ b/src/view/home/component/voice/index.less
@@ -0,0 +1,54 @@
+.voice-record {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ padding: 12px 0px;
+ box-shadow: 1px 2px 4px 3px #eee;
+ .adm-progress-circle-info {
+ height: 32px;
+ }
+ .isRecording {
+ .adm-progress-circle {
+ border-radius: 50%;
+ animation: recordingButtonPulse 1s infinite;
+ }
+ }
+ .tips {
+ color: #9f9f9f;
+ }
+ .circle {
+ display: inline-block;
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ background: rgba(22, 119, 255, 1);
+ }
+
+ .cancleBtn {
+ position: absolute;
+ border: 2px solid rgba(230, 244, 255, 1);
+ width: 72px;
+ height: 72px;
+ right: 60px;
+ border-radius: 50%;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+
+ @keyframes recordingButtonPulse {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(22, 119, 255, 0.5);
+ }
+ 30% {
+ box-shadow: 0 0 0 14px rgba(255, 77, 79, 0);
+ }
+ 70% {
+ box-shadow: 0 0 0 14px rgba(255, 77, 79, 0);
+ }
+ 100% {
+ box-shadow: 0 0 0 0 rgba(22, 119, 255, 0.5);
+ }
+ }
+}
diff --git a/src/view/home/component/voice/index.tsx b/src/view/home/component/voice/index.tsx
new file mode 100644
index 0000000..196ec00
--- /dev/null
+++ b/src/view/home/component/voice/index.tsx
@@ -0,0 +1,277 @@
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { AudioRecorder, useAudioRecorder } from "react-audio-voice-recorder";
+import { Button, Dialog, Image, ProgressCircle, Toast } from "antd-mobile";
+import microphoneSvg from "@/assets/translate/microphone.svg";
+import microphoneDisabledSvg from "@/assets/translate/microphoneDisabledSvg.svg";
+import { createStartRecordSound, createSendSound } from "@/utils/voice";
+import "./index.less";
+import { Message } from "../../types";
+import { CloseCircleOutline } from "antd-mobile-icons";
+
+interface DefinedProps {
+ onRecordingComplete: (url: string, finalDuration: number) => void;
+ isRecording: boolean;
+ onSetIsRecording: (flag: boolean) => void;
+}
+function Index(props: DefinedProps) {
+ const { isRecording } = props;
+ const [hasPermission, setHasPermission] = useState(false); //是否有权限
+ const [isPermissioning, setIsPermissioning] = useState(true); //获取权限中
+ const [recordingDuration, setRecordingDuration] = useState(0); //录音时长进度
+ const [isModal, setIsModal] = useState(false);
+ const recordingTimerRef = useRef();
+ const isCancelledRef = useRef(false);
+ const recordingStartTimeRef = useRef(0); //录音时长
+ // 音效相关
+ const sendSoundRef = useRef(null);
+ const startRecordSoundRef = useRef(null);
+ useEffect(() => {
+ initializeSounds();
+ checkMicrophonePermission();
+ }, [hasPermission]);
+
+ useEffect(() => {
+ if (isRecording) {
+ recorderControls.startRecording();
+ } else {
+ }
+ }, [isRecording]);
+
+ //重置状态
+ const onResetRecordingState = () => {
+ props.onSetIsRecording(false);
+ setRecordingDuration(0);
+ recordingStartTimeRef.current = 0;
+ if (recordingTimerRef.current) {
+ clearInterval(recordingTimerRef.current);
+ recordingTimerRef.current = undefined;
+ }
+ };
+
+ const initializeSounds = () => {
+ try {
+ // 发送音效 - 使用Web Audio API生成
+ sendSoundRef.current = createSendSound();
+
+ // 开始录音音效
+ startRecordSoundRef.current = createStartRecordSound();
+
+ console.log("音效初始化完成");
+ } catch (error) {
+ console.error("音效初始化失败:", error);
+ }
+ };
+
+ const renderBtn = useCallback(() => {
+ if (!hasPermission) {
+ //没有权限
+ return (
+ <>
+
+ {isPermissioning ? (
+ 获取麦克风权限中...
+ ) : (
+ 麦克风没有开启权限
+ )}
+ >
+ );
+ }
+ if (isRecording) {
+ //正在录音中
+ return (
+
+ );
+ } else {
+ //麦克风状态
+ return (
+
+ );
+ }
+ }, [hasPermission, isRecording, recordingDuration]);
+ const checkMicrophonePermission = useCallback(async () => {
+ try {
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
+ setHasPermission(false);
+ setIsPermissioning(false);
+ Toast.show("浏览器不支持录音功能");
+ return false;
+ }
+
+ if (navigator.permissions && navigator.permissions.query) {
+ try {
+ const permissionStatus = await navigator.permissions.query({
+ name: "microphone" as PermissionName,
+ });
+
+ if (permissionStatus.state === "denied") {
+ setHasPermission(false);
+ setIsPermissioning(false);
+ setIsModal(true);
+
+ return false;
+ }
+ if (permissionStatus.state === "granted") {
+ setHasPermission(true);
+ setIsModal(false);
+ return true;
+ }
+ } catch (permError) {
+ console.log("权限查询不支持,继续使用getUserMedia检查");
+ }
+ }
+ const stream = await navigator.mediaDevices.getUserMedia({
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ });
+
+ stream.getTracks().forEach((track) => track.stop());
+ setHasPermission(true);
+ return true;
+ } catch (error: any) {
+ if (error.message.includes("user denied permission")) {
+ setIsModal(true);
+ }
+
+ setHasPermission(false);
+ return false;
+ }
+ }, []);
+
+ useEffect(() => {
+ if (isModal) {
+ Dialog.confirm({
+ content: "重新获取麦克风权限",
+ onConfirm: async () => {
+ setIsModal(true);
+ await navigator.mediaDevices.getUserMedia({
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ });
+ setHasPermission(true);
+ },
+ });
+ }
+ }, [isModal]);
+
+ const recorderControls = useAudioRecorder(
+ {
+ noiseSuppression: true,
+ echoCancellation: true,
+ autoGainControl: true,
+ },
+ (err) => {
+ console.error("录音错误:", err);
+ Toast.show("录音失败,请重试");
+ onResetRecordingState();
+ }
+ );
+
+ // 播放音效
+ const playSound = async (soundRef: React.RefObject) => {
+ try {
+ if (soundRef.current) {
+ await soundRef.current.play();
+ }
+ } catch (error: any) {
+ Toast.show(`播放音效失败:${error.message}`);
+ }
+ };
+
+ //开始录音
+ const onStartRecording = () => {
+ isCancelledRef.current = false;
+ if (recordingTimerRef.current) {
+ clearInterval(recordingTimerRef.current);
+ recordingTimerRef.current = undefined;
+ }
+ props.onSetIsRecording(true);
+ // recorderControls.startRecording();
+ recordingStartTimeRef.current = Date.now();
+ // 立即开始计时
+ recordingTimerRef.current = setInterval(() => {
+ setRecordingDuration((prev) => prev + 1);
+ }, 1000);
+ };
+ const onStopRecording = useCallback(() => {
+ recorderControls.stopRecording();
+ onResetRecordingState();
+ }, [recorderControls, recordingDuration]);
+ //录音完成
+ // 在发送时检查录音时长
+ const onRecordingComplete = useCallback(
+ (blob: Blob) => {
+ if (isCancelledRef.current) {
+ Toast.show("已取消");
+ return;
+ }
+ // 检查blob有效性
+ if (!blob || blob.size === 0) {
+ Toast.show("录音数据无效,请重新录音");
+
+ return;
+ }
+ const audioUrl = URL.createObjectURL(blob);
+ const audio = new Audio();
+ audio.src = audioUrl;
+ // 计算实际录音时长
+
+ audio.addEventListener("loadedmetadata", () => {
+ if (audio.duration < 1) {
+ Toast.show("录音时间太短,请重新录音");
+ return;
+ }
+ alert(audio.duration);
+ playSound(sendSoundRef);
+ props.onRecordingComplete?.(audioUrl, Math.floor(audio.duration));
+ });
+ },
+ [isCancelledRef, isRecording, sendSoundRef]
+ );
+
+ const cancelRecording = useCallback(() => {
+ isCancelledRef.current = true;
+ recorderControls.stopRecording();
+ onResetRecordingState();
+ }, []);
+
+ return (
+ <>
+
+ {renderBtn()}
+ >
+ );
+}
+
+export default React.memo(Index);
diff --git a/src/view/home/detail.tsx b/src/view/home/detail.tsx
new file mode 100644
index 0000000..2717d85
--- /dev/null
+++ b/src/view/home/detail.tsx
@@ -0,0 +1,5 @@
+function Index() {
+ return
;
+}
+
+export default Index;
diff --git a/src/view/home/index.less b/src/view/home/index.less
new file mode 100644
index 0000000..e32b8cc
--- /dev/null
+++ b/src/view/home/index.less
@@ -0,0 +1,39 @@
+.home {
+ height: 100%;
+ overflow: hidden;
+ .adm-tabs {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+ .adm-tabs-content {
+ flex: 1;
+ overflow: hidden;
+ padding: 0px;
+ }
+ .adm-tabs-header {
+ border: 0 none;
+ position: sticky;
+ top: 0px;
+ background: #fff;
+ z-index: 99;
+ }
+
+ .adm-tabs-tab {
+ font-size: 20px;
+ color: rgba(0, 0, 0, 0.25);
+ font-weight: 600;
+ &.adm-tabs-tab-active {
+ color: #000;
+ }
+ }
+ .adm-tabs-tab-line {
+ height: 0px;
+ }
+
+ .translate-container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+}
diff --git a/src/view/home/index.tsx b/src/view/home/index.tsx
new file mode 100644
index 0000000..41f03b8
--- /dev/null
+++ b/src/view/home/index.tsx
@@ -0,0 +1,31 @@
+import MainLayout from "@/layout/main/mainLayout";
+import { Button, Tabs } from "antd-mobile";
+import Translate from "./translate";
+import "./index.less";
+
+function Index() {
+ const handleRecordComplete = (audioData: AudioData): void => {
+ console.log("录音完成:", audioData);
+ };
+
+ const handleError = (error: Error): void => {
+ console.error("录音错误:", error);
+ };
+
+ return (
+
+
+
+
+
+
+
+ 2
+
+
+
+
+ );
+}
+
+export default Index;
diff --git a/src/view/home/translate.tsx b/src/view/home/translate.tsx
new file mode 100644
index 0000000..77a928e
--- /dev/null
+++ b/src/view/home/translate.tsx
@@ -0,0 +1,154 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import FloatingMenu, { FloatMenuItemConfig } from "@/component/floatingMenu";
+import { Image, Toast } from "antd-mobile";
+import MessageCom from "./component/message";
+import VoiceRecord from "./component/voice";
+import XPopup from "@/component/xpopup";
+import type { Message } from "./types";
+
+import { mockTranslateAudio } from "@/utils/voice";
+import dogSvg from "@/assets/translate/dog.svg";
+import catSvg from "@/assets/translate/cat.svg";
+import pigSvg from "@/assets/translate/pig.svg";
+import { MoreTwo } from "@icon-park/react";
+interface DefinedProps {}
+const menuItems: FloatMenuItemConfig[] = [
+ { icon: , type: "dog" },
+ { icon: , type: "cat" },
+ { icon: , type: "pig" },
+ {
+ icon: (
+
+ ),
+ type: "add",
+ },
+];
+function Index(props: DefinedProps) {
+ // const data: Message[] = [
+ // {
+ // id: 1,
+ // audioUrl: "",
+ // duration: 0,
+ // translatedText: "",
+ // isTranslating: true,
+ // isPlaying: false,
+ // timestamp: 0,
+ // },
+ // ];
+ const [currentPlayingId, setCurrentPlayingId] = useState(null); //当前播放id
+ const [messages, setMessages] = useState([]);
+ const [isRecording, setIsRecording] = useState(false); //是否录音中
+ const [currentLanguage, setCurrentLanguage] = useState();
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ setCurrentLanguage(menuItems[0]);
+ }, []);
+
+ //完成录音
+ const onRecordingComplete = useCallback(
+ (audioUrl: string, actualDuration: number) => {
+ const newMessage: Message = {
+ id: Date.now(),
+ type: "dog",
+ audioUrl,
+ name: "生无可恋喵",
+ duration: actualDuration,
+ timestamp: Date.now(),
+ isTranslating: true,
+ };
+
+ setMessages((prev) => [...prev, newMessage]);
+ setTimeout(() => {
+ onTranslateAudio(newMessage.id);
+ }, 1000);
+
+ Toast.show("语音已发送");
+ },
+ [messages]
+ );
+
+ //翻译
+ const onTranslateAudio = useCallback(
+ async (messageId: number) => {
+ try {
+ const translatedText = await mockTranslateAudio();
+
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === messageId
+ ? { ...msg, translatedText, isTranslating: false }
+ : msg
+ )
+ );
+ } catch (error) {
+ console.error("翻译失败:", error);
+ Toast.show("翻译失败,请重试");
+
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === messageId
+ ? {
+ ...msg,
+ isTranslating: false,
+ translatedText: "翻译失败,请重试",
+ }
+ : msg
+ )
+ );
+ }
+ },
+ [messages]
+ );
+
+ const onSetIsRecording = (flag: boolean) => {
+ setIsRecording(flag);
+ };
+
+ const onLanguage = (item: FloatMenuItemConfig) => {
+ if (item.type === "add") {
+ setVisible(true);
+ } else {
+ setCurrentLanguage(item);
+ }
+ };
+
+ return (
+
+
+
+
+
{
+ setVisible(false);
+ }}
+ >
+
+
+ 宠物语种
+
+
+
+ );
+}
+
+export default Index;
diff --git a/src/view/home/types.ts b/src/view/home/types.ts
new file mode 100644
index 0000000..9e0d223
--- /dev/null
+++ b/src/view/home/types.ts
@@ -0,0 +1,12 @@
+export interface Message {
+ id: number;
+ type?: "dog" | "cat" | "pig";
+ audioUrl: string;
+ name: string; //名字
+ duration: number; //时长
+ timestamp: number; //时间
+ translatedText?: string;
+ isTranslating?: boolean;
+ avatar?: string;
+ isPlaying?: boolean;
+}
diff --git a/src/view/setting/index.tsx b/src/view/setting/index.tsx
new file mode 100644
index 0000000..b7cbbd5
--- /dev/null
+++ b/src/view/setting/index.tsx
@@ -0,0 +1,13 @@
+import useI18n from "@/hooks/i18n.ts";
+import MainLayout from "@/layout/main/mainLayout";
+import { Button } from "antd-mobile";
+import { useI18nStore } from "@/store/i18n.ts";
+
+function Index() {
+ const t = useI18n();
+ const i18nStore = useI18nStore();
+
+ return qq ;
+}
+
+export default Index;
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..62e4f5d
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1,9 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_BASE_URL: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..4a6e1df
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ // "jsx": "react",
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+
+ "baseUrl": "./",
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..42872c5
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..b2e4138
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,21 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react-swc";
+import path from "path";
+import basicSsl from "@vitejs/plugin-basic-ssl";
+export default defineConfig({
+ // plugins: [react()],
+ plugins: [react(), basicSsl()],
+ server: {
+ https: {},
+ port: 3000,
+ host: "0.0.0.0",
+ open: true,
+ },
+
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ "~": "/src",
+ },
+ },
+});