Compare commits
24 Commits
ddbee614e8
...
qianpw
| Author | SHA1 | Date | |
|---|---|---|---|
| f2cb55a844 | |||
| daf2bf503e | |||
| ae8e5cf384 | |||
| fc789b135f | |||
| bfa3d914e8 | |||
| 81fd471fd9 | |||
| 7f6aaff61c | |||
| e1b0be79f3 | |||
| edb54eef6c | |||
| 8282838dde | |||
| a6d84a1390 | |||
| 4116ef03e6 | |||
| ee96c5feb8 | |||
| 32b4a7e624 | |||
| 60c28c2297 | |||
| 7296a13e88 | |||
| 2378ad3a40 | |||
| 042f8c31a2 | |||
| a84d634949 | |||
| bb330be484 | |||
| f46a851ee5 | |||
| c9c9c8fa67 | |||
| 242a15c589 | |||
| 85244a451e |
16
.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# @see: http://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*] # 表示所有文件适用
|
||||||
|
charset = utf-8 # 设置文件字符集为 utf-8
|
||||||
|
end_of_line = lf # 控制换行类型(lf | cr | crlf)
|
||||||
|
insert_final_newline = true # 始终在文件末尾插入一个新行
|
||||||
|
indent_style = space # 缩进风格(tab | space)
|
||||||
|
indent_size = 2 # 缩进大小
|
||||||
|
max_line_length = 100 # 最大行长度
|
||||||
|
|
||||||
|
[*.md] # 表示仅 md 文件适用以下规则
|
||||||
|
insert_final_newline = false # 关闭末尾新行插入
|
||||||
|
max_line_length = off # 关闭最大行长度限制
|
||||||
|
trim_trailing_whitespace = false # 关闭末尾空格修剪
|
||||||
42
.eslintrc.js
@@ -1,42 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2020: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
"eslint:recommended",
|
|
||||||
"@typescript-eslint/recommended",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
],
|
|
||||||
ignorePatterns: ["dist", ".eslintrc.js", "node_modules"],
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "module",
|
|
||||||
},
|
|
||||||
plugins: ["react-refresh"],
|
|
||||||
rules: {
|
|
||||||
"react-refresh/only-export-components": [
|
|
||||||
"warn",
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ["packages/**/*.ts", "packages/**/*.tsx"],
|
|
||||||
rules: {
|
|
||||||
"no-console": "warn",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["projects/**/*.ts", "projects/**/*.tsx"],
|
|
||||||
rules: {
|
|
||||||
"no-console": "off",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
23
README.md
@@ -22,26 +22,13 @@
|
|||||||
2. 全局根字体大小断点(`src/index.css`)
|
2. 全局根字体大小断点(`src/index.css`)
|
||||||
|
|
||||||
```html
|
```html
|
||||||
html { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-size:
|
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%; /* 字体稍微大一点 */ } }
|
||||||
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`)
|
3. 组件库全局配色(`src/index.css`)
|
||||||
|
|
||||||
```html
|
```html
|
||||||
:root { --primary-color: #FFC300; } :root:root { --adm-color-primary: #FFC300;
|
: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; }
|
||||||
--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. 修改语言
|
4. 修改语言
|
||||||
@@ -99,8 +86,7 @@ export const useFetchXXX = () => {
|
|||||||
// set the url
|
// set the url
|
||||||
const url = `/xxx/xxx`;
|
const url = `/xxx/xxx`;
|
||||||
// fetch the data
|
// fetch the data
|
||||||
const [{ data, loading, error }, refetch] =
|
const [{ data, loading, error }, refetch] = useAxios < Result < MockResult >> url;
|
||||||
useAxios < Result < MockResult >> url;
|
|
||||||
// to do something
|
// to do something
|
||||||
return { data, loading, error, refetch };
|
return { data, loading, error, refetch };
|
||||||
};
|
};
|
||||||
@@ -113,8 +99,7 @@ export const useFetchPageXXX = (page: number, size: number) => {
|
|||||||
// set the url
|
// set the url
|
||||||
const url = `/xxx/xxx?page=${page}&size=${size}`;
|
const url = `/xxx/xxx?page=${page}&size=${size}`;
|
||||||
// fetch the data
|
// fetch the data
|
||||||
const [{ data, loading, error }, refetch] =
|
const [{ data, loading, error }, refetch] = useAxios < Page < MockResult >> url;
|
||||||
useAxios < Page < MockResult >> url;
|
|
||||||
// to do something
|
// to do something
|
||||||
return { data, loading, error, refetch };
|
return { data, loading, error, refetch };
|
||||||
};
|
};
|
||||||
|
|||||||
111
eslint.config.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// import antfu from "@antfu/eslint-config";
|
||||||
|
|
||||||
|
// export default antfu({
|
||||||
|
// vue: false,
|
||||||
|
// react: true, // @eslint-react/eslint-plugin
|
||||||
|
// typescript: true,
|
||||||
|
// stylistic: true,
|
||||||
|
// formatters: true, // eslint-plugin-format 格式化
|
||||||
|
// rules: {
|
||||||
|
// "react/no-array-index-key": "off",
|
||||||
|
// "react/no-children-to-array": "off",
|
||||||
|
// "jsdoc/check-alignment": "off",
|
||||||
|
// "react-hooks/exhaustive-deps": "off",
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// eslint.config.js
|
||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import tsParser from "@typescript-eslint/parser";
|
||||||
|
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
// 忽略文件
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"dist/**",
|
||||||
|
"node_modules/**",
|
||||||
|
"packages/*/dist/**",
|
||||||
|
"projects/*/dist/**",
|
||||||
|
"**/*.config.js",
|
||||||
|
"**/*.config.ts",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// 基础 JavaScript 配置
|
||||||
|
{
|
||||||
|
files: ["**/*.{js,jsx,ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: "module",
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.es2020,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...js.configs.recommended.rules,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// TypeScript 配置
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"@typescript-eslint": tsPlugin,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...tsPlugin.configs.recommended.rules,
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ argsIgnorePattern: "^_" },
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// React 配置
|
||||||
|
{
|
||||||
|
files: ["**/*.{jsx,tsx}"],
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react-refresh/only-export-components": [
|
||||||
|
"warn",
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 项目特定规则
|
||||||
|
{
|
||||||
|
files: ["projects/**/*.{ts,tsx}"],
|
||||||
|
rules: {
|
||||||
|
"no-console": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 包特定规则
|
||||||
|
{
|
||||||
|
files: ["packages/**/*.{ts,tsx}"],
|
||||||
|
rules: {
|
||||||
|
"no-console": "warn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
39
index.html
@@ -1,39 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, viewport-fit=cover, initial-scale=1.0"
|
|
||||||
,
|
|
||||||
/>
|
|
||||||
<title>tashow-h5</title>
|
|
||||||
<style>
|
|
||||||
/* 设置CSS变量用于安全区域 */
|
|
||||||
/* 防止页面滚动的全局样式 */
|
|
||||||
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;
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
35
package.json
@@ -6,7 +6,6 @@
|
|||||||
"dev:translate-h5": "pnpm --filter translate-h5 dev",
|
"dev:translate-h5": "pnpm --filter translate-h5 dev",
|
||||||
"build:translate-h5": "pnpm --filter translate-h5 build",
|
"build:translate-h5": "pnpm --filter translate-h5 build",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"dev:https": "vite --https",
|
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
@@ -16,11 +15,14 @@
|
|||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
"@workspace/hooks": "workspace:*",
|
||||||
|
"@workspace/shared": "workspace:*",
|
||||||
|
"@workspace/utils": "workspace:*",
|
||||||
"antd-mobile": "^5.33.0",
|
"antd-mobile": "^5.33.0",
|
||||||
"antd-mobile-icons": "^0.3.0",
|
"antd-mobile-icons": "^0.3.0",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"axios-hooks": "^5.0.2",
|
"axios-hooks": "^5.0.2",
|
||||||
|
"framer-motion": "^12.23.12",
|
||||||
"js-audio-recorder": "^1.0.7",
|
"js-audio-recorder": "^1.0.7",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"less": "^4.2.0",
|
"less": "^4.2.0",
|
||||||
@@ -31,22 +33,35 @@
|
|||||||
"react-router-dom": "^6.20.0",
|
"react-router-dom": "^6.20.0",
|
||||||
"react-slick": "^0.29.0",
|
"react-slick": "^0.29.0",
|
||||||
"slick-carousel": "^1.8.1",
|
"slick-carousel": "^1.8.1",
|
||||||
|
"vconsole": "^3.15.1",
|
||||||
"weixin-js-sdk": "^1.6.5",
|
"weixin-js-sdk": "^1.6.5",
|
||||||
"zustand": "^4.4.6"
|
"zustand": "^4.4.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@eslint-react/eslint-plugin": "^1.22.1",
|
||||||
|
"@eslint/js": "^9.15.0",
|
||||||
|
"@react-dev-inspector/vite-plugin": "^2.0.1",
|
||||||
"@types/react": "^18.3.24",
|
"@types/react": "^18.3.24",
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
"@types/react-slick": "^0.23.12",
|
"@types/react-slick": "^0.23.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||||
"@typescript-eslint/parser": "^6.10.0",
|
"@typescript-eslint/parser": "^8.15.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
||||||
"eslint": "^9.34.0",
|
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.14",
|
||||||
|
"globals": "^15.12.0",
|
||||||
"postcss-pxtorem": "^6.0.0",
|
"postcss-pxtorem": "^6.0.0",
|
||||||
|
"react-dev-inspector": "^2.0.1",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.0.0"
|
"vite": "^6.0.5",
|
||||||
|
"vite-plugin-checker": "^0.8.0"
|
||||||
|
},
|
||||||
|
"simple-git-hooks": {
|
||||||
|
"pre-commit": "npx lint-staged"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*": "eslint --fix"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import useDocumentTitle from "./src/useDocumentTitle";
|
||||||
|
import useSafeNavigate from "./src/useSafeNavigate";
|
||||||
|
export { useDocumentTitle, useSafeNavigate };
|
||||||
|
|||||||
16
packages/hooks/package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "@workspace/hooks",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "./index.ts",
|
||||||
|
"types": "./index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./index.ts",
|
||||||
|
"types": "./index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint src --ext .ts,.tsx",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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<LocationState>({
|
|
||||||
loaded: false,
|
|
||||||
coordinates: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 用于存储任何错误消息的状态
|
|
||||||
const [error, setError] = useState<ErrorState>(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};
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import {useState, useEffect} from 'react';
|
|
||||||
import isEqual from 'lodash.isequal';
|
|
||||||
|
|
||||||
function useSessionStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
|
|
||||||
// 初始化状态
|
|
||||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
||||||
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;
|
|
||||||
16
packages/hooks/src/useDocumentTitle.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// hooks/useDocumentTitle.js
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
const useDocumentTitle = (title: string) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (title) {
|
||||||
|
document.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件卸载时可以恢复默认标题
|
||||||
|
return () => {
|
||||||
|
document.title = "默认标题"; // 或者从配置中获取
|
||||||
|
};
|
||||||
|
}, [title]);
|
||||||
|
};
|
||||||
|
export default useDocumentTitle;
|
||||||
23
packages/hooks/src/useSafeNavigate.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// hooks/useSafeNavigate.ts
|
||||||
|
import { useNavigate, NavigateOptions } from "react-router-dom";
|
||||||
|
|
||||||
|
const useSafeNavigate = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (path: string, options: NavigateOptions = {}) => {
|
||||||
|
const isBaidu = /baiduboxapp|baidubrowser|baidu/i.test(navigator.userAgent.toLowerCase());
|
||||||
|
|
||||||
|
if (isBaidu) {
|
||||||
|
const fullPath = path.startsWith("http") ? path : window.location.origin + path;
|
||||||
|
|
||||||
|
if (options.replace) {
|
||||||
|
window.location.replace(fullPath);
|
||||||
|
} else {
|
||||||
|
window.location.href = fullPath;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigate(path, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export default useSafeNavigate;
|
||||||
@@ -12,12 +12,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint src --ext .ts,.tsx",
|
"lint": "eslint src --ext .ts,.tsx",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"antd-mobile": "^5.34.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
packages/shared/src/error-page/errorBoundary/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// components/ErrorBoundary/index.tsx
|
||||||
|
import { Component, ReactNode } from "react";
|
||||||
|
import { Result, Button } from "antd-mobile";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: any) {
|
||||||
|
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
if (this.props.fallback) {
|
||||||
|
return this.props.fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "20px", textAlign: "center" }}>
|
||||||
|
<Result status="error" title="页面出错了" description="抱歉,页面出现了错误" />
|
||||||
|
<Button color="primary" onClick={() => window.location.reload()}>
|
||||||
|
刷新页面
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
20
packages/shared/src/error-page/index.module.less
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// components/ErrorPages/ErrorPages.module.less
|
||||||
|
.errorPage {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
packages/shared/src/error-page/notFound/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// components/ErrorPages/NotFound.tsx
|
||||||
|
import React from "react";
|
||||||
|
import { Button, Result } from "antd-mobile";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import styles from "../index.module.less";
|
||||||
|
|
||||||
|
const NotFound: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.errorPage}>
|
||||||
|
<Result status="error" title="页面不存在" description="抱歉,您访问的页面不存在" />
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Button color="primary" onClick={() => navigate(-1)} className={styles.button}>
|
||||||
|
返回上页
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => navigate("/")} className={styles.button}>
|
||||||
|
回到首页
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
29
packages/shared/src/error-page/serverError/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// components/ErrorPages/ServerError.tsx
|
||||||
|
import React from "react";
|
||||||
|
import { Button, Result } from "antd-mobile";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import styles from "../index.module.less";
|
||||||
|
|
||||||
|
const ServerError: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.errorPage}>
|
||||||
|
<Result status="error" title="服务器错误" description="服务器开小差了,请稍后再试" />
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Button color="primary" onClick={handleRefresh} className={styles.button}>
|
||||||
|
刷新页面
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => navigate("/")} className={styles.button}>
|
||||||
|
回到首页
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerError;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback } from "react";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
||||||
const VoiceIcon = (props: { isPlaying: boolean; onChange?: () => void }) => {
|
const VoiceIcon = (props: { isPlaying: boolean; onChange?: () => void }) => {
|
||||||
@@ -7,10 +7,7 @@ const VoiceIcon = (props: { isPlaying: boolean; onChange?: () => void }) => {
|
|||||||
props.onChange?.();
|
props.onChange?.();
|
||||||
}, [isPlaying]);
|
}, [isPlaying]);
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`voice-icon ${isPlaying ? "playing" : ""}`} onClick={onChange}>
|
||||||
className={`voice-icon ${isPlaying ? "playing" : ""}`}
|
|
||||||
onClick={onChange}
|
|
||||||
>
|
|
||||||
<div className="wave wave1"></div>
|
<div className="wave wave1"></div>
|
||||||
<div className="wave wave2"></div>
|
<div className="wave wave2"></div>
|
||||||
<div className="wave wave3"></div>
|
<div className="wave wave3"></div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function XPopup(props: DefinedProps) {
|
|||||||
const { visible, title, children, onClose } = props;
|
const { visible, title, children, onClose } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup visible={visible} closeOnMaskClick={true} className="xpopup">
|
<Popup visible={visible} closeOnMaskClick={true} className="xpopup" onMaskClick={onClose}>
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<h3 className="title">{title}</h3>
|
<h3 className="title">{title}</h3>
|
||||||
<span className="closeIcon" onClick={onClose}>
|
<span className="closeIcon" onClick={onClose}>
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"typeRoots": ["./types"]
|
||||||
"exclude": ["node_modules"]
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
packages/shared/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// src/types/global.d.ts
|
||||||
|
declare module "*.module.less" {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.module.css" {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他资源文件
|
||||||
|
declare module "*.png";
|
||||||
|
declare module "*.jpg";
|
||||||
|
declare module "*.jpeg";
|
||||||
|
declare module "*.gif";
|
||||||
|
declare module "*.svg";
|
||||||
|
declare module "*.ico";
|
||||||
|
declare module "*.webp";
|
||||||
43
packages/styles/variables.less
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// variables.less
|
||||||
|
// 颜色变量
|
||||||
|
@primary-color: #1890ff;
|
||||||
|
@secondary-color: #6c757d;
|
||||||
|
@success-color: #52c41a;
|
||||||
|
@warning-color: #faad14;
|
||||||
|
@error-color: #f5222d;
|
||||||
|
@white: #ffffff;
|
||||||
|
@black: #000000;
|
||||||
|
@gray-1: #f7f8fa;
|
||||||
|
@gray-2: #ebedf0;
|
||||||
|
@gray-3: #c8c9cc;
|
||||||
|
@gray-4: #969799;
|
||||||
|
@gray-5: #646566;
|
||||||
|
|
||||||
|
// 尺寸变量
|
||||||
|
@border-radius: 6px;
|
||||||
|
@border-radius-sm: 4px;
|
||||||
|
@border-radius-lg: 8px;
|
||||||
|
|
||||||
|
// 动画变量
|
||||||
|
@ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||||
|
@ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
||||||
|
@ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
|
||||||
|
// 间距变量
|
||||||
|
@spacing-xs: 4px;
|
||||||
|
@spacing-sm: 8px;
|
||||||
|
@spacing-md: 16px;
|
||||||
|
@spacing-lg: 24px;
|
||||||
|
@spacing-xl: 32px;
|
||||||
|
|
||||||
|
// 字体变量
|
||||||
|
@font-size-sm: 12px;
|
||||||
|
@font-size-base: 14px;
|
||||||
|
@font-size-lg: 16px;
|
||||||
|
@font-size-xl: 18px;
|
||||||
|
@font-size-xxl: 20px;
|
||||||
|
|
||||||
|
// 阴影变量
|
||||||
|
@box-shadow-base: 0 2px 8px fade(@black, 15%);
|
||||||
|
@box-shadow-card: 0 1px 2px -2px fade(@black, 16%), 0 3px 6px 0 fade(@black, 12%),
|
||||||
|
0 5px 12px 4px fade(@black, 9%);
|
||||||
2824
pnpm-lock.yaml
generated
@@ -1,13 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
'postcss-pxtorem': {
|
|
||||||
rootValue: 16,
|
|
||||||
unitPrecision: 5,
|
|
||||||
propList: ['*'],
|
|
||||||
selectorBlackList: [],
|
|
||||||
replace: true,
|
|
||||||
mediaQuery: false,
|
|
||||||
minPixelValue: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -4,49 +4,11 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"dev:https": "vite --https",
|
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@icon-park/react": "^1.4.2",
|
"browser-id3-writer": "^6.3.1"
|
||||||
"@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",
|
|
||||||
"@workspace/shared": "workspace:*",
|
|
||||||
"@workspace/utils": "workspace:*"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
projects/translate-h5/postcss.config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
"postcss-pxtorem": {
|
||||||
|
rootValue: 16, // 75表示750设计稿,37.5表示375设计稿(建议设置成37.5,因为可以兼容大部分移动端ui库)
|
||||||
|
unitPrecision: 5,
|
||||||
|
propList: ["*"],
|
||||||
|
selectorBlackList: [],
|
||||||
|
replace: true,
|
||||||
|
mediaQuery: false,
|
||||||
|
minPixelValue: 0,
|
||||||
|
selectorBlackList: ["norem"], // 过滤掉norem-开头的class,不进行rem转换
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
40
projects/translate-h5/src/api/getDialog.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import useAxios from "axios-hooks";
|
||||||
|
import { Result } from "@/types/http";
|
||||||
|
|
||||||
|
export interface MockResult {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockPage {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetch the data
|
||||||
|
* 详细使用可以查看 useAxios 的文档
|
||||||
|
*/
|
||||||
|
export const useGetDialog = () => {
|
||||||
|
const url = `/app-api/ai/dialog/getDialog`;
|
||||||
|
|
||||||
|
const [{ data, loading, error }, execute] = useAxios<Result<any>>(
|
||||||
|
{
|
||||||
|
url,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ manual: true } // 手动触发
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDialog = (params: { pageNo: number; pageSize: number }) => {
|
||||||
|
return execute({
|
||||||
|
params,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { data, loading, error, getDialog };
|
||||||
|
};
|
||||||
40
projects/translate-h5/src/api/translate.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import useAxios from "axios-hooks";
|
||||||
|
import { Result } from "@/types/http";
|
||||||
|
|
||||||
|
export interface MockResult {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockPage {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetch the data
|
||||||
|
* 详细使用可以查看 useAxios 的文档
|
||||||
|
*/
|
||||||
|
export const useUploadAudio = () => {
|
||||||
|
const url = `/app-api/ai/dialog/translate`;
|
||||||
|
|
||||||
|
const [{ data, loading, error }, execute] = useAxios<Result<any>>(
|
||||||
|
{
|
||||||
|
url,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ manual: true } // 手动触发
|
||||||
|
);
|
||||||
|
|
||||||
|
const uploadAudio = (formData: FormData) => {
|
||||||
|
return execute({
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { data, loading, error, uploadAudio };
|
||||||
|
};
|
||||||
BIN
projects/translate-h5/src/assets/translate/def-avatar.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
projects/translate-h5/src/assets/translate/emotion/emotion-1.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
projects/translate-h5/src/assets/translate/emotion/emotion-2.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
projects/translate-h5/src/assets/translate/emotion/emotion-3.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
projects/translate-h5/src/assets/translate/emotion/emotion-4.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
projects/translate-h5/src/assets/translate/emotion/emotion-5.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
projects/translate-h5/src/assets/translate/emotion/emotion-6.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
projects/translate-h5/src/assets/translate/emotion/emotion-7.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
projects/translate-h5/src/assets/translate/emotion/emotion-8.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
projects/translate-h5/src/assets/translate/emotion/emotion-9.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
projects/translate-h5/src/assets/user/cart.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
7
projects/translate-h5/src/assets/user/header.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
projects/translate-h5/src/assets/user/tool.png
Normal file
|
After Width: | Height: | Size: 137 KiB |
12
projects/translate-h5/src/hooks/i18n.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
63
projects/translate-h5/src/hooks/location.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import useI18n from "./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<LocationState>({
|
||||||
|
loaded: false,
|
||||||
|
coordinates: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 用于存储任何错误消息的状态
|
||||||
|
const [error, setError] = useState<ErrorState>(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 };
|
||||||
|
};
|
||||||
27
projects/translate-h5/src/hooks/session.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// import {useState, useEffect} from 'react';
|
||||||
|
|
||||||
|
// function useSessionStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
|
||||||
|
// // 初始化状态
|
||||||
|
// const [storedValue, setStoredValue] = useState<T>(() => {
|
||||||
|
// 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;
|
||||||
@@ -2,9 +2,9 @@ import Axios, {
|
|||||||
AxiosError,
|
AxiosError,
|
||||||
AxiosInstance as AxiosType,
|
AxiosInstance as AxiosType,
|
||||||
AxiosResponse,
|
AxiosResponse,
|
||||||
InternalAxiosRequestConfig
|
InternalAxiosRequestConfig,
|
||||||
} from 'axios';
|
} from "axios";
|
||||||
import {STORAGE_AUTHORIZE_KEY} from "@/composables/authorization.ts";
|
import { STORAGE_AUTHORIZE_KEY } from "@/composables/authorization.ts";
|
||||||
|
|
||||||
export interface ResponseBody<T = any> {
|
export interface ResponseBody<T = any> {
|
||||||
code: number;
|
code: number;
|
||||||
@@ -12,7 +12,9 @@ export interface ResponseBody<T = any> {
|
|||||||
msg: string;
|
msg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestHandler(config: InternalAxiosRequestConfig): Promise<InternalAxiosRequestConfig> {
|
async function requestHandler(
|
||||||
|
config: InternalAxiosRequestConfig
|
||||||
|
): Promise<InternalAxiosRequestConfig> {
|
||||||
const token = localStorage.getItem(STORAGE_AUTHORIZE_KEY);
|
const token = localStorage.getItem(STORAGE_AUTHORIZE_KEY);
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers[STORAGE_AUTHORIZE_KEY] = token;
|
config.headers[STORAGE_AUTHORIZE_KEY] = token;
|
||||||
@@ -34,7 +36,7 @@ class AxiosInstance {
|
|||||||
private readonly instance: AxiosType;
|
private readonly instance: AxiosType;
|
||||||
|
|
||||||
constructor(baseURL: string) {
|
constructor(baseURL: string) {
|
||||||
this.instance = Axios.create({baseURL});
|
this.instance = Axios.create({ baseURL });
|
||||||
|
|
||||||
this.instance.interceptors.request.use(requestHandler, errorHandler);
|
this.instance.interceptors.request.use(requestHandler, errorHandler);
|
||||||
this.instance.interceptors.response.use(responseHandler, errorHandler);
|
this.instance.interceptors.response.use(responseHandler, errorHandler);
|
||||||
@@ -44,6 +46,10 @@ class AxiosInstance {
|
|||||||
return this.instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log(import.meta.env.VITE_BASE_URL, "import.meta.env.VITE_BASE_URL");
|
||||||
const baseURL = import.meta.env.VITE_BASE_URL || 'http://127.0.0.1:8080';
|
// https://petshy.tashowz.com
|
||||||
|
//192.168.1.231:48080
|
||||||
|
// || "http://192.168.1.231:48080"
|
||||||
|
const baseURL = import.meta.env.VITE_BASE_URL || "https://petshy.tashowz.com";
|
||||||
|
// const baseURL = import.meta.env.VITE_BASE_URL || "http://192.168.1.231:48080";
|
||||||
export const axiosInstance = new AxiosInstance(baseURL).getInstance();
|
export const axiosInstance = new AxiosInstance(baseURL).getInstance();
|
||||||
|
|||||||
@@ -91,12 +91,13 @@ img {
|
|||||||
|
|
||||||
--adm-font-size-main: var(--adm-font-size-5); */
|
--adm-font-size-main: var(--adm-font-size-5); */
|
||||||
|
|
||||||
--adm-font-family: -apple-system, blinkmacsystemfont, "Helvetica Neue",
|
--adm-font-family: -apple-system, blinkmacsystemfont, "Helvetica Neue", helvetica, segoe ui, arial,
|
||||||
helvetica, segoe ui, arial, roboto, "PingFang SC", "miui",
|
roboto, "PingFang SC", "miui", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;
|
||||||
"Hiragino Sans GB", "Microsoft Yahei", sans-serif;
|
|
||||||
}
|
}
|
||||||
.i-icon {
|
.i-icon {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
svg {
|
svg {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
43
projects/translate-h5/src/layout/main/index.module.less
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
.main-layout {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.layout-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
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;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: #fff;
|
||||||
|
:global {
|
||||||
|
.adm-tab-bar {
|
||||||
|
// 去除默认阴影
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 8px 16px;
|
||||||
|
|
||||||
|
.adm-tab-bar-item-active {
|
||||||
|
// 添加选中时的背景圆角
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #0000000a;
|
||||||
|
border-radius: 100px;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { NavBar, SafeArea, TabBar, Toast } from "antd-mobile";
|
import { NavBar, SafeArea, TabBar } from "antd-mobile";
|
||||||
import { useNavigate, useLocation } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { User, CattleZodiac } from "@icon-park/react";
|
// import "./index.less";
|
||||||
import "./index.less";
|
import styles from "./index.module.less";
|
||||||
|
|
||||||
|
import { Translate, Electrocardiogram, GithubOne, Blossom, User } from "@icon-park/react";
|
||||||
|
|
||||||
interface MainLayoutProps {
|
interface MainLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
isShowNavBar?: boolean;
|
isShowNavBar?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
navBarRight?: React.ReactNode;
|
||||||
onLink?: () => void;
|
onLink?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,43 +19,53 @@ const MainLayout: React.FC<MainLayoutProps> = ({
|
|||||||
children,
|
children,
|
||||||
onLink,
|
onLink,
|
||||||
title,
|
title,
|
||||||
|
navBarRight,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
// const location = useLocation();
|
||||||
const { pathname } = location;
|
const [pathname, setPathname] = React.useState(location.pathname);
|
||||||
const [activeKey, setActiveKey] = React.useState(pathname);
|
|
||||||
|
|
||||||
const setRouteActive = (value: string) => {
|
|
||||||
if (value !== "/") {
|
|
||||||
Toast.show("待开发");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
key: "/",
|
key: "/translate",
|
||||||
title: "宠物翻译",
|
title: "宠物翻译",
|
||||||
icon: <CattleZodiac />,
|
icon: <Translate />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "/set",
|
key: "/mood",
|
||||||
title: "待办",
|
title: "情绪监控",
|
||||||
icon: <User />,
|
icon: <Electrocardiogram />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "/message",
|
key: "/archives",
|
||||||
title: "消息",
|
title: "我的宠物",
|
||||||
icon: <User />,
|
icon: <GithubOne />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "/me",
|
key: "/service",
|
||||||
title: "我的",
|
title: "宠物服务",
|
||||||
|
icon: <Blossom />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "/user",
|
||||||
|
title: "个人中心",
|
||||||
|
|
||||||
icon: <User size="24" />,
|
icon: <User />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const showTabBarRoutes = tabs.map((tab) => tab.key);
|
||||||
|
|
||||||
|
const setRouteActive = (value: string) => {
|
||||||
|
console.log(value);
|
||||||
|
|
||||||
|
setPathname(value);
|
||||||
|
navigate(value);
|
||||||
|
};
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
|
// 打印路由栈
|
||||||
|
|
||||||
|
// debugger;
|
||||||
if (onLink) {
|
if (onLink) {
|
||||||
onLink?.();
|
onLink?.();
|
||||||
} else {
|
} else {
|
||||||
@@ -60,22 +73,25 @@ const MainLayout: React.FC<MainLayoutProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="main-layout">
|
<div className={styles["main-layout"]}>
|
||||||
<SafeArea position="top" />
|
<SafeArea position="top" />
|
||||||
{isShowNavBar ? <NavBar onBack={goBack}>{title}</NavBar> : ""}
|
{isShowNavBar ? (
|
||||||
<div className="layout-content">{children}</div>
|
<NavBar onBack={goBack} style={{ backgroundColor: "#f5f5f5" }} right={navBarRight}>
|
||||||
|
{title}
|
||||||
<div className="footer layout-tab">
|
</NavBar>
|
||||||
{/* <TabBar
|
) : (
|
||||||
activeKey={pathname}
|
""
|
||||||
onChange={(value) => setRouteActive(value)}
|
)}
|
||||||
safeArea={true}
|
<div className={styles["layout-content"]}>{children}</div>
|
||||||
>
|
{showTabBarRoutes.includes(location.pathname) && (
|
||||||
|
<div className={styles["layout-tab"]}>
|
||||||
|
<TabBar activeKey={pathname} onChange={(value) => setRouteActive(value)} safeArea={true}>
|
||||||
{tabs.map((item) => (
|
{tabs.map((item) => (
|
||||||
<TabBar.Item key={item.key} icon={item.icon} title={item.title} />
|
<TabBar.Item key={item.key} icon={item.icon} title={item.title} />
|
||||||
))}
|
))}
|
||||||
</TabBar> */}
|
</TabBar>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import App from "@/view/app/App.tsx";
|
import App from "@/view/app/App.tsx";
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, {useEffect} from 'react';
|
import React, { useEffect } from "react";
|
||||||
import {useNavigate} from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
interface AuthRouteProps {
|
interface AuthRouteProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
auth?: boolean;
|
auth?: boolean;
|
||||||
|
path: string;
|
||||||
|
meta?: {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,14 +16,32 @@ interface AuthRouteProps {
|
|||||||
* @param auth 是否需要认证
|
* @param auth 是否需要认证
|
||||||
* @constructor 认证路由组件
|
* @constructor 认证路由组件
|
||||||
*/
|
*/
|
||||||
const AuthRoute: React.FC<AuthRouteProps> = ({children, auth}) => {
|
const AuthRoute: React.FC<AuthRouteProps> = ({ children, auth, meta }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const token = localStorage.getItem('token'); // 或者其他认证令牌的获取方式
|
const token = localStorage.getItem("token"); // 或者其他认证令牌的获取方式
|
||||||
const isAuthenticated = Boolean(token); // 认证逻辑
|
const isAuthenticated = Boolean(token); // 认证逻辑
|
||||||
|
console.log(auth);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (meta?.title) {
|
||||||
|
document.title = meta.title;
|
||||||
|
}
|
||||||
|
}, [meta]);
|
||||||
|
useEffect(() => {
|
||||||
|
// 识别微信浏览器
|
||||||
|
const userAgent = navigator.userAgent.toLowerCase();
|
||||||
|
console.log(userAgent.includes("micromessenger"));
|
||||||
|
if (userAgent.includes("micromessenger")) {
|
||||||
|
navigate("/wxIndex");
|
||||||
|
} else {
|
||||||
|
// 如果是'/wxIndex'跳转到首页
|
||||||
|
if (location.pathname === "/wxIndex") {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查角色权限
|
||||||
if (auth && !isAuthenticated) {
|
if (auth && !isAuthenticated) {
|
||||||
navigate('/login'); // 如果未认证且路由需要认证,则重定向到登录
|
navigate("/login"); // 如果未认证且路由需要认证,则重定向到登录
|
||||||
}
|
}
|
||||||
}, [auth, isAuthenticated, navigate]);
|
}, [auth, isAuthenticated, navigate]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import {Route, Routes} from 'react-router-dom';
|
import { Route, Routes } from "react-router-dom";
|
||||||
import {routes, AppRoute} from './routes';
|
import { routes, AppRoute } from "./routes";
|
||||||
import AuthRoute from './auth.tsx';
|
import AuthRoute from "./auth.tsx";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { DotLoading } from "antd-mobile";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染路由
|
* 渲染路由
|
||||||
@@ -8,12 +10,12 @@ import AuthRoute from './auth.tsx';
|
|||||||
*/
|
*/
|
||||||
export const RenderRoutes = () => {
|
export const RenderRoutes = () => {
|
||||||
const renderRoutes = (routes: AppRoute[]) => {
|
const renderRoutes = (routes: AppRoute[]) => {
|
||||||
return routes.map(route => (
|
return routes.map((route) => (
|
||||||
<Route
|
<Route
|
||||||
key={route.path}
|
key={route.path}
|
||||||
path={route.path}
|
path={route.path}
|
||||||
element={
|
element={
|
||||||
<AuthRoute auth={route.auth}>
|
<AuthRoute auth={route.auth} path={route.path} meta={route.meta}>
|
||||||
{route.element}
|
{route.element}
|
||||||
</AuthRoute>
|
</AuthRoute>
|
||||||
}
|
}
|
||||||
@@ -23,5 +25,24 @@ export const RenderRoutes = () => {
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Routes>{renderRoutes(routes)}</Routes>;
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "24px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DotLoading />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Routes>{renderRoutes(routes)}</Routes>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,122 @@
|
|||||||
import React from "react";
|
import { Navigate } from "react-router-dom";
|
||||||
import Home from "@/view/home";
|
import { lazy } from "react";
|
||||||
import TranslateDetail from "@/view/home/detail";
|
|
||||||
import Setting from "@/view/setting";
|
|
||||||
import Page404 from "@/view/error/page404";
|
|
||||||
export interface AppRoute {
|
export interface AppRoute {
|
||||||
path: string;
|
path: string;
|
||||||
element: React.ReactNode;
|
element: React.ReactNode;
|
||||||
auth?: boolean;
|
auth?: boolean;
|
||||||
children?: AppRoute[];
|
children?: AppRoute[];
|
||||||
|
meta?: {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
const Home = lazy(() => import("@/view/home"));
|
||||||
|
const Page404 = lazy(() => import("@/view/error/page404"));
|
||||||
|
const TranslateDetail = lazy(() => import("@/view/home/detail"));
|
||||||
|
const TranslateMood = lazy(() => import("@/view/mood"));
|
||||||
|
const TranslateArchives = lazy(() => import("@/view/archives"));
|
||||||
|
const TranslateArchivesAdd = lazy(() => import("@/view/addArchives"));
|
||||||
|
const Service = lazy(() => import("@/view/service"));
|
||||||
|
const ServiceDetail = lazy(() => import("@/view/serviceDetail"));
|
||||||
|
const User = lazy(() => import("@/view/user"));
|
||||||
|
const Order = lazy(() => import("@/view/order"));
|
||||||
|
const OrderDetail = lazy(() => import("@/view/orderDetail"));
|
||||||
|
const Payment = lazy(() => import("@/view/payment"));
|
||||||
|
const Result = lazy(() => import("@/view/result"));
|
||||||
|
const WXIndex = lazy(() => import("@/view/wxIndex/index"));
|
||||||
|
|
||||||
export const routes: AppRoute[] = [
|
export const routes: AppRoute[] = [
|
||||||
{ path: "/", element: <Home />, auth: false },
|
{
|
||||||
{ path: "/set", element: <Setting />, auth: false },
|
path: "/",
|
||||||
{ path: "/detail", element: <TranslateDetail />, auth: false },
|
element: <Navigate to="/translate" replace />,
|
||||||
{ path: "/mood", element: <Setting />, auth: false },
|
auth: false,
|
||||||
|
meta: {
|
||||||
|
title: "宠物翻译",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/wxIndex",
|
||||||
|
element: <WXIndex />,
|
||||||
|
auth: false,
|
||||||
|
meta: {
|
||||||
|
title: "微信跳转",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/translate",
|
||||||
|
element: <Home />,
|
||||||
|
auth: false,
|
||||||
|
meta: {
|
||||||
|
title: "宠物翻译",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ path: "/translate/detail", element: <TranslateDetail />, auth: false },
|
||||||
|
{
|
||||||
|
path: "/mood",
|
||||||
|
element: <TranslateMood />,
|
||||||
|
auth: false,
|
||||||
|
meta: {
|
||||||
|
title: "情绪监控",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/archives",
|
||||||
|
element: <TranslateArchives />,
|
||||||
|
auth: false,
|
||||||
|
meta: {
|
||||||
|
title: "宠物档案",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/addArchives",
|
||||||
|
element: <TranslateArchivesAdd />,
|
||||||
|
auth: false,
|
||||||
|
meta: {
|
||||||
|
title: "新建档案",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/service",
|
||||||
|
element: <Service />,
|
||||||
|
auth: false,
|
||||||
|
meta: {
|
||||||
|
title: "宠物服务",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/service/detail",
|
||||||
|
element: <ServiceDetail />,
|
||||||
|
auth: false,
|
||||||
|
meta: {
|
||||||
|
title: "服务详情",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/user",
|
||||||
|
element: <User />,
|
||||||
|
auth: false,
|
||||||
|
meta: {
|
||||||
|
title: "个人中心",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/order",
|
||||||
|
element: <Order />,
|
||||||
|
auth: false,
|
||||||
|
meta: {
|
||||||
|
title: "我的订单",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{ path: "/order/detail", element: <OrderDetail />, auth: false, meta: { title: "订单详情" } },
|
||||||
|
{
|
||||||
|
path: "/payment",
|
||||||
|
element: <Payment />,
|
||||||
|
auth: false,
|
||||||
|
meta: {
|
||||||
|
title: "选择服务",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ path: "/result", element: <Result />, auth: false, meta: { title: "支付结果" } },
|
||||||
{ path: "*", element: <Page404 />, auth: false },
|
{ path: "*", element: <Page404 />, auth: false },
|
||||||
];
|
];
|
||||||
|
|||||||
19
projects/translate-h5/src/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// src/types/global.d.ts
|
||||||
|
declare module "*.module.less" {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.module.css" {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他资源文件
|
||||||
|
declare module "*.png";
|
||||||
|
declare module "*.jpg";
|
||||||
|
declare module "*.jpeg";
|
||||||
|
declare module "*.gif";
|
||||||
|
declare module "*.svg";
|
||||||
|
declare module "*.ico";
|
||||||
|
declare module "*.webp";
|
||||||
94
projects/translate-h5/src/view/addArchives/index.module.less
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
.archives {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 100%;
|
||||||
|
|
||||||
|
.archivesInfo {
|
||||||
|
border-radius: 16px;
|
||||||
|
// 超出隐藏
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #fff;
|
||||||
|
.archivesFrom {
|
||||||
|
padding: 16px;
|
||||||
|
padding-top: 0;
|
||||||
|
.archivesFromItem {
|
||||||
|
height: 56px;
|
||||||
|
line-height: 56px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 0.5px solid #f5f5f5;
|
||||||
|
|
||||||
|
.archivesFromInput {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #9c9c9c;
|
||||||
|
.archivesTag {
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0 10px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #1f1f1f;
|
||||||
|
margin-left: 8px;
|
||||||
|
&.active {
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.editInput {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveButton {
|
||||||
|
--background-color: #000;
|
||||||
|
--border-color: #000;
|
||||||
|
--text-color: #fff;
|
||||||
|
height: 40px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.archivesAvatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 128px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popupHeader {
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popupBottom {
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
// 设置中间间隔
|
||||||
|
.popupButton {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 22px;
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
.cancelButton {
|
||||||
|
--background-color: #fff;
|
||||||
|
--border-color: #e9e9e9;
|
||||||
|
--text-color: #000000;
|
||||||
|
}
|
||||||
|
.confirmButton {
|
||||||
|
--background-color: #1c1c1c;
|
||||||
|
--border-color: #1c1c1c;
|
||||||
|
--text-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
189
projects/translate-h5/src/view/addArchives/index.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import MainLayout from "@/layout/main/mainLayout";
|
||||||
|
import styles from "./index.module.less";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Image, Button, Popup, Space } from "antd-mobile";
|
||||||
|
import { CloseOutline } from "antd-mobile-icons";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { Delete } from "@icon-park/react";
|
||||||
|
|
||||||
|
interface DefinedProps {
|
||||||
|
visible: boolean;
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
showBottom?: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MyPopup(props: DefinedProps) {
|
||||||
|
const { visible, title, children, onClose, showBottom = false } = props;
|
||||||
|
return (
|
||||||
|
<Popup
|
||||||
|
visible={visible}
|
||||||
|
closeOnMaskClick={true}
|
||||||
|
className={styles.xpopup}
|
||||||
|
onMaskClick={onClose}
|
||||||
|
>
|
||||||
|
<div className={styles.popupHeader}>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<span onClick={onClose}>
|
||||||
|
<CloseOutline style={{ fontSize: "16px" }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>{children}</div>
|
||||||
|
|
||||||
|
{showBottom && (
|
||||||
|
<div className={styles.popupBottom}>
|
||||||
|
<div className={styles.popupButton}>
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
shape="rounded"
|
||||||
|
className={styles.cancelButton}
|
||||||
|
>
|
||||||
|
取 消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.popupButton}>
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
shape="rounded"
|
||||||
|
className={styles.confirmButton}
|
||||||
|
>
|
||||||
|
确 定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Popup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add() {
|
||||||
|
const archivesAvatarPng = "http://qiniu.bydj.tashowz.com/assets/translate/archives-avatar.png";
|
||||||
|
const catDogPng = "http://qiniu.bydj.tashowz.com/assets/translate/cat-dog.png";
|
||||||
|
const archivesVarietyPng = "http://qiniu.bydj.tashowz.com/assets/translate/archives-variety.png";
|
||||||
|
const archivesDatePng = "http://qiniu.bydj.tashowz.com/assets/translate/archives-date.png";
|
||||||
|
const archivesWeightPng = "http://qiniu.bydj.tashowz.com/assets/translate/archives-weight.png";
|
||||||
|
|
||||||
|
const [visibleVariety, setVisibleVariety] = useState<boolean>(false);
|
||||||
|
const [visibleWeight, setVisibleWeight] = useState<boolean>(false);
|
||||||
|
const [visibleDate, setVisibleDate] = useState<boolean>(false);
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const flag = searchParams.get("flag");
|
||||||
|
|
||||||
|
const right = (
|
||||||
|
<div style={{ fontSize: 20 }}>
|
||||||
|
<Space style={{ "--gap": "20px" }}>
|
||||||
|
<Delete fill="#FF4D4F" />
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout
|
||||||
|
navBarRight={flag === "edit" && right}
|
||||||
|
isShowNavBar={true}
|
||||||
|
title={flag === "add" ? "添加档案" : "编辑档案"}
|
||||||
|
>
|
||||||
|
<div className={styles.archives}>
|
||||||
|
<div className={styles.archivesAvatar}>
|
||||||
|
<Image src={archivesAvatarPng} width={80} height={80} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.archivesInfo}>
|
||||||
|
{flag === "add" && <Image src={catDogPng} />}
|
||||||
|
|
||||||
|
<div className={styles.archivesFrom}>
|
||||||
|
<div className={styles.archivesFromItem}>
|
||||||
|
<div>昵称*</div>
|
||||||
|
<div className={`${styles.archivesFromInput} ${flag === "edit" && styles.editInput}`}>
|
||||||
|
{flag === "add" ? "请输入昵称" : "钱多多"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => setVisibleVariety(true)} className={styles.archivesFromItem}>
|
||||||
|
<div>品种*</div>
|
||||||
|
<div className={`${styles.archivesFromInput} ${flag === "edit" && styles.editInput}`}>
|
||||||
|
{flag === "add" ? "请选择" : "布偶猫"}
|
||||||
|
{">"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => setVisibleWeight(true)} className={styles.archivesFromItem}>
|
||||||
|
<div>体重*</div>
|
||||||
|
<div className={`${styles.archivesFromInput} ${flag === "edit" && styles.editInput}`}>
|
||||||
|
{flag === "add" ? "请选择" : "45kg"}
|
||||||
|
{">"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.archivesFromItem}>
|
||||||
|
<div>性别*</div>
|
||||||
|
<div className={styles.archivesFromInput}>
|
||||||
|
<div className={styles.archivesTag}>未知</div>
|
||||||
|
<div className={styles.archivesTag}>弟弟</div>
|
||||||
|
<div className={`${styles.archivesTag} ${flag === "edit" && styles.active}`}>
|
||||||
|
妹妹
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => setVisibleDate(true)} className={styles.archivesFromItem}>
|
||||||
|
<div>出生日期*</div>
|
||||||
|
<div className={`${styles.archivesFromInput} ${flag === "edit" && styles.editInput}`}>
|
||||||
|
{flag === "add" ? "请选择" : "2018-12-01"}
|
||||||
|
{">"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.archivesFromItem}>
|
||||||
|
<div>绝育情况*</div>
|
||||||
|
<div className={styles.archivesFromInput}>
|
||||||
|
<div className={styles.archivesTag}>未知</div>
|
||||||
|
<div className={styles.archivesTag}>已绝育</div>
|
||||||
|
<div className={`${styles.archivesTag} ${flag === "edit" && styles.active}`}>
|
||||||
|
未绝育
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.archivesFromItem}>
|
||||||
|
<Button block shape="rounded" className={styles.saveButton}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MyPopup
|
||||||
|
title="选择品种"
|
||||||
|
visible={visibleVariety}
|
||||||
|
onClose={() => {
|
||||||
|
setVisibleVariety(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image src={archivesVarietyPng}></Image>
|
||||||
|
</MyPopup>
|
||||||
|
|
||||||
|
<MyPopup
|
||||||
|
title="您宝贝的体重是?"
|
||||||
|
visible={visibleWeight}
|
||||||
|
onClose={() => {
|
||||||
|
setVisibleWeight(false);
|
||||||
|
}}
|
||||||
|
showBottom={true}
|
||||||
|
>
|
||||||
|
<Image src={archivesWeightPng}></Image>
|
||||||
|
</MyPopup>
|
||||||
|
<MyPopup
|
||||||
|
title="选择日期"
|
||||||
|
visible={visibleDate}
|
||||||
|
onClose={() => {
|
||||||
|
setVisibleDate(false);
|
||||||
|
}}
|
||||||
|
showBottom={true}
|
||||||
|
>
|
||||||
|
<Image src={archivesDatePng}></Image>
|
||||||
|
</MyPopup>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Add;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { RenderRoutes } from "@/route/render-routes.tsx";
|
import { RenderRoutes } from "@/route/render-routes.tsx";
|
||||||
import { axiosInstance } from "@/http/axios-instance.ts";
|
import { axiosInstance } from "@/http/axios-instance.ts";
|
||||||
import { configure } from "axios-hooks";
|
import { configure } from "axios-hooks";
|
||||||
@@ -12,6 +11,10 @@ function App() {
|
|||||||
axios: axiosInstance,
|
axios: axiosInstance,
|
||||||
});
|
});
|
||||||
const i18nStore = useI18nStore();
|
const i18nStore = useI18nStore();
|
||||||
|
// 持久化一个messageList的本地化数据
|
||||||
|
console.log("degfmessageList");
|
||||||
|
|
||||||
|
localStorage.setItem("messageList", JSON.stringify([]));
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConfigProvider locale={i18nStore.lang === "en_US" ? enUS : zhCN}>
|
<ConfigProvider locale={i18nStore.lang === "en_US" ? enUS : zhCN}>
|
||||||
|
|||||||
43
projects/translate-h5/src/view/archives/index.module.less
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
.archives {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 100%;
|
||||||
|
.img {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.placeholder-bottom {
|
||||||
|
height: 100px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.homeHeader {
|
||||||
|
display: flex;
|
||||||
|
padding: 12px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 99;
|
||||||
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 60px;
|
||||||
|
opacity: 0.98;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archivesAvatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 128px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
@@ -1,11 +1,95 @@
|
|||||||
// 档案
|
// 档案
|
||||||
import MainLayout from "@/layout/main/mainLayout";
|
import MainLayout from "@/layout/main/mainLayout";
|
||||||
import "./index.less";
|
import styles from "./index.module.less";
|
||||||
|
// import archivesPicturePng from "@/assets/translate/archives-picture.png";
|
||||||
|
// import archivesDataPng from "@/assets/translate/archives-data.png";
|
||||||
|
// import archivesEquipmentPng from "@/assets/translate/archives-equipment.png";
|
||||||
|
// import archivesSwPng from "@/assets/translate/archives-sw.png";
|
||||||
|
// import archivesFunctionPng from "@/assets/translate/archives-function.png";
|
||||||
|
// import archivesBottomPng from "@/assets/translate/archives-bottom.png";
|
||||||
|
// import archivesCardPng from "@/assets/translate/archives-card.png";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Outlet } from "react-router-dom"; // 添加这行
|
||||||
|
|
||||||
|
import { Image, Space } from "antd-mobile";
|
||||||
|
import { ListView, More, AddOne, AlignHorizontalCenterTwo } from "@icon-park/react";
|
||||||
function Index() {
|
function Index() {
|
||||||
|
const archivesPicturePng = "http://qiniu.bydj.tashowz.com/assets/translate/archives-picture.png";
|
||||||
|
const archivesDataPng = "http://qiniu.bydj.tashowz.com/assets/translate/archives-data.png";
|
||||||
|
const archivesEquipmentPng =
|
||||||
|
"http://qiniu.bydj.tashowz.com/assets/translate/archives-equipment.png";
|
||||||
|
const archivesSwPng = "http://qiniu.bydj.tashowz.com/assets/translate/archives-sw.png";
|
||||||
|
const archivesFunctionPng =
|
||||||
|
"http://qiniu.bydj.tashowz.com/assets/translate/archives-function.png";
|
||||||
|
const archivesBottomPng = "http://qiniu.bydj.tashowz.com/assets/translate/archives-bottom.png";
|
||||||
|
const archivesCardPng = "http://qiniu.bydj.tashowz.com/assets/translate/archives-card.png";
|
||||||
|
|
||||||
|
const [listShow, setListShow] = useState(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onLink = (link: string) => {
|
||||||
|
navigate(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
// const right = (
|
||||||
|
// <div style={{ fontSize: 16 }}>
|
||||||
|
// <Space style={{ "--gap": "16px" }}>
|
||||||
|
// {listShow ? (
|
||||||
|
// <AlignHorizontalCenterTwo onClick={() => setListShow(false)} fill="#333" />
|
||||||
|
// ) : (
|
||||||
|
// <ListView onClick={() => setListShow(true)} fill="#333" />
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// <AddOne onClick={() => onLink("/translate/addArchives?flag=add")} fill="#333" />
|
||||||
|
// <More fill="#333" />
|
||||||
|
// </Space>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout isShowNavBar={true}>
|
<MainLayout>
|
||||||
<div className="archives"></div>
|
<div className={styles.homeHeader}>
|
||||||
|
<h3>我的宠物</h3>
|
||||||
|
<Space style={{ fontSize: "20px", "--gap": "16px" }}>
|
||||||
|
{listShow ? (
|
||||||
|
<AlignHorizontalCenterTwo onClick={() => setListShow(false)} fill="#333" />
|
||||||
|
) : (
|
||||||
|
<ListView onClick={() => setListShow(true)} fill="#333" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AddOne onClick={() => onLink("/addArchives?flag=add")} fill="#333" />
|
||||||
|
<More fill="#333" />
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
{listShow ? (
|
||||||
|
<div className={styles.archives}>
|
||||||
|
<Image className={styles.img} src={archivesCardPng}></Image>
|
||||||
|
<Image className={styles.img} src={archivesCardPng}></Image>
|
||||||
|
<Image className={styles.img} src={archivesCardPng}></Image>
|
||||||
|
<Image className={styles.img} src={archivesCardPng}></Image>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className={styles.bottom}>
|
||||||
|
<Image src={archivesBottomPng}></Image>
|
||||||
|
</div>
|
||||||
|
<div className={styles.archives}>
|
||||||
|
<Image src={archivesPicturePng}></Image>
|
||||||
|
<Image
|
||||||
|
onClick={() => onLink("/addArchives?flag=edit")}
|
||||||
|
className={styles.img}
|
||||||
|
src={archivesDataPng}
|
||||||
|
></Image>
|
||||||
|
<Image className={styles.img} src={archivesEquipmentPng}></Image>
|
||||||
|
<Image className={styles.img} src={archivesSwPng}></Image>
|
||||||
|
<Image className={styles.img} src={archivesFunctionPng}></Image>
|
||||||
|
<div className={styles["placeholder-bottom"]}></div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Outlet /> {/* 添加这行 */}
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { Divider, Image, SpinLoading, Toast } from "antd-mobile";
|
|
||||||
import { VoiceIcon } from "@workspace/shared";
|
|
||||||
import dogSvg from "@/assets/translate/dog.svg";
|
|
||||||
import catSvg from "@/assets/translate/cat.svg";
|
|
||||||
import pigSvg from "@/assets/translate/pig.svg";
|
|
||||||
import { Message } from "../../types";
|
|
||||||
import "./index.less";
|
|
||||||
|
|
||||||
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<number>();
|
|
||||||
|
|
||||||
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") {
|
|
||||||
<Image
|
|
||||||
src={pigSvg}
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
fit="cover"
|
|
||||||
style={{ borderRadius: 32 }}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
if (type === "cat") {
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
src={catSvg}
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
fit="cover"
|
|
||||||
style={{ borderRadius: 32 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
src={dogSvg}
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
fit="cover"
|
|
||||||
style={{ borderRadius: 32 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="message">
|
|
||||||
{data.map((item, index) => (
|
|
||||||
<div
|
|
||||||
className="item"
|
|
||||||
key={index}
|
|
||||||
onClick={() => playAudio(item.id, item.audioUrl)}
|
|
||||||
>
|
|
||||||
{renderAvatar(item.type)}
|
|
||||||
<div className="rig">
|
|
||||||
<div>
|
|
||||||
<span className="name">{item.name}</span>
|
|
||||||
<Divider direction="vertical" style={{ margin: "0px 8px" }} />
|
|
||||||
<span className="">{item.timestamp}</span>
|
|
||||||
</div>
|
|
||||||
<div className="voice-container">
|
|
||||||
<VoiceIcon
|
|
||||||
onChange={onVoiceChange}
|
|
||||||
isPlaying={isPlaying && currentPlayingId === item.id}
|
|
||||||
/>
|
|
||||||
<div className="time">{item.duration}''</div>
|
|
||||||
</div>
|
|
||||||
{item.isTranslating ? (
|
|
||||||
<div className="translate">
|
|
||||||
<SpinLoading color="default" style={{ "--size": "12px" }} />
|
|
||||||
<span>翻译中...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="translate">{item.translatedText}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="item">
|
|
||||||
<div className="avatar"></div>
|
|
||||||
<div className="rig">
|
|
||||||
<div>
|
|
||||||
<span className="name">生无可恋喵</span>
|
|
||||||
<Divider direction="vertical" style={{ margin: "0px 8px" }} />
|
|
||||||
<span className="">15:00</span>
|
|
||||||
</div>
|
|
||||||
<div className="voice-container">
|
|
||||||
<VoiceIcon isPlaying={false} />
|
|
||||||
<div className="tips">
|
|
||||||
{isRecording ? "录制中..." : "轻点麦克风录制"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Index;
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
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<string>("全部宠物");
|
|
||||||
const animenuRef = useRef<DropdownRef>(null);
|
|
||||||
const handleAniSelect = (val: RadioValue) => {
|
|
||||||
setAniName(allAni[val as number]);
|
|
||||||
animenuRef.current?.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="search">
|
|
||||||
<SearchBar
|
|
||||||
placeholder="请输入翻译内容"
|
|
||||||
style={{
|
|
||||||
"--border-radius": "6px",
|
|
||||||
"--height": "32px",
|
|
||||||
"--padding-left": "12px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Dropdown className="all" ref={animenuRef}>
|
|
||||||
<Dropdown.Item key="ani" title={aniName}>
|
|
||||||
<div style={{ padding: 12 }}>
|
|
||||||
<Radio.Group defaultValue="default" onChange={handleAniSelect}>
|
|
||||||
<Space direction="vertical" block>
|
|
||||||
<Radio block value="0">
|
|
||||||
全部宠物
|
|
||||||
</Radio>
|
|
||||||
<Radio block value="1">
|
|
||||||
丑丑
|
|
||||||
</Radio>
|
|
||||||
<Radio block value="2">
|
|
||||||
胖胖
|
|
||||||
</Radio>
|
|
||||||
<Radio block value="3">
|
|
||||||
可可
|
|
||||||
</Radio>
|
|
||||||
</Space>
|
|
||||||
</Radio.Group>
|
|
||||||
</div>
|
|
||||||
</Dropdown.Item>
|
|
||||||
</Dropdown>
|
|
||||||
{/* <div className="all" onClick={handleAllAni}>
|
|
||||||
<span>全部宠物</span>
|
|
||||||
<DownOne
|
|
||||||
theme="filled"
|
|
||||||
size="16"
|
|
||||||
strokeWidth={3}
|
|
||||||
strokeLinecap="butt"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ActionSheet
|
|
||||||
visible={visible}
|
|
||||||
actions={actions}
|
|
||||||
onClose={() => setVisible(false)}
|
|
||||||
/> */}
|
|
||||||
{/* <Popup
|
|
||||||
visible={visible}
|
|
||||||
onClose={() => {
|
|
||||||
setVisible(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="popup-head">
|
|
||||||
<span>宠物</span>
|
|
||||||
</div>
|
|
||||||
11111
|
|
||||||
</Popup> */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Index;
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
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<boolean>(false); //是否有权限
|
|
||||||
const [isPermissioning, setIsPermissioning] = useState<boolean>(true); //获取权限中
|
|
||||||
const [recordingDuration, setRecordingDuration] = useState<number>(0); //录音时长进度
|
|
||||||
const [isModal, setIsModal] = useState<boolean>(false);
|
|
||||||
const recordingTimerRef = useRef<NodeJS.Timeout>();
|
|
||||||
const isCancelledRef = useRef(false);
|
|
||||||
const recordingStartTimeRef = useRef<number>(0); //录音时长
|
|
||||||
// 音效相关
|
|
||||||
const sendSoundRef = useRef<HTMLAudioElement | null>(null);
|
|
||||||
const startRecordSoundRef = useRef<HTMLAudioElement | null>(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 (
|
|
||||||
<>
|
|
||||||
<Image height={80} width={80} src={microphoneDisabledSvg} />
|
|
||||||
{isPermissioning ? (
|
|
||||||
<div className="tips">获取麦克风权限中...</div>
|
|
||||||
) : (
|
|
||||||
<div className="tips">麦克风没有开启权限</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (isRecording) {
|
|
||||||
//正在录音中
|
|
||||||
return (
|
|
||||||
<div onClick={onStopRecording} className="isRecording">
|
|
||||||
<ProgressCircle
|
|
||||||
percent={recordingDuration}
|
|
||||||
style={{ "--size": "80px" }}
|
|
||||||
>
|
|
||||||
<div className="recording-dot">
|
|
||||||
<span className="circle"></span>
|
|
||||||
</div>
|
|
||||||
</ProgressCircle>
|
|
||||||
<Button fill="none" onClick={cancelRecording} className="cancleBtn">
|
|
||||||
取消录制
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
//麦克风状态
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
height={80}
|
|
||||||
width={80}
|
|
||||||
src={microphoneSvg}
|
|
||||||
onClick={onStartRecording}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [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<HTMLAudioElement>) => {
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<AudioRecorder
|
|
||||||
onRecordingComplete={onRecordingComplete}
|
|
||||||
recorderControls={recorderControls}
|
|
||||||
audioTrackConstraints={{
|
|
||||||
noiseSuppression: true,
|
|
||||||
echoCancellation: true,
|
|
||||||
autoGainControl: true,
|
|
||||||
}}
|
|
||||||
showVisualizer={false}
|
|
||||||
/>
|
|
||||||
<div className={` voice-record`}>{renderBtn()}</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(Index);
|
|
||||||
@@ -1,39 +1,59 @@
|
|||||||
.home {
|
.home {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
.adm-tabs {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
.home-header {
|
||||||
}
|
display: flex;
|
||||||
.adm-tabs-content {
|
padding: 12px;
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
.adm-tabs-header {
|
|
||||||
border: 0 none;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
}
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
.adm-tabs-tab {
|
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
color: rgba(0, 0, 0, 0.25);
|
|
||||||
font-weight: 600;
|
|
||||||
&.adm-tabs-tab-active {
|
|
||||||
color: #000;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.adm-tabs-tab-line {
|
// .adm-tabs {
|
||||||
height: 0px;
|
// 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;
|
||||||
|
// font-size: 20px;
|
||||||
|
// font-weight: 600;
|
||||||
|
// &.adm-tabs-tab-active {
|
||||||
|
// color: #000;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .adm-tabs-tab-line {
|
||||||
|
// height: 0px;
|
||||||
|
// }
|
||||||
|
|
||||||
.translate-container {
|
.translate-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
.header {
|
||||||
|
padding: 0px 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,55 @@
|
|||||||
import MainLayout from "@/layout/main/mainLayout";
|
import MainLayout from "@/layout/main/mainLayout";
|
||||||
import { Button, Tabs } from "antd-mobile";
|
import { useState } from "react";
|
||||||
import Translate from "./translate";
|
import { Divider, Space } from "antd-mobile";
|
||||||
|
import Translate from "./translate/index";
|
||||||
|
import { Filter, HamburgerButton } from "@icon-park/react";
|
||||||
|
|
||||||
|
// import { useSafeNavigate } from "@workspace/hooks";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
||||||
function Index() {
|
function Index() {
|
||||||
const handleRecordComplete = (audioData: AudioData): void => {
|
const [visible, setVisible] = useState<boolean>(false);
|
||||||
console.log("录音完成:", audioData);
|
// const safeNavigate = useSafeNavigate();
|
||||||
};
|
|
||||||
|
|
||||||
const handleError = (error: Error): void => {
|
|
||||||
console.error("录音错误:", error);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<div className="home">
|
<div className="home">
|
||||||
<Tabs stretch={false}>
|
<div className="home-header">
|
||||||
<Tabs.Tab title="宠物翻译" key="1">
|
<h3>宠物翻译</h3>
|
||||||
<Translate />
|
|
||||||
</Tabs.Tab>
|
<Space style={{ fontSize: "20px", "--gap": "16px" }}>
|
||||||
<Tabs.Tab title="宠物档案" key="2">
|
<Filter
|
||||||
2
|
theme="outline"
|
||||||
</Tabs.Tab>
|
fill={visible ? "#118fff" : "#333"}
|
||||||
</Tabs>
|
strokeWidth={3}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
onClick={() => setVisible(!visible)}
|
||||||
|
/>
|
||||||
|
<Divider direction="vertical" />
|
||||||
|
{/* <Electrocardiogram
|
||||||
|
theme="outline"
|
||||||
|
fill="#333"
|
||||||
|
strokeWidth={3}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
onClick={() => onLink("/translate/mood")}
|
||||||
|
/>
|
||||||
|
<GithubOne
|
||||||
|
theme="outline"
|
||||||
|
fill="#333"
|
||||||
|
strokeWidth={3}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
onClick={() => onLink("/translate/archives")}
|
||||||
|
/> */}
|
||||||
|
<HamburgerButton
|
||||||
|
theme="outline"
|
||||||
|
size="24"
|
||||||
|
fill="#333"
|
||||||
|
strokeWidth={3}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<Translate searchVisible={visible} />
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { Image, Toast } from "antd-mobile";
|
|
||||||
import MessageCom from "./component/message";
|
|
||||||
import VoiceRecord from "./component/voice";
|
|
||||||
import {
|
|
||||||
XPopup,
|
|
||||||
FloatingMenu,
|
|
||||||
type FloatMenuItemConfig,
|
|
||||||
} from "@workspace/shared";
|
|
||||||
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: <Image src={dogSvg} />, type: "dog" },
|
|
||||||
{ icon: <Image src={catSvg} />, type: "cat" },
|
|
||||||
{ icon: <Image src={pigSvg} />, type: "pig" },
|
|
||||||
{
|
|
||||||
icon: (
|
|
||||||
<MoreTwo
|
|
||||||
theme="outline"
|
|
||||||
size="24"
|
|
||||||
fill="#666"
|
|
||||||
strokeWidth={3}
|
|
||||||
strokeLinecap="butt"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
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<string | null>(null); //当前播放id
|
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
|
||||||
const [isRecording, setIsRecording] = useState(false); //是否录音中
|
|
||||||
const [currentLanguage, setCurrentLanguage] = useState<FloatMenuItemConfig>();
|
|
||||||
const [visible, setVisible] = useState<boolean>(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 (
|
|
||||||
<div className="translate-container">
|
|
||||||
<MessageCom data={messages} isRecording={isRecording}></MessageCom>
|
|
||||||
<VoiceRecord
|
|
||||||
onRecordingComplete={onRecordingComplete}
|
|
||||||
isRecording={isRecording}
|
|
||||||
onSetIsRecording={onSetIsRecording}
|
|
||||||
/>
|
|
||||||
<FloatingMenu
|
|
||||||
menuItems={menuItems}
|
|
||||||
value={currentLanguage}
|
|
||||||
onChange={onLanguage}
|
|
||||||
/>
|
|
||||||
<XPopup
|
|
||||||
title="选择翻译语种"
|
|
||||||
visible={visible}
|
|
||||||
onClose={() => {
|
|
||||||
setVisible(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="card">
|
|
||||||
<span>快捷选项</span>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<div className="card">
|
|
||||||
<span>宠物语种</span>
|
|
||||||
</div>
|
|
||||||
</XPopup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Index;
|
|
||||||
@@ -2,6 +2,12 @@
|
|||||||
flex: 1 auto;
|
flex: 1 auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
.message-title {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
.item {
|
.item {
|
||||||
color: rgba(0, 0, 0, 0.45);
|
color: rgba(0, 0, 0, 0.45);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -24,6 +30,7 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
|
width: 100px;
|
||||||
.time {
|
.time {
|
||||||
color: rgba(0, 0, 0, 0.88);
|
color: rgba(0, 0, 0, 0.88);
|
||||||
}
|
}
|
||||||
@@ -35,12 +42,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.translate {
|
.translate {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: rgba(0, 0, 0, 0.02);
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
color: #000000e0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
max-width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
// 超出换行
|
||||||
|
white-space: normal;
|
||||||
|
.expression {
|
||||||
|
position: absolute;
|
||||||
|
right: -25px;
|
||||||
|
top: -30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Avatar, Divider, Space, SpinLoading, Toast, Image } from "antd-mobile";
|
||||||
|
import { VoiceIcon } from "@workspace/shared";
|
||||||
|
import { Message } from "../../../types";
|
||||||
|
import "./index.less";
|
||||||
|
import { DigitalWatches, Refresh } from "@icon-park/react";
|
||||||
|
import DefAvatar from "@/assets/translate/def-avatar.png";
|
||||||
|
import Emotion1Png from "@/assets/translate/emotion/emotion-1.png";
|
||||||
|
import Emotion2Png from "@/assets/translate/emotion/emotion-2.png";
|
||||||
|
import Emotion3Png from "@/assets/translate/emotion/emotion-3.png";
|
||||||
|
import Emotion4Png from "@/assets/translate/emotion/emotion-4.png";
|
||||||
|
import Emotion5Png from "@/assets/translate/emotion/emotion-5.png";
|
||||||
|
import Emotion6Png from "@/assets/translate/emotion/emotion-6.png";
|
||||||
|
import Emotion7Png from "@/assets/translate/emotion/emotion-7.png";
|
||||||
|
import Emotion8Png from "@/assets/translate/emotion/emotion-8.png";
|
||||||
|
import Emotion9Png from "@/assets/translate/emotion/emotion-9.png";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface DefinedProps {
|
||||||
|
data: Message[];
|
||||||
|
isRecording: boolean;
|
||||||
|
onRefresh: (formData: FormData, messageId: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Index(props: DefinedProps) {
|
||||||
|
const { data, isRecording } = props;
|
||||||
|
const audioRefs = useRef<{ [key: string]: HTMLAudioElement }>({});
|
||||||
|
const [currentPlayingId, setCurrentPlayingId] = useState<number>();
|
||||||
|
const emotionList = [
|
||||||
|
Emotion1Png,
|
||||||
|
Emotion4Png,
|
||||||
|
Emotion7Png,
|
||||||
|
Emotion2Png,
|
||||||
|
Emotion5Png,
|
||||||
|
Emotion8Png,
|
||||||
|
Emotion3Png,
|
||||||
|
Emotion6Png,
|
||||||
|
Emotion9Png,
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRecording && currentPlayingId) {
|
||||||
|
audioRefs.current[currentPlayingId].pause();
|
||||||
|
audioRefs.current[currentPlayingId].currentTime = 0;
|
||||||
|
setCurrentPlayingId(undefined);
|
||||||
|
}
|
||||||
|
}, [isRecording, currentPlayingId]);
|
||||||
|
|
||||||
|
const playAudio = async (id: number, audioUrl: string) => {
|
||||||
|
if (isRecording) {
|
||||||
|
Toast.show("录音中,无法播放音频");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentPlayingId && audioRefs.current[currentPlayingId]) {
|
||||||
|
audioRefs.current[currentPlayingId].pause();
|
||||||
|
audioRefs.current[currentPlayingId].currentTime = 0;
|
||||||
|
}
|
||||||
|
if (currentPlayingId !== id) {
|
||||||
|
console.log(id);
|
||||||
|
|
||||||
|
setCurrentPlayingId(id);
|
||||||
|
// console.log(audioUrl, "audioUrl");
|
||||||
|
|
||||||
|
// const response = await fetch(audioUrl);
|
||||||
|
// console.log("response", response);
|
||||||
|
|
||||||
|
// const arrayBuffer = await response.arrayBuffer();
|
||||||
|
// console.log("arrayBuffer", arrayBuffer);
|
||||||
|
|
||||||
|
// // 创建ID3Writer实例并移除ID3标签
|
||||||
|
// const writer = new ID3Writer(new Uint8Array(arrayBuffer));
|
||||||
|
// writer.removeTag(); // 移除 ID3 标签
|
||||||
|
|
||||||
|
// writer.addTag(); // 必须调用 addTag 才能生成带标签的音频数据
|
||||||
|
// const blob = writer.getBlob(); // 使用 getBlob 获取正确的 Blob 数据
|
||||||
|
|
||||||
|
// const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// console.log("url", url);
|
||||||
|
|
||||||
|
audioRefs.current[id] = new Audio(audioUrl);
|
||||||
|
audioRefs.current[id].play();
|
||||||
|
audioRefs.current[id].onended = () => {
|
||||||
|
setCurrentPlayingId(undefined);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
audioRefs.current[id].pause();
|
||||||
|
audioRefs.current[id].currentTime = 0;
|
||||||
|
setCurrentPlayingId(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAvatar = (item: Message) => {
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
src={item.petAvatar || DefAvatar}
|
||||||
|
style={{ "--border-radius": "32px", flexShrink: "0" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshMessage = async (item: Message, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log("item", item);
|
||||||
|
|
||||||
|
if (!item.file || !item.dialogId || !item.contentText) {
|
||||||
|
Toast.show("文件不存在,无法重新翻译");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", item.file);
|
||||||
|
formData.append("dialogId", String(item.dialogId));
|
||||||
|
formData.append("contentDuration", String(item.contentDuration));
|
||||||
|
|
||||||
|
props.onRefresh(formData, item.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTranslateResult = (item: Message) => {
|
||||||
|
if (item.isTranslating) {
|
||||||
|
return (
|
||||||
|
<div className="translate">
|
||||||
|
<SpinLoading color="default" style={{ "--size": "12px" }} />
|
||||||
|
<span>翻译中...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (item.transStatus === 1) {
|
||||||
|
return item.transResult?.length ? (
|
||||||
|
<div className="translate" style={{ verticalAlign: "middle", width: "240px" }}>
|
||||||
|
<span>{item.transResult}</span>
|
||||||
|
<div className="expression">
|
||||||
|
{item.emotion !== undefined &&
|
||||||
|
item.emotion > 0 &&
|
||||||
|
item.emotion <= emotionList.length && (
|
||||||
|
<Image width={"40px"} height={"40px"} src={emotionList[item.emotion - 1]} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Space justify={"between"} className="translate" style={{ verticalAlign: "middle" }}>
|
||||||
|
<span>未知语句</span>
|
||||||
|
<Refresh onClick={(e) => refreshMessage(item, e)} size="12" fill="#333" />
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log("item.transResult", item);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="translate"
|
||||||
|
style={{ verticalAlign: "middle" }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{item.transResult}</span>
|
||||||
|
<Refresh onClick={(e) => refreshMessage(item, e)} size="12" fill="#333" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="message">
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{!(item.isTranslating && !item.isRefresh) && (
|
||||||
|
<div className="message-title">
|
||||||
|
<Space
|
||||||
|
style={{
|
||||||
|
fontSize: "10px",
|
||||||
|
color: "#BFBFBF",
|
||||||
|
"--gap": "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{item.createTime}</div>
|
||||||
|
<Divider style={{ margin: "0px" }} direction="vertical" />
|
||||||
|
<DigitalWatches />
|
||||||
|
<div>{item.terminal ?? "来自App"}</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="item" key={index}>
|
||||||
|
{renderAvatar(item)}
|
||||||
|
<div className="rig">
|
||||||
|
<div>
|
||||||
|
<span className="name">
|
||||||
|
{item.isTranslating && !item.isRefresh ? "" : item.petName ?? ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="voice-container" onClick={() => playAudio(item.id, item.contentText)}>
|
||||||
|
<VoiceIcon
|
||||||
|
// onChange={onVoiceChange}
|
||||||
|
isPlaying={currentPlayingId === item.id}
|
||||||
|
/>
|
||||||
|
<div className="time">{item.contentDuration}''</div>
|
||||||
|
</div>
|
||||||
|
{renderTranslateResult(item)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
<div style={{ height: "130px", width: "100%" }}></div>
|
||||||
|
....
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index;
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { Calendar, Dropdown, type DropdownRef, Radio, SearchBar, Space } from "antd-mobile";
|
||||||
|
import { RadioValue } from "antd-mobile/es/components/radio";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
|
interface PropsConfig {
|
||||||
|
handleAllAni: () => void;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
const allAni = ["全部宠物", "丑丑", "胖胖", "可可"];
|
||||||
|
function SearchCom(props: PropsConfig) {
|
||||||
|
const { value } = props;
|
||||||
|
const [aniName, setAniName] = useState<string>("全部宠物");
|
||||||
|
const animenuRef = useRef<DropdownRef>(null);
|
||||||
|
const handleAniSelect = (val: RadioValue) => {
|
||||||
|
setAniName(allAni[val as number]);
|
||||||
|
animenuRef.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="search">
|
||||||
|
<SearchBar
|
||||||
|
placeholder="请输入翻译内容"
|
||||||
|
style={{
|
||||||
|
"--border-radius": "6px",
|
||||||
|
"--height": "32px",
|
||||||
|
"--padding-left": "12px",
|
||||||
|
}}
|
||||||
|
defaultValue={value}
|
||||||
|
/>
|
||||||
|
<Dropdown key="ani" className="all" ref={animenuRef}>
|
||||||
|
<Dropdown.Item key="ani" title={aniName}>
|
||||||
|
<div style={{ padding: 12 }}>
|
||||||
|
<Radio.Group defaultValue="default" onChange={handleAniSelect}>
|
||||||
|
<Space direction="vertical" block>
|
||||||
|
<Radio block value="0">
|
||||||
|
全部宠物
|
||||||
|
</Radio>
|
||||||
|
<Radio block value="1">
|
||||||
|
丑丑
|
||||||
|
</Radio>
|
||||||
|
<Radio block value="2">
|
||||||
|
胖胖
|
||||||
|
</Radio>
|
||||||
|
<Radio block value="3">
|
||||||
|
可可
|
||||||
|
</Radio>
|
||||||
|
</Space>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item key="time" title="时间 ">
|
||||||
|
<div style={{ padding: 12 }}>
|
||||||
|
<Calendar
|
||||||
|
prevMonthButton={<span>上一月</span>}
|
||||||
|
nextMonthButton={<span>下一月</span>}
|
||||||
|
prevYearButton={<span>上一年</span>}
|
||||||
|
nextYearButton={<span>下一年</span>}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item key="device" title="设备 ">
|
||||||
|
<div style={{ padding: 12 }}>
|
||||||
|
<Radio.Group defaultValue="default">
|
||||||
|
<Space direction="vertical" block>
|
||||||
|
<Radio block value="0">
|
||||||
|
AXR智能语音项圈
|
||||||
|
</Radio>
|
||||||
|
<Radio block value="1">
|
||||||
|
XSWL宠物手机
|
||||||
|
</Radio>
|
||||||
|
</Space>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item key="mood" title="情绪">
|
||||||
|
<div style={{ padding: 12 }}>
|
||||||
|
<Radio.Group defaultValue="default">
|
||||||
|
<Space direction="vertical" block>
|
||||||
|
<Radio block value="0">
|
||||||
|
兴奋/玩耍
|
||||||
|
</Radio>
|
||||||
|
<Radio block value="1">
|
||||||
|
愉快/满足
|
||||||
|
</Radio>
|
||||||
|
<Radio block value="2">
|
||||||
|
问候/好奇
|
||||||
|
</Radio>
|
||||||
|
<Radio block value="3">
|
||||||
|
平静/放松
|
||||||
|
</Radio>
|
||||||
|
</Space>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchCom;
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
.voice-record {
|
.voice-record {
|
||||||
position: relative;
|
position: fixed;
|
||||||
|
bottom: 60px;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 12px 0px;
|
padding: 12px 0px;
|
||||||
box-shadow: 1px 2px 4px 3px #eee;
|
// box-shadow: 1px 2px 4px 3px #eee;
|
||||||
|
// 不被挤压
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: 100px; /* 添加 min-height 防止被压缩 */
|
||||||
|
height: 100px; /* 保持原始高度 */
|
||||||
|
flex-basis: 100px; /* 明确指定基础大小,防止 flex 缩放影响 */
|
||||||
.adm-progress-circle-info {
|
.adm-progress-circle-info {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { AudioRecorder, useAudioRecorder } from "react-audio-voice-recorder";
|
import { AudioRecorder, useAudioRecorder } from "react-audio-voice-recorder";
|
||||||
import { Button, Dialog, Image, ProgressCircle, Toast } from "antd-mobile";
|
import { Button, Dialog, Image, ProgressCircle, Toast } from "antd-mobile";
|
||||||
import microphoneSvg from "@/assets/translate/microphone.svg";
|
import microphoneSvg from "@/assets/translate/microphone.svg";
|
||||||
import microphoneDisabledSvg from "@/assets/translate/microphoneDisabledSvg.svg";
|
import microphoneDisabledSvg from "@/assets/translate/microphoneDisabledSvg.svg";
|
||||||
import { createStartRecordSound, createSendSound } from "@/utils/voice";
|
import { createStartRecordSound, createSendSound } from "@/utils/voice";
|
||||||
|
import VConsole from "vconsole";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
import { Message } from "../../types";
|
|
||||||
import { CloseCircleOutline } from "antd-mobile-icons";
|
|
||||||
|
|
||||||
interface DefinedProps {
|
interface DefinedProps {
|
||||||
onRecordingComplete: (url: string, finalDuration: number) => void;
|
onRecordingComplete: (url: string, finalDuration: number, formData: FormData) => void;
|
||||||
isRecording: boolean;
|
isRecording: boolean;
|
||||||
onSetIsRecording: (flag: boolean) => void;
|
onSetIsRecording: (flag: boolean) => void;
|
||||||
|
dialogId: number;
|
||||||
}
|
}
|
||||||
function Index(props: DefinedProps) {
|
function Index(props: DefinedProps) {
|
||||||
const { isRecording } = props;
|
const { isRecording, dialogId } = props;
|
||||||
const [hasPermission, setHasPermission] = useState<boolean>(false); //是否有权限
|
const [hasPermission, setHasPermission] = useState<boolean>(false); //是否有权限
|
||||||
const [isPermissioning, setIsPermissioning] = useState<boolean>(true); //获取权限中
|
const [isPermissioning, setIsPermissioning] = useState<boolean>(true); //获取权限中
|
||||||
const [recordingDuration, setRecordingDuration] = useState<number>(0); //录音时长进度
|
const [recordingDuration, setRecordingDuration] = useState<number>(0); //录音时长进度
|
||||||
@@ -25,17 +24,23 @@ function Index(props: DefinedProps) {
|
|||||||
// 音效相关
|
// 音效相关
|
||||||
const sendSoundRef = useRef<HTMLAudioElement | null>(null);
|
const sendSoundRef = useRef<HTMLAudioElement | null>(null);
|
||||||
const startRecordSoundRef = useRef<HTMLAudioElement | null>(null);
|
const startRecordSoundRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
// 只在开发环境启用
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
new VConsole();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeSounds();
|
initializeSounds();
|
||||||
checkMicrophonePermission();
|
checkMicrophonePermission();
|
||||||
}, [hasPermission]);
|
}, [hasPermission]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (isRecording) {
|
// if (isRecording) {
|
||||||
recorderControls.startRecording();
|
// recorderControls.startRecording();
|
||||||
} else {
|
// } else {
|
||||||
}
|
// }
|
||||||
}, [isRecording]);
|
// }, [isRecording]);
|
||||||
|
|
||||||
//重置状态
|
//重置状态
|
||||||
const onResetRecordingState = () => {
|
const onResetRecordingState = () => {
|
||||||
@@ -61,8 +66,35 @@ function Index(props: DefinedProps) {
|
|||||||
console.error("音效初始化失败:", error);
|
console.error("音效初始化失败:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
//开始录音
|
||||||
const renderBtn = useCallback(() => {
|
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);
|
||||||
|
};
|
||||||
|
// 使用 useMemo 缓存 Image 组件
|
||||||
|
const MicrophoneImage = useMemo(
|
||||||
|
() => (
|
||||||
|
<Image
|
||||||
|
height={80}
|
||||||
|
width={80}
|
||||||
|
src={microphoneSvg}
|
||||||
|
onClick={onStartRecording}
|
||||||
|
placeholder={<div style={{ width: 80, height: 80 }} />} // 添加占位符
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[microphoneSvg, onStartRecording]
|
||||||
|
);
|
||||||
|
const renderBtn = () => {
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
//没有权限
|
//没有权限
|
||||||
return (
|
return (
|
||||||
@@ -80,10 +112,7 @@ function Index(props: DefinedProps) {
|
|||||||
//正在录音中
|
//正在录音中
|
||||||
return (
|
return (
|
||||||
<div onClick={onStopRecording} className="isRecording">
|
<div onClick={onStopRecording} className="isRecording">
|
||||||
<ProgressCircle
|
<ProgressCircle percent={recordingDuration} style={{ "--size": "80px" }}>
|
||||||
percent={recordingDuration}
|
|
||||||
style={{ "--size": "80px" }}
|
|
||||||
>
|
|
||||||
<div className="recording-dot">
|
<div className="recording-dot">
|
||||||
<span className="circle"></span>
|
<span className="circle"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,17 +123,9 @@ function Index(props: DefinedProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
//麦克风状态
|
return MicrophoneImage;
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
height={80}
|
|
||||||
width={80}
|
|
||||||
src={microphoneSvg}
|
|
||||||
onClick={onStartRecording}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [hasPermission, isRecording, recordingDuration]);
|
};
|
||||||
const checkMicrophonePermission = useCallback(async () => {
|
const checkMicrophonePermission = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
@@ -200,53 +221,113 @@ function Index(props: DefinedProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//开始录音
|
// 添加音频转换函数
|
||||||
const onStartRecording = () => {
|
const convertToWav = async (blob: Blob): Promise<Blob> => {
|
||||||
isCancelledRef.current = false;
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
if (recordingTimerRef.current) {
|
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||||
clearInterval(recordingTimerRef.current);
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||||
recordingTimerRef.current = undefined;
|
|
||||||
}
|
// 转换为 WAV 格式
|
||||||
props.onSetIsRecording(true);
|
const wavBuffer = audioBufferToWav(audioBuffer);
|
||||||
// recorderControls.startRecording();
|
return new Blob([wavBuffer], { type: "audio/wav" });
|
||||||
recordingStartTimeRef.current = Date.now();
|
|
||||||
// 立即开始计时
|
|
||||||
recordingTimerRef.current = setInterval(() => {
|
|
||||||
setRecordingDuration((prev) => prev + 1);
|
|
||||||
}, 1000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// WAV 转换辅助函数
|
||||||
|
const audioBufferToWav = (buffer: AudioBuffer): ArrayBuffer => {
|
||||||
|
const length = buffer.length;
|
||||||
|
const numberOfChannels = buffer.numberOfChannels;
|
||||||
|
const sampleRate = buffer.sampleRate;
|
||||||
|
const bitsPerSample = 16;
|
||||||
|
const byteRate = (sampleRate * numberOfChannels * bitsPerSample) / 8;
|
||||||
|
const blockAlign = (numberOfChannels * bitsPerSample) / 8;
|
||||||
|
const dataSize = length * numberOfChannels * (bitsPerSample / 8);
|
||||||
|
const bufferLength = 44 + dataSize;
|
||||||
|
const arrayBuffer = new ArrayBuffer(bufferLength);
|
||||||
|
const view = new DataView(arrayBuffer);
|
||||||
|
// RIFF identifier
|
||||||
|
writeString(view, 0, "RIFF");
|
||||||
|
// file length
|
||||||
|
view.setUint32(4, 36 + dataSize, true);
|
||||||
|
// RIFF type
|
||||||
|
writeString(view, 8, "WAVE");
|
||||||
|
// format chunk identifier
|
||||||
|
writeString(view, 12, "fmt ");
|
||||||
|
// format chunk length
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
// sample format (raw)
|
||||||
|
view.setUint16(20, 1, true);
|
||||||
|
// channel count
|
||||||
|
view.setUint16(22, numberOfChannels, true);
|
||||||
|
// sample rate
|
||||||
|
view.setUint32(24, sampleRate, true);
|
||||||
|
// byte rate (sample rate * block align)
|
||||||
|
view.setUint32(28, byteRate, true);
|
||||||
|
// block align (channel count * bytes per sample)
|
||||||
|
view.setUint16(32, blockAlign, true);
|
||||||
|
// bits per sample
|
||||||
|
view.setUint16(34, bitsPerSample, true);
|
||||||
|
// data chunk identifier
|
||||||
|
writeString(view, 36, "data");
|
||||||
|
// data chunk length
|
||||||
|
view.setUint32(40, dataSize, true);
|
||||||
|
|
||||||
|
// write the PCM samples
|
||||||
|
let offset = 44;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
for (let channel = 0; channel < numberOfChannels; channel++) {
|
||||||
|
const sample = Math.max(-1, Math.min(1, buffer.getChannelData(channel)[i]));
|
||||||
|
view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
|
||||||
|
offset += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeString = (view: DataView, offset: number, string: string) => {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onStopRecording = useCallback(() => {
|
const onStopRecording = useCallback(() => {
|
||||||
recorderControls.stopRecording();
|
recorderControls.stopRecording();
|
||||||
onResetRecordingState();
|
onResetRecordingState();
|
||||||
}, [recorderControls, recordingDuration]);
|
}, [recorderControls, recordingDuration]);
|
||||||
|
|
||||||
//录音完成
|
//录音完成
|
||||||
// 在发送时检查录音时长
|
// 在发送时检查录音时长
|
||||||
const onRecordingComplete = useCallback(
|
const onRecordingComplete = useCallback(
|
||||||
(blob: Blob) => {
|
async (blob: Blob) => {
|
||||||
if (isCancelledRef.current) {
|
if (isCancelledRef.current) {
|
||||||
Toast.show("已取消");
|
Toast.show("已取消");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查blob有效性
|
// 检查blob有效性
|
||||||
if (!blob || blob.size === 0) {
|
if (!blob || blob.size === 0) {
|
||||||
Toast.show("录音数据无效,请重新录音");
|
Toast.show("录音数据无效,请重新录音");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const audioUrl = URL.createObjectURL(blob);
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
const audio = new Audio();
|
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||||
audio.src = audioUrl;
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||||
// 计算实际录音时长
|
const accurateDuration = audioBuffer.duration;
|
||||||
|
if (accurateDuration < 1) {
|
||||||
audio.addEventListener("loadedmetadata", () => {
|
|
||||||
if (audio.duration < 1) {
|
|
||||||
Toast.show("录音时间太短,请重新录音");
|
Toast.show("录音时间太短,请重新录音");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
alert(audio.duration);
|
// 转换为 WAV 格式以获得最佳兼容性
|
||||||
|
const wavBlob = await convertToWav(blob);
|
||||||
|
const formData: FormData = new FormData();
|
||||||
|
formData.append("file", wavBlob, new Date().getTime() + ".wav");
|
||||||
|
formData.append("dialogId", `${dialogId}`);
|
||||||
|
const audioUrl = URL.createObjectURL(blob);
|
||||||
|
const audio = new Audio();
|
||||||
|
audio.src = audioUrl;
|
||||||
playSound(sendSoundRef);
|
playSound(sendSoundRef);
|
||||||
props.onRecordingComplete?.(audioUrl, Math.floor(audio.duration));
|
const contentDuration = Math.round(accurateDuration);
|
||||||
});
|
formData.append("contentDuration", `${contentDuration}`);
|
||||||
|
props.onRecordingComplete?.(audioUrl, contentDuration, formData);
|
||||||
},
|
},
|
||||||
[isCancelledRef, isRecording, sendSoundRef]
|
[isCancelledRef, isRecording, sendSoundRef]
|
||||||
);
|
);
|
||||||
259
projects/translate-h5/src/view/home/translate/index.tsx
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Image, Toast } from "antd-mobile";
|
||||||
|
import MessageCom from "./component/message";
|
||||||
|
import VoiceRecord from "./component/voice";
|
||||||
|
import { XPopup, type FloatMenuItemConfig } from "@workspace/shared";
|
||||||
|
import type { Message } from "../types";
|
||||||
|
import { useGetDialog } from "@/api/getDialog";
|
||||||
|
import { useUploadAudio } from "@/api/translate";
|
||||||
|
|
||||||
|
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";
|
||||||
|
import SearchCom from "./component/search";
|
||||||
|
interface DefinedProps {
|
||||||
|
searchVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems: FloatMenuItemConfig[] = [
|
||||||
|
{ icon: <Image src={dogSvg} />, type: "dog" },
|
||||||
|
{ icon: <Image src={catSvg} />, type: "cat" },
|
||||||
|
{ icon: <Image src={pigSvg} />, type: "pig" },
|
||||||
|
{
|
||||||
|
icon: <MoreTwo theme="outline" size="24" fill="#666" strokeWidth={3} strokeLinecap="butt" />,
|
||||||
|
type: "add",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
function Index(props: DefinedProps) {
|
||||||
|
const { searchVisible } = props;
|
||||||
|
const { loading: _uploadLoading, error: _uploadError, getDialog } = useGetDialog();
|
||||||
|
const { loading: _audioLoading, error: _audioError, uploadAudio } = useUploadAudio();
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [isRecording, setIsRecording] = useState(false); //是否录音中
|
||||||
|
const [_currentLanguage, setCurrentLanguage] = useState<FloatMenuItemConfig>();
|
||||||
|
const [visible, setVisible] = useState<boolean>(false);
|
||||||
|
const [dialogId, setDialogId] = useState<number>(0);
|
||||||
|
|
||||||
|
// 创建稳定化的消息字符串(排除 isTranslating 字段变化的影响)
|
||||||
|
const stableMessagesString = useMemo(() => {
|
||||||
|
const stableMessages = messages.map((msg) => {
|
||||||
|
const { isTranslating, isRefresh, ...rest } = msg;
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
return JSON.stringify(stableMessages);
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentLanguage(menuItems[0]);
|
||||||
|
fetchInitialMessages();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
const scrollToBottom = useCallback(() => {
|
||||||
|
const container = document.querySelector(".message");
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
console.log("container", container.scrollHeight);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 添加初始化数据的逻辑
|
||||||
|
const fetchInitialMessages = async () => {
|
||||||
|
try {
|
||||||
|
const messages = JSON.parse(localStorage.getItem("messageList") ?? "[]");
|
||||||
|
console.log("messages", messages);
|
||||||
|
|
||||||
|
// 这里替换为实际的API调用
|
||||||
|
// const response = await fetch('/api/messages');
|
||||||
|
const response = await getDialog({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 99,
|
||||||
|
});
|
||||||
|
const initialMessages: Message[] = response.data?.data?.messages.list || [];
|
||||||
|
|
||||||
|
setDialogId(response.data?.data?.dialogId);
|
||||||
|
if (messages.length > 0) {
|
||||||
|
setMessages(messages);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMessages(initialMessages);
|
||||||
|
localStorage.setItem("messageList", JSON.stringify(initialMessages));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取初始化数据失败:", error);
|
||||||
|
Toast.show("获取消息失败");
|
||||||
|
// 失败时设置为空数组
|
||||||
|
setMessages([]);
|
||||||
|
localStorage.setItem("messageList", JSON.stringify([]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听消息变化,自动滚动到底部
|
||||||
|
useEffect(() => {
|
||||||
|
if (messages.length > 0) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [stableMessagesString, scrollToBottom]);
|
||||||
|
|
||||||
|
//完成录音
|
||||||
|
const onRecordingComplete = useCallback(
|
||||||
|
async (audioUrl: string, actualDuration: number, formData: FormData) => {
|
||||||
|
console.log(audioUrl, "audioUrl");
|
||||||
|
const newMessage: Message = {
|
||||||
|
id: Date.now(),
|
||||||
|
contentText: audioUrl,
|
||||||
|
contentDuration: actualDuration,
|
||||||
|
isTranslating: true,
|
||||||
|
terminal: "",
|
||||||
|
file: formData.get("file") as Blob,
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, newMessage]);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
await onTranslateAudio(formData, newMessage.id);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
Toast.show("语音已发送");
|
||||||
|
},
|
||||||
|
[messages]
|
||||||
|
);
|
||||||
|
|
||||||
|
//翻译
|
||||||
|
const onTranslateAudio = useCallback(
|
||||||
|
async (formData: FormData, id: number) => {
|
||||||
|
try {
|
||||||
|
const response = await uploadAudio(formData);
|
||||||
|
const translatedData = response.data;
|
||||||
|
|
||||||
|
if (translatedData.data.transStatus) {
|
||||||
|
setMessages((prev) => {
|
||||||
|
const newMessages = prev.map((msg) =>
|
||||||
|
msg.id === id
|
||||||
|
? {
|
||||||
|
...msg,
|
||||||
|
...translatedData.data,
|
||||||
|
id: id,
|
||||||
|
isTranslating: false,
|
||||||
|
isRefresh: false,
|
||||||
|
}
|
||||||
|
: msg
|
||||||
|
);
|
||||||
|
localStorage.setItem("messageList", JSON.stringify(newMessages));
|
||||||
|
return newMessages;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(translatedData);
|
||||||
|
|
||||||
|
setMessages((prev) => {
|
||||||
|
const newMessages = prev.map((msg) =>
|
||||||
|
msg.id === id
|
||||||
|
? {
|
||||||
|
...msg,
|
||||||
|
...translatedData.data,
|
||||||
|
id: id,
|
||||||
|
isTranslating: false,
|
||||||
|
isRefresh: false,
|
||||||
|
}
|
||||||
|
: msg
|
||||||
|
);
|
||||||
|
localStorage.setItem("messageList", JSON.stringify(newMessages));
|
||||||
|
return newMessages;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("翻译失败:", error);
|
||||||
|
Toast.show("翻译失败,请重试");
|
||||||
|
setMessages((prev) => {
|
||||||
|
const newMessages = prev.map((msg) =>
|
||||||
|
msg.id === id
|
||||||
|
? {
|
||||||
|
...msg,
|
||||||
|
id: id,
|
||||||
|
isTranslating: false,
|
||||||
|
isRefresh: false,
|
||||||
|
}
|
||||||
|
: msg
|
||||||
|
);
|
||||||
|
localStorage.setItem("messageList", JSON.stringify(newMessages));
|
||||||
|
return newMessages;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[messages]
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshMessages = (formData: FormData, messageId: number) => {
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((msg) =>
|
||||||
|
msg.id === messageId
|
||||||
|
? {
|
||||||
|
...msg,
|
||||||
|
isTranslating: true,
|
||||||
|
isRefresh: true,
|
||||||
|
}
|
||||||
|
: msg
|
||||||
|
)
|
||||||
|
);
|
||||||
|
onTranslateAudio(formData, messageId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSetIsRecording = (flag: boolean) => {
|
||||||
|
setIsRecording(flag);
|
||||||
|
};
|
||||||
|
|
||||||
|
// const onLanguage = (item: FloatMenuItemConfig) => {
|
||||||
|
// if (item.type === "add") {
|
||||||
|
// setVisible(true);
|
||||||
|
// } else {
|
||||||
|
// setCurrentLanguage(item);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="translate-container">
|
||||||
|
{searchVisible && (
|
||||||
|
<div className="header">
|
||||||
|
<SearchCom handleAllAni={() => {}} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MessageCom
|
||||||
|
data={messages}
|
||||||
|
isRecording={isRecording}
|
||||||
|
onRefresh={refreshMessages}
|
||||||
|
></MessageCom>
|
||||||
|
|
||||||
|
<VoiceRecord
|
||||||
|
dialogId={dialogId}
|
||||||
|
onRecordingComplete={onRecordingComplete}
|
||||||
|
isRecording={isRecording}
|
||||||
|
onSetIsRecording={onSetIsRecording}
|
||||||
|
/>
|
||||||
|
{/* <FloatingMenu menuItems={menuItems} value={currentLanguage} onChange={onLanguage} /> */}
|
||||||
|
<XPopup
|
||||||
|
title="选择翻译语种"
|
||||||
|
visible={visible}
|
||||||
|
onClose={() => {
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="card">
|
||||||
|
<span>快捷选项</span>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<span>宠物语种</span>
|
||||||
|
</div>
|
||||||
|
</XPopup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index;
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
export interface Message {
|
export interface Message {
|
||||||
id: number;
|
id: number;
|
||||||
type?: "dog" | "cat" | "pig";
|
petType?: "dog" | "cat" | "pig" | "";
|
||||||
audioUrl: string;
|
contentText: string;
|
||||||
name: string; //名字
|
petName?: string; //名字
|
||||||
duration: number; //时长
|
contentDuration: number; //时长
|
||||||
timestamp: number; //时间
|
createTime?: number; //时间
|
||||||
translatedText?: string;
|
transResult?: string;
|
||||||
isTranslating?: boolean;
|
isTranslating?: boolean;
|
||||||
avatar?: string;
|
petAvatar?: string;
|
||||||
isPlaying?: boolean;
|
isPlaying?: boolean;
|
||||||
|
messageStatus?: 0 | 1;
|
||||||
|
transStatus?: 0 | 1;
|
||||||
|
isRefresh?: boolean;
|
||||||
|
emotion?: number;
|
||||||
|
terminal: string;
|
||||||
|
file?: Blob;
|
||||||
|
dialogId?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
41
projects/translate-h5/src/view/mood/index.module.less
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
.mood {
|
||||||
|
overflow-x: hidden; // 阻止横向滚动
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
.mood-title {
|
||||||
|
padding: 18px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
.mood-abstract-button {
|
||||||
|
color: #1677ff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mood-filter {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.mood-mask-1 {
|
||||||
|
position: absolute;
|
||||||
|
height: 54%;
|
||||||
|
width: 30%;
|
||||||
|
top: 10%;
|
||||||
|
left: 6%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.homeHeader {
|
||||||
|
display: flex;
|
||||||
|
padding: 12px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
background: #fff;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 99;
|
||||||
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
projects/translate-h5/src/view/mood/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import MainLayout from "@/layout/main/mainLayout";
|
||||||
|
import styles from "./index.module.less";
|
||||||
|
// import moodFilterPng from "@/assets/translate/mood-filter.png";
|
||||||
|
// import moodChartPng from "@/assets/translate/mood-chart.png";
|
||||||
|
// import moodAbstractPng from "@/assets/translate/mood-abstract.png";
|
||||||
|
// import moodPopupPng from "@/assets/translate/mood-popup.png";
|
||||||
|
import { Image, Space } from "antd-mobile";
|
||||||
|
import { Switch, Filter, More } from "@icon-park/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { XPopup } from "@workspace/shared";
|
||||||
|
|
||||||
|
const Moods = () => {
|
||||||
|
const moodFilterPng = "http://qiniu.bydj.tashowz.com/assets/translate/mood-filter.png";
|
||||||
|
const moodChartPng = "http://qiniu.bydj.tashowz.com/assets/translate/mood-chart.png";
|
||||||
|
const moodAbstractPng = "http://qiniu.bydj.tashowz.com/assets/translate/mood-abstract.png";
|
||||||
|
const moodPopupPng = "http://qiniu.bydj.tashowz.com/assets/translate/mood-popup.png";
|
||||||
|
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<div className={styles.homeHeader}>
|
||||||
|
<h3>情绪监控</h3>
|
||||||
|
<Space style={{ fontSize: "20px", "--gap": "16px" }}>
|
||||||
|
<Filter fill="#333" />
|
||||||
|
<More fill="#333" />
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.mood}>
|
||||||
|
<div className={styles["mood-filter"]}>
|
||||||
|
<div onClick={() => setVisible(true)} className={styles["mood-mask-1"]}></div>
|
||||||
|
<Image src={moodFilterPng}></Image>
|
||||||
|
</div>
|
||||||
|
<Image src={moodChartPng}></Image>
|
||||||
|
<div className={styles["mood-title"]}>
|
||||||
|
<h2>情绪摘要</h2>
|
||||||
|
<div className={styles["mood-abstract-button"]}>
|
||||||
|
<Switch theme="outline" fill="#1677FF" />
|
||||||
|
<span style={{ marginLeft: "4px" }}>饼状视图</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Image src={moodAbstractPng}></Image>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "20px",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<XPopup
|
||||||
|
title="宠物筛选"
|
||||||
|
visible={visible}
|
||||||
|
onClose={() => {
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image src={moodPopupPng}></Image>
|
||||||
|
</XPopup>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Moods;
|
||||||
32
projects/translate-h5/src/view/order/index.less
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
.order {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 100%;
|
||||||
|
.tab-box {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
// 隐藏滚动条
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于 antd-mobile 的 Image 组件包装器
|
||||||
|
.adm-image {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
// 图片自适应高度
|
||||||
|
img {
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
max-width: none; // 允许图片超过容器宽度
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
projects/translate-h5/src/view/order/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import MainLayout from "@/layout/main/mainLayout";
|
||||||
|
import "./index.less";
|
||||||
|
import { Image } from "antd-mobile";
|
||||||
|
// import tabsSvg from "@/assets/order/tabs.svg";
|
||||||
|
// import contant from "@/assets/order/contant.svg";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
function Order() {
|
||||||
|
const tabsSvg = "http://qiniu.bydj.tashowz.com/assets/order/tabs.svg";
|
||||||
|
const contant = "http://qiniu.bydj.tashowz.com/assets/order/contant.svg";
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<MainLayout isShowNavBar={true} title="我的订单">
|
||||||
|
<div className="order">
|
||||||
|
<div className="tab-box">
|
||||||
|
<Image alt="tabs" draggable={false} src={tabsSvg}></Image>
|
||||||
|
</div>
|
||||||
|
<Image onClick={() => navigate("/order/detail")} src={contant}></Image>
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Order;
|
||||||
14
projects/translate-h5/src/view/orderDetail/index.less
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.order-detail {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 100%;
|
||||||
|
.order-contant {
|
||||||
|
padding-bottom: 90px;
|
||||||
|
}
|
||||||
|
.order-bottom {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
projects/translate-h5/src/view/orderDetail/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import MainLayout from "@/layout/main/mainLayout";
|
||||||
|
import "./index.less";
|
||||||
|
import { Image } from "antd-mobile";
|
||||||
|
// import gdSvg from "@/assets/order/gd.svg";
|
||||||
|
// import gdPng from "@/assets/order/gd.png";
|
||||||
|
// import bottomSvg from "@/assets/order/bottom.svg";
|
||||||
|
|
||||||
|
function OrderDetail() {
|
||||||
|
const gdSvg = "http://qiniu.bydj.tashowz.com/assets/order/gd.svg";
|
||||||
|
const gdPng = "http://qiniu.bydj.tashowz.com/assets/order/gd.png";
|
||||||
|
const bottomSvg = "http://qiniu.bydj.tashowz.com/assets/order/bottom.svg";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout isShowNavBar={true} title="订单详情">
|
||||||
|
<div className="order-detail">
|
||||||
|
<div className="order-contant">
|
||||||
|
<Image src={gdPng}></Image>
|
||||||
|
<Image src={gdSvg}></Image>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="order-bottom">
|
||||||
|
<Image src={bottomSvg}></Image>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrderDetail;
|
||||||
64
projects/translate-h5/src/view/payment/index.less
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
.payment {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 100%;
|
||||||
|
.payment-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.payment-content {
|
||||||
|
padding: 12px;
|
||||||
|
padding-bottom: 160px;
|
||||||
|
.payment-cart {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
.mask {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99;
|
||||||
|
width: 100%;
|
||||||
|
height: 45.5%;
|
||||||
|
top: 21%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.payment-bottom {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-bottom {
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
// 设置中间间隔
|
||||||
|
.popup-button {
|
||||||
|
flex: 1;
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
.cancel-button {
|
||||||
|
--background-color: #fff;
|
||||||
|
--border-color: #e9e9e9;
|
||||||
|
--text-color: #000000;
|
||||||
|
}
|
||||||
|
.confirm-button {
|
||||||
|
--background-color: #f5222d;
|
||||||
|
--border-color: #f5222d;
|
||||||
|
--text-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
projects/translate-h5/src/view/payment/index.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import MainLayout from "@/layout/main/mainLayout";
|
||||||
|
import "./index.less";
|
||||||
|
import { Button, Image, Popup } from "antd-mobile";
|
||||||
|
// import HeaderSvg from "@/assets/payment/header.svg";
|
||||||
|
// import Cart1Svg from "@/assets/payment/cart-1.svg";
|
||||||
|
// import Cart2Svg from "@/assets/payment/cart-2.svg";
|
||||||
|
// import Cart3Png from "@/assets/payment/cart-3.png";
|
||||||
|
// import BottomPng from "@/assets/payment/bottom.png";
|
||||||
|
// import PopupPng from "@/assets/payment/popup.png";
|
||||||
|
import { CloseOutline } from "antd-mobile-icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
interface DefinedProps {
|
||||||
|
visible: boolean;
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
showBottom?: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
function MyPopup(props: DefinedProps) {
|
||||||
|
const { visible, title, children, onClose, showBottom = false } = props;
|
||||||
|
return (
|
||||||
|
<Popup visible={visible} closeOnMaskClick={true} className="xpopup" onMaskClick={onClose}>
|
||||||
|
<div className="popup-header">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<span onClick={onClose}>
|
||||||
|
<CloseOutline style={{ fontSize: "16px" }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="popup-content">{children}</div>
|
||||||
|
|
||||||
|
{showBottom && (
|
||||||
|
<div className="popup-bottom">
|
||||||
|
<div className="popup-button">
|
||||||
|
<Button onClick={onClose} block size="large" shape="rounded" className="confirm-button">
|
||||||
|
确 定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Popup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Payment() {
|
||||||
|
const HeaderSvg = "http://qiniu.bydj.tashowz.com/assets/payment/header.svg";
|
||||||
|
const Cart1Svg = "http://qiniu.bydj.tashowz.com/assets/payment/cart-1.svg";
|
||||||
|
const Cart2Svg = "http://qiniu.bydj.tashowz.com/assets/payment/cart-2.svg";
|
||||||
|
const Cart3Png = "http://qiniu.bydj.tashowz.com/assets/payment/cart-3.png";
|
||||||
|
const BottomPng = "http://qiniu.bydj.tashowz.com/assets/payment/bottom.png";
|
||||||
|
const PopupPng = "http://qiniu.bydj.tashowz.com/assets/payment/popup.png";
|
||||||
|
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onLink = (link: string) => {
|
||||||
|
navigate(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout isShowNavBar={true} title="选择服务">
|
||||||
|
<div className="payment">
|
||||||
|
<div className="payment-header">
|
||||||
|
<Image src={HeaderSvg} />
|
||||||
|
</div>
|
||||||
|
<div className="payment-content">
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
setVisible(true);
|
||||||
|
}}
|
||||||
|
className="payment-cart"
|
||||||
|
>
|
||||||
|
<div className="mask"></div>
|
||||||
|
<Image src={Cart1Svg}></Image>
|
||||||
|
</div>
|
||||||
|
<div className="payment-cart">
|
||||||
|
<Image src={Cart2Svg}></Image>
|
||||||
|
</div>
|
||||||
|
<div className="payment-cart">
|
||||||
|
<Image src={Cart3Png}></Image>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="payment-bottom">
|
||||||
|
<Image onClick={() => onLink("/result")} src={BottomPng}></Image>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MyPopup
|
||||||
|
title="选择品种"
|
||||||
|
visible={visible}
|
||||||
|
onClose={() => {
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
showBottom={true}
|
||||||
|
>
|
||||||
|
<div className="popup-content">
|
||||||
|
<Image src={PopupPng}></Image>
|
||||||
|
</div>
|
||||||
|
</MyPopup>
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Payment;
|
||||||
13
projects/translate-h5/src/view/result/index.less
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.result {
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 0 24px;
|
||||||
|
padding-top: 92px;
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
projects/translate-h5/src/view/result/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import MainLayout from "@/layout/main/mainLayout";
|
||||||
|
import "./index.less";
|
||||||
|
import { Image } from "antd-mobile";
|
||||||
|
|
||||||
|
// import TitlePng from "@/assets/result/title.png";
|
||||||
|
// import ContentSvg from "@/assets/result/content.svg";
|
||||||
|
// import BottomSvg from "@/assets/result/bottom.svg";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
function Result() {
|
||||||
|
const TitlePng = "http://qiniu.bydj.tashowz.com/assets/result/title.png";
|
||||||
|
const ContentSvg = "http://qiniu.bydj.tashowz.com/assets/result/content.svg";
|
||||||
|
const BottomSvg = "http://qiniu.bydj.tashowz.com/assets/result/bottom.svg";
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onLink = (link: string) => {
|
||||||
|
navigate(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<div className="result">
|
||||||
|
<Image src={TitlePng} className="title" />
|
||||||
|
<Image style={{ marginTop: "30px" }} src={ContentSvg} className="content" />
|
||||||
|
<div className="bottom">
|
||||||
|
<Image onClick={() => onLink("/service")} src={BottomSvg} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Result;
|
||||||
29
projects/translate-h5/src/view/service/index.module.less
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.service {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 12px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.homeHeader {
|
||||||
|
display: flex;
|
||||||
|
padding: 12px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 99;
|
||||||
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.headerItem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
p {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
projects/translate-h5/src/view/service/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import MainLayout from "@/layout/main/mainLayout";
|
||||||
|
import styles from "./index.module.less";
|
||||||
|
import { Space, Image } from "antd-mobile";
|
||||||
|
import { HeadsetOne, TextMessage, TransactionOrder } from "@icon-park/react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
function Service() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const content = "http://qiniu.bydj.tashowz.com/assets/service/content.png";
|
||||||
|
|
||||||
|
const onLink = (link: string) => {
|
||||||
|
navigate(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<div className={styles.homeHeader}>
|
||||||
|
<h3>宠物服务</h3>
|
||||||
|
<Space style={{ fontSize: "20px", "--gap": "16px" }}>
|
||||||
|
<div className={styles.headerItem}>
|
||||||
|
<HeadsetOne fill="#333" />
|
||||||
|
<p>客服</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.headerItem}>
|
||||||
|
<TextMessage fill="#333" />
|
||||||
|
<p>消息</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div onClick={() => onLink("/order")} className={styles.headerItem}>
|
||||||
|
<TransactionOrder fill="#333" />
|
||||||
|
<p>订单</p>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div className={styles.service}>
|
||||||
|
<Image onClick={() => onLink("/service/detail")} src={content}></Image>
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Service;
|
||||||
64
projects/translate-h5/src/view/serviceDetail/index.less
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
.service-detail {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 100%;
|
||||||
|
.swipers {
|
||||||
|
padding-top: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
.swiper-item {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
padding-bottom: 92px;
|
||||||
|
.service-item {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
&:first-child {
|
||||||
|
padding: 16px;
|
||||||
|
.tabs {
|
||||||
|
margin: 12px 0;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
// 隐藏滚动条
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于 antd-mobile 的 Image 组件包装器
|
||||||
|
.adm-image {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
// 图片自适应高度
|
||||||
|
img {
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
max-width: none; // 允许图片超过容器宽度
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-bottom: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
93
projects/translate-h5/src/view/serviceDetail/index.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import MainLayout from "@/layout/main/mainLayout";
|
||||||
|
import "./index.less";
|
||||||
|
import { Image, Swiper } from "antd-mobile";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
function ServiceDetail() {
|
||||||
|
const Swiper1Png = "http://qiniu.bydj.tashowz.com/assets/service/swiper-1.png";
|
||||||
|
const Swiper2Png = "http://qiniu.bydj.tashowz.com/assets/service/swiper-2.png";
|
||||||
|
const Swiper3Png = "http://qiniu.bydj.tashowz.com/assets/service/swiper-3.png";
|
||||||
|
const DetailPricePng = "http://qiniu.bydj.tashowz.com/assets/service/detail-price.png";
|
||||||
|
const DetailTapsSvg = "http://qiniu.bydj.tashowz.com/assets/service/detail-taps.svg";
|
||||||
|
const DetailTitlePng = "http://qiniu.bydj.tashowz.com/assets/service/detail-title.png";
|
||||||
|
const SzzPng = "http://qiniu.bydj.tashowz.com/assets/service/szz.png";
|
||||||
|
const SzSvg = "http://qiniu.bydj.tashowz.com/assets/service/sz.svg";
|
||||||
|
const NoBoxSvg = "http://qiniu.bydj.tashowz.com/assets/service/noBox.svg";
|
||||||
|
const BottomLSvg = "http://qiniu.bydj.tashowz.com/assets/service/bottom-l.svg";
|
||||||
|
const BottomRSvg = "http://qiniu.bydj.tashowz.com/assets/service/bottom-r.svg";
|
||||||
|
|
||||||
|
// 批量导入
|
||||||
|
// const images = import.meta.glob("@/assets/service/detail/*.png", { eager: true, as: "url" });
|
||||||
|
|
||||||
|
const images = Array.from({ length: 19 }, (_, index) => {
|
||||||
|
const num = index + 1;
|
||||||
|
return `http://qiniu.bydj.tashowz.com/assets/service/detail/${num}.png`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onLink = (link: string) => {
|
||||||
|
navigate(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
const swipers = [{ src: Swiper1Png }, { src: Swiper2Png }, { src: Swiper3Png }];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout isShowNavBar={true} title="服务详情">
|
||||||
|
<div className="service-detail">
|
||||||
|
<div className="swipers">
|
||||||
|
<Swiper
|
||||||
|
trackOffset={5}
|
||||||
|
stuckAtBoundary={false}
|
||||||
|
slideSize={90}
|
||||||
|
total={3}
|
||||||
|
style={{
|
||||||
|
"--border-radius": "8px",
|
||||||
|
}}
|
||||||
|
defaultIndex={1}
|
||||||
|
>
|
||||||
|
{swipers.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<Swiper.Item key={index}>
|
||||||
|
<div className="swiper-item">
|
||||||
|
<Image src={item.src} />
|
||||||
|
</div>
|
||||||
|
</Swiper.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
<div className="content">
|
||||||
|
<div className="service-item">
|
||||||
|
<Image src={DetailPricePng}></Image>
|
||||||
|
<div className="tabs">
|
||||||
|
<Image src={DetailTapsSvg}></Image>
|
||||||
|
</div>
|
||||||
|
<Image src={DetailTitlePng}></Image>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="service-item">
|
||||||
|
<Image src={SzSvg}></Image>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="service-item">
|
||||||
|
<Image src={SzzPng}></Image>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="service-item">
|
||||||
|
{images.map((src, index) => {
|
||||||
|
return <Image key={index} src={src} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<Image src={NoBoxSvg}></Image>
|
||||||
|
</div>
|
||||||
|
<div className="bottom">
|
||||||
|
<Image src={BottomLSvg}></Image>
|
||||||
|
<Image onClick={() => onLink("/payment")} src={BottomRSvg}></Image>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServiceDetail;
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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 <MainLayout>qq</MainLayout>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Index;
|
|
||||||
30
projects/translate-h5/src/view/user/index.module.less
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
.user {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.homeHeader {
|
||||||
|
display: flex;
|
||||||
|
padding: 12px;
|
||||||
|
padding-left: 0;
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 99;
|
||||||
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.headerItem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
p {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||