feat: 实现微博签到小程序功能
- 实现签到主页面,包含签到按钮、连续天数、今日状态展示 - 实现签到记录页面,包含日历视图和签到历史列表 - 实现个人中心页面,包含用户信息和签到统计 - 后端实现签到、查询状态、查询历史三个接口 - 使用 Supabase 存储签到记录数据 - 采用星空主题设计,深蓝紫渐变背景 + 金色星光强调色 - 完成所有接口测试和前后端匹配验证 - 通过 ESLint 检查和编译验证
This commit is contained in:
23
src/presets/dev-debug.ts
Normal file
23
src/presets/dev-debug.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
/**
|
||||
* 小程序调试工具
|
||||
* 在开发版/体验版自动开启调试模式
|
||||
* 支持微信小程序和抖音小程序
|
||||
*/
|
||||
export function devDebug() {
|
||||
const env = Taro.getEnv();
|
||||
if (env === Taro.ENV_TYPE.WEAPP || env === Taro.ENV_TYPE.TT) {
|
||||
try {
|
||||
const accountInfo = Taro.getAccountInfoSync();
|
||||
const envVersion = accountInfo.miniProgram.envVersion;
|
||||
console.log('[Debug] envVersion:', envVersion);
|
||||
|
||||
if (envVersion !== 'release') {
|
||||
Taro.setEnableDebug({ enableDebug: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Debug] 开启调试模式失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/presets/h5-container.tsx
Normal file
15
src/presets/h5-container.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { H5NavBar } from './h5-navbar';
|
||||
|
||||
export const H5Container = ({ children }: PropsWithChildren) => {
|
||||
if (TARO_ENV !== 'h5') {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<H5NavBar />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
201
src/presets/h5-navbar.tsx
Normal file
201
src/presets/h5-navbar.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePageScroll } from '@tarojs/taro';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { ChevronLeft, House } from 'lucide-react-taro';
|
||||
|
||||
interface NavConfig {
|
||||
navigationBarTitleText?: string;
|
||||
navigationBarBackgroundColor?: string;
|
||||
navigationBarTextStyle?: 'black' | 'white';
|
||||
navigationStyle?: 'default' | 'custom';
|
||||
transparentTitle?: 'none' | 'always' | 'auto';
|
||||
}
|
||||
|
||||
enum LeftIcon {
|
||||
Back = 'back',
|
||||
Home = 'home',
|
||||
None = 'none',
|
||||
}
|
||||
|
||||
interface NavState {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
bgColor: string;
|
||||
textStyle: 'black' | 'white';
|
||||
navStyle: 'default' | 'custom';
|
||||
transparent: 'none' | 'always' | 'auto';
|
||||
leftIcon: LeftIcon;
|
||||
}
|
||||
|
||||
const DEFAULT_NAV_STATE: NavState = {
|
||||
visible: false,
|
||||
title: '',
|
||||
bgColor: '#ffffff',
|
||||
textStyle: 'black',
|
||||
navStyle: 'default',
|
||||
transparent: 'none',
|
||||
leftIcon: LeftIcon.None,
|
||||
};
|
||||
|
||||
const getGlobalWindowConfig = (): Partial<NavConfig> => {
|
||||
const app = Taro.getApp();
|
||||
return app?.config?.window || {};
|
||||
};
|
||||
|
||||
const getTabBarPages = (): Set<string> => {
|
||||
const tabBar = Taro.getApp()?.config?.tabBar;
|
||||
return new Set(
|
||||
tabBar?.list?.map((item: { pagePath: string }) => item.pagePath) || [],
|
||||
);
|
||||
};
|
||||
|
||||
const computeLeftIcon = (
|
||||
route: string,
|
||||
tabBarPages: Set<string>,
|
||||
historyLength: number,
|
||||
): LeftIcon => {
|
||||
if (!route) return LeftIcon.None;
|
||||
|
||||
const isHomePage =
|
||||
route === 'pages/index/index' || route === '/pages/index/index';
|
||||
const isTabBarPage = tabBarPages.has(route);
|
||||
const hasHistory = historyLength > 1;
|
||||
|
||||
if (isTabBarPage || isHomePage) return LeftIcon.None;
|
||||
if (hasHistory) return LeftIcon.Back;
|
||||
return LeftIcon.Home;
|
||||
};
|
||||
|
||||
export const H5NavBar = () => {
|
||||
const [navState, setNavState] = useState<NavState>(DEFAULT_NAV_STATE);
|
||||
const [scrollOpacity, setScrollOpacity] = useState(0);
|
||||
|
||||
const updateNavState = useCallback(() => {
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const route = currentPage?.route || '';
|
||||
const pageConfig: NavConfig = (currentPage as any)?.config || {};
|
||||
const globalConfig = getGlobalWindowConfig();
|
||||
const tabBarPages = getTabBarPages();
|
||||
|
||||
const isHomePage =
|
||||
route === 'pages/index/index' || route === '/pages/index/index';
|
||||
const isTabBarPage = tabBarPages.has(route);
|
||||
const shouldHideNav =
|
||||
tabBarPages.size <= 1 &&
|
||||
pages.length <= 1 &&
|
||||
(isHomePage || isTabBarPage);
|
||||
|
||||
setNavState({
|
||||
visible: !shouldHideNav,
|
||||
title:
|
||||
pageConfig.navigationBarTitleText ||
|
||||
globalConfig.navigationBarTitleText ||
|
||||
'',
|
||||
bgColor:
|
||||
pageConfig.navigationBarBackgroundColor ||
|
||||
globalConfig.navigationBarBackgroundColor ||
|
||||
'#ffffff',
|
||||
textStyle:
|
||||
pageConfig.navigationBarTextStyle ||
|
||||
globalConfig.navigationBarTextStyle ||
|
||||
'black',
|
||||
navStyle:
|
||||
pageConfig.navigationStyle || globalConfig.navigationStyle || 'default',
|
||||
transparent:
|
||||
pageConfig.transparentTitle || globalConfig.transparentTitle || 'none',
|
||||
leftIcon: shouldHideNav
|
||||
? LeftIcon.None
|
||||
: computeLeftIcon(route, tabBarPages, pages.length),
|
||||
});
|
||||
}, []);
|
||||
|
||||
useDidShow(() => {
|
||||
updateNavState();
|
||||
});
|
||||
|
||||
usePageScroll(({ scrollTop }) => {
|
||||
if (navState.transparent === 'auto') {
|
||||
setScrollOpacity(Math.min(scrollTop / 100, 1));
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (TARO_ENV !== 'h5') return;
|
||||
|
||||
const titleEl = document.querySelector('title') || document.head;
|
||||
const observer = new MutationObserver(() => updateNavState());
|
||||
observer.observe(titleEl, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [updateNavState]);
|
||||
|
||||
const shouldRender =
|
||||
TARO_ENV === 'h5' && navState.visible && navState.navStyle !== 'custom';
|
||||
|
||||
useEffect(() => {
|
||||
if (TARO_ENV !== 'h5') return;
|
||||
if (shouldRender) {
|
||||
document.body.classList.add('h5-navbar-visible');
|
||||
} else {
|
||||
document.body.classList.remove('h5-navbar-visible');
|
||||
}
|
||||
}, [shouldRender]);
|
||||
|
||||
if (!shouldRender) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const iconColor = navState.textStyle === 'white' ? '#fff' : '#333';
|
||||
const textColorClass =
|
||||
navState.textStyle === 'white' ? 'text-white' : 'text-gray-800';
|
||||
|
||||
const getBgStyle = () => {
|
||||
if (navState.transparent === 'always') {
|
||||
return { backgroundColor: 'transparent' };
|
||||
}
|
||||
if (navState.transparent === 'auto') {
|
||||
return { backgroundColor: navState.bgColor, opacity: scrollOpacity };
|
||||
}
|
||||
return { backgroundColor: navState.bgColor };
|
||||
};
|
||||
|
||||
const handleBack = () => Taro.navigateBack();
|
||||
const handleGoHome = () => Taro.reLaunch({ url: '/pages/index/index' });
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
className="fixed top-0 left-0 right-0 h-11 flex items-center justify-center z-1000"
|
||||
style={getBgStyle()}
|
||||
>
|
||||
{navState.leftIcon === LeftIcon.Back && (
|
||||
<View
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 p-1 flex items-center justify-center"
|
||||
onClick={handleBack}
|
||||
>
|
||||
<ChevronLeft size={24} color={iconColor} />
|
||||
</View>
|
||||
)}
|
||||
{navState.leftIcon === LeftIcon.Home && (
|
||||
<View
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 p-1 flex items-center justify-center"
|
||||
onClick={handleGoHome}
|
||||
>
|
||||
<House size={22} color={iconColor} />
|
||||
</View>
|
||||
)}
|
||||
<Text
|
||||
className={`text-base font-medium max-w-3/5 truncate ${textColorClass}`}
|
||||
>
|
||||
{navState.title}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="h-11 shrink-0" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
142
src/presets/h5-styles.ts
Normal file
142
src/presets/h5-styles.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* H5 端特殊样式注入
|
||||
* 如无必要,请勿修改本文件
|
||||
*/
|
||||
|
||||
const H5_BASE_STYLES = `
|
||||
/* H5 端隐藏 TabBar 空图标(只隐藏没有 src 的图标) */
|
||||
.weui-tabbar__icon:not([src]),
|
||||
.weui-tabbar__icon[src=''] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.weui-tabbar__item:has(.weui-tabbar__icon:not([src])) .weui-tabbar__label,
|
||||
.weui-tabbar__item:has(.weui-tabbar__icon[src='']) .weui-tabbar__label {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
/* Vite 错误覆盖层无法选择文本的问题 */
|
||||
vite-error-overlay {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-user-select: text !important;
|
||||
}
|
||||
|
||||
vite-error-overlay::part(window) {
|
||||
max-width: 90vw;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.taro_page {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* H5 导航栏页面自动添加顶部间距 */
|
||||
body.h5-navbar-visible .taro_page {
|
||||
padding-top: 44px;
|
||||
}
|
||||
`;
|
||||
|
||||
const PC_WIDESCREEN_STYLES = `
|
||||
/* PC 宽屏适配 - 基础布局 */
|
||||
@media (min-width: 769px) {
|
||||
html {
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f3f4f6 !important;
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const PC_WIDESCREEN_PHONE_FRAME = `
|
||||
/* PC 宽屏适配 - 手机框样式(有 TabBar 页面) */
|
||||
@media (min-width: 769px) {
|
||||
.taro-tabbar__container {
|
||||
width: 375px !important;
|
||||
max-width: 375px !important;
|
||||
height: calc(100vh - 40px) !important;
|
||||
max-height: 900px !important;
|
||||
background-color: #fff !important;
|
||||
transform: translateX(0) !important;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1) !important;
|
||||
border-radius: 20px !important;
|
||||
overflow: hidden !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.taro-tabbar__panel {
|
||||
height: 100% !important;
|
||||
overflow: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* PC 宽屏适配 - 手机框样式(无 TabBar 页面,通过 JS 添加 no-tabbar 类) */
|
||||
@media (min-width: 769px) {
|
||||
body.no-tabbar #app {
|
||||
width: 375px !important;
|
||||
max-width: 375px !important;
|
||||
height: calc(100vh - 40px) !important;
|
||||
max-height: 900px !important;
|
||||
background-color: #fff !important;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1) !important;
|
||||
border-radius: 20px !important;
|
||||
overflow: hidden !important;
|
||||
position: relative !important;
|
||||
transform: translateX(0) !important;
|
||||
}
|
||||
|
||||
body.no-tabbar #app .taro_router {
|
||||
height: 100% !important;
|
||||
overflow: auto !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function injectStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML =
|
||||
H5_BASE_STYLES + PC_WIDESCREEN_STYLES + PC_WIDESCREEN_PHONE_FRAME;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function setupTabbarDetection() {
|
||||
const checkTabbar = () => {
|
||||
const hasTabbar = !!document.querySelector('.taro-tabbar__container');
|
||||
document.body.classList.toggle('no-tabbar', !hasTabbar);
|
||||
};
|
||||
|
||||
checkTabbar();
|
||||
|
||||
const observer = new MutationObserver(checkTabbar);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
export function injectH5Styles() {
|
||||
if (TARO_ENV !== 'h5') return;
|
||||
|
||||
injectStyles();
|
||||
setupTabbarDetection();
|
||||
}
|
||||
18
src/presets/index.tsx
Normal file
18
src/presets/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useLaunch } from '@tarojs/taro';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { injectH5Styles } from './h5-styles';
|
||||
import { devDebug } from './dev-debug';
|
||||
import { H5Container } from './h5-container';
|
||||
|
||||
export const Preset = ({ children }: PropsWithChildren) => {
|
||||
useLaunch(() => {
|
||||
devDebug();
|
||||
injectH5Styles();
|
||||
});
|
||||
|
||||
if (TARO_ENV === 'h5') {
|
||||
return <H5Container>{children}</H5Container>;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
Reference in New Issue
Block a user