Compare commits

...

24 Commits

Author SHA1 Message Date
f2cb55a844 feat: 10.28beta版本 2025-10-28 14:27:46 +08:00
daf2bf503e feat: 添加tabber 2025-10-22 14:06:06 +08:00
ae8e5cf384 Merge branch 'wuxichen' of http://gitea.tashowz.com/tashow/tashow-h5 into qianpw 2025-10-17 10:16:58 +08:00
fc789b135f feat: 删除未使用 2025-10-17 10:16:21 +08:00
bfa3d914e8 fix: 录音中 暂停语音播放 2025-10-17 10:14:56 +08:00
81fd471fd9 fix: 修复audio播放问题 2025-10-17 10:01:32 +08:00
7f6aaff61c feat: 修改bug 2025-10-16 18:17:42 +08:00
e1b0be79f3 Merge branch 'wuxichen' of http://gitea.tashowz.com/tashow/tashow-h5 into qianpw 2025-10-16 15:38:02 +08:00
edb54eef6c Merge branch 'qianpw' of http://gitea.tashowz.com/tashow/tashow-h5 into qianpw 2025-10-16 15:37:15 +08:00
8282838dde feat: 图片清晰度加强 2025-10-16 15:35:49 +08:00
a6d84a1390 feat: 头像 2025-10-15 18:21:47 +08:00
4116ef03e6 feat: 所有格式转换成wav 2025-10-15 12:02:18 +08:00
ee96c5feb8 feat: translate 2025-10-15 10:34:22 +08:00
32b4a7e624 feat: del unuse code 2025-10-13 18:06:59 +08:00
60c28c2297 feat: 上传 2025-09-27 15:14:02 +08:00
7296a13e88 feat: search 2025-09-15 14:48:58 +08:00
2378ad3a40 feat: into 2025-09-15 13:51:35 +08:00
042f8c31a2 feat: into 2025-09-15 13:50:04 +08:00
a84d634949 feat: into 2025-09-13 15:17:41 +08:00
bb330be484 feat: into 2025-09-13 15:16:23 +08:00
f46a851ee5 feat: init 2025-09-05 18:20:53 +08:00
c9c9c8fa67 feat: init 2025-09-05 16:56:05 +08:00
242a15c589 feat: init 2025-09-05 16:44:12 +08:00
85244a451e feat: init 2025-09-05 15:18:10 +08:00
184 changed files with 5451 additions and 10896 deletions

16
.editorconfig Normal file
View 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 # 关闭末尾空格修剪

1
.env
View File

@@ -1 +0,0 @@
HTTPS=true

View File

@@ -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",
},
},
],
};

View File

@@ -22,26 +22,13 @@
2. 全局根字体大小断点(`src/index.css`
```html
html { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-size:
100%; } /* 小屏幕设备(如手机)*/ @media (max-width: 600px) { html { font-size:
90%; /* 字体稍微小一点 */ } } /* 中等屏幕设备(如平板)*/ @media (min-width:
601px) and (max-width: 1024px) { html { font-size: 100%; /* 标准大小 */ } } /*
大屏幕设备(如桌面)*/ @media (min-width: 1025px) { html { font-size: 110%; /*
字体稍微大一点 */ } }
html { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-size: 100%; } /* 小屏幕设备(如手机)*/ @media (max-width: 600px) { html { font-size: 90%; /* 字体稍微小一点 */ } } /* 中等屏幕设备(如平板)*/ @media (min-width: 601px) and (max-width: 1024px) { html { font-size: 100%; /* 标准大小 */ } } /* 大屏幕设备(如桌面)*/ @media (min-width: 1025px) { html { font-size: 110%; /* 字体稍微大一点 */ } }
```
3. 组件库全局配色(`src/index.css`
```html
:root { --primary-color: #FFC300; } :root:root { --adm-color-primary: #FFC300;
--adm-color-success: #00b578; --adm-color-warning: #ff8f1f; --adm-color-danger:
#ff3141; --adm-color-white: #ffffff; --adm-color-text: #333333;
--adm-color-text-secondary: #666666; --adm-color-weak: #999999;
--adm-color-light: #cccccc; --adm-color-border: #eeeeee; --adm-color-box:
#f5f5f5; --adm-color-background: #ffffff; --adm-font-size-main:
var(--adm-font-size-5); --adm-font-family: -apple-system, blinkmacsystemfont,
'Helvetica Neue', helvetica, segoe ui, arial, roboto, 'PingFang SC', 'miui',
'Hiragino Sans GB', 'Microsoft Yahei', sans-serif; }
:root { --primary-color: #FFC300; } :root:root { --adm-color-primary: #FFC300; --adm-color-success: #00b578; --adm-color-warning: #ff8f1f; --adm-color-danger: #ff3141; --adm-color-white: #ffffff; --adm-color-text: #333333; --adm-color-text-secondary: #666666; --adm-color-weak: #999999; --adm-color-light: #cccccc; --adm-color-border: #eeeeee; --adm-color-box: #f5f5f5; --adm-color-background: #ffffff; --adm-font-size-main: var(--adm-font-size-5); --adm-font-family: -apple-system, blinkmacsystemfont, 'Helvetica Neue', helvetica, segoe ui, arial, roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif; }
```
4. 修改语言
@@ -99,8 +86,7 @@ export const useFetchXXX = () => {
// set the url
const url = `/xxx/xxx`;
// fetch the data
const [{ data, loading, error }, refetch] =
useAxios < Result < MockResult >> url;
const [{ data, loading, error }, refetch] = useAxios < Result < MockResult >> url;
// to do something
return { data, loading, error, refetch };
};
@@ -113,8 +99,7 @@ export const useFetchPageXXX = (page: number, size: number) => {
// set the url
const url = `/xxx/xxx?page=${page}&size=${size}`;
// fetch the data
const [{ data, loading, error }, refetch] =
useAxios < Page < MockResult >> url;
const [{ data, loading, error }, refetch] = useAxios < Page < MockResult >> url;
// to do something
return { data, loading, error, refetch };
};

111
eslint.config.js Normal file
View 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",
},
},
];

