diff --git a/README.md b/README.md
index b652faf..8d85257 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,9 @@
## 功能特性
-- 🎮 **多平台社交登录**: 支持Discord、Google、Apple登录
+- 🎮 **多平台社交登录**: 支持Discord、Google登录
- 🔐 **完整认证流程**: OAuth2.0认证,JWT token管理
- 📱 **设备管理**: 自动设备ID生成和管理
-- 🎭 **开发环境Mock**: 使用MSW进行API模拟
- 🛡️ **中间件保护**: 路由级别的认证保护
## 快速开始
@@ -15,7 +14,7 @@
### 1. 安装依赖
```bash
-npm install
+pnpm install
```
### 2. 环境变量配置
@@ -46,27 +45,20 @@ DISCORD_CLIENT_SECRET=your_discord_client_secret_here
# 应用URL(生产环境需要修改)
NEXT_PUBLIC_APP_URL=http://localhost:3000
-# 开发环境Mock配置
-NEXT_PUBLIC_ENABLE_MOCK=true
```
#### 其他OAuth配置(可选)
-```env
+````env
# Google OAuth(未来支持)
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your_google_client_id_here
-GOOGLE_CLIENT_SECRET=your_google_client_secret_here
-# Apple OAuth(未来支持)
-NEXT_PUBLIC_APPLE_CLIENT_ID=your_apple_client_id_here
-APPLE_CLIENT_SECRET=your_apple_client_secret_here
-```
### 3. 启动开发服务器
```bash
-npm run dev
-```
+pnpm run dev
+````
应用将在 http://localhost:3000 启动。
@@ -97,40 +89,6 @@ npm run dev
- 前端保存token并重定向到首页
- 完成整个登录流程
-## 开发环境Mock
-
-项目使用MSW进行API模拟,在开发环境中:
-
-- 设置 `NEXT_PUBLIC_ENABLE_MOCK=true` 启用Mock
-- 所有认证API请求都会被MSW拦截并返回模拟数据
-- 支持Discord、Google、Apple等第三方登录的模拟
-
-### Mock功能特性
-
-- ✅ 设备ID验证
-- ✅ 第三方登录模拟
-- ✅ Token管理
-- ✅ 用户信息管理
-- ✅ 错误场景模拟
-
-## 项目结构
-
-```
-src/
-├── app/ # Next.js App Router
-│ ├── (auth)/ # 认证相关页面
-│ ├── (main)/ # 主应用页面
-│ └── api/ # API路由
-├── components/ # 可复用组件
-├── lib/ # 工具库
-│ ├── auth/ # 认证管理
-│ ├── http/ # HTTP客户端
-│ └── oauth/ # OAuth服务
-├── services/ # 业务服务
-├── mocks/ # MSW Mock配置
-└── types/ # TypeScript类型定义
-```
-
## API文档
### 认证相关API
@@ -167,53 +125,6 @@ Headers:
AUTH_DID: "设备ID"
```
-## 贡献指南
-
-1. Fork本项目
-2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
-3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
-4. 推送分支 (`git push origin feature/AmazingFeature`)
-5. 创建Pull Request
-
## 许可证
本项目采用 MIT 许可证。详见 [LICENSE](LICENSE) 文件。
-
-## 文案清单导出(一次性盘点)
-
-为便于产品/运营统一校对当前所有展示文案,项目提供静态扫描脚本,自动抽取源码中的用户可见与可感知文案并导出为 Excel。
-
-### 覆盖范围
-
-- JSX 文本节点与按钮/链接文案
-- 属性文案:`placeholder` / `title` / `alt` / `aria-*` / `label`
-- 交互文案:`toast.*` / `message.*` / `alert` / `confirm` / `Dialog`/`Tooltip` 等常见调用
-- 表单校验与错误提示:`form.setError(..., { message })`、校验链条中的 `{ message: '...' }`
-
-### 运行
-
-```bash
-# 生成 docs/copy-audit.xlsx
-npx ts-node scripts/extract-copy.ts # 若 ESM 运行报错,请改用下行
-node scripts/extract-copy.cjs
-```
-
-输出文件:`docs/copy-audit.xlsx`
-
-### Excel 字段说明(Sheet: copy)
-
-- `route`: Next.js App Router 路由(如 `(main)/home`)或 `shared`
-- `file`: 文案所在文件(相对仓库根路径)
-- `componentOrFn`: 组件或函数名(无法解析时为文件名)
-- `kind`: 文案类型(`text` | `placeholder` | `title` | `alt` | `aria` | `label` | `toast` | `dialog` | `error` | `validation`)
-- `keyOrLocator`: 定位信息(如 `Button.placeholder`、`toast.success`)
-- `text`: 实际文案内容
-- `line`: 文案起始行号(近似定位)
-- `count`: 在同一路由下相同文案出现次数(已聚合)
-- `notes`: 预留备注
-
-### 说明与边界
-
-- 仅提取可静态分析到的硬编码字符串;运行时拼接(仅变量)无法还原将被忽略
-- 会过滤明显的“代码样”字符串(如过长的标识符)
-- 扫描目录为 `src/`,忽略 `node_modules/.next/__tests__/mocks` 等
diff --git a/docs/AiReplySuggestions-Refactor.md b/docs/AiReplySuggestions-Refactor.md
deleted file mode 100644
index 9755e16..0000000
--- a/docs/AiReplySuggestions-Refactor.md
+++ /dev/null
@@ -1,256 +0,0 @@
-# AI 建议回复功能重构说明
-
-## 重构日期
-
-2025-11-17
-
-## 最新更新
-
-2025-11-17 - 优化请求逻辑,确保每次只发送 3 次 API 请求
-
-## 问题描述
-
-重构前的 AI 建议功能存在以下问题:
-
-1. **状态管理复杂**:维护了大量状态(`allSuggestions`、`loadedPages`、`batchNo`、`excContentList`、`isPageLoading`、`isRequesting` 等)
-2. **页面切换逻辑复杂**:需要跟踪哪些页面已加载,切换时判断是否显示骨架屏
-3. **显示逻辑不清晰**:`isLoading` 只在首次加载时为 true,分页切换时骨架屏显示不正确
-4. **用户体验问题**:会出现 AI 辅助提示为空的情况(如图所示)
-
-## 重构目标
-
-1. **简化状态管理**:只保留必要的状态
-2. **统一交互逻辑**:每次打开建议面板时,重置到第1页并重新获取数据
-3. **清晰的骨架屏显示**:数据未加载完成时始终显示骨架屏
-
-## 重构内容
-
-### 1. `useAiReplySuggestions` Hook 重构
-
-#### 状态简化
-
-**重构前:**
-
-```typescript
-const [allSuggestions, setAllSuggestions] = useState([])
-const [loadedPages, setLoadedPages] = useState>(new Set([1]))
-const [isPageLoading, setIsPageLoading] = useState(false)
-const [isRequesting, setIsRequesting] = useState(false)
-// ... 其他状态
-```
-
-**重构后:**
-
-```typescript
-const [pageData, setPageData] = useState
- CrushLevel keeps 20% of each image's sales as a service fee.
+ Spicyxx.AI keeps 20% of each image's sales as a service fee.
Offering a few free images can encourage users to interact with
@@ -203,25 +203,25 @@ const AlbumPriceSetting = ({ defaultUnlockPrice, onConfirm, children }: AlbumPri
type="text"
placeholder="20~9999"
onInput={(e) => {
- const target = e.target as HTMLInputElement
+ const target = e.target as HTMLInputElement;
// 只允许输入数字
- let value = target.value.replace(/[^0-9]/g, '')
+ let value = target.value.replace(/[^0-9]/g, '');
// 如果为空,允许继续输入
if (value === '') {
- target.value = ''
- field.onChange('')
- return
+ target.value = '';
+ field.onChange('');
+ return;
}
// 转换为数字并限制范围
- const numValue = parseInt(value)
+ const numValue = parseInt(value);
if (numValue > 99999) {
- value = '99999'
+ value = '99999';
}
- target.value = value
- field.onChange(value)
+ target.value = value;
+ field.onChange(value);
}}
prefixIcon={
@@ -268,7 +268,7 @@ const AlbumPriceSetting = ({ defaultUnlockPrice, onConfirm, children }: AlbumPri
- )
-}
+ );
+};
-export default AlbumPriceSetting
+export default AlbumPriceSetting;
diff --git a/src/components/features/charge-drawer.tsx b/src/components/features/charge-drawer.tsx
index 8788e0d..3023785 100644
--- a/src/components/features/charge-drawer.tsx
+++ b/src/components/features/charge-drawer.tsx
@@ -1,12 +1,12 @@
-'use client'
+'use client';
import {
useGetChargeProductList,
useGetWalletBalance,
useGetWebChargeCheckout,
useGetWebChargePreOrder,
-} from '@/hooks/useWallet'
-import { Button, IconButton } from '../ui/button'
+} from '@/hooks/useWallet';
+import { Button, IconButton } from '../ui/button';
import {
Drawer,
DrawerContent,
@@ -14,57 +14,57 @@ import {
DrawerFooter,
DrawerHeader,
DrawerTitle,
-} from '../ui/drawer'
-import { useEffect, useState } from 'react'
-import { formatFromCents } from '@/utils/number'
-import Image from 'next/image'
-import { Checkbox } from '../ui/checkbox'
-import Link from 'next/link'
-import { toast } from 'sonner'
-import { Skeleton } from '../ui/skeleton'
-import { usePathname, useSearchParams } from 'next/navigation'
-import QueryString from 'qs'
-import { useAtom } from 'jotai'
-import { isChargeDrawerOpenAtom } from '@/atoms/im'
-import { useCurrentUser } from '@/hooks/auth'
+} from '../ui/drawer';
+import { useEffect, useState } from 'react';
+import { formatFromCents } from '@/utils/number';
+import Image from 'next/image';
+import { Checkbox } from '../ui/checkbox';
+import Link from 'next/link';
+import { toast } from 'sonner';
+import { Skeleton } from '../ui/skeleton';
+import { usePathname, useSearchParams } from 'next/navigation';
+import QueryString from 'qs';
+import { useAtom } from 'jotai';
+import { isChargeDrawerOpenAtom } from '@/atoms/im';
+import { useCurrentUser } from '@/hooks/auth';
const ChargeDrawer = () => {
- const [isChargeDrawerOpen, setIsChargeDrawerOpen] = useAtom(isChargeDrawerOpenAtom)
- const [policyCheck, setpolicyCheck] = useState(true)
- const [selectedProductId, setSelectedProductId] = useState(null)
+ const [isChargeDrawerOpen, setIsChargeDrawerOpen] = useAtom(isChargeDrawerOpenAtom);
+ const [policyCheck, setpolicyCheck] = useState(true);
+ const [selectedProductId, setSelectedProductId] = useState(null);
- const { data, isLoading } = useGetChargeProductList()
- const { productList } = data || {}
- const pathname = usePathname()
- const searchParams = useSearchParams()
+ const { data, isLoading } = useGetChargeProductList();
+ const { productList } = data || {};
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
// 初始化获取钱包余额
- useGetWalletBalance()
+ useGetWalletBalance();
// 支付相关的hooks
- const preOrderMutation = useGetWebChargePreOrder()
- const checkoutMutation = useGetWebChargeCheckout()
+ const preOrderMutation = useGetWebChargePreOrder();
+ const checkoutMutation = useGetWebChargeCheckout();
// 默认选中第一个产品
useEffect(() => {
if (productList && productList.length > 0 && !selectedProductId) {
- setSelectedProductId(productList[0].productId || null)
+ setSelectedProductId(productList[0].productId || null);
}
- }, [productList, selectedProductId])
+ }, [productList, selectedProductId]);
- const selectedProduct = productList?.find((p) => p.productId === selectedProductId)
- const selectedProductAmount = formatFromCents(selectedProduct?.payAmount || 0, 2)
+ const selectedProduct = productList?.find((p) => p.productId === selectedProductId);
+ const selectedProductAmount = formatFromCents(selectedProduct?.payAmount || 0, 2);
const canPay =
- selectedProductId && policyCheck && !preOrderMutation.isPending && !checkoutMutation.isPending
+ selectedProductId && policyCheck && !preOrderMutation.isPending && !checkoutMutation.isPending;
const handleClose = () => {
- setIsChargeDrawerOpen(false)
- }
+ setIsChargeDrawerOpen(false);
+ };
// 支付处理函数
const handlePayment = async () => {
if (!selectedProductId || !policyCheck) {
- return
+ return;
}
try {
@@ -72,21 +72,21 @@ const ChargeDrawer = () => {
const preOrderResponse = await preOrderMutation.mutateAsync({
productId: selectedProductId,
version: 1,
- })
+ });
if (!preOrderResponse.tradeNo) {
- throw new Error('Pre-order failed: No trade number obtained')
+ throw new Error('Pre-order failed: No trade number obtained');
}
- const baseURL = `${process.env.NEXT_PUBLIC_APP_URL}/wallet/charge/result`
- const originalSearch = searchParams.toString()
- const fullPath = originalSearch ? `${pathname}?${originalSearch}` : pathname
+ const baseURL = `${process.env.NEXT_PUBLIC_APP_URL}/wallet/charge/result`;
+ const originalSearch = searchParams.toString();
+ const fullPath = originalSearch ? `${pathname}?${originalSearch}` : pathname;
const params = QueryString.stringify({
redirectURL: fullPath,
back: 1,
- })
- const returnUrl = `${baseURL}?${params}`
- const cancelUrl = `${baseURL}?${params}`
+ });
+ const returnUrl = `${baseURL}?${params}`;
+ const cancelUrl = `${baseURL}?${params}`;
// 2. 结账
const checkoutResponse = await checkoutMutation.mutateAsync({
@@ -94,18 +94,18 @@ const ChargeDrawer = () => {
payChannel: 'STRIPE',
returnUrl,
cancelUrl,
- })
+ });
// 3. 跳转到支付页面
if (checkoutResponse.paymentUrl) {
- window.location.href = checkoutResponse.paymentUrl
+ window.location.href = checkoutResponse.paymentUrl;
} else {
- throw new Error('Failed to get payment link')
+ throw new Error('Failed to get payment link');
}
} catch (error) {
- toast.error('Payment failure, please try again')
+ toast.error('Payment failure, please try again');
}
- }
+ };
const renderList = () => {
if (isLoading) {
@@ -123,13 +123,13 @@ const ChargeDrawer = () => {
))}
- )
+ );
}
return (
{productList?.map((product) => {
- const isSelected = selectedProductId === product.productId
+ const isSelected = selectedProductId === product.productId;
return (
{
${formatFromCents(product.payAmount, 2)}
- )
+ );
})}
- )
- }
+ );
+ };
return (
@@ -177,7 +177,7 @@ const ChargeDrawer = () => {
By recharging, you agree to the{' '}
e.stopPropagation()}>
- CrushLevel Recharge Service Agreement
+ Spicyxx.AI Recharge Service Agreement
@@ -200,7 +200,7 @@ const ChargeDrawer = () => {
- )
-}
+ );
+};
-export default ChargeDrawer
+export default ChargeDrawer;
diff --git a/src/components/mock-provider.tsx b/src/components/mock-provider.tsx
deleted file mode 100644
index dca0b43..0000000
--- a/src/components/mock-provider.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-'use client'
-
-import { useEffect, useState } from 'react'
-import { tokenManager } from '@/lib/auth/token'
-
-export function MockProvider({ children }: { children: React.ReactNode }) {
- const [mockEnabled, setMockEnabled] = useState(false)
-
- useEffect(() => {
- async function initMocking() {
- // 首先初始化设备ID(确保用户第一次访问就有设备ID)
- tokenManager.initializeDeviceId()
-
- // 只在开发环境和浏览器环境中启用
- if (process.env.NODE_ENV !== 'development' || typeof window === 'undefined') {
- setMockEnabled(true)
- return
- }
-
- // 检查是否启用 mock
- const shouldMock = process.env.NEXT_PUBLIC_ENABLE_MOCK === 'true'
- console.log('🔧 Mock enabled:', shouldMock)
-
- if (!shouldMock) {
- console.log('🚫 MSW 已禁用')
- setMockEnabled(true)
- return
- }
-
- try {
- // 动态导入 MSW
- const { worker } = await import('../mocks/browser')
-
- // 启动 Service Worker
- await worker.start({
- onUnhandledRequest: 'bypass',
- serviceWorker: {
- url: '/mockServiceWorker.js',
- },
- // 添加选项以避免拦截页面导航
- quiet: false,
- })
-
- console.log('🎭 MSW Worker started successfully')
- setMockEnabled(true)
- } catch (error) {
- console.error('❌ Failed to start MSW:', error)
- setMockEnabled(true) // 即使失败也继续渲染
- }
- }
-
- initMocking()
- }, [])
-
- // 在 mock 初始化完成前显示加载状态
- if (!mockEnabled) {
- return (
-
-
-
-
Initializing development environment...
-
-
- )
- }
-
- return <>{children}>
-}
diff --git a/src/components/test-heartbeat-loader.tsx b/src/components/test-heartbeat-loader.tsx
deleted file mode 100644
index dd60e0e..0000000
--- a/src/components/test-heartbeat-loader.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-'use client'
-
-import { useEffect } from 'react'
-
-/**
- * 测试工具加载器组件
- * 在开发环境中加载心动等级测试工具
- */
-export function TestHeartbeatLoader() {
- useEffect(() => {
- // 只在开发环境加载测试工具
- if (process.env.NODE_ENV === 'development') {
- import('@/utils/testHeartbeatLevel')
- .then(() => {
- console.log('🔧 心动等级测试工具已加载')
- })
- .catch((error) => {
- console.error('加载测试工具失败:', error)
- })
- }
- }, [])
-
- return null // 不渲染任何内容
-}
diff --git a/src/lib/server-mock.ts b/src/lib/server-mock.ts
deleted file mode 100644
index 7a5b3c0..0000000
--- a/src/lib/server-mock.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-// 服务端 Mock 初始化
-let isServerMockEnabled = false
-
-export async function initServerMock() {
- // 避免重复初始化
- if (isServerMockEnabled) {
- return
- }
-
- // 只在开发环境启用
- if (process.env.NODE_ENV !== 'development') {
- return
- }
-
- // 检查是否启用 mock
- const shouldMock = process.env.NEXT_PUBLIC_ENABLE_MOCK === 'true'
- if (!shouldMock) {
- return
- }
-
- try {
- // 动态导入服务端 MSW
- const { server } = await import('../mocks/server')
-
- // 启动服务端 mock
- server.listen({
- onUnhandledRequest: 'bypass',
- })
-
- isServerMockEnabled = true
- console.log('🎭 MSW Server started successfully')
- } catch (error) {
- console.error('❌ Failed to start MSW Server:', error)
- }
-}
-
-// 在服务端自动初始化
-if (typeof window === 'undefined') {
- initServerMock()
-}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 3d2d1c4..d36bee9 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,128 +1,123 @@
-import { clsx, type ClassValue } from 'clsx'
-import { twMerge } from 'tailwind-merge'
-import dayjs from 'dayjs'
-import numeral from 'numeral'
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+import dayjs from 'dayjs';
+import numeral from 'numeral';
// 合并类名
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
}
// 计算年龄
export function calculateAge(year: string, month: string, day: string) {
- const birthDate = dayjs(`${year}-${month}-${day}`)
- const today = dayjs()
- return today.diff(birthDate, 'year')
+ const birthDate = dayjs(`${year}-${month}-${day}`);
+ const today = dayjs();
+ return today.diff(birthDate, 'year');
}
export function calculateAgeByBirthday(birthday?: string) {
- if (!birthday) return ''
- const birthDate = dayjs(birthday)
- const today = dayjs()
- return today.diff(birthDate, 'year')
+ if (!birthday) return '';
+ const birthDate = dayjs(birthday);
+ const today = dayjs();
+ return today.diff(birthDate, 'year');
}
// 获取月份天数
export function getDaysInMonth(year: string, month: string) {
return Array.from({ length: dayjs(`${year}-${month}`).daysInMonth() }, (_, i) =>
`${i + 1}`.padStart(2, '0')
- )
+ );
}
// 延迟函数
export function delay(ms: number) {
- return new Promise((resolve) => setTimeout(resolve, ms))
+ return new Promise((resolve) => setTimeout(resolve, ms));
}
// 加载图片
export async function loadImageAsync(url: string) {
return new Promise((resolve, reject) => {
- const img = new Image()
- img.crossOrigin = 'anonymous'
- img.onload = () => resolve(img)
- img.onerror = () => reject(new Error(`Failed to load image: ${url}`))
- img.src = url
- })
+ const img = new Image();
+ img.crossOrigin = 'anonymous';
+ img.onload = () => resolve(img);
+ img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
+ img.src = url;
+ });
}
// 数字简化显示(K/M/B格式)
export function formatNumberToKMB(number: number) {
if (number >= 1000000000) {
- return numeral(Math.floor(number / 100000000) / 10).format('0.0') + 'B'
+ return numeral(Math.floor(number / 100000000) / 10).format('0.0') + 'B';
}
if (number >= 1000000) {
- return numeral(Math.floor(number / 100000) / 10).format('0.0') + 'M'
+ return numeral(Math.floor(number / 100000) / 10).format('0.0') + 'M';
}
if (number >= 1000) {
- return numeral(Math.floor(number / 100) / 10).format('0.0') + 'K'
+ return numeral(Math.floor(number / 100) / 10).format('0.0') + 'K';
}
- return number || 0
+ return number || 0;
}
// 根据时间戳(毫秒)获取年龄
export function getAge(birthday: number) {
- if (!birthday) return 0
- const birthDate = dayjs(birthday)
- const today = dayjs()
- let age = today.year() - birthDate.year()
+ if (!birthday) return 0;
+ const birthDate = dayjs(birthday);
+ const today = dayjs();
+ let age = today.year() - birthDate.year();
// 如果今年还没过生日,年龄减一
if (
today.month() < birthDate.month() ||
(today.month() === birthDate.month() && today.date() < birthDate.date())
) {
- age--
+ age--;
}
- return age
+ return age;
}
export function getConversationTime(timestamp: number) {
- const now = dayjs()
- const timestampDate = dayjs(timestamp)
+ const now = dayjs();
+ const timestampDate = dayjs(timestamp);
// 当天显示HH:mm
if (now.isSame(timestampDate, 'day')) {
- return timestampDate.format('HH:mm')
+ return timestampDate.format('HH:mm');
}
// 昨天显示昨天HH:mm
if (now.subtract(1, 'day').isSame(timestampDate, 'day')) {
- return 'Yesterday ' + timestampDate.format('HH:mm')
+ return 'Yesterday ' + timestampDate.format('HH:mm');
}
// 今年显示MM-DD
if (now.isSame(timestampDate, 'year')) {
- return timestampDate.format('MM-DD')
+ return timestampDate.format('MM-DD');
}
- return timestampDate.format('YYYY-MM-DD')
-}
-
-// 格式化心动等级值
-export function formatHeartbeatLevel(value: number) {
- return `${value.toFixed(1)}℃`
+ return timestampDate.format('YYYY-MM-DD');
}
export function openApp(schemeURI: string) {
- const schemeUrl = schemeURI
- const downloadUrl = 'https://crushlevel.com/download' // 下载页 URL
- const startTime = Date.now()
- window.location.href = schemeUrl
+ const schemeUrl = schemeURI;
+ const downloadUrl = 'https://crushlevel.com/download'; // 下载页 URL
+ const startTime = Date.now();
+ window.location.href = schemeUrl;
setTimeout(() => {
if (Date.now() - startTime < 2100) {
// 唤起成功
- return
+ return;
}
- window.location.href = downloadUrl // 失败跳转下载
- }, 2000)
+ window.location.href = downloadUrl; // 失败跳转下载
+ }, 2000);
}
// 毫秒转mm:ss, 如果是小时则为hh:mm
export function durationText(duration: number) {
- const hours = Math.floor(duration / 3600000)
- const minutes = Math.floor((duration % 3600000) / 60000)
- const seconds = Math.floor((duration % 60000) / 1000)
+ const hours = Math.floor(duration / 3600000);
+ const minutes = Math.floor((duration % 3600000) / 60000);
+ const seconds = Math.floor((duration % 60000) / 1000);
if (hours > 0) {
- return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
} else if (minutes > 0) {
- return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
+ return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
} else {
- return `00:${seconds.toString().padStart(2, '0')}`
+ return `00:${seconds.toString().padStart(2, '0')}`;
}
}