Skip to content

Commit b173665

Browse files
committed
Opti. 分享按钮迁移至顶部操作栏并修复分享功能
- 将分享按钮从底部固定位置迁移到顶部导航栏 - 桌面端和移动端均可通过操作栏访问分享功能 - 修复分享所有计时器时的二维码生成错误 - 优化数据序列化,支持正计时和世界时钟类型 - 增强错误处理,数据过长时显示友好提示
1 parent 8eb0355 commit b173665

5 files changed

Lines changed: 115 additions & 76 deletions

File tree

.DS_Store

6 KB
Binary file not shown.

components/Layout/Header.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { useState, useEffect, useRef } from 'react';
22
import { motion, AnimatePresence } from 'framer-motion';
3-
import { FiMenu, FiX, FiSettings, FiMoon, FiSun, FiUser, FiMaximize, FiMinimize, FiEdit, FiSave, FiGlobe, FiPlus } from 'react-icons/fi';
3+
import { FiMenu, FiX, FiSettings, FiMoon, FiSun, FiUser, FiMaximize, FiMinimize, FiEdit, FiSave, FiGlobe, FiPlus, FiShare2 } from 'react-icons/fi';
44
import { useTimers } from '../../context/TimerContext';
55
import { useTheme } from '../../context/ThemeContext';
66
import { useTranslation } from '../../hooks/useTranslation';
77
import LoginModal from '../UI/LoginModal';
8+
import ShareModal from '../UI/ShareModal';
89
import TimerTypeModal from '../UI/TimerTypeModal';
910
import AddTimerModal from '../UI/AddTimerModal';
1011
import AddStopwatchModal from '../UI/AddStopwatchModal';
@@ -18,6 +19,7 @@ export default function Header() {
1819
const [isMenuOpen, setIsMenuOpen] = useState(false);
1920
const [isManageOpen, setIsManageOpen] = useState(false);
2021
const [isLoginOpen, setIsLoginOpen] = useState(false);
22+
const [isShareOpen, setIsShareOpen] = useState(false);
2123
const [isFullscreen, setIsFullscreen] = useState(false);
2224
const [editingTimer, setEditingTimer] = useState(null);
2325
const [showColorPicker, setShowColorPicker] = useState(false);
@@ -483,6 +485,20 @@ export default function Header() {
483485
<FiPlus className="text-xl" />
484486
</button>
485487

488+
{/* 分享按钮 */}
489+
<button
490+
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300 cursor-pointer"
491+
onClick={() => {
492+
setIsShareOpen(true);
493+
if (window.location.hash !== '#share') {
494+
window.location.hash = 'share';
495+
}
496+
}}
497+
data-umami-event={t('timer.share')}
498+
>
499+
<FiShare2 className="text-xl" />
500+
</button>
501+
486502
{/* 全屏按钮 */}
487503
<button
488504
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300 cursor-pointer"
@@ -625,7 +641,7 @@ export default function Header() {
625641

626642
{/* 添加"添加计时器"按钮 */}
627643
<button
628-
className="flex items-center justify-between p-3 rounded-lg col-span-2 bg-white/10 dark:bg-black/10 backdrop-blur-sm border border-gray-200/60 dark:border-white/10 text-gray-700 dark:text-gray-300 hover:bg-white/20 dark:hover:bg-black/20 cursor-pointer transition-colors"
644+
className="flex items-center justify-between p-3 rounded-lg bg-white/10 dark:bg-black/10 backdrop-blur-sm border border-gray-200/60 dark:border-white/10 text-gray-700 dark:text-gray-300 hover:bg-white/20 dark:hover:bg-black/20 cursor-pointer transition-colors"
629645
onClick={() => {
630646
setIsTimerTypeModalOpen(true);
631647
setIsMenuOpen(false);
@@ -638,6 +654,22 @@ export default function Header() {
638654
<FiPlus className="text-xl" />
639655
<span className="text-xs ml-2 flex-1 text-right">{t('timer.create')}</span>
640656
</button>
657+
658+
{/* 添加"分享"按钮 */}
659+
<button
660+
className="flex items-center justify-between p-3 rounded-lg bg-white/10 dark:bg-black/10 backdrop-blur-sm border border-gray-200/60 dark:border-white/10 text-gray-700 dark:text-gray-300 hover:bg-white/20 dark:hover:bg-black/20 cursor-pointer transition-colors"
661+
onClick={() => {
662+
setIsShareOpen(true);
663+
setIsMenuOpen(false);
664+
if (window.location.hash !== '#share') {
665+
window.location.hash = 'share';
666+
}
667+
}}
668+
data-umami-event={t('timer.share')}
669+
>
670+
<FiShare2 className="text-xl" />
671+
<span className="text-xs ml-2 flex-1 text-right">{t('timer.share')}</span>
672+
</button>
641673
</div>
642674
</div>
643675

@@ -944,6 +976,18 @@ export default function Header() {
944976
<AddWorldClockModal onClose={closeAllModals} />
945977
)}
946978
</AnimatePresence>
979+
980+
{/* 分享模态框 */}
981+
<AnimatePresence>
982+
{isShareOpen && (
983+
<ShareModal onClose={() => {
984+
setIsShareOpen(false);
985+
if (window.location.hash === '#share') {
986+
window.location.hash = '';
987+
}
988+
}} />
989+
)}
990+
</AnimatePresence>
947991
</header>
948992
);
949993
}

components/UI/ShareModal.js

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,25 @@ export default function ShareModal({ onClose }) {
1616
const [copied, setCopied] = useState(false);
1717
const [selectedTimer, setSelectedTimer] = useState(activeTimerId);
1818
const [shareAll, setShareAll] = useState(false);
19+
const [qrError, setQrError] = useState(false);
1920

2021
// 生成分享URL
2122
useEffect(() => {
22-
const url = createShareUrl(
23-
shareAll
24-
? timers
25-
: timers.filter(timer => timer.id === selectedTimer)
26-
);
27-
28-
// 构建完整URL
29-
const fullUrl = `${window.location.origin}${window.location.pathname}?share=${url}`;
30-
setShareUrl(fullUrl);
23+
try {
24+
const url = createShareUrl(
25+
shareAll
26+
? timers
27+
: timers.filter(timer => timer.id === selectedTimer)
28+
);
29+
30+
// 构建完整URL
31+
const fullUrl = `${window.location.origin}${window.location.pathname}?share=${url}`;
32+
setShareUrl(fullUrl);
33+
setQrError(false);
34+
} catch (error) {
35+
console.error('生成分享URL失败:', error);
36+
setQrError(true);
37+
}
3138
}, [timers, selectedTimer, shareAll]);
3239

3340
// 复制URL到剪贴板
@@ -135,20 +142,27 @@ export default function ShareModal({ onClose }) {
135142

136143
<div className="mb-6 flex justify-center">
137144
<div className="p-4 bg-white rounded-xl">
138-
<QRCodeSVG
139-
value={shareUrl}
140-
size={200}
141-
level="H"
142-
includeMargin={true}
143-
imageSettings={{
144-
src: "/favicon.ico",
145-
x: undefined,
146-
y: undefined,
147-
height: 24,
148-
width: 24,
149-
excavate: true,
150-
}}
151-
/>
145+
{qrError || shareUrl.length > 2048 ? (
146+
<div className="w-[200px] h-[200px] flex flex-col items-center justify-center text-center text-gray-500">
147+
<p className="text-sm mb-2">{t('share.qrError', '数据过长,无法生成二维码')}</p>
148+
<p className="text-xs">{t('share.qrErrorHint', '请尝试分享单个计时器')}</p>
149+
</div>
150+
) : (
151+
<QRCodeSVG
152+
value={shareUrl}
153+
size={200}
154+
level="M"
155+
includeMargin={true}
156+
imageSettings={{
157+
src: "/favicon.ico",
158+
x: undefined,
159+
y: undefined,
160+
height: 24,
161+
width: 24,
162+
excavate: true,
163+
}}
164+
/>
165+
)}
152166
</div>
153167
</div>
154168

pages/index.js

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@ import AddTimerModal from '../components/UI/AddTimerModal';
99
import AddStopwatchModal from '../components/UI/AddStopwatchModal';
1010
import AddWorldClockModal from '../components/UI/AddWorldClockModal';
1111
import TimerTypeModal from '../components/UI/TimerTypeModal';
12-
import ShareModal from '../components/UI/ShareModal';
1312
import LoginModal from '../components/UI/LoginModal';
1413
import { useTimers } from '../context/TimerContext';
1514
import { useTheme } from '../context/ThemeContext';
1615
import { useTranslation } from '../hooks/useTranslation';
17-
import { FaShareAlt } from 'react-icons/fa';
1816
import { parseShareUrl } from '../utils/shareUtils';
1917

2018
export default function Home() {
@@ -24,7 +22,6 @@ export default function Home() {
2422
const router = useRouter();
2523

2624
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
27-
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
2825
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
2926
const [isTimerTypeModalOpen, setIsTimerTypeModalOpen] = useState(false);
3027
const [isCountdownModalOpen, setIsCountdownModalOpen] = useState(false);
@@ -130,34 +127,6 @@ export default function Home() {
130127
</main>
131128
</Layout>
132129

133-
{/* 将按钮移到Layout组件外部,确保它们总是在最上层 */}
134-
{/* 分享按钮 - 保持使用动态主题色,在Footer显示时隐藏 */}
135-
<motion.div
136-
className="fixed bottom-6 left-6"
137-
style={{ zIndex: 50 }}
138-
animate={{
139-
opacity: isFooterVisible ? 0 : 1,
140-
scale: isFooterVisible ? 0.8 : 1,
141-
pointerEvents: isFooterVisible ? 'none' : 'auto'
142-
}}
143-
transition={{ duration: 0.3 }}
144-
>
145-
<motion.button
146-
whileHover={{ scale: 1.05 }}
147-
whileTap={{ scale: 0.95 }}
148-
className="p-4 rounded-full glass-card shadow-lg cursor-pointer"
149-
style={{ color: accentColor }}
150-
onClick={() => {
151-
setIsShareModalOpen(true);
152-
if (window.location.hash !== '#share') {
153-
window.location.hash = 'share';
154-
}
155-
}}
156-
data-umami-event={t('timer.share')}
157-
>
158-
<FaShareAlt className="text-xl" />
159-
</motion.button>
160-
</motion.div>
161130

162131
{/* 弹窗内容 */}
163132
<AnimatePresence>
@@ -199,17 +168,6 @@ export default function Home() {
199168
)}
200169
</AnimatePresence>
201170

202-
{/* 分享弹窗 */}
203-
<AnimatePresence>
204-
{isShareModalOpen && (
205-
<ShareModal onClose={() => {
206-
setIsShareModalOpen(false);
207-
if (window.location.hash === '#share') {
208-
window.location.hash = '';
209-
}
210-
}} />
211-
)}
212-
</AnimatePresence>
213171

214172
{/* 登录弹窗 */}
215173
<AnimatePresence>

utils/shareUtils.js

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,43 @@
66
* @returns {string} 编码后的分享字符串
77
*/
88
export function createShareUrl(timers) {
9-
// 只保留必要的字段以减小URL长度
10-
const minimalTimers = timers.map(timer => ({
11-
id: timer.id,
12-
name: timer.name,
13-
targetDate: timer.targetDate,
14-
timezone: timer.timezone,
15-
color: timer.color,
16-
}));
9+
// 只保留必要的字段以减小URL长度,根据计时器类型包含不同字段
10+
const minimalTimers = timers.map(timer => {
11+
const baseTimer = {
12+
id: timer.id,
13+
name: timer.name,
14+
type: timer.type,
15+
color: timer.color,
16+
};
17+
18+
// 根据计时器类型添加特定字段
19+
if (timer.type === 'stopwatch') {
20+
baseTimer.startTime = timer.startTime;
21+
baseTimer.isRunning = timer.isRunning;
22+
} else if (timer.type === 'worldclock') {
23+
baseTimer.timezone = timer.timezone;
24+
baseTimer.city = timer.city;
25+
baseTimer.country = timer.country;
26+
} else {
27+
// 默认倒计时
28+
baseTimer.targetDate = timer.targetDate;
29+
}
30+
31+
return baseTimer;
32+
});
1733

1834
// 将数据转换为JSON字符串
1935
const data = JSON.stringify({ timers: minimalTimers });
2036

2137
// 使用 encodeURIComponent 处理非 Latin1 字符,然后再使用 Base64 编码
22-
return btoa(encodeURIComponent(data));
38+
const encodedData = btoa(encodeURIComponent(data));
39+
40+
// 检查编码后的数据长度,如果过长则警告用户
41+
if (encodedData.length > 2000) {
42+
console.warn('分享数据过长,可能导致二维码生成失败或URL过长问题');
43+
}
44+
45+
return encodedData;
2346
}
2447

2548
/**

0 commit comments

Comments
 (0)