View File

@@ -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>

View File

@@ -6,7 +6,6 @@
"dev:translate-h5": "pnpm --filter translate-h5 dev",
"build:translate-h5": "pnpm --filter translate-h5 build",
"dev": "vite",
"dev:https": "vite --https",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
@@ -16,11 +15,14 @@
"@types/node": "^20.10.0",
"@types/react-router-dom": "^5.3.3",
"@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-icons": "^0.3.0",
"axios": "^1.6.2",
"axios-hooks": "^5.0.2",
"framer-motion": "^12.23.12",
"js-audio-recorder": "^1.0.7",
"jsqr": "^1.4.0",
"less": "^4.2.0",
@@ -31,22 +33,35 @@
"react-router-dom": "^6.20.0",
"react-slick": "^0.29.0",
"slick-carousel": "^1.8.1",
"vconsole": "^3.15.1",
"weixin-js-sdk": "^1.6.5",
"zustand": "^4.4.6"
},
"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-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",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitejs/plugin-basic-ssl": "^2.1.0",
"@vitejs/plugin-react-swc": "^3.7.2",
"eslint": "^9.15.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.12.0",
"postcss-pxtorem": "^6.0.0",
"react-dev-inspector": "^2.0.1",
"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"
}
}

View File

@@ -0,0 +1,3 @@
import useDocumentTitle from "./src/useDocumentTitle";
import useSafeNavigate from "./src/useSafeNavigate";
export { useDocumentTitle, useSafeNavigate };

View 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"
}
}

View File

@@ -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;
}
}

View File

@@ -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};
};

View File

@@ -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;

View 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;

View 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;

View File

@@ -12,12 +12,5 @@
"scripts": {
"lint": "eslint src --ext .ts,.tsx",
"type-check": "tsc --noEmit"
},
"dependencies": {
"react": "^18.2.0",
"antd-mobile": "^5.34.0"
},
"peerDependencies": {
"react": ">=18.0.0"
}
}

View 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;

View 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;
}
}

View 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;

