feat: init

This commit is contained in:
2025-09-05 15:18:10 +08:00
parent ddbee614e8
commit 85244a451e
126 changed files with 3020 additions and 10278 deletions

View File

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

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

@@ -1,198 +0,0 @@
// hooks/useFileUpload.ts (更新)
import { useState, useCallback } from "react";
import {
UploadConfig,
UploadProgress,
UploadResponse,
VoiceUploadStatus,
} from "../types/upload";
interface UseFileUploadReturn {
uploadStatus: VoiceUploadStatus;
uploadFile: (
file: Blob,
fileName: string,
config: UploadConfig
) => Promise<UploadResponse>;
resetUpload: () => void;
}
export const useFileUpload = (): UseFileUploadReturn => {
const [uploadStatus, setUploadStatus] = useState<VoiceUploadStatus>({
status: "idle",
});
// 检测文件类型并转换文件名
const getFileExtension = (mimeType: string): string => {
const mimeToExt: Record<string, string> = {
"audio/webm": ".webm",
"audio/mp4": ".m4a",
"audio/aac": ".aac",
"audio/wav": ".wav",
"audio/ogg": ".ogg",
"audio/mpeg": ".mp3",
};
// 处理带codecs的MIME类型
const baseMimeType = mimeType.split(";")[0];
return mimeToExt[baseMimeType] || ".webm";
};
const uploadFile = useCallback(
async (
file: Blob,
fileName: string,
config: UploadConfig
): Promise<UploadResponse> => {
// 检查文件大小
if (config.maxFileSize && file.size > config.maxFileSize) {
const error = `文件大小超过限制 (${Math.round(
config.maxFileSize / 1024 / 1024
)}MB)`;
setUploadStatus({
status: "error",
error,
});
throw new Error(error);
}
// 更宽松的文件类型检查支持iOS格式
const allowedTypes = config.allowedTypes || [
"audio/webm",
"audio/mp4",
"audio/aac",
"audio/wav",
"audio/ogg",
"audio/mpeg",
];
const baseMimeType = file.type.split(";")[0];
const isTypeAllowed = allowedTypes.some(
(type) => baseMimeType === type || baseMimeType === type.split(";")[0]
);
if (!isTypeAllowed) {
console.warn(`文件类型 ${file.type} 不在允许列表中,但继续上传`);
}
// 根据实际MIME类型调整文件名
const extension = getFileExtension(file.type);
const adjustedFileName = fileName.replace(/\.[^/.]+$/, "") + extension;
setUploadStatus({
status: "uploading",
progress: { loaded: 0, total: file.size, percentage: 0 },
});
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append(config.fieldName || "file", file, adjustedFileName);
// 添加额外的元数据
formData.append("fileName", adjustedFileName);
formData.append("originalFileName", fileName);
formData.append("fileSize", file.size.toString());
formData.append("fileType", file.type);
formData.append("uploadTime", new Date().toISOString());
formData.append("userAgent", navigator.userAgent);
const xhr = new XMLHttpRequest();
// 上传进度监听
xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable) {
const progress: UploadProgress = {
loaded: event.loaded,
total: event.total,
percentage: Math.round((event.loaded / event.total) * 100),
};
setUploadStatus({
status: "uploading",
progress,
});
}
});
// 上传完成监听
xhr.addEventListener("load", () => {
try {
const response: UploadResponse = JSON.parse(xhr.responseText);
if (xhr.status >= 200 && xhr.status < 300 && response.success) {
setUploadStatus({
status: "success",
response,
progress: {
loaded: file.size,
total: file.size,
percentage: 100,
},
});
resolve(response);
} else {
const error = response.error || `上传失败: ${xhr.status}`;
setUploadStatus({
status: "error",
error,
});
reject(new Error(error));
}
} catch (parseError) {
const error = "服务器响应格式错误";
setUploadStatus({
status: "error",
error,
});
reject(new Error(error));
}
});
// 上传错误监听
xhr.addEventListener("error", () => {
const error = "网络错误,上传失败";
setUploadStatus({
status: "error",
error,
});
reject(new Error(error));
});
// 上传中断监听
xhr.addEventListener("abort", () => {
const error = "上传已取消";
setUploadStatus({
status: "error",
error,
});
reject(new Error(error));
});
// 设置请求头
const headers = {
"X-Requested-With": "XMLHttpRequest",
...config.headers,
};
Object.entries(headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});
// 发送请求
xhr.open(config.method || "POST", config.url);
xhr.send(formData);
});
},
[]
);
const resetUpload = useCallback(() => {
setUploadStatus({ status: "idle" });
}, []);
return {
uploadStatus,
uploadFile,
resetUpload,
};
};

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 React, { 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: 16px;
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

@@ -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"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"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

View File

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