Compare commits
17 Commits
e626eb182b
...
23931df1da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23931df1da | ||
|
|
01796ac695 | ||
|
|
6c032cefbf | ||
| 71b4f8a679 | |||
|
|
9fb78bb5ee | ||
|
|
8a95dbcb59 | ||
|
|
6ba24c59aa | ||
|
|
544a7ea156 | ||
|
|
530f54a4a3 | ||
|
|
3d1d7857df | ||
|
|
4f4c190c3a | ||
|
|
b4e4f33e3b | ||
|
|
b010aefbe2 | ||
|
|
4ec56de988 | ||
|
|
f328b2fd31 | ||
|
|
f7fd849cb1 | ||
|
|
ef955db99f |
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.expo
|
||||||
|
dist
|
||||||
|
dist-web
|
||||||
|
dist-android
|
||||||
|
dist-android-update.zip
|
||||||
|
android
|
||||||
|
screenshots
|
||||||
|
*.log
|
||||||
310
.gitea/workflows/build.yml
Normal file
310
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
name: Frontend CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
- dev
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
publish_ota:
|
||||||
|
description: 'Publish Android OTA'
|
||||||
|
required: false
|
||||||
|
default: 'true'
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: code.littlelan.cn
|
||||||
|
IMAGE_NAME: carrot_bbs/frontend-web
|
||||||
|
OTA_PLATFORM: android
|
||||||
|
OTA_PUBLISH_URL: https://updates.littlelan.cn/admin/publish
|
||||||
|
OTA_MANIFEST_URL: https://updates.littlelan.cn/api/manifest
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ota-android:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.publish_ota == 'true')
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '25.6.1'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Resolve runtime version
|
||||||
|
id: runtime
|
||||||
|
run: |
|
||||||
|
RUNTIME_VERSION="$(node -p "require('./app.json').expo.version")"
|
||||||
|
echo "runtime_version=${RUNTIME_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Resolved runtimeVersion: ${RUNTIME_VERSION}"
|
||||||
|
|
||||||
|
- name: Export Android update bundle
|
||||||
|
run: |
|
||||||
|
rm -rf dist-android dist-android-update.zip
|
||||||
|
npx expo export --platform android --output-dir dist-android
|
||||||
|
npx expo config --type public --json > dist-android/expoConfig.json
|
||||||
|
|
||||||
|
- name: Archive Android update bundle
|
||||||
|
run: |
|
||||||
|
python - <<'PY'
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
with zipfile.ZipFile('dist-android-update.zip', 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for root, _, files in os.walk('dist-android'):
|
||||||
|
for name in files:
|
||||||
|
src = os.path.join(root, name)
|
||||||
|
arc = os.path.relpath(src, 'dist-android')
|
||||||
|
zf.write(src, arc)
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Publish OTA
|
||||||
|
env:
|
||||||
|
OTA_AUTH_TOKEN: ${{ secrets.OTA_AUTH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
test -n "${OTA_AUTH_TOKEN}" || (echo "Missing secret OTA_AUTH_TOKEN" && exit 1)
|
||||||
|
curl -fSs -X POST "${OTA_PUBLISH_URL}?runtimeVersion=${{ steps.runtime.outputs.runtime_version }}&platform=${OTA_PLATFORM}" \
|
||||||
|
-H "Authorization: Bearer ${OTA_AUTH_TOKEN}" \
|
||||||
|
-H "Content-Type: application/zip" \
|
||||||
|
--data-binary @"dist-android-update.zip"
|
||||||
|
|
||||||
|
- name: Verify OTA manifest
|
||||||
|
run: |
|
||||||
|
REMOTE="$(curl -sS "${OTA_MANIFEST_URL}" \
|
||||||
|
-H "expo-platform: ${OTA_PLATFORM}" \
|
||||||
|
-H "expo-runtime-version: ${{ steps.runtime.outputs.runtime_version }}" \
|
||||||
|
-H "expo-protocol-version: 1" \
|
||||||
|
| python -c "import sys, json, re; s=sys.stdin.read(); m=re.search(r'\\{\\\"id\\\":.*\\\"extra\\\":\\{.*\\}\\}', s); j=json.loads(m.group(0)); print(j['launchAsset']['url'].split('/')[-1].split('&')[0])")"
|
||||||
|
LOCAL="$(python -c "import json; m=json.load(open('dist-android/metadata.json')); print(m['fileMetadata']['android']['bundle'].split('/')[-1])")"
|
||||||
|
echo "Remote bundle: ${REMOTE}"
|
||||||
|
echo "Local bundle: ${LOCAL}"
|
||||||
|
test "${REMOTE}" = "${LOCAL}"
|
||||||
|
|
||||||
|
build-android-apk:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: reactnativecommunity/react-native-android:latest
|
||||||
|
env:
|
||||||
|
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.parallel=false -Dorg.gradle.workers.max=1 -Xmx2g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=0 -XX:ReservedCodeCacheSize=128m"
|
||||||
|
_JAVA_OPTIONS: "-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=0 -XX:ReservedCodeCacheSize=128m"
|
||||||
|
NODE_OPTIONS: "--max-old-space-size=2048"
|
||||||
|
NODE_ENV: "production"
|
||||||
|
NDK_NUM_JOBS: "1"
|
||||||
|
CMAKE_BUILD_PARALLEL_LEVEL: "1"
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '21'
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '25.6.1'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Generate Android native project
|
||||||
|
run: npx expo prebuild --platform android
|
||||||
|
|
||||||
|
- name: Configure Gradle with Aliyun Maven mirror
|
||||||
|
run: |
|
||||||
|
cd android
|
||||||
|
|
||||||
|
# Update settings.gradle with Aliyun Maven mirror
|
||||||
|
cat > settings.gradle << 'SETTINGS_EOF'
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/public' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/central' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
|
||||||
|
}
|
||||||
|
def reactNativeGradlePlugin = new File(
|
||||||
|
providers.exec {
|
||||||
|
workingDir(rootDir)
|
||||||
|
commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
|
||||||
|
}.standardOutput.asText.get().trim()
|
||||||
|
).getParentFile().absolutePath
|
||||||
|
includeBuild(reactNativeGradlePlugin)
|
||||||
|
|
||||||
|
def expoPluginsPath = new File(
|
||||||
|
providers.exec {
|
||||||
|
workingDir(rootDir)
|
||||||
|
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
|
||||||
|
}.standardOutput.asText.get().trim(),
|
||||||
|
"../android/expo-gradle-plugin"
|
||||||
|
).absolutePath
|
||||||
|
includeBuild(expoPluginsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.facebook.react.settings")
|
||||||
|
id("expo-autolinking-settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
||||||
|
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
|
||||||
|
ex.autolinkLibrariesFromCommand()
|
||||||
|
} else {
|
||||||
|
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expoAutolinking.useExpoModules()
|
||||||
|
|
||||||
|
rootProject.name = '萝卜社区'
|
||||||
|
|
||||||
|
expoAutolinking.useExpoVersionCatalog()
|
||||||
|
|
||||||
|
include ':app'
|
||||||
|
includeBuild(expoAutolinking.reactNativeGradlePlugin)
|
||||||
|
SETTINGS_EOF
|
||||||
|
|
||||||
|
# Update build.gradle with Aliyun Maven mirror
|
||||||
|
cat > build.gradle << 'BUILD_EOF'
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/public' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/central' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath('com.android.tools.build:gradle')
|
||||||
|
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||||
|
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://www.jitpack.io' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/public' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/central' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: "expo-root-project"
|
||||||
|
apply plugin: "com.facebook.react.rootproject"
|
||||||
|
BUILD_EOF
|
||||||
|
|
||||||
|
# Update gradle.properties
|
||||||
|
cat > gradle.properties << 'PROPS_EOF'
|
||||||
|
org.gradle.daemon=false
|
||||||
|
org.gradle.parallel=false
|
||||||
|
org.gradle.configureondemand=false
|
||||||
|
org.gradle.workers.max=1
|
||||||
|
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=0 -XX:ReservedCodeCacheSize=128m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.enableJetifier=false
|
||||||
|
android.useAndroidX=true
|
||||||
|
hermesEnabled=true
|
||||||
|
reactNativeArchitectures=arm64-v8a
|
||||||
|
newArchEnabled=true
|
||||||
|
edgeToEdgeEnabled=true
|
||||||
|
expo.gif.enabled=true
|
||||||
|
expo.webp.enabled=true
|
||||||
|
expo.webp.animated=false
|
||||||
|
expo.useLegacyPackaging=false
|
||||||
|
PROPS_EOF
|
||||||
|
|
||||||
|
- name: Build Android release APK (arm64 only)
|
||||||
|
run: |
|
||||||
|
cd android
|
||||||
|
chmod +x gradlew
|
||||||
|
./gradlew :app:assembleRelease -PreactNativeArchitectures=arm64-v8a --max-workers=1
|
||||||
|
|
||||||
|
- name: Upload APK artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: carrot-bbs-android-release-apk
|
||||||
|
path: android/app/build/outputs/apk/release/app-release.apk
|
||||||
|
|
||||||
|
build-and-push-web:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to Gitea Container Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.GIT_USERNAME }}
|
||||||
|
password: ${{ secrets.GIT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=sha
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.web
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
platforms: linux/amd64
|
||||||
|
provenance: false
|
||||||
|
|
||||||
|
- name: Show image tags
|
||||||
|
run: |
|
||||||
|
echo "Built image tags:"
|
||||||
|
echo "${{ steps.meta.outputs.tags }}"
|
||||||
|
echo "Digest: ${{ steps.meta.outputs.digest }}"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## Frontend CI Summary" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "**Web image:** ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "**Tags:**" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "${{ steps.meta.outputs.tags }}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "**Digest:** ${{ steps.meta.outputs.digest }}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
2
.idea/.gitignore
generated
vendored
2
.idea/.gitignore
generated
vendored
@@ -6,3 +6,5 @@
|
|||||||
# Datasource local storage ignored files
|
# Datasource local storage ignored files
|
||||||
/dataSources/
|
/dataSources/
|
||||||
/dataSources.local.xml
|
/dataSources.local.xml
|
||||||
|
# Screenshots
|
||||||
|
screenshots/
|
||||||
|
|||||||
15
Dockerfile.web
Normal file
15
Dockerfile.web
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npx expo export --platform web --output-dir dist-web
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
COPY --from=builder /app/dist-web /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
@@ -63,6 +63,7 @@ export type MainTabParamList = {
|
|||||||
export type HomeStackParamList = {
|
export type HomeStackParamList = {
|
||||||
Home: undefined;
|
Home: undefined;
|
||||||
Search: undefined;
|
Search: undefined;
|
||||||
|
CreatePost: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageStackParamList = {
|
export type MessageStackParamList = {
|
||||||
@@ -133,6 +134,15 @@ function HomeStackNavigatorComponent() {
|
|||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<HomeStack.Screen
|
||||||
|
name="CreatePost"
|
||||||
|
component={CreatePostScreen}
|
||||||
|
options={{
|
||||||
|
title: '发帖',
|
||||||
|
headerShown: false,
|
||||||
|
presentation: 'modal',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</HomeStack.Navigator>
|
</HomeStack.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ import VoteEditor from '../../components/business/VoteEditor';
|
|||||||
import { useResponsive, useResponsiveValue } from '../../hooks';
|
import { useResponsive, useResponsiveValue } from '../../hooks';
|
||||||
import { RootStackParamList } from '../../navigation/types';
|
import { RootStackParamList } from '../../navigation/types';
|
||||||
|
|
||||||
|
// Props 接口
|
||||||
|
interface CreatePostScreenProps {
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
const MAX_TITLE_LENGTH = 100;
|
const MAX_TITLE_LENGTH = 100;
|
||||||
const MAX_CONTENT_LENGTH = 2000;
|
const MAX_CONTENT_LENGTH = 2000;
|
||||||
|
|
||||||
@@ -73,7 +78,8 @@ const getPublishErrorMessage = (error: unknown): string => {
|
|||||||
return '发布失败,请重试';
|
return '发布失败,请重试';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreatePostScreen: React.FC = () => {
|
export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||||
|
const { onClose } = props;
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const route = useRoute<RouteProp<RootStackParamList, 'CreatePost'>>();
|
const route = useRoute<RouteProp<RootStackParamList, 'CreatePost'>>();
|
||||||
const isEditMode = route.params?.mode === 'edit' && !!route.params?.postId;
|
const isEditMode = route.params?.mode === 'edit' && !!route.params?.postId;
|
||||||
@@ -130,8 +136,14 @@ export const CreatePostScreen: React.FC = () => {
|
|||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
title: isEditMode ? '编辑帖子' : '发布帖子',
|
title: isEditMode ? '编辑帖子' : '发布帖子',
|
||||||
|
// 当作为 Modal 使用时,显示关闭按钮
|
||||||
|
headerLeft: onClose ? () => (
|
||||||
|
<TouchableOpacity onPress={onClose} style={{ padding: 8 }}>
|
||||||
|
<MaterialCommunityIcons name="close" size={24} color={colors.text.primary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : undefined,
|
||||||
});
|
});
|
||||||
}, [navigation, isEditMode]);
|
}, [navigation, isEditMode, onClose]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isEditMode || !editPostID) {
|
if (!isEditMode || !editPostID) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
NativeSyntheticEvent,
|
NativeSyntheticEvent,
|
||||||
Alert,
|
Alert,
|
||||||
Clipboard,
|
Clipboard,
|
||||||
|
Modal,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
@@ -33,6 +34,7 @@ import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid
|
|||||||
import { HomeStackParamList, RootStackParamList } from '../../navigation/types';
|
import { HomeStackParamList, RootStackParamList } from '../../navigation/types';
|
||||||
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||||
import { SearchScreen } from './SearchScreen';
|
import { SearchScreen } from './SearchScreen';
|
||||||
|
import { CreatePostScreen } from '../create/CreatePostScreen';
|
||||||
|
|
||||||
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'Home'> & NativeStackNavigationProp<RootStackParamList>;
|
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'Home'> & NativeStackNavigationProp<RootStackParamList>;
|
||||||
|
|
||||||
@@ -88,6 +90,9 @@ export const HomeScreen: React.FC = () => {
|
|||||||
// 搜索显示状态(用于内嵌搜索页面)
|
// 搜索显示状态(用于内嵌搜索页面)
|
||||||
const [showSearch, setShowSearch] = useState(false);
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
|
|
||||||
|
// 发帖弹窗状态
|
||||||
|
const [showCreatePost, setShowCreatePost] = useState(false);
|
||||||
|
|
||||||
// 用于跟踪当前页面显示的帖子 ID,以便从 store 同步状态
|
// 用于跟踪当前页面显示的帖子 ID,以便从 store 同步状态
|
||||||
const postIdsRef = React.useRef<Set<string>>(new Set());
|
const postIdsRef = React.useRef<Set<string>>(new Set());
|
||||||
const inFlightRequestKeysRef = React.useRef<Set<string>>(new Set());
|
const inFlightRequestKeysRef = React.useRef<Set<string>>(new Set());
|
||||||
@@ -144,7 +149,8 @@ export const HomeScreen: React.FC = () => {
|
|||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return insets.bottom + MOBILE_TAB_BAR_HEIGHT + MOBILE_TAB_FLOATING_MARGIN + MOBILE_FAB_GAP;
|
// 往底部导航栏靠近一个按钮的位置
|
||||||
|
return insets.bottom + MOBILE_TAB_BAR_HEIGHT + MOBILE_TAB_FLOATING_MARGIN + MOBILE_FAB_GAP - MOBILE_TAB_BAR_HEIGHT;
|
||||||
}, [isMobile, insets.bottom]);
|
}, [isMobile, insets.bottom]);
|
||||||
|
|
||||||
const appendUniquePosts = useCallback((prevPosts: Post[], incomingPosts: Post[]) => {
|
const appendUniquePosts = useCallback((prevPosts: Post[], incomingPosts: Post[]) => {
|
||||||
@@ -398,9 +404,9 @@ export const HomeScreen: React.FC = () => {
|
|||||||
setShowImageViewer(true);
|
setShowImageViewer(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 跳转到发帖页面
|
// 跳转到发帖页面(使用 Modal 方式)
|
||||||
const handleCreatePost = () => {
|
const handleCreatePost = () => {
|
||||||
navigation.getParent()?.navigate('CreatePost');
|
setShowCreatePost(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染帖子卡片(列表模式)
|
// 渲染帖子卡片(列表模式)
|
||||||
@@ -733,6 +739,18 @@ export const HomeScreen: React.FC = () => {
|
|||||||
onClose={() => setShowImageViewer(false)}
|
onClose={() => setShowImageViewer(false)}
|
||||||
enableSave
|
enableSave
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 发帖弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={showCreatePost}
|
||||||
|
animationType="slide"
|
||||||
|
presentationStyle="pageSheet"
|
||||||
|
onRequestClose={() => setShowCreatePost(false)}
|
||||||
|
>
|
||||||
|
<CreatePostScreen
|
||||||
|
onClose={() => setShowCreatePost(false)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|||||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import { colors, fontSizes, spacing, borderRadius, shadows } from '../../theme';
|
import { colors, fontSizes, spacing, borderRadius, shadows } from '../../theme';
|
||||||
|
import { useResponsive } from '../../hooks/useResponsive';
|
||||||
import {
|
import {
|
||||||
Course,
|
Course,
|
||||||
COURSE_COLORS,
|
COURSE_COLORS,
|
||||||
TimeSlot,
|
TimeSlot,
|
||||||
getCourseColor,
|
getCourseColor,
|
||||||
hasCourseInWeek,
|
hasCourseInWeek,
|
||||||
getCurrentWeek,
|
|
||||||
} from '../../types/schedule';
|
} from '../../types/schedule';
|
||||||
import type { ScheduleStackParamList } from '../../navigation/types';
|
import type { ScheduleStackParamList } from '../../navigation/types';
|
||||||
import { scheduleService } from '../../services/scheduleService';
|
import { scheduleService } from '../../services/scheduleService';
|
||||||
@@ -44,8 +44,8 @@ const VISIBLE_WEEKDAY_NAMES = ['周一', '周二', '周三', '周四', '周五',
|
|||||||
// 星期列宽度(根据屏幕宽度自动计算)
|
// 星期列宽度(根据屏幕宽度自动计算)
|
||||||
const getDayColumnWidth = () => Math.max(46, (SCREEN_WIDTH - TIME_COLUMN_WIDTH - 2) / VISIBLE_DAY_VALUES.length);
|
const getDayColumnWidth = () => Math.max(46, (SCREEN_WIDTH - TIME_COLUMN_WIDTH - 2) / VISIBLE_DAY_VALUES.length);
|
||||||
|
|
||||||
// 每个大节(两小节)高度
|
// 每个大节(两小节)高度 - 默认值,会在组件中动态调整
|
||||||
const SECTION_HEIGHT = 105;
|
const DEFAULT_SECTION_HEIGHT = 105;
|
||||||
// 顶部周选择器高度
|
// 顶部周选择器高度
|
||||||
const WEEK_SELECTOR_HEIGHT = 44;
|
const WEEK_SELECTOR_HEIGHT = 44;
|
||||||
// 星期标题行高度
|
// 星期标题行高度
|
||||||
@@ -106,6 +106,18 @@ const getTodayColumnIndex = () => {
|
|||||||
// 第1周固定从2026年3月9日(周一)开始
|
// 第1周固定从2026年3月9日(周一)开始
|
||||||
const FIRST_WEEK_START = new Date(2026, 2, 9); // 2026年3月9日
|
const FIRST_WEEK_START = new Date(2026, 2, 9); // 2026年3月9日
|
||||||
|
|
||||||
|
// 计算当前日期对应的周数(返回1-20)
|
||||||
|
const getCurrentWeekNumber = (): number => {
|
||||||
|
const now = new Date();
|
||||||
|
// 计算当前日期与第一周周一的差距天数
|
||||||
|
const diffTime = now.getTime() - FIRST_WEEK_START.getTime();
|
||||||
|
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
// 计算周数(从1开始)
|
||||||
|
const weekNumber = Math.floor(diffDays / 7) + 1;
|
||||||
|
// 确保周数在有效范围内
|
||||||
|
return Math.max(1, Math.min(TOTAL_WEEKS, weekNumber));
|
||||||
|
};
|
||||||
|
|
||||||
// 获取指定周的起始日期、每天的日期和月份
|
// 获取指定周的起始日期、每天的日期和月份
|
||||||
// weekIndex: 0表示第1周,1表示第2周,以此类推
|
// weekIndex: 0表示第1周,1表示第2周,以此类推
|
||||||
const getWeekInfo = (weekIndex: number = 0) => {
|
const getWeekInfo = (weekIndex: number = 0) => {
|
||||||
@@ -131,18 +143,41 @@ const getWeekDates = (weekOffset: number = 0) => {
|
|||||||
return getWeekInfo(weekOffset).dates;
|
return getWeekInfo(weekOffset).dates;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 学期起始日期字符串(用于 getCurrentWeek 函数)
|
// 获取初始周数(仅在组件初始化时计算一次)
|
||||||
const SEMESTER_START_DATE = '2026-03-09';
|
const INITIAL_WEEK = getCurrentWeekNumber();
|
||||||
|
|
||||||
// 计算当前实际周数
|
|
||||||
const getInitialWeek = (): number => {
|
|
||||||
return getCurrentWeek(SEMESTER_START_DATE, TOTAL_WEEKS);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ScheduleScreen: React.FC = () => {
|
export const ScheduleScreen: React.FC = () => {
|
||||||
const navigation = useNavigation<NativeStackNavigationProp<ScheduleStackParamList>>();
|
const navigation = useNavigation<NativeStackNavigationProp<ScheduleStackParamList>>();
|
||||||
const [currentWeek, setCurrentWeek] = useState(getInitialWeek);
|
// 使用响应式 hook 检测屏幕尺寸
|
||||||
|
const { width: screenWidth, height: screenHeight, isMobile, platform } = useResponsive();
|
||||||
|
const isWeb = platform.isWeb;
|
||||||
|
const [currentWeek, setCurrentWeek] = useState(INITIAL_WEEK);
|
||||||
const [courses, setCourses] = useState<Course[]>([]);
|
const [courses, setCourses] = useState<Course[]>([]);
|
||||||
|
|
||||||
|
// 动态计算日列宽度:移动端填满屏幕宽度,电脑端也填满
|
||||||
|
// Web端需要特殊处理,确保周一到周日刚好横向填满
|
||||||
|
const dayColumnWidth = useMemo(() => {
|
||||||
|
if (isWeb) {
|
||||||
|
// Web端:使用更大的宽度确保填满
|
||||||
|
return Math.max(80, (screenWidth - TIME_COLUMN_WIDTH) / VISIBLE_DAY_VALUES.length);
|
||||||
|
}
|
||||||
|
// 移动端和原生桌面端
|
||||||
|
return Math.max(46, (screenWidth - TIME_COLUMN_WIDTH - 2) / VISIBLE_DAY_VALUES.length);
|
||||||
|
}, [screenWidth, isWeb]);
|
||||||
|
|
||||||
|
// 动态计算每节课的高度
|
||||||
|
// 手机端:第六节课底部刚好跟底部导航栏持平(可用高度 = 屏幕高度 - 周选择器 - 星期标题 - 底部Tab栏)
|
||||||
|
// 电脑端:第六节课刚好填到底部(可用高度 = 屏幕高度 - 周选择器 - 星期标题)
|
||||||
|
const sectionHeight = useMemo((): number => {
|
||||||
|
const totalHeaderHeight = WEEK_SELECTOR_HEIGHT + HEADER_HEIGHT;
|
||||||
|
const bottomOffset = isMobile ? TAB_BAR_HEIGHT : 0;
|
||||||
|
// 可用高度 = 屏幕高度 - 顶部区域 - 底部偏移
|
||||||
|
const availableHeight = screenHeight - totalHeaderHeight - bottomOffset;
|
||||||
|
// 6节课,每节课高度 = 可用高度 / 6
|
||||||
|
const calculatedHeight = Math.floor(availableHeight / 6);
|
||||||
|
// 确保高度不低于默认值
|
||||||
|
return Math.max(DEFAULT_SECTION_HEIGHT, calculatedHeight);
|
||||||
|
}, [screenHeight, isMobile]);
|
||||||
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
||||||
const [pendingDay, setPendingDay] = useState<number>(0);
|
const [pendingDay, setPendingDay] = useState<number>(0);
|
||||||
const [pendingMergedSection, setPendingMergedSection] = useState<number>(1);
|
const [pendingMergedSection, setPendingMergedSection] = useState<number>(1);
|
||||||
@@ -172,13 +207,10 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
const todayColumnIndex = getTodayColumnIndex();
|
const todayColumnIndex = getTodayColumnIndex();
|
||||||
// 获取当前实际周数,用于判断是否在查看当前周
|
// 当前正在查看的周是否为真实的当前周
|
||||||
const actualCurrentWeek = getInitialWeek();
|
const isViewingCurrentWeek = currentWeek === INITIAL_WEEK;
|
||||||
// 只有在查看当前实际周时才高亮今日列
|
|
||||||
const isViewingCurrentWeek = currentWeek === actualCurrentWeek;
|
|
||||||
const activeTodayColumn = isViewingCurrentWeek ? todayColumnIndex : -1;
|
const activeTodayColumn = isViewingCurrentWeek ? todayColumnIndex : -1;
|
||||||
|
|
||||||
const dayColumnWidth = getDayColumnWidth();
|
|
||||||
const usedCourseColors = useMemo(
|
const usedCourseColors = useMemo(
|
||||||
() => new Set(courses.map(item => normalizeHexColor(item.color || '')).filter(Boolean)),
|
() => new Set(courses.map(item => normalizeHexColor(item.color || '')).filter(Boolean)),
|
||||||
[courses]
|
[courses]
|
||||||
@@ -396,7 +428,7 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
style={[
|
style={[
|
||||||
styles.dayHeader,
|
styles.dayHeader,
|
||||||
{
|
{
|
||||||
width: dayColumnWidth,
|
flex: 1,
|
||||||
backgroundColor: index === activeTodayColumn ? `${colors.primary.main}10` : 'transparent',
|
backgroundColor: index === activeTodayColumn ? `${colors.primary.main}10` : 'transparent',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -433,7 +465,7 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
key={slot.section}
|
key={slot.section}
|
||||||
style={[
|
style={[
|
||||||
styles.timeCell,
|
styles.timeCell,
|
||||||
{ height: SECTION_HEIGHT },
|
{ height: sectionHeight },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<View style={styles.timeSectionBadge}>
|
<View style={styles.timeSectionBadge}>
|
||||||
@@ -558,8 +590,8 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
const startMergedSection = getMergedSectionIndex(course.startSection);
|
const startMergedSection = getMergedSectionIndex(course.startSection);
|
||||||
const endMergedSection = getMergedSectionIndex(course.endSection);
|
const endMergedSection = getMergedSectionIndex(course.endSection);
|
||||||
const duration = endMergedSection - startMergedSection + 1;
|
const duration = endMergedSection - startMergedSection + 1;
|
||||||
const top = (startMergedSection - 1) * SECTION_HEIGHT;
|
const top = (startMergedSection - 1) * sectionHeight;
|
||||||
const height = duration * SECTION_HEIGHT - 4;
|
const height = duration * sectionHeight - 4;
|
||||||
const laneGap = 2;
|
const laneGap = 2;
|
||||||
const baseLeft = 2;
|
const baseLeft = 2;
|
||||||
const availableWidth = dayColumnWidth - baseLeft * 2;
|
const availableWidth = dayColumnWidth - baseLeft * 2;
|
||||||
@@ -618,9 +650,8 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
style={[
|
style={[
|
||||||
styles.dayColumn,
|
styles.dayColumn,
|
||||||
{
|
{
|
||||||
width: dayColumnWidth,
|
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
backgroundColor: columnIndex === activeTodayColumn ? `${colors.primary.main}08` : 'transparent',
|
backgroundColor: columnIndex === activeTodayColumn ? `${colors.primary.main}08` : colors.background.default,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -633,7 +664,7 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
key={slot.section}
|
key={slot.section}
|
||||||
style={[
|
style={[
|
||||||
styles.gridCell,
|
styles.gridCell,
|
||||||
{ height: SECTION_HEIGHT },
|
{ height: sectionHeight },
|
||||||
]}
|
]}
|
||||||
activeOpacity={1}
|
activeOpacity={1}
|
||||||
delayLongPress={280}
|
delayLongPress={280}
|
||||||
@@ -652,7 +683,11 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 渲染课程表主体
|
// 渲染课程表主体
|
||||||
const renderScheduleBody = () => (
|
const renderScheduleBody = () => {
|
||||||
|
// 移动端和电脑端都不添加额外底部间距,让内容填满整个区域
|
||||||
|
// 动态计算的高度已经确保第六节课显示在正确位置
|
||||||
|
|
||||||
|
return (
|
||||||
<PanGestureHandler
|
<PanGestureHandler
|
||||||
activeOffsetX={[-8, 8]}
|
activeOffsetX={[-8, 8]}
|
||||||
failOffsetY={[-80, 80]}
|
failOffsetY={[-80, 80]}
|
||||||
@@ -661,13 +696,14 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
<View style={styles.scheduleBodyGestureLayer}>
|
<View style={styles.scheduleBodyGestureLayer}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scheduleBody}
|
style={styles.scheduleBody}
|
||||||
showsVerticalScrollIndicator={true}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.scheduleBodyContent}
|
contentContainerStyle={styles.scheduleBodyContent}
|
||||||
>
|
>
|
||||||
{renderTimeColumn()}
|
{renderTimeColumn()}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={{ flexGrow: 1 }}
|
||||||
>
|
>
|
||||||
<View style={styles.daysContainer}>
|
<View style={styles.daysContainer}>
|
||||||
{VISIBLE_DAY_VALUES.map((dayOfWeek, index) =>
|
{VISIBLE_DAY_VALUES.map((dayOfWeek, index) =>
|
||||||
@@ -679,6 +715,7 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</PanGestureHandler>
|
</PanGestureHandler>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
<SafeAreaView style={styles.container} edges={['top']}>
|
||||||
@@ -1063,11 +1100,11 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
height: HEADER_HEIGHT,
|
height: HEADER_HEIGHT,
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.divider,
|
borderBottomColor: colors.divider,
|
||||||
},
|
},
|
||||||
timeHeader: {
|
timeHeader: {
|
||||||
borderRightWidth: StyleSheet.hairlineWidth,
|
borderRightWidth: 1,
|
||||||
borderRightColor: colors.divider,
|
borderRightColor: colors.divider,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -1079,6 +1116,7 @@ const styles = StyleSheet.create({
|
|||||||
letterSpacing: 0.3,
|
letterSpacing: 0.3,
|
||||||
},
|
},
|
||||||
dayHeader: {
|
dayHeader: {
|
||||||
|
flex: 1,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingVertical: 3,
|
paddingVertical: 3,
|
||||||
@@ -1118,7 +1156,6 @@ const styles = StyleSheet.create({
|
|||||||
scheduleBody: {
|
scheduleBody: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background.default,
|
backgroundColor: colors.background.default,
|
||||||
marginBottom: TAB_BAR_HEIGHT,
|
|
||||||
},
|
},
|
||||||
scheduleBodyGestureLayer: {
|
scheduleBodyGestureLayer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -1164,11 +1201,16 @@ const styles = StyleSheet.create({
|
|||||||
// ── 日期列容器 ────────────────────────────────────────
|
// ── 日期列容器 ────────────────────────────────────────
|
||||||
daysContainer: {
|
daysContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.background.default,
|
||||||
|
minWidth: TIME_COLUMN_WIDTH, // 确保最小宽度
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 单日列 ────────────────────────────────────────────
|
// ── 单日列 ────────────────────────────────────────────
|
||||||
dayColumn: {
|
dayColumn: {
|
||||||
borderRightWidth: StyleSheet.hairlineWidth,
|
flex: 1,
|
||||||
|
backgroundColor: colors.background.default, // 使用主题默认背景色
|
||||||
|
borderRightWidth: 1,
|
||||||
borderRightColor: '#EBEBEB',
|
borderRightColor: '#EBEBEB',
|
||||||
},
|
},
|
||||||
gridCell: {
|
gridCell: {
|
||||||
|
|||||||
@@ -45,17 +45,7 @@ interface SyncScheduleResponse {
|
|||||||
error_message?: string;
|
error_message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toCourse = (dto: ScheduleCourseDTO): Course => {
|
const toCourse = (dto: ScheduleCourseDTO): Course => ({
|
||||||
// 调试日志:检查后端返回的 weeks 字段
|
|
||||||
console.log('[ScheduleService] 转换课程 DTO:', {
|
|
||||||
id: dto.id,
|
|
||||||
name: dto.name,
|
|
||||||
day_of_week: dto.day_of_week,
|
|
||||||
weeks: dto.weeks,
|
|
||||||
weeksType: typeof dto.weeks,
|
|
||||||
weeksLength: dto.weeks?.length,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
teacher: dto.teacher,
|
teacher: dto.teacher,
|
||||||
@@ -65,22 +55,12 @@ const toCourse = (dto: ScheduleCourseDTO): Course => {
|
|||||||
endSection: dto.end_section,
|
endSection: dto.end_section,
|
||||||
weeks: dto.weeks ?? [],
|
weeks: dto.weeks ?? [],
|
||||||
color: dto.color,
|
color: dto.color,
|
||||||
};
|
});
|
||||||
};
|
|
||||||
|
|
||||||
class ScheduleService {
|
class ScheduleService {
|
||||||
async getCourses(week?: number): Promise<Course[]> {
|
async getCourses(week?: number): Promise<Course[]> {
|
||||||
const params = week ? { week } : undefined;
|
const params = week ? { week } : undefined;
|
||||||
console.log('[ScheduleService] 获取课程列表, params:', params);
|
|
||||||
const response = await api.get<ScheduleCourseListResponse>('/schedule/courses', params);
|
const response = await api.get<ScheduleCourseListResponse>('/schedule/courses', params);
|
||||||
console.log('[ScheduleService] 课程列表响应, 数量:', response.data.list.length);
|
|
||||||
// 打印原始响应数据中的 weeks 字段
|
|
||||||
response.data.list.forEach((item, index) => {
|
|
||||||
console.log(`[ScheduleService] 课程[${index}] ${item.name}:`, {
|
|
||||||
day_of_week: item.day_of_week,
|
|
||||||
weeks: item.weeks,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return response.data.list.map(toCourse);
|
return response.data.list.map(toCourse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +79,7 @@ class ScheduleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async syncSchedule(req: SyncScheduleRequest): Promise<SyncScheduleResponse> {
|
async syncSchedule(req: SyncScheduleRequest): Promise<SyncScheduleResponse> {
|
||||||
console.log('[ScheduleService] 开始同步教务系统, username:', req.username);
|
|
||||||
const response = await api.post<SyncScheduleResponse>('/schedule/sync', req);
|
const response = await api.post<SyncScheduleResponse>('/schedule/sync', req);
|
||||||
console.log('[ScheduleService] 同步响应:', response.data);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user