View 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;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState } from "react";
import React, { useCallback } from "react";
import "./index.less";
const VoiceIcon = (props: { isPlaying: boolean; onChange?: () => void }) => {
@@ -7,10 +7,7 @@ const VoiceIcon = (props: { isPlaying: boolean; onChange?: () => void }) => {
props.onChange?.();
}, [isPlaying]);
return (
<div
className={`voice-icon ${isPlaying ? "playing" : ""}`}
onClick={onChange}
>
<div className={`voice-icon ${isPlaying ? "playing" : ""}`} onClick={onChange}>
<div className="wave wave1"></div>
<div className="wave wave2"></div>
<div className="wave wave3"></div>

View File

@@ -12,7 +12,7 @@ function XPopup(props: DefinedProps) {
const { visible, title, children, onClose } = props;
return (
<Popup visible={visible} closeOnMaskClick={true} className="xpopup">
<Popup visible={visible} closeOnMaskClick={true} className="xpopup" onMaskClick={onClose}>
<div className="header">
<h3 className="title">{title}</h3>
<span className="closeIcon" onClick={onClose}>

View File

@@ -1,10 +1,26 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"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,
"jsx": "react-jsx"
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
"typeRoots": ["./types"]
}
}

19
packages/shared/types/global.d.ts vendored Normal file
View 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";

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +0,0 @@
export default {
plugins: {
'postcss-pxtorem': {
rootValue: 16,
unitPrecision: 5,
propList: ['*'],
selectorBlackList: [],
replace: true,
mediaQuery: false,
minPixelValue: 0,
},
},
};

View File

@@ -4,49 +4,11 @@
"type": "module",
"scripts": {
"dev": "vite",
"dev:https": "vite --https",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@icon-park/react": "^1.4.2",
"@types/node": "^20.10.0",
"@types/react-router-dom": "^5.3.3",
"@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-basic-ssl": "^2.1.0",
"antd-mobile": "^5.33.0",
"antd-mobile-icons": "^0.3.0",
"axios": "^1.6.2",
"axios-hooks": "^5.0.2",
"js-audio-recorder": "^1.0.7",
"jsqr": "^1.4.0",
"less": "^4.2.0",
"query-string": "^8.1.0",
"react": "^18.2.0",
"react-audio-voice-recorder": "^2.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"react-slick": "^0.29.0",
"slick-carousel": "^1.8.1",
"weixin-js-sdk": "^1.6.5",
"zustand": "^4.4.6",
"@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"
"browser-id3-writer": "^6.3.1"
}
}

View 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转换
},
},
};

View 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 };
};

View 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 };
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View 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;
};
}

View 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 };
};

View 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;

View File

@@ -2,8 +2,8 @@ import Axios, {
AxiosError,
AxiosInstance as AxiosType,
AxiosResponse,
InternalAxiosRequestConfig
} from 'axios';
InternalAxiosRequestConfig,
} from "axios";
import { STORAGE_AUTHORIZE_KEY } from "@/composables/authorization.ts";
export interface ResponseBody<T = any> {
@@ -12,7 +12,9 @@ export interface ResponseBody<T = any> {
msg: string;
}
async function requestHandler(config: InternalAxiosRequestConfig): Promise<InternalAxiosRequestConfig> {
async function requestHandler(
config: InternalAxiosRequestConfig
): Promise<InternalAxiosRequestConfig> {
const token = localStorage.getItem(STORAGE_AUTHORIZE_KEY);
if (token) {
config.headers[STORAGE_AUTHORIZE_KEY] = token;
@@ -44,6 +46,10 @@ class AxiosInstance {
return this.instance;
}
}
const baseURL = import.meta.env.VITE_BASE_URL || 'http://127.0.0.1:8080';
console.log(import.meta.env.VITE_BASE_URL, "import.meta.env.VITE_BASE_URL");
// 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();

View File

@@ -91,12 +91,13 @@ img {
--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-font-family: -apple-system, blinkmacsystemfont, "Helvetica Neue", helvetica, segoe ui, arial,
roboto, "PingFang SC", "miui", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;
}
.i-icon {
height: 100%;
display: flex;
align-items: center;
}
svg {
height: 100%;

View File

@@ -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);
}
}

View 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;
}
}
}
}
}
}

View File

@@ -1,13 +1,16 @@
import React from "react";
import { NavBar, SafeArea, TabBar, Toast } from "antd-mobile";
import { useNavigate, useLocation } from "react-router-dom";
import { User, CattleZodiac } from "@icon-park/react";
import "./index.less";
import { NavBar, SafeArea, TabBar } from "antd-mobile";
import { useNavigate } from "react-router-dom";
// import "./index.less";
import styles from "./index.module.less";
import { Translate, Electrocardiogram, GithubOne, Blossom, User } from "@icon-park/react";
interface MainLayoutProps {
children: React.ReactNode;
isShowNavBar?: boolean;
title?: string;
navBarRight?: React.ReactNode;
onLink?: () => void;
}
@@ -16,43 +19,53 @@ const MainLayout: React.FC<MainLayoutProps> = ({
children,
onLink,
title,
navBarRight,
}) => {
const navigate = useNavigate();
const location = useLocation();
const { pathname } = location;
const [activeKey, setActiveKey] = React.useState(pathname);
const setRouteActive = (value: string) => {
if (value !== "/") {
Toast.show("待开发");
}
};
// const location = useLocation();
const [pathname, setPathname] = React.useState(location.pathname);
const tabs = [
{
key: "/",
key: "/translate",
title: "宠物翻译",
icon: <CattleZodiac />,
icon: <Translate />,
},
{
key: "/set",
title: "待办",
icon: <User />,
key: "/mood",
title: "情绪监控",
icon: <Electrocardiogram />,
},
{
key: "/message",
title: "消息",
icon: <User />,
key: "/archives",
title: "我的宠物",
icon: <GithubOne />,
},
{
key: "/me",
title: "我的",
key: "/service",
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 = () => {
// 打印路由栈
// debugger;
if (onLink) {
onLink?.();
} else {
@@ -60,22 +73,25 @@ const MainLayout: React.FC<MainLayoutProps> = ({
}
};
return (
<div className="main-layout">
<div className={styles["main-layout"]}>
<SafeArea position="top" />
{isShowNavBar ? <NavBar onBack={goBack}>{title}</NavBar> : ""}
<div className="layout-content">{children}</div>
<div className="footer layout-tab">
{/* <TabBar
activeKey={pathname}
onChange={(value) => setRouteActive(value)}
safeArea={true}
>
{isShowNavBar ? (
<NavBar onBack={goBack} style={{ backgroundColor: "#f5f5f5" }} right={navBarRight}>
{title}
</NavBar>
) : (
""
)}
<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) => (
<TabBar.Item key={item.key} icon={item.icon} title={item.title} />
))}
</TabBar> */}
</TabBar>
</div>
)}
</div>
);
};

View File

@@ -1,4 +1,3 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "@/view/app/App.tsx";

View File

@@ -1,9 +1,13 @@
import React, {useEffect} from 'react';
import {useNavigate} from 'react-router-dom';
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
interface AuthRouteProps {
children: React.ReactNode;
auth?: boolean;
path: string;
meta?: {
title: string;
};
}
/**
@@ -12,14 +16,32 @@ interface AuthRouteProps {
* @param auth 是否需要认证
* @constructor 认证路由组件
*/
const AuthRoute: React.FC<AuthRouteProps> = ({children, auth}) => {
const AuthRoute: React.FC<AuthRouteProps> = ({ children, auth, meta }) => {
const navigate = useNavigate();
const token = localStorage.getItem('token'); // 或者其他认证令牌的获取方式
const token = localStorage.getItem("token"); // 或者其他认证令牌的获取方式
const isAuthenticated = Boolean(token); // 认证逻辑
console.log(auth);
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) {
navigate('/login'); // 如果未认证且路由需要认证,则重定向到登录
navigate("/login"); // 如果未认证且路由需要认证,则重定向到登录
}
}, [auth, isAuthenticated, navigate]);

View File

@@ -1,6 +1,8 @@
import {Route, Routes} from 'react-router-dom';
import {routes, AppRoute} from './routes';
import AuthRoute from './auth.tsx';
import { Route, Routes } from "react-router-dom";
import { routes, AppRoute } from "./routes";
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 = () => {
const renderRoutes = (routes: AppRoute[]) => {
return routes.map(route => (
return routes.map((route) => (
<Route
key={route.path}
path={route.path}
element={
<AuthRoute auth={route.auth}>
<AuthRoute auth={route.auth} path={route.path} meta={route.meta}>
{route.element}
</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>
);
};

View File

@@ -1,19 +1,122 @@
import React from "react";
import Home from "@/view/home";
import TranslateDetail from "@/view/home/detail";
import Setting from "@/view/setting";
import Page404 from "@/view/error/page404";
import { Navigate } from "react-router-dom";
import { lazy } from "react";
export interface AppRoute {
path: string;
element: React.ReactNode;
auth?: boolean;
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[] = [
{ path: "/", element: <Home />, auth: false },
{ path: "/set", element: <Setting />, auth: false },
{ path: "/detail", element: <TranslateDetail />, auth: false },
{ path: "/mood", element: <Setting />, auth: false },
{
path: "/",
element: <Navigate to="/translate" replace />,
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 },
];

View 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";

View 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;
}
}
}

View 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;

View File

@@ -1,4 +1,3 @@
import React from "react";
import { RenderRoutes } from "@/route/render-routes.tsx";
import { axiosInstance } from "@/http/axios-instance.ts";
import { configure } from "axios-hooks";
@@ -12,6 +11,10 @@ function App() {
axios: axiosInstance,
});
const i18nStore = useI18nStore();
// 持久化一个messageList的本地化数据
console.log("degfmessageList");
localStorage.setItem("messageList", JSON.stringify([]));
return (
<>
<ConfigProvider locale={i18nStore.lang === "en_US" ? enUS : zhCN}>

View 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;
}

View File

@@ -1,11 +1,95 @@
// 档案
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() {
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 (
<MainLayout isShowNavBar={true}>
<div className="archives"></div>
<MainLayout>
<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>
);
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -1,39 +1,59 @@
.home {
height: 100%;
width: 100%;
overflow: hidden;
.adm-tabs {
display: flex;
flex-direction: column;
height: 100%;
}
.adm-tabs-content {
flex: 1;
overflow: hidden;
padding: 0px;
}
.adm-tabs-header {
border: 0 none;
.home-header {
display: flex;
padding: 12px;
position: sticky;
top: 0px;
background: #fff;
justify-content: space-between;
align-items: center;
z-index: 99;
}
.adm-tabs-tab {
h3 {
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 {
height: 0px;
}
// .adm-tabs {
// display: flex;
// flex-direction: column;
// height: 100%;
// }
// .adm-tabs-content {
// flex: 1;
// overflow: hidden;
// padding: 0px;
// }
// .adm-tabs-header {
// border: 0 none;
// position: sticky;
// top: 0px;
// background: #fff;
// z-index: 99;
// }
// .adm-tabs-tab {
// font-size: 20px;
// font-size: 20px;
// font-weight: 600;
// &.adm-tabs-tab-active {
// color: #000;
// }
// }
// .adm-tabs-tab-line {
// height: 0px;
// }
.translate-container {
display: flex;
flex-direction: column;
height: 100%;
.header {
padding: 0px 16px;
}
}
}

View File

@@ -1,28 +1,55 @@
import MainLayout from "@/layout/main/mainLayout";
import { Button, Tabs } from "antd-mobile";
import Translate from "./translate";
import { useState } from "react";
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";
function Index() {
const handleRecordComplete = (audioData: AudioData): void => {
console.log("录音完成:", audioData);
};
const handleError = (error: Error): void => {
console.error("录音错误:", error);
};
const [visible, setVisible] = useState<boolean>(false);
// const safeNavigate = useSafeNavigate();
return (
<MainLayout>
<div className="home">
<Tabs stretch={false}>
<Tabs.Tab title="宠物翻译" key="1">
<Translate />
</Tabs.Tab>
<Tabs.Tab title="宠物档案" key="2">
2
</Tabs.Tab>
</Tabs>
<div className="home-header">
<h3></h3>
<Space style={{ fontSize: "20px", "--gap": "16px" }}>
<Filter
theme="outline"
fill={visible ? "#118fff" : "#333"}
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>
</MainLayout>
);

View File

@@ -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;

View File

@@ -2,6 +2,12 @@
flex: 1 auto;
overflow-y: auto;
padding: 12px;
.message-title {
width: 100%;
display: flex;
justify-content: center;
margin-bottom: 8px;
}
.item {
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
@@ -24,6 +30,7 @@
border-radius: 8px;
margin-top: 8px;
padding-right: 12px;
width: 100px;
.time {
color: rgba(0, 0, 0, 0.88);
}
@@ -35,12 +42,23 @@
}
}
.translate {
position: relative;
display: flex;
gap: 12px;
padding: 12px;
align-items: center;
background: rgba(0, 0, 0, 0.02);
color: #000000e0;
border-radius: 8px;
margin-top: 12px;
max-width: 100%;
font-size: 14px;
// 超出换行
white-space: normal;
.expression {
position: absolute;
right: -25px;
top: -30px;
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,11 +1,19 @@
.voice-record {
position: relative;
position: fixed;
bottom: 60px;
width: 100%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
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 {
height: 32px;
}

View File

@@ -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 { 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 VConsole from "vconsole";
import "./index.less";
import { Message } from "../../types";
import { CloseCircleOutline } from "antd-mobile-icons";
interface DefinedProps {
onRecordingComplete: (url: string, finalDuration: number) => void;
onRecordingComplete: (url: string, finalDuration: number, formData: FormData) => void;
isRecording: boolean;
onSetIsRecording: (flag: boolean) => void;
dialogId: number;
}
function Index(props: DefinedProps) {
const { isRecording } = props;
const { isRecording, dialogId } = props;
const [hasPermission, setHasPermission] = useState<boolean>(false); //是否有权限
const [isPermissioning, setIsPermissioning] = useState<boolean>(true); //获取权限中
const [recordingDuration, setRecordingDuration] = useState<number>(0); //录音时长进度
@@ -25,17 +24,23 @@ function Index(props: DefinedProps) {
// 音效相关
const sendSoundRef = useRef<HTMLAudioElement | null>(null);
const startRecordSoundRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
// 只在开发环境启用
if (process.env.NODE_ENV === "development") {
new VConsole();
}
}, []);
useEffect(() => {
initializeSounds();
checkMicrophonePermission();
}, [hasPermission]);
useEffect(() => {
if (isRecording) {
recorderControls.startRecording();
} else {
}
}, [isRecording]);
// useEffect(() => {
// if (isRecording) {
// recorderControls.startRecording();
// } else {
// }
// }, [isRecording]);
//重置状态
const onResetRecordingState = () => {
@@ -61,8 +66,35 @@ function Index(props: DefinedProps) {
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) {
//没有权限
return (
@@ -80,10 +112,7 @@ function Index(props: DefinedProps) {
//正在录音中
return (
<div onClick={onStopRecording} className="isRecording">
<ProgressCircle
percent={recordingDuration}
style={{ "--size": "80px" }}
>
<ProgressCircle percent={recordingDuration} style={{ "--size": "80px" }}>
<div className="recording-dot">
<span className="circle"></span>
</div>
@@ -94,17 +123,9 @@ function Index(props: DefinedProps) {
</div>
);
} else {
//麦克风状态
return (
<Image
height={80}
width={80}
src={microphoneSvg}
onClick={onStartRecording}
/>
);
return MicrophoneImage;
}
}, [hasPermission, isRecording, recordingDuration]);
};
const checkMicrophonePermission = useCallback(async () => {
try {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
@@ -200,53 +221,113 @@ function Index(props: DefinedProps) {
}
};
//开始录音
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 convertToWav = async (blob: Blob): Promise<Blob> => {
const arrayBuffer = await blob.arrayBuffer();
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
// 转换为 WAV 格式
const wavBuffer = audioBufferToWav(audioBuffer);
return new Blob([wavBuffer], { type: "audio/wav" });
};
// 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(() => {
recorderControls.stopRecording();
onResetRecordingState();
}, [recorderControls, recordingDuration]);
//录音完成
// 在发送时检查录音时长
const onRecordingComplete = useCallback(
(blob: Blob) => {
async (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) {
const arrayBuffer = await blob.arrayBuffer();
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
const accurateDuration = audioBuffer.duration;
if (accurateDuration < 1) {
Toast.show("录音时间太短,请重新录音");
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);
props.onRecordingComplete?.(audioUrl, Math.floor(audio.duration));
});
const contentDuration = Math.round(accurateDuration);
formData.append("contentDuration", `${contentDuration}`);
props.onRecordingComplete?.(audioUrl, contentDuration, formData);
},
[isCancelledRef, isRecording, sendSoundRef]
);

View 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;

View File

@@ -1,12 +1,19 @@
export interface Message {
id: number;
type?: "dog" | "cat" | "pig";
audioUrl: string;
name: string; //名字
duration: number; //时长
timestamp: number; //时间
translatedText?: string;
petType?: "dog" | "cat" | "pig" | "";
contentText: string;
petName?: string; //名字
contentDuration: number; //时长
createTime?: number; //时间
transResult?: string;
isTranslating?: boolean;
avatar?: string;
petAvatar?: string;
isPlaying?: boolean;
messageStatus?: 0 | 1;
transStatus?: 0 | 1;
isRefresh?: boolean;
emotion?: number;
terminal: string;
file?: Blob;
dialogId?: number;
}

View 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;
}
}

View 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;

View 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; // 允许图片超过容器宽度
}
}
}
}

View 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;

View 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%;
}
}

View 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;

View 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;
}
}
}

View 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;

View 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;
}
}

View 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;

View 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;
}
}
}

View 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;

View 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;
}
}

View 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;

View File

@@ -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;

View 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;
}
}
}

Some files were not shown because too many files have changed in this diff Show More