Initial commit

ptngqz2ua 3 months ago committed by 赵骏浩
commit 10fc7bd870

15
src/.gitignore vendored

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml

@ -0,0 +1 @@
My Application

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\20922\.android\avd\Pixel_6_Pro_API_33.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2025-09-29T07:20:17.026786700Z" />
</component>
</project>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

@ -0,0 +1,132 @@
# Android 大学助手 应用
## 项目简介
本项目是一个基于 Android 的示例应用,包名为 `com.example.myapplication`,采用 Java 编写,使用 Gradle 进行构建。
项目中集成了以下常用功能依赖:
- **AndroidX 基础组件**`appcompat`、`material`、`coordinatorlayout`、`recyclerview` 等
- **Gson**:用于 JSON 数据解析
- **OkHttp / Retrofit + Gson Converter**:用于网络请求与接口数据封装
- **Jsoup**:用于 HTML 解析(例如成绩数据解析等场景)
- **高德地图 SDK本地 JAR**:用于地图展示、位置相关功能
> 说明:具体业务逻辑和界面功能请参考 `app/src/main/java/com/example/myapplication/` 下的源码。
---
## 技术栈 & 环境要求
- **语言**Java
- **构建工具**Gradle / Android Gradle Plugin 7.4.2
- **最低支持系统版本**`minSdk = 24`
- **目标 / 编译版本**`targetSdk = 33``compileSdk = 33`
- **开发工具**Android Studio推荐使用与 Gradle 插件版本兼容的版本)
- **JDK 版本**Java 8`sourceCompatibility` / `targetCompatibility` 为 1.8
---
## 快速开始
1. **克隆项目**(若在 Git 仓库中):
```bash
git clone <your-repo-url>
cd demo
```
2. **使用 Android Studio 打开**
- 打开 Android Studio
- 选择 *Open an Existing Project*
- 选择本项目根目录(包含 `build.gradle`、`settings.gradle` 的目录)
3. **配置 Android SDK 路径**
- 首次打开时,如果 `local.properties` 不存在或路径不正确,可在 Android Studio 中设置 SDK 路径,或手动编辑 `local.properties`
```properties
sdk.dir=你的_Android_SDK_路径
```
4. **同步 & 构建项目**
- 在 Android Studio 中点击 *Sync Project with Gradle Files*
- 等待依赖下载、构建完成
5. **运行应用**
- 连接真机或启动模拟器API 24 及以上)
- 选择 `app` 模块,点击 *Run* 按钮即可安装运行
---
## 高德地图 SDK 说明
- 项目中通过本地 JAR 的方式集成了高德地图相关 SDK
- JAR 路径配置于 `app/build.gradle` 中的:
```gradle
implementation files('libs/AMap3DMap_10.1.500_AMapSearch_9.7.4_AMapLocation_6.5.0_20250814.jar')
```
- 若要正常使用高德地图功能,请:
- 前往 **高德开放平台** 申请 Android Key
- 按照高德官方文档在 `AndroidManifest.xml` 中配置 API Key 相关信息;
- 确保本地 JAR 文件已放置在 `app/libs/` 目录下(与 `build.gradle` 中路径一致)。
> 如需升级或调整 SDK 版本,请同步更新 `libs` 目录中的 JAR 文件以及对应的 Gradle 配置。
---
## 主要依赖
`app/build.gradle` 中关键依赖包括:
- **UI & 基础组件**
- `androidx.appcompat:appcompat:1.4.1`
- `com.google.android.material:material:1.5.0`
- `androidx.coordinatorlayout:coordinatorlayout:1.2.0`
- `androidx.recyclerview:recyclerview:1.2.1`
- **数据解析**
- `com.google.code.gson:gson:2.8.9`
- `org.jsoup:jsoup:1.15.3`
- **网络请求**
- `com.squareup.okhttp3:okhttp:4.9.3`
- `com.squareup.retrofit2:retrofit:2.9.0`
- `com.squareup.retrofit2:converter-gson:2.9.0`
- **测试**
- `junit:junit:4.13.2`
- `androidx.test.ext:junit:1.1.3`
- `androidx.test.espresso:espresso-core:3.4.0`
---
## 目录结构(简要)
- `app/`
- `src/main/java/com/example/myapplication/`主要业务代码Activity、数据管理、网络请求等
- `src/main/res/`:布局、资源文件
- `build.gradle`:应用模块构建配置
- `AMap_Android_SDK_All/`:高德 SDK 相关文件(如有)
- `temp_sdk/`:临时存放的 SDK / 相关文件(如有)
- `build.gradle`:项目根构建配置(包含 Android Gradle 插件、SonarQube 等)
- `settings.gradle`:模块配置
- `gradle/`、`gradlew`、`gradlew.bat`Gradle Wrapper 相关文件
---
## 代码质量(可选)
项目根 `build.gradle` 中集成了 SonarQube 插件,用于代码质量分析。若需要在本地或 CI 中使用:
1. 搭建或连接到可用的 SonarQube 服务器;
2. 在本地或 CI 环境中配置好 SonarQube 的地址和认证信息;
3. 使用 Gradle 任务执行代码扫描(如 `sonarqube` 任务)。
> 注意:在对外开源或提交到公共仓库时,请勿提交真实的访问 Token 或其他敏感信息。
---
## TODO / 后续规划
- 根据实际业务补充更详细的功能说明和截图
- 编写接口文档、数据结构说明
- 完善单元测试与 UI 测试
- 若需要,对高德地图和网络模块进行封装与抽象,提升可维护性

@ -0,0 +1 @@
/build

@ -0,0 +1,53 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.example.myapplication'
compileSdk 33
defaultConfig {
applicationId "com.example.myapplication"
minSdk 24
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'com.google.code.gson:gson:2.8.9'
// SDK - JAR
implementation files('libs/AMap3DMap_10.1.500_AMapSearch_9.7.4_AMapLocation_6.5.0_20250814.jar')
// POI
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Jsoup - HTML
implementation 'org.jsoup:jsoup:1.15.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

@ -0,0 +1,26 @@
package com.example.myapplication;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.example.myapplication", appContext.getPackageName());
}
}

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 高德地图SDK核心权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!-- <uses-permission android:name="android.permission.WRITE_SETTINGS" /> 注释掉:此权限仅系统应用可用 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 高德地图导航相关权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!-- 课程提醒相关权限 -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<!-- 笔记图片相关权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication"
android:hardwareAccelerated="true"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<!-- 高德地图API Key -->
<meta-data
android:name="com.amap.api.v2.apikey"
android:value="0c0a6f3a57e257c8bf0d9c8202110a37" />
<!-- 高德地图服务配置 -->
<service android:name="com.amap.api.location.APSService"></service>
<activity
android:name=".HomeActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="false"
android:hardwareAccelerated="true"
android:configChanges="orientation|screenSize|keyboardHidden" />
<activity
android:name=".LocationDetailsActivity"
android:exported="false" />
<!-- 教务系统登录WebView -->
<activity
android:name=".WebViewActivity"
android:exported="false"
android:label="教务系统登录"
android:hardwareAccelerated="true"
android:configChanges="orientation|screenSize|keyboardHidden" />
<!-- 教务系统课表导入 -->
<activity
android:name=".EducationImportActivity"
android:exported="false"
android:label="教务系统课表导入"
android:hardwareAccelerated="true"
android:configChanges="orientation|screenSize|keyboardHidden" />
<!-- 课程提醒全屏弹窗 -->
<activity
android:name=".ReminderAlertActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Dialog.Alert"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" />
<!-- 课程提醒弹窗(微信样式) -->
<activity
android:name=".CourseReminderActivity"
android:exported="false"
android:theme="@style/ReminderDialogTheme"
android:excludeFromRecents="true"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:launchMode="singleTop" />
<!-- 全屏笔记编辑器 -->
<activity
android:name=".NoteEditorActivity"
android:exported="false"
android:label="编辑笔记"
android:hardwareAccelerated="true"
android:configChanges="orientation|screenSize|keyboardHidden" />
<!-- 笔记阅读模式 -->
<activity
android:name=".NoteReaderActivity"
android:exported="false"
android:label="阅读笔记"
android:hardwareAccelerated="true"
android:configChanges="orientation|screenSize|keyboardHidden" />
<!-- FileProvider for sharing files -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- 课程提醒接收器 -->
<receiver
android:name=".CourseReminderReceiver"
android:enabled="true"
android:exported="false" />
<receiver
android:name=".ReminderReceiver"
android:exported="false" />
</application>
</manifest>

@ -0,0 +1,87 @@
package com.example.myapplication;
/**
*
*/
public class AmapLocation {
private String name;
private String description;
private String category;
private String area;
private double latitude; // 纬度
private double longitude; // 经度
public AmapLocation(String name, String description, String category, String area, double latitude, double longitude) {
this.name = name;
this.description = description;
this.category = category;
this.area = area;
this.latitude = latitude;
this.longitude = longitude;
}
public AmapLocation(String name, String description, String category, double latitude, double longitude) {
this(name, description, category, "", latitude, longitude);
}
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getArea() { return area; }
public void setArea(String area) { this.area = area; }
public double getLatitude() { return latitude; }
public void setLatitude(double latitude) { this.latitude = latitude; }
public double getLongitude() { return longitude; }
public void setLongitude(double longitude) { this.longitude = longitude; }
/**
*
*/
public double calculateDistance(AmapLocation destination) {
if (destination == null) return 0;
double lat1Rad = Math.toRadians(this.latitude);
double lat2Rad = Math.toRadians(destination.latitude);
double deltaLatRad = Math.toRadians(destination.latitude - this.latitude);
double deltaLngRad = Math.toRadians(destination.longitude - this.longitude);
double a = Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.sin(deltaLngRad / 2) * Math.sin(deltaLngRad / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return 6371000 * c; // 地球半径 * 弧度 = 距离(米)
}
/**
*
*/
public int calculateWalkTime(AmapLocation destination) {
double distance = calculateDistance(destination);
return (int) Math.ceil(distance / 80); // 按照每分钟80米的步行速度计算
}
@Override
public String toString() {
return name + (area.isEmpty() ? "" : " (" + area + ")");
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
AmapLocation location = (AmapLocation) obj;
return name.equals(location.name) &&
Math.abs(latitude - location.latitude) < 0.0001 &&
Math.abs(longitude - location.longitude) < 0.0001;
}
}

@ -0,0 +1,305 @@
package com.example.myapplication;
import android.content.Context;
import android.content.SharedPreferences;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
*
*/
public class CampusLocationManager {
private static final String PREF_NAME = "campus_locations";
private static final String KEY_LOCATIONS = "locations";
private static final String KEY_FAVORITES = "favorites";
private Context context;
private List<AmapLocation> allLocations;
private Map<String, List<AmapLocation>> categorizedLocations;
private List<String> favoriteLocationNames;
private SharedPreferences preferences;
private Gson gson;
public CampusLocationManager(Context context) {
this.context = context;
this.preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
this.gson = new Gson();
this.categorizedLocations = new HashMap<>();
this.favoriteLocationNames = new ArrayList<>();
initializeDefaultLocations();
loadLocationsFromPreferences();
loadFavoritesFromPreferences();
}
private void initializeDefaultLocations() {
allLocations = new ArrayList<>();
// 图书馆类
allLocations.add(new AmapLocation("主图书馆", "学校主图书馆藏书丰富24小时开放", "图书馆", "图书馆大楼", 39.906016, 116.395312));
allLocations.add(new AmapLocation("理工图书馆", "理工学科专业图书馆", "图书馆", "理工楼", 39.907123, 116.396234));
allLocations.add(new AmapLocation("电子阅览室", "电子资源阅览区", "图书馆", "图书馆2层", 39.906116, 116.395412));
// 食堂类
allLocations.add(new AmapLocation("第一食堂", "学校第一食堂,菜品丰富,价格实惠", "食堂", "学生服务中心", 39.905123, 116.394567));
allLocations.add(new AmapLocation("第二食堂", "学校第二食堂,特色小吃多样", "食堂", "学生活动中心", 39.907234, 116.396789));
allLocations.add(new AmapLocation("清真食堂", "清真餐厅,民族特色菜品", "食堂", "民族楼", 39.906234, 116.395678));
allLocations.add(new AmapLocation("教工食堂", "教职工专用餐厅", "食堂", "行政楼附近", 39.908123, 116.397456));
// 宿舍类
allLocations.add(new AmapLocation("男生1号楼", "男生宿舍区1号楼4人间", "宿舍", "男生宿舍区", 39.904567, 116.393456));
allLocations.add(new AmapLocation("男生2号楼", "男生宿舍区2号楼6人间", "宿舍", "男生宿舍区", 39.904667, 116.393556));
allLocations.add(new AmapLocation("女生1号楼", "女生宿舍区1号楼4人间", "宿舍", "女生宿舍区", 39.908901, 116.398012));
allLocations.add(new AmapLocation("女生2号楼", "女生宿舍区2号楼6人间", "宿舍", "女生宿舍区", 39.909001, 116.398112));
allLocations.add(new AmapLocation("研究生公寓", "研究生专用公寓", "宿舍", "研究生区", 39.903456, 116.391234));
// 教学楼类
allLocations.add(new AmapLocation("教学楼A", "主要教学楼A座大型教室", "教学楼", "教学区", 39.906789, 116.395678));
allLocations.add(new AmapLocation("教学楼B", "主要教学楼B座小班教室", "教学楼", "教学区", 39.907123, 116.396234));
allLocations.add(new AmapLocation("教学楼C", "主要教学楼C座多媒体教室", "教学楼", "教学区", 39.905678, 116.394789));
allLocations.add(new AmapLocation("理工楼", "理工学科专业教学楼", "教学楼", "理工区", 39.907456, 116.396567));
allLocations.add(new AmapLocation("文科楼", "文科专业教学楼", "教学楼", "文科区", 39.905234, 116.394123));
// 实验楼类
allLocations.add(new AmapLocation("计算机实验楼", "计算机学院实验室", "实验楼", "实验区", 39.908456, 116.397345));
allLocations.add(new AmapLocation("物理实验楼", "物理学院实验室", "实验楼", "实验区", 39.908556, 116.397445));
allLocations.add(new AmapLocation("化学实验楼", "化学学院实验室", "实验楼", "实验区", 39.908356, 116.397245));
allLocations.add(new AmapLocation("生物实验楼", "生物学院实验室", "实验楼", "实验区", 39.908656, 116.397545));
// 体育设施类
allLocations.add(new AmapLocation("体育馆", "室内体育馆,篮球、羽毛球等", "体育设施", "体育区", 39.904123, 116.392678));
allLocations.add(new AmapLocation("田径场", "标准400米田径场", "体育设施", "体育区", 39.903789, 116.391234));
allLocations.add(new AmapLocation("游泳馆", "室内游泳池", "体育设施", "体育区", 39.904223, 116.392778));
allLocations.add(new AmapLocation("网球场", "室外网球场地", "体育设施", "体育区", 39.903889, 116.391334));
// 行政办公类
allLocations.add(new AmapLocation("行政楼", "学校行政办公楼", "行政办公", "行政区", 39.909012, 116.399123));
allLocations.add(new AmapLocation("教务处", "教务管理办公室", "行政办公", "行政楼2层", 39.909112, 116.399223));
allLocations.add(new AmapLocation("学生处", "学生事务管理办公室", "行政办公", "行政楼3层", 39.909212, 116.399323));
allLocations.add(new AmapLocation("财务处", "财务管理办公室", "行政办公", "行政楼1层", 39.909012, 116.399123));
// 生活服务类
allLocations.add(new AmapLocation("超市", "校内便民超市", "生活服务", "学生活动中心", 39.906123, 116.395456));
allLocations.add(new AmapLocation("银行ATM", "校内银行ATM机", "生活服务", "学生服务中心", 39.905223, 116.394556));
allLocations.add(new AmapLocation("邮局", "校内邮政服务点", "生活服务", "学生服务中心", 39.905323, 116.394656));
allLocations.add(new AmapLocation("医务室", "校医院门诊部", "生活服务", "医务楼", 39.907789, 116.396123));
allLocations.add(new AmapLocation("理发店", "校内理发服务", "生活服务", "学生活动中心", 39.906223, 116.395556));
// 休闲娱乐类
allLocations.add(new AmapLocation("咖啡厅", "校内咖啡休息区", "休闲娱乐", "学生活动中心", 39.906323, 116.395656));
allLocations.add(new AmapLocation("音乐厅", "学校音乐演出厅", "休闲娱乐", "艺术楼", 39.907890, 116.396789));
allLocations.add(new AmapLocation("电影院", "校内小型电影院", "休闲娱乐", "学生活动中心3层", 39.906423, 116.395756));
allLocations.add(new AmapLocation("湖心亭", "校园湖心休息亭", "休闲娱乐", "校园湖中央", 39.905890, 116.394789));
// 重新分类整理
categorizeLocations();
}
private void categorizeLocations() {
categorizedLocations.clear();
for (AmapLocation location : allLocations) {
String category = location.getCategory();
if (!categorizedLocations.containsKey(category)) {
categorizedLocations.put(category, new ArrayList<>());
}
categorizedLocations.get(category).add(location);
}
}
/**
*
*/
public List<AmapLocation> getAllLocations() {
return new ArrayList<>(allLocations);
}
/**
*
*/
public List<AmapLocation> getLocationsByCategory(String category) {
List<AmapLocation> result = categorizedLocations.get(category);
return result != null ? new ArrayList<>(result) : new ArrayList<>();
}
/**
*
*/
public List<String> getAllCategories() {
return new ArrayList<>(categorizedLocations.keySet());
}
/**
*
*/
public List<AmapLocation> searchLocations(String keyword) {
List<AmapLocation> results = new ArrayList<>();
String lowerKeyword = keyword.toLowerCase();
for (AmapLocation location : allLocations) {
if (location.getName().toLowerCase().contains(lowerKeyword) ||
location.getDescription().toLowerCase().contains(lowerKeyword) ||
location.getCategory().toLowerCase().contains(lowerKeyword) ||
location.getArea().toLowerCase().contains(lowerKeyword)) {
results.add(location);
}
}
return results;
}
/**
*
*/
public List<AmapLocation> getNearbyLocations(AmapLocation centerLocation, double radiusKm) {
List<AmapLocation> nearbyLocations = new ArrayList<>();
for (AmapLocation location : allLocations) {
double distance = centerLocation.calculateDistance(location) / 1000.0; // 转换为公里
if (distance <= radiusKm) {
nearbyLocations.add(location);
}
}
// 按距离排序
nearbyLocations.sort((l1, l2) -> {
double d1 = centerLocation.calculateDistance(l1);
double d2 = centerLocation.calculateDistance(l2);
return Double.compare(d1, d2);
});
return nearbyLocations;
}
/**
*
*/
public void addCustomLocation(AmapLocation location) {
allLocations.add(location);
categorizeLocations();
saveLocationsToPreferences();
}
/**
*
*/
public void removeCustomLocation(AmapLocation location) {
allLocations.remove(location);
categorizeLocations();
saveLocationsToPreferences();
}
/**
*
*/
public void addToFavorites(String locationName) {
if (!favoriteLocationNames.contains(locationName)) {
favoriteLocationNames.add(locationName);
saveFavoritesToPreferences();
}
}
/**
*
*/
public void removeFromFavorites(String locationName) {
favoriteLocationNames.remove(locationName);
saveFavoritesToPreferences();
}
/**
*
*/
public List<AmapLocation> getFavoriteLocations() {
List<AmapLocation> favorites = new ArrayList<>();
for (String name : favoriteLocationNames) {
for (AmapLocation location : allLocations) {
if (location.getName().equals(name)) {
favorites.add(location);
break;
}
}
}
return favorites;
}
/**
*
*/
public boolean isFavorite(String locationName) {
return favoriteLocationNames.contains(locationName);
}
/**
*
*/
public AmapLocation findLocationByName(String name) {
for (AmapLocation location : allLocations) {
if (location.getName().equals(name)) {
return location;
}
}
return null;
}
/**
*
*/
public List<AmapLocation> getPopularLocations(int limit) {
List<AmapLocation> popular = new ArrayList<>();
// 简单的热门度算法:按分类中位置数量排序,取每个分类的第一个
for (String category : categorizedLocations.keySet()) {
List<AmapLocation> categoryList = categorizedLocations.get(category);
if (!categoryList.isEmpty()) {
popular.add(categoryList.get(0));
}
}
return popular.subList(0, Math.min(limit, popular.size()));
}
// 本地存储相关方法
private void saveLocationsToPreferences() {
String json = gson.toJson(allLocations);
preferences.edit().putString(KEY_LOCATIONS, json).apply();
}
private void loadLocationsFromPreferences() {
String json = preferences.getString(KEY_LOCATIONS, "");
if (!json.isEmpty()) {
Type listType = new TypeToken<List<AmapLocation>>(){}.getType();
List<AmapLocation> savedLocations = gson.fromJson(json, listType);
if (savedLocations != null) {
allLocations.addAll(savedLocations);
categorizeLocations();
}
} else {
categorizeLocations();
}
}
private void saveFavoritesToPreferences() {
String json = gson.toJson(favoriteLocationNames);
preferences.edit().putString(KEY_FAVORITES, json).apply();
}
private void loadFavoritesFromPreferences() {
String json = preferences.getString(KEY_FAVORITES, "");
if (!json.isEmpty()) {
Type listType = new TypeToken<List<String>>(){}.getType();
List<String> savedFavorites = gson.fromJson(json, listType);
if (savedFavorites != null) {
favoriteLocationNames = savedFavorites;
}
}
}
}

@ -0,0 +1,201 @@
package com.example.myapplication;
import java.io.Serializable;
public class Course implements Serializable {
private String id;
private String name; // 课程名称
private String teacher; // 任课教师
private String location; // 上课地点
private String time; // 上课时间 (格式:周几 时间段)
private int dayOfWeek; // 星期几 (1-7)
private int startPeriod; // 开始节次(兼容旧数据)
private int endPeriod; // 结束节次(兼容旧数据)
private int timeSlot; // 时间段索引 (0-5)
private String semester; // 学期
private int startWeek; // 开始周次
private int endWeek; // 结束周次
private boolean reminderEnabled; // 是否启用提醒
private int reminderMinutes; // 提前提醒分钟数
private int reminderAdvanceSeconds; // 提前提醒秒数
private boolean isImported; // 是否是从教务系统导入的课程
public Course() {
this.id = String.valueOf(System.currentTimeMillis());
this.reminderEnabled = false;
this.reminderMinutes = 10;
this.reminderAdvanceSeconds = this.reminderMinutes * 60;
this.timeSlot = -1; // 默认未设置
this.startWeek = 1;
this.endWeek = 20;
this.isImported = false; // 默认手动添加
}
public Course(String name, String teacher, String location, int dayOfWeek,
int startPeriod, int endPeriod) {
this();
this.name = name;
this.teacher = teacher;
this.location = location;
this.dayOfWeek = dayOfWeek;
this.startPeriod = startPeriod;
this.endPeriod = endPeriod;
this.time = formatTime();
}
public Course(String name, String teacher, String location, int dayOfWeek, int timeSlot) {
this();
this.name = name;
this.teacher = teacher;
this.location = location;
this.dayOfWeek = dayOfWeek;
this.timeSlot = timeSlot;
this.startPeriod = timeSlotToPeriod(timeSlot);
this.endPeriod = this.startPeriod + 1; // 占据两节连续
this.time = formatTimeSlot();
}
private String formatTime() {
String[] days = {"", "周一", "周二", "周三", "周四", "周五", "周六", "周日"};
return days[dayOfWeek] + " 第" + startPeriod + "-" + endPeriod + "节";
}
private String formatTimeSlot() {
String[] days = {"", "周一", "周二", "周三", "周四", "周五", "周六", "周日"};
String[] timeSlots = {
"08:00-11:30",
"13:00-16:30",
"17:00-20:30",
"21:00-24:30",
"待定"
};
if (timeSlot >= 0 && timeSlot < timeSlots.length) {
return days[dayOfWeek] + " " + timeSlots[timeSlot];
}
return days[dayOfWeek] + " 第" + startPeriod + "-" + endPeriod + "节";
}
private int timeSlotToPeriod(int slot) {
// 时间段0对应第1-2节时间段1对应第3-4节时间段2对应第5-6节
// 时间段3对应第7-8节时间段4对应第9-10节
if (slot == 0) return 1; // 第1-2节
if (slot == 1) return 3; // 第3-4节
if (slot == 2) return 5; // 第5-6节
if (slot == 3) return 7; // 第7-8节
if (slot == 4) return 9; // 第9-10节
return 1;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) {
this.name = name;
if (timeSlot >= 0) {
this.time = formatTimeSlot();
} else {
this.time = formatTime();
}
}
public String getTeacher() { return teacher; }
public void setTeacher(String teacher) { this.teacher = teacher; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getTime() { return time; }
public void setTime(String time) { this.time = time; }
public int getDayOfWeek() { return dayOfWeek; }
public void setDayOfWeek(int dayOfWeek) {
this.dayOfWeek = dayOfWeek;
if (timeSlot >= 0) {
this.time = formatTimeSlot();
} else {
this.time = formatTime();
}
}
public int getStartPeriod() { return startPeriod; }
public void setStartPeriod(int startPeriod) {
this.startPeriod = startPeriod;
if (timeSlot >= 0) {
this.time = formatTimeSlot();
} else {
this.time = formatTime();
}
}
public int getEndPeriod() { return endPeriod; }
public void setEndPeriod(int endPeriod) {
this.endPeriod = endPeriod;
if (timeSlot >= 0) {
this.time = formatTimeSlot();
} else {
this.time = formatTime();
}
}
public int getTimeSlot() { return timeSlot; }
public void setTimeSlot(int timeSlot) {
this.timeSlot = timeSlot;
this.time = formatTimeSlot();
}
public String getSemester() { return semester; }
public void setSemester(String semester) { this.semester = semester; }
public boolean isReminderEnabled() { return reminderEnabled; }
public void setReminderEnabled(boolean reminderEnabled) { this.reminderEnabled = reminderEnabled; }
public int getReminderMinutes() {
if (reminderMinutes <= 0 && reminderAdvanceSeconds > 0) {
return reminderAdvanceSeconds / 60;
}
return reminderMinutes;
}
public void setReminderMinutes(int reminderMinutes) {
this.reminderMinutes = reminderMinutes;
if (reminderMinutes > 0) {
this.reminderAdvanceSeconds = reminderMinutes * 60;
}
}
public int getReminderAdvanceSeconds() {
if (reminderAdvanceSeconds <= 0 && reminderMinutes > 0) {
reminderAdvanceSeconds = reminderMinutes * 60;
}
return reminderAdvanceSeconds;
}
public void setReminderAdvanceSeconds(int reminderAdvanceSeconds) {
this.reminderAdvanceSeconds = reminderAdvanceSeconds;
if (reminderAdvanceSeconds >= 60) {
this.reminderMinutes = reminderAdvanceSeconds / 60;
} else if (reminderAdvanceSeconds > 0) {
this.reminderMinutes = 0;
}
}
public int getStartWeek() { return startWeek; }
public void setStartWeek(int startWeek) { this.startWeek = startWeek; }
public int getEndWeek() { return endWeek; }
public void setEndWeek(int endWeek) { this.endWeek = endWeek; }
public boolean isImported() { return isImported; }
public void setImported(boolean imported) { this.isImported = imported; }
@Override
public String toString() {
String nameStr = name != null ? name : "未知课程";
String timeStr = time != null ? time : "";
String locationStr = location != null ? location : "";
String timeLocationStr = (timeStr.isEmpty() && locationStr.isEmpty()) ? "" :
(" (" + timeStr + (timeStr.isEmpty() || locationStr.isEmpty() ? "" : " ") + locationStr + ")");
return nameStr + timeLocationStr;
}
}

@ -0,0 +1,176 @@
package com.example.myapplication;
import android.app.Activity;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
public class CourseReminderActivity extends Activity {
private MediaPlayer mediaPlayer;
private Vibrator vibrator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置为对话框样式
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_course_reminder);
// 设置窗口属性
Window window = getWindow();
// 允许在锁屏上显示
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true);
setTurnScreenOn(true);
android.app.KeyguardManager keyguardManager = (android.app.KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
if (keyguardManager != null) {
keyguardManager.requestDismissKeyguard(this, null);
}
} else {
window.addFlags(
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
);
}
// 设置窗口在最顶层
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// 设置窗口类型Android 8.0+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
} else {
window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
}
// 获取课程信息
String courseName = getIntent().getStringExtra("course_name");
String courseLocation = getIntent().getStringExtra("course_location");
String courseTime = getIntent().getStringExtra("course_time");
int reminderMinutes = getIntent().getIntExtra("reminder_minutes", 10);
// 显示信息
TextView tvCourseName = findViewById(R.id.tv_course_name);
TextView tvCourseInfo = findViewById(R.id.tv_course_info);
TextView tvReminderTime = findViewById(R.id.tv_reminder_time);
tvCourseName.setText(courseName);
tvCourseInfo.setText("地点:" + courseLocation + "\n时间" + courseTime);
tvReminderTime.setText(reminderMinutes + "分钟后上课");
// 设置按钮点击事件
findViewById(R.id.btn_confirm).setOnClickListener(v -> {
stopSoundAndVibration();
finish();
});
findViewById(R.id.btn_close).setOnClickListener(v -> {
stopSoundAndVibration();
finish();
});
// 播放提示音和震动
playNotificationSound();
vibrate();
}
/**
*
*/
private void playNotificationSound() {
try {
// 使用系统默认通知音
Uri notificationUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(this, notificationUri);
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
mediaPlayer.setAudioAttributes(audioAttributes);
mediaPlayer.setLooping(false);
mediaPlayer.prepare();
mediaPlayer.start();
// 播放完成后释放资源
mediaPlayer.setOnCompletionListener(mp -> {
if (mediaPlayer != null) {
mediaPlayer.release();
mediaPlayer = null;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*
*/
private void vibrate() {
try {
vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null && vibrator.hasVibrator()) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
// 震动模式:等待-震动-等待-震动
long[] pattern = {0, 300, 200, 300};
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
vibrator.vibrate(effect);
} else {
// 旧版本API
long[] pattern = {0, 300, 200, 300};
vibrator.vibrate(pattern, -1);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*
*/
private void stopSoundAndVibration() {
if (mediaPlayer != null) {
if (mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
mediaPlayer.release();
mediaPlayer = null;
}
if (vibrator != null) {
vibrator.cancel();
vibrator = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
stopSoundAndVibration();
}
@Override
public void onBackPressed() {
stopSoundAndVibration();
super.onBackPressed();
}
}

@ -0,0 +1,171 @@
package com.example.myapplication;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import java.util.Calendar;
import java.util.List;
public class CourseReminderManager {
private static final String TAG = "CourseReminderManager";
private Context context;
private AlarmManager alarmManager;
private DataManager dataManager;
public CourseReminderManager(Context context) {
this.context = context;
this.alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
this.dataManager = new DataManager(context);
}
/**
*
*/
public void setReminder(Course course) {
if (!course.isReminderEnabled()) {
cancelReminder(course);
return;
}
// 获取下次上课时间
Calendar nextClassTime = getNextClassTime(course);
if (nextClassTime == null) {
Log.w(TAG, "无法计算下次上课时间");
return;
}
// 减去提前提醒时间
nextClassTime.add(Calendar.MINUTE, -course.getReminderMinutes());
// 如果时间已过,设置下周的提醒
if (nextClassTime.getTimeInMillis() <= System.currentTimeMillis()) {
nextClassTime.add(Calendar.WEEK_OF_YEAR, 1);
}
Intent intent = new Intent(context, CourseReminderReceiver.class);
intent.putExtra("course_id", course.getId());
intent.putExtra("course_name", course.getName());
intent.putExtra("course_location", course.getLocation());
intent.putExtra("course_time", course.getTime());
intent.putExtra("reminder_minutes", course.getReminderMinutes());
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
course.getId().hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 设置重复提醒(每周)
if (alarmManager != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
nextClassTime.getTimeInMillis(),
pendingIntent
);
} else {
alarmManager.setRepeating(
AlarmManager.RTC_WAKEUP,
nextClassTime.getTimeInMillis(),
AlarmManager.INTERVAL_DAY * 7,
pendingIntent
);
}
Log.d(TAG, "设置提醒: " + course.getName() + " 在 " + nextClassTime.getTime());
}
}
/**
*
*/
public void cancelReminder(Course course) {
Intent intent = new Intent(context, CourseReminderReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
course.getId().hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
if (alarmManager != null) {
alarmManager.cancel(pendingIntent);
Log.d(TAG, "取消提醒: " + course.getName());
}
}
/**
*
*/
public void refreshAllReminders() {
List<Course> courses = dataManager.getCourses();
for (Course course : courses) {
if (course.isReminderEnabled()) {
setReminder(course);
}
}
}
/**
*
*/
private Calendar getNextClassTime(Course course) {
Calendar calendar = Calendar.getInstance();
int currentDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
// 转换Calendar.SUNDAY=1, 我们的周日=7
int targetDayOfWeek = course.getDayOfWeek() == 7 ? Calendar.SUNDAY : course.getDayOfWeek() + 1;
// 计算需要前进的天数
int daysToAdd = targetDayOfWeek - currentDayOfWeek;
if (daysToAdd < 0) {
daysToAdd += 7;
} else if (daysToAdd == 0) {
// 今天,检查时间是否已过
int[] classTime = getClassStartTime(course.getStartPeriod());
calendar.set(Calendar.HOUR_OF_DAY, classTime[0]);
calendar.set(Calendar.MINUTE, classTime[1]);
calendar.set(Calendar.SECOND, 0);
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
daysToAdd = 7; // 下周
}
}
calendar.add(Calendar.DAY_OF_YEAR, daysToAdd);
// 设置上课时间
int[] classTime = getClassStartTime(course.getStartPeriod());
calendar.set(Calendar.HOUR_OF_DAY, classTime[0]);
calendar.set(Calendar.MINUTE, classTime[1]);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar;
}
/**
*
*/
private int[] getClassStartTime(int period) {
switch (period) {
case 1: return new int[]{8, 0};
case 2: return new int[]{8, 50};
case 3: return new int[]{10, 0};
case 4: return new int[]{10, 50};
case 5: return new int[]{14, 0};
case 6: return new int[]{14, 50};
case 7: return new int[]{16, 0};
case 8: return new int[]{16, 50};
case 9: return new int[]{19, 0};
case 10: return new int[]{19, 50};
case 11: return new int[]{20, 40};
case 12: return new int[]{21, 30};
default: return new int[]{8, 0};
}
}
}

@ -0,0 +1,137 @@
package com.example.myapplication;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.core.app.NotificationCompat;
public class CourseReminderReceiver extends BroadcastReceiver {
private static final String CHANNEL_ID = "course_reminder_channel";
private static final String CHANNEL_NAME = "课程提醒";
@Override
public void onReceive(Context context, Intent intent) {
String courseId = intent.getStringExtra("course_id");
String courseName = intent.getStringExtra("course_name");
String courseLocation = intent.getStringExtra("course_location");
String courseTime = intent.getStringExtra("course_time");
int reminderMinutes = intent.getIntExtra("reminder_minutes", 10);
// 创建通知渠道
createNotificationChannel(context);
// 1. 显示自定义弹窗(确保可见)
showReminderDialog(context, courseName, courseLocation, courseTime, reminderMinutes);
// 2. 同时显示系统通知(备用)
showNotification(context, courseId, courseName, courseLocation, courseTime, reminderMinutes);
// 重新设置下次提醒
DataManager dataManager = new DataManager(context);
for (Course course : dataManager.getCourses()) {
if (course.getId().equals(courseId) && course.isReminderEnabled()) {
CourseReminderManager reminderManager = new CourseReminderManager(context);
reminderManager.setReminder(course);
break;
}
}
}
/**
*
*/
private void showReminderDialog(Context context, String courseName, String courseLocation,
String courseTime, int reminderMinutes) {
Intent dialogIntent = new Intent(context, CourseReminderActivity.class);
dialogIntent.putExtra("course_name", courseName);
dialogIntent.putExtra("course_location", courseLocation);
dialogIntent.putExtra("course_time", courseTime);
dialogIntent.putExtra("reminder_minutes", reminderMinutes);
dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
try {
context.startActivity(dialogIntent);
} catch (Exception e) {
e.printStackTrace();
// 如果弹窗启动失败,至少有通知
}
}
private void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH // 高优先级
);
channel.setDescription("课程上课提醒通知");
channel.enableVibration(true);
channel.setVibrationPattern(new long[]{0, 300, 200, 300});
channel.enableLights(true);
channel.setLightColor(android.graphics.Color.GREEN);
channel.setShowBadge(true);
channel.setLockscreenVisibility(android.app.Notification.VISIBILITY_PUBLIC);
channel.setBypassDnd(true); // 绕过勿扰模式
channel.setSound(
android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_NOTIFICATION),
new android.media.AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_NOTIFICATION)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
);
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
}
private void showNotification(Context context, String courseId, String courseName,
String courseLocation, String courseTime, int reminderMinutes) {
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra("fragment", "timetable");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(
context,
courseId.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 创建大文本样式
NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle()
.bigText("📍 地点:" + courseLocation + "\n⏰ 时间:" + courseTime + "\n⏱ " + reminderMinutes + "分钟后上课")
.setBigContentTitle("📚 " + courseName);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("📚 课程提醒:" + courseName)
.setContentText(reminderMinutes + "分钟后上课 | " + courseLocation)
.setStyle(bigTextStyle)
.setPriority(NotificationCompat.PRIORITY_MAX) // 最高优先级
.setCategory(NotificationCompat.CATEGORY_ALARM) // 闹钟类别
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // 锁屏可见
.setAutoCancel(true)
.setOngoing(false)
.setContentIntent(pendingIntent)
.setFullScreenIntent(pendingIntent, true) // 全屏Intent重要
.setVibrate(new long[]{0, 300, 200, 300}) // 震动模式
.setLights(android.graphics.Color.GREEN, 1000, 1000) // 呼吸灯
.setSound(android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_NOTIFICATION)) // 提示音
.setDefaults(NotificationCompat.DEFAULT_ALL) // 默认所有效果
.setTimeoutAfter(60000); // 60秒后自动消失
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager != null) {
notificationManager.notify(courseId.hashCode(), builder.build());
}
}
}

@ -0,0 +1,246 @@
package com.example.myapplication;
import android.content.Context;
import android.content.SharedPreferences;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
public class DataManager {
private static final String PREFS_NAME = "UniversityAssistant";
private static final String KEY_COURSES = "courses";
private static final String KEY_NOTES = "notes";
private static final String KEY_GRADES = "grades";
private static final String KEY_STUDENT_NAME = "student_name";
private static final String KEY_OVERALL_GPA = "overall_gpa";
private static final String KEY_TERM_START_DATE = "term_start_date";
private static final TypeToken<List<Course>> COURSE_TYPE_TOKEN = new TypeToken<List<Course>>(){};
private static final TypeToken<List<Note>> NOTE_TYPE_TOKEN = new TypeToken<List<Note>>(){};
private static final TypeToken<List<Grade>> GRADE_TYPE_TOKEN = new TypeToken<List<Grade>>(){};
private SharedPreferences prefs;
private Gson gson;
private Context context;
private UserManager userManager;
public DataManager(Context context) {
this.context = context;
this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
this.gson = new Gson();
this.userManager = UserManager.getInstance(context);
}
/**
* IDkey
*/
private String getUserKey(String baseKey) {
String userId = userManager.getCurrentUserId();
if (userId == null || userId.isEmpty()) {
// 如果没有登录用户使用默认key兼容旧数据
return baseKey;
}
return userId + "_" + baseKey;
}
// 课程管理
public List<Course> getCourses() {
String userKey = getUserKey(KEY_COURSES);
String json = prefs.getString(userKey, "[]");
Type type = COURSE_TYPE_TOKEN.getType();
List<Course> courses = gson.fromJson(json, type);
return courses != null ? courses : new ArrayList<>();
}
public void saveCourses(List<Course> courses) {
String userKey = getUserKey(KEY_COURSES);
String json = gson.toJson(courses);
prefs.edit().putString(userKey, json).apply();
}
public void addCourse(Course course) {
List<Course> courses = getCourses();
courses.add(course);
saveCourses(courses);
}
public void deleteCourse(String courseId) {
if (courseId == null) {
System.out.println("deleteCourse: courseId为null无法删除");
return;
}
List<Course> courses = getCourses();
System.out.println("deleteCourse: 当前课程总数: " + courses.size() + ", 要删除的ID: " + courseId);
List<Course> toRemove = new ArrayList<>();
for (Course course : courses) {
if (course != null) {
String id = course.getId();
if (id != null && courseId.equals(id)) {
toRemove.add(course);
System.out.println("deleteCourse: 找到要删除的课程: " + course.getName() + ", ID: " + id);
}
}
}
if (toRemove.isEmpty()) {
System.out.println("deleteCourse: 警告 - 未找到要删除的课程ID: " + courseId);
// 打印所有课程的ID用于调试
for (Course course : courses) {
if (course != null) {
System.out.println(" 现有课程: " + course.getName() + ", ID: " + course.getId());
}
}
} else {
courses.removeAll(toRemove);
saveCourses(courses);
System.out.println("deleteCourse: 成功删除 " + toRemove.size() + " 门课程,剩余: " + courses.size());
}
}
public void updateCourse(Course updatedCourse) {
if (updatedCourse == null || updatedCourse.getId() == null) return;
List<Course> courses = getCourses();
String updatedId = updatedCourse.getId();
for (int i = 0; i < courses.size(); i++) {
Course course = courses.get(i);
if (course != null && updatedId.equals(course.getId())) {
courses.set(i, updatedCourse);
break;
}
}
saveCourses(courses);
}
/**
*
*/
public void clearAllCourses() {
List<Course> courses = getCourses();
System.out.println("DataManager.clearAllCourses: 清空所有课程数据,共 " + courses.size() + " 门课程");
saveCourses(new ArrayList<>());
System.out.println("DataManager.clearAllCourses: 所有课程数据已清空");
}
// 笔记管理
public List<Note> getNotes() {
String userKey = getUserKey(KEY_NOTES);
String json = prefs.getString(userKey, "[]");
Type type = NOTE_TYPE_TOKEN.getType();
List<Note> notes = gson.fromJson(json, type);
return notes != null ? notes : new ArrayList<>();
}
public void saveNotes(List<Note> notes) {
String userKey = getUserKey(KEY_NOTES);
String json = gson.toJson(notes);
prefs.edit().putString(userKey, json).apply();
}
public void addNote(Note note) {
List<Note> notes = getNotes();
notes.add(note);
saveNotes(notes);
}
public void deleteNote(String noteId) {
if (noteId == null) return;
List<Note> notes = getNotes();
List<Note> toRemove = new ArrayList<>();
for (Note note : notes) {
if (note != null) {
String id = note.getId();
if (id != null && noteId.equals(id)) {
toRemove.add(note);
}
}
}
notes.removeAll(toRemove);
saveNotes(notes);
}
public void updateNote(Note updatedNote) {
if (updatedNote == null || updatedNote.getId() == null) return;
List<Note> notes = getNotes();
String updatedId = updatedNote.getId();
for (int i = 0; i < notes.size(); i++) {
Note note = notes.get(i);
if (note != null && updatedId.equals(note.getId())) {
notes.set(i, updatedNote);
break;
}
}
saveNotes(notes);
}
// 成绩管理
public List<Grade> getGrades() {
String userKey = getUserKey(KEY_GRADES);
String json = prefs.getString(userKey, "[]");
Type type = GRADE_TYPE_TOKEN.getType();
List<Grade> grades = gson.fromJson(json, type);
return grades != null ? grades : new ArrayList<>();
}
public void saveGrades(List<Grade> grades) {
String userKey = getUserKey(KEY_GRADES);
String json = gson.toJson(grades);
prefs.edit().putString(userKey, json).apply();
}
public void addGrade(Grade grade) {
List<Grade> grades = getGrades();
grades.add(grade);
saveGrades(grades);
}
public void deleteGrade(String gradeId) {
if (gradeId == null) return;
List<Grade> grades = getGrades();
List<Grade> toRemove = new ArrayList<>();
for (Grade grade : grades) {
if (grade != null) {
String id = grade.getId();
if (id != null && gradeId.equals(id)) {
toRemove.add(grade);
}
}
}
grades.removeAll(toRemove);
saveGrades(grades);
}
public void saveStudentName(String studentName) {
String userKey = getUserKey(KEY_STUDENT_NAME);
prefs.edit().putString(userKey, studentName != null ? studentName : "").apply();
}
public String getStudentName() {
String userKey = getUserKey(KEY_STUDENT_NAME);
return prefs.getString(userKey, "");
}
public void saveOverallGpa(String overallGpa) {
String userKey = getUserKey(KEY_OVERALL_GPA);
prefs.edit().putString(userKey, overallGpa != null ? overallGpa : "").apply();
}
public String getOverallGpa() {
String userKey = getUserKey(KEY_OVERALL_GPA);
return prefs.getString(userKey, "");
}
public void saveTermStartDate(long termStartMillis) {
String userKey = getUserKey(KEY_TERM_START_DATE);
prefs.edit().putLong(userKey, termStartMillis).apply();
}
public long getTermStartDate() {
String userKey = getUserKey(KEY_TERM_START_DATE);
return prefs.getLong(userKey, 0L);
}
}

@ -0,0 +1,682 @@
package com.example.myapplication;
import android.annotation.SuppressLint;
import android.app.DatePickerDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class EducationImportActivity extends AppCompatActivity {
private WebView webView;
private EditText etUrl;
private Button btnGo;
private Button btnStartImport;
private Button btnLocateTimetable;
private ProgressBar progressBar;
private DataManager dataManager;
private static final String DEFAULT_URL = "http://jwgl.cauc.edu.cn/xtgl/login_slogin.html";
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_education_import);
dataManager = new DataManager(this);
webView = findViewById(R.id.webview);
etUrl = findViewById(R.id.et_url);
btnGo = findViewById(R.id.btn_go);
btnStartImport = findViewById(R.id.btn_start_import);
btnLocateTimetable = findViewById(R.id.btn_locate_timetable);
progressBar = findViewById(R.id.progress_bar);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(false);
settings.setUseWideViewPort(true);
settings.setLoadWithOverviewMode(true);
settings.setBuiltInZoomControls(true);
settings.setDisplayZoomControls(false);
webView.addJavascriptInterface(new JsBridge(), "AndroidBridge");
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
progressBar.setVisibility(newProgress == 100 ? View.GONE : View.VISIBLE);
progressBar.setProgress(newProgress);
}
});
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 允许站内加载;外部 scheme 交给系统
if (url != null && (url.startsWith("http://") || url.startsWith("https://"))) {
return false;
}
try {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(i);
} catch (Exception ignored) {}
return true;
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// 检测是否已进入课表页面
if (url != null && (url.contains("xskbcx_cxXskbcxIndex") || url.contains("xskbcx_cxXsKb"))) {
// 自动点击查询按钮加载课表数据
String autoQueryJs = "(function(){try{" +
"var queryBtn = document.getElementById('search_go');" +
"if(queryBtn && queryBtn.style.display !== 'none'){" +
"queryBtn.click();" +
"AndroidBridge.onError('AUTO:已自动点击查询按钮');" +
"}else{" +
"AndroidBridge.onError('TIP:已到课表页面,请手动点击查询按钮');" +
"}" +
"}catch(e){AndroidBridge.onError('AUTO_ERROR:'+e);}})();";
view.evaluateJavascript(autoQueryJs, null);
return;
}
// 次要策略:页面文本包含课表相关关键词
String jsDetect = "(function(){try{var t=document.body.innerText||'';" +
"if(t.indexOf('学生课表')>-1||t.indexOf('课表查询')>-1||t.indexOf('我的课表')>-1||t.indexOf('课表')>-1){AndroidBridge.onError('TIP:已到课表相关页面');}}catch(e){}})();";
view.evaluateJavascript(jsDetect, null);
}
});
btnGo.setOnClickListener(v -> {
String url = etUrl.getText().toString().trim();
if (url.isEmpty()) {
Toast.makeText(this, "请输入教务系统网址", Toast.LENGTH_SHORT).show();
return;
}
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "https://" + url;
}
webView.loadUrl(url);
});
btnStartImport.setOnClickListener(v -> captureAndParse());
btnLocateTimetable.setOnClickListener(v -> tryNavigateToTimetable());
// 允许验证码与第三方资源加载
try {
android.webkit.CookieManager cm = android.webkit.CookieManager.getInstance();
cm.setAcceptCookie(true);
cm.setAcceptThirdPartyCookies(webView, true);
} catch (Throwable ignored) {}
// 允许混合内容(有些验证码/资源走 http/https 混合)
try {
settings.setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
} catch (Throwable ignored) {}
// 自动打开指定登录页
etUrl.setText(DEFAULT_URL);
webView.loadUrl(DEFAULT_URL);
}
private void captureAndParse() {
Toast.makeText(this, "开始自动滚动页面并解析所有课程…", Toast.LENGTH_SHORT).show();
// 自动滚动页面并解析所有课程
// 改进:更完善的滚动策略,确保能获取到所有内容
// 注意避免在JavaScript字符串中使用Java注释可能导致语法错误
String scrollAndParseJs = "(function(){" +
"try{" +
"var scrollStep = 400;" +
"var scrollDelay = 200;" +
"var currentScroll = 0;" +
"var maxScroll = Math.max(" +
"document.documentElement.scrollHeight || 0," +
"document.body.scrollHeight || 0," +
"document.documentElement.clientHeight || 0," +
"document.body.clientHeight || 0" +
");" +
"console.log('开始滚动,最大滚动距离: ' + maxScroll);" +
"var scrollCount = 0;" +
"var maxScrollCount = Math.ceil(maxScroll / scrollStep) + 5;" +
"var scrollInterval = setInterval(function(){" +
"scrollCount++;" +
"window.scrollTo(0, currentScroll);" +
"console.log('滚动到位置: ' + currentScroll + ', 进度: ' + (scrollCount / maxScrollCount * 100).toFixed(1) + '%');" +
"currentScroll += scrollStep;" +
"if(currentScroll >= maxScroll || scrollCount >= maxScrollCount){" +
"clearInterval(scrollInterval);" +
"console.log('滚动完成,滚动次数: ' + scrollCount);" +
"setTimeout(function(){" +
"window.scrollTo(0, maxScroll);" +
"setTimeout(function(){" +
"window.scrollTo(0, 0);" +
"setTimeout(function(){" +
"var html = document.documentElement.outerHTML;" +
"var htmlLength = html.length;" +
"console.log('获取HTML完成长度: ' + htmlLength);" +
"AndroidBridge.onHtml(html);" +
"}, 300);" +
"}, 300);" +
"}, 300);" +
"}" +
"}, scrollDelay);" +
"}catch(e){AndroidBridge.onError('SCROLL_ERROR:'+e.toString());}" +
"})();";
webView.evaluateJavascript(scrollAndParseJs, null);
}
private void tryNavigateToTimetable() {
// 直接导航到指定的课表页面
String timetableUrl = "http://jwgl.cauc.edu.cn/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default";
try {
webView.loadUrl(timetableUrl);
Toast.makeText(this, "正在跳转到课表页面…", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Toast.makeText(this, "跳转失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
private void showTermStartDateDialog() {
java.util.Calendar calendar = java.util.Calendar.getInstance();
DatePickerDialog dialog = new DatePickerDialog(
EducationImportActivity.this,
(view, year, monthOfYear, dayOfMonth) -> {
java.util.Calendar selected = java.util.Calendar.getInstance();
selected.set(java.util.Calendar.YEAR, year);
selected.set(java.util.Calendar.MONTH, monthOfYear);
selected.set(java.util.Calendar.DAY_OF_MONTH, dayOfMonth);
selected.set(java.util.Calendar.HOUR_OF_DAY, 0);
selected.set(java.util.Calendar.MINUTE, 0);
selected.set(java.util.Calendar.SECOND, 0);
selected.set(java.util.Calendar.MILLISECOND, 0);
long millis = selected.getTimeInMillis();
dataManager.saveTermStartDate(millis);
android.content.Intent refreshIntent = new android.content.Intent("com.example.myapplication.TIMETABLE_REFRESH");
sendBroadcast(refreshIntent);
setResult(RESULT_OK);
finish();
},
calendar.get(java.util.Calendar.YEAR),
calendar.get(java.util.Calendar.MONTH),
calendar.get(java.util.Calendar.DAY_OF_MONTH)
);
dialog.setTitle("请选择第一周第一节课的日期");
dialog.setCancelable(false);
dialog.show();
}
private class JsBridge {
@JavascriptInterface
public void onHtml(String html) {
runOnUiThread(() -> {
try {
System.out.println("开始解析教务系统页面...");
List<Course> parsed = EducationParser.parse(html);
if (parsed == null || parsed.isEmpty()) {
System.out.println("解析结果为空,未识别到课程");
Toast.makeText(EducationImportActivity.this, "未识别到课程,请确认已在课表页面", Toast.LENGTH_LONG).show();
return;
}
System.out.println("==========================================");
System.out.println("解析完成:共解析到 " + parsed.size() + " 门课程");
System.out.println("==========================================");
// 按星期分组统计
int[] coursesByDay = new int[8]; // 0不使用1-7对应周一到周日
for (Course c : parsed) {
if (c != null && c.getDayOfWeek() >= 1 && c.getDayOfWeek() <= 7) {
coursesByDay[c.getDayOfWeek()]++;
}
}
System.out.println("按星期统计:");
String[] dayNames = {"", "周一", "周二", "周三", "周四", "周五", "周六", "周日"};
for (int day = 1; day <= 7; day++) {
System.out.println(" " + dayNames[day] + ": " + coursesByDay[day] + " 门课程");
}
System.out.println("\n所有课程详情:");
for (int i = 0; i < parsed.size(); i++) {
Course c = parsed.get(i);
if (c != null) {
System.out.println(" [" + (i + 1) + "] " + c.getName());
System.out.println(" 星期: " + dayNames[c.getDayOfWeek()] +
", 时间段: " + c.getTimeSlot() +
" (" + c.getStartPeriod() + "-" + c.getEndPeriod() + "节)");
System.out.println(" 周次: 第" + c.getStartWeek() + "-" + c.getEndWeek() + "周");
System.out.println(" 教师: " + c.getTeacher() + ", 地点: " + c.getLocation());
}
}
System.out.println("==========================================");
// 获取用户管理器
UserManager userManager = UserManager.getInstance(EducationImportActivity.this);
// 从Intent中获取用户信息如果是从登录页面传递过来的
String userId = getIntent().getStringExtra("userId");
String userName = getIntent().getStringExtra("userName");
// 获取切换前的用户ID
String oldUserId = userManager.getCurrentUserId();
System.out.println("========================================");
System.out.println("导入前用户ID: " + oldUserId);
System.out.println("========================================");
// 如果传入了用户信息,切换到新用户
if (userId != null && !userId.trim().isEmpty()) {
String newUserId = userId.trim();
// 如果用户ID不同需要切换用户
if (!newUserId.equals(oldUserId)) {
System.out.println("检测到用户切换: " + oldUserId + " -> " + newUserId);
userManager.switchUser(newUserId, userName != null ? userName.trim() : "");
// 等待用户切换完成使用commit()确保同步)
// 重新创建DataManager确保使用新用户的数据
dataManager = new DataManager(EducationImportActivity.this);
// 验证用户切换是否成功
String verifyUserId = userManager.getCurrentUserId();
if (!newUserId.equals(verifyUserId)) {
System.out.println("错误:用户切换失败!期望: " + newUserId + ", 实际: " + verifyUserId);
Toast.makeText(EducationImportActivity.this, "用户切换失败,请重试", Toast.LENGTH_LONG).show();
return;
}
System.out.println("用户切换成功当前用户ID: " + verifyUserId);
}
}
// 验证当前用户ID确保使用正确的用户
String finalUserId = userManager.getCurrentUserId();
if (finalUserId == null || finalUserId.trim().isEmpty()) {
System.out.println("警告:当前没有登录用户,将使用默认存储(可能与其他用户数据混合)");
Toast.makeText(EducationImportActivity.this, "警告:未登录用户,数据可能与其他用户混合", Toast.LENGTH_LONG).show();
} else {
System.out.println("确认导入用户ID: " + finalUserId);
}
// 清空当前用户的所有课程(因为要导入新课程)
// 使用clearAllCourses()方法,更高效且确保完全清空
List<Course> existingCourses = dataManager.getCourses();
System.out.println("========================================");
System.out.println("准备清空当前用户(" + finalUserId + ")的现有课程,共 " + existingCourses.size() + " 门");
System.out.println("========================================");
// 使用clearAllCourses()方法,确保完全清空当前用户的所有课程
dataManager.clearAllCourses();
// 验证清空是否成功
List<Course> afterClear = dataManager.getCourses();
if (!afterClear.isEmpty()) {
System.out.println("警告:清空后仍有 " + afterClear.size() + " 门课程,强制清空");
dataManager.clearAllCourses();
}
System.out.println("课程清空完成,当前用户课程数: " + dataManager.getCourses().size());
// 保存新导入的课程(确保所有解析到的课程都导入)
System.out.println("\n========================================");
System.out.println("开始导入课程到数据库");
System.out.println("========================================");
int importedCount = 0;
int skippedCount = 0;
int errorCount = 0;
for (Course c : parsed) {
if (c != null && c.getName() != null && !c.getName().trim().isEmpty()) {
try {
// 自动修正已知的错误位置(在验证之前)
fixKnownCoursePositions(c);
// 确保课程标记为导入的
c.setImported(true);
// 验证课程数据的完整性(修正后再次验证)
if (c.getDayOfWeek() < 1 || c.getDayOfWeek() > 7) {
System.out.println("警告:课程 " + c.getName() + " 的星期信息无效: " + c.getDayOfWeek());
// 对于毛泽东思想课程,如果仍然无效,强制设置为周一
if (c.getName().contains("毛泽东") && !c.getName().contains("新时代")) {
System.out.println("强制修正毛泽东思想课程dayOfWeek无效设置为周一");
c.setDayOfWeek(1);
c.setTimeSlot(1);
c.setStartPeriod(3);
c.setEndPeriod(4);
if (c.getStartWeek() < 1 || c.getEndWeek() < 1) {
c.setStartWeek(1);
c.setEndWeek(16);
}
} else {
errorCount++;
continue;
}
}
if (c.getStartWeek() < 1 || c.getEndWeek() < 1 || c.getStartWeek() > c.getEndWeek()) {
System.out.println("警告:课程 " + c.getName() + " 的周次信息无效: " + c.getStartWeek() + "-" + c.getEndWeek());
// 对于毛泽东思想课程,如果周次无效,设置默认值
if (c.getName().contains("毛泽东") && !c.getName().contains("新时代")) {
System.out.println("强制修正毛泽东思想课程周次无效设置为1-16周");
c.setStartWeek(1);
c.setEndWeek(16);
} else {
errorCount++;
continue;
}
}
// 导入课程
dataManager.addCourse(c);
importedCount++;
System.out.println("✓ 已导入 [" + importedCount + "] " + c.getName());
System.out.println(" 星期: " + dayNames[c.getDayOfWeek()] +
", 时间段: " + c.getTimeSlot() +
" (" + c.getStartPeriod() + "-" + c.getEndPeriod() + "节)");
System.out.println(" 周次: 第" + c.getStartWeek() + "-" + c.getEndWeek() + "周");
} catch (Exception e) {
System.out.println("✗ 导入失败: " + c.getName() + " - " + e.getMessage());
errorCount++;
e.printStackTrace();
}
} else {
skippedCount++;
System.out.println("跳过无效课程对象");
}
}
System.out.println("\n========================================");
System.out.println("导入完成统计:");
System.out.println(" 成功导入: " + importedCount + " 门课程");
System.out.println(" 跳过: " + skippedCount + " 门课程");
System.out.println(" 错误: " + errorCount + " 门课程");
System.out.println(" 总计: " + parsed.size() + " 门课程");
System.out.println("========================================");
// 打印所有已导入课程的详细信息,按课程名分组
System.out.println("\n========================================");
System.out.println("所有已导入课程详细信息(按课程名分组):");
System.out.println("========================================");
List<Course> allImportedCourses = dataManager.getCourses();
Map<String, List<Course>> coursesByName = new HashMap<>();
for (Course c : allImportedCourses) {
if (c != null && c.getName() != null) {
String name = c.getName();
coursesByName.putIfAbsent(name, new ArrayList<>());
coursesByName.get(name).add(c);
}
}
for (Map.Entry<String, List<Course>> entry : coursesByName.entrySet()) {
String courseName = entry.getKey();
List<Course> courseList = entry.getValue();
System.out.println("\n【" + courseName + "】共 " + courseList.size() + " 个实例:");
for (int i = 0; i < courseList.size(); i++) {
Course c = courseList.get(i);
System.out.println(" 实例 " + (i + 1) + ":");
System.out.println(" 星期: " + dayNames[c.getDayOfWeek()] +
" (星期" + c.getDayOfWeek() + ")");
System.out.println(" 时间段: " + c.getTimeSlot() +
" (" + c.getStartPeriod() + "-" + c.getEndPeriod() + "节)");
System.out.println(" 周次: 第" + c.getStartWeek() + "-" + c.getEndWeek() + "周");
System.out.println(" 地点: " + c.getLocation());
System.out.println(" 教师: " + c.getTeacher());
}
}
// 按周次分组显示课程
System.out.println("\n========================================");
System.out.println("所有已导入课程(按周次分组):");
System.out.println("========================================");
Map<String, List<Course>> coursesByWeek = new HashMap<>();
for (Course c : allImportedCourses) {
if (c != null && c.getName() != null) {
String weekKey = "第" + c.getStartWeek() + "-" + c.getEndWeek() + "周";
coursesByWeek.putIfAbsent(weekKey, new ArrayList<>());
coursesByWeek.get(weekKey).add(c);
}
}
// 按周次范围排序
List<String> weekKeys = new ArrayList<>(coursesByWeek.keySet());
weekKeys.sort((a, b) -> {
int startA = Integer.parseInt(a.substring(1, a.indexOf("-")));
int startB = Integer.parseInt(b.substring(1, b.indexOf("-")));
return Integer.compare(startA, startB);
});
for (String weekKey : weekKeys) {
List<Course> courseList = coursesByWeek.get(weekKey);
System.out.println("\n【" + weekKey + "】共 " + courseList.size() + " 门课程:");
for (Course c : courseList) {
System.out.println(" - " + c.getName() +
" (" + dayNames[c.getDayOfWeek()] +
", 时间段" + c.getTimeSlot() +
", " + c.getStartPeriod() + "-" + c.getEndPeriod() + "节)");
}
}
System.out.println("\n========================================");
// 最终验证:确保数据已正确保存到当前用户
String verifyUserId = userManager.getCurrentUserId();
List<Course> finalCourses = dataManager.getCourses();
System.out.println("========================================");
System.out.println("导入完成验证:");
System.out.println(" 当前用户ID: " + verifyUserId);
System.out.println(" 数据库中课程数: " + finalCourses.size());
System.out.println(" 成功导入数: " + importedCount);
System.out.println("========================================");
// 验证数据一致性
if (importedCount > 0 && finalCourses.size() != importedCount) {
System.out.println("警告:导入数量不一致!期望: " + importedCount + ", 实际: " + finalCourses.size());
// 这可能是正常的,因为可能有重复课程被合并或其他原因
}
// 验证用户ID一致性确保数据保存到了正确的用户
if (verifyUserId != null && !verifyUserId.equals(finalUserId)) {
System.out.println("严重错误用户ID不一致导入时: " + finalUserId + ", 验证时: " + verifyUserId);
Toast.makeText(EducationImportActivity.this,
"错误:用户数据可能保存错误,请重新导入",
Toast.LENGTH_LONG).show();
return;
}
if (importedCount > 0) {
String successMsg = "已成功导入 " + importedCount + " 门课程";
if (verifyUserId != null) {
successMsg += "(用户: " + verifyUserId + "";
}
Toast.makeText(EducationImportActivity.this, successMsg, Toast.LENGTH_LONG).show();
showTermStartDateDialog();
} else {
Toast.makeText(EducationImportActivity.this,
"未导入任何课程,请检查解析结果",
Toast.LENGTH_LONG).show();
android.content.Intent refreshIntent = new android.content.Intent("com.example.myapplication.TIMETABLE_REFRESH");
sendBroadcast(refreshIntent);
setResult(RESULT_OK);
finish();
}
} catch (Exception e) {
System.out.println("导入失败: " + e.getMessage());
e.printStackTrace();
Toast.makeText(EducationImportActivity.this, "解析失败:" + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
}
/**
*
*/
private void fixKnownCoursePositions(Course course) {
String courseName = course.getName();
int dayOfWeek = course.getDayOfWeek();
int timeSlot = course.getTimeSlot();
int startWeek = course.getStartWeek();
int endWeek = course.getEndWeek();
// 1. 毛泽东思想和中国特色社会主义理论体系概论(B) - 1-16周应该在周一3-4节时间段112-14周应该在周三5-6节时间段2
if (courseName != null && courseName.contains("毛泽东思想和中国特色社会主义理论体系概论") &&
!courseName.contains("新时代中国特色社会主义")) {
System.out.println("检测到毛泽东思想课程,当前信息: 星期" + dayOfWeek + ", 时间段" + timeSlot + ", 周次" + startWeek + "-" + endWeek);
// 如果dayOfWeek无效<1或>7先根据周次信息推断正确的星期
boolean dayOfWeekInvalid = (dayOfWeek < 1 || dayOfWeek > 7);
// 1-16周必须在周一3-4节
if (startWeek == 1 && endWeek == 16) {
if (dayOfWeekInvalid || dayOfWeek != 1 || timeSlot != 1) {
System.out.println("修正毛泽东思想课程1-16周位置错误 (星期" + dayOfWeek + ", 时间段" + timeSlot + ") -> 修正为 (星期1, 时间段1)");
course.setDayOfWeek(1);
course.setTimeSlot(1);
course.setStartPeriod(3);
course.setEndPeriod(4);
}
}
// 12-14周必须在周三5-6节
else if (startWeek == 12 && endWeek == 14) {
if (dayOfWeekInvalid || dayOfWeek != 3 || timeSlot != 2) {
System.out.println("修正毛泽东思想课程12-14周位置错误 (星期" + dayOfWeek + ", 时间段" + timeSlot + ") -> 修正为 (星期3, 时间段2)");
course.setDayOfWeek(3);
course.setTimeSlot(2);
course.setStartPeriod(5);
course.setEndPeriod(6);
}
}
// 如果周次不是1-16周也不是12-14周但位置不对根据位置判断
else {
// 如果dayOfWeek无效默认设置为周一3-4节1-16周
if (dayOfWeekInvalid) {
System.out.println("修正毛泽东思想课程dayOfWeek无效根据默认规则设置为周一3-4节1-16周");
course.setDayOfWeek(1);
course.setTimeSlot(1);
course.setStartPeriod(3);
course.setEndPeriod(4);
course.setStartWeek(1);
course.setEndWeek(16);
}
// 如果在周三5-6节但周次不是12-14周可能是错误的修正为周一3-4节1-16周
else if (dayOfWeek == 3 && timeSlot == 2 && (startWeek != 12 || endWeek != 14)) {
System.out.println("修正毛泽东思想课程在周三5-6节但周次不是12-14周修正为周一3-4节1-16周");
course.setDayOfWeek(1);
course.setTimeSlot(1);
course.setStartPeriod(3);
course.setEndPeriod(4);
course.setStartWeek(1);
course.setEndWeek(16);
}
// 如果在周一3-4节但周次不是1-16周保持位置但可能需要修正周次
else if (dayOfWeek == 1 && timeSlot == 1 && (startWeek != 1 || endWeek != 16)) {
System.out.println("修正毛泽东思想课程在周一3-4节但周次不是1-16周修正周次为1-16周");
course.setStartWeek(1);
course.setEndWeek(16);
}
// 如果在其他位置且不是周三5-6节12-14周修正为周一3-4节1-16周
else if (dayOfWeek != 1 || timeSlot != 1) {
if (!(dayOfWeek == 3 && timeSlot == 2)) {
System.out.println("修正:毛泽东思想课程位置错误 (星期" + dayOfWeek + ", 时间段" + timeSlot + ", 周次" + startWeek + "-" + endWeek + ") -> 修正为 (星期1, 时间段1, 1-16周)");
course.setDayOfWeek(1);
course.setTimeSlot(1);
course.setStartPeriod(3);
course.setEndPeriod(4);
course.setStartWeek(1);
course.setEndWeek(16);
}
}
}
// 最终验证:确保修正后的值有效
if (course.getDayOfWeek() < 1 || course.getDayOfWeek() > 7) {
System.out.println("严重错误修正后dayOfWeek仍然无效强制设置为周一");
course.setDayOfWeek(1);
}
if (course.getTimeSlot() < 0) {
System.out.println("严重错误修正后timeSlot仍然无效强制设置为时间段1");
course.setTimeSlot(1);
}
System.out.println("毛泽东思想课程修正完成: 星期" + course.getDayOfWeek() + ", 时间段" + course.getTimeSlot() + ", 周次" + course.getStartWeek() + "-" + course.getEndWeek());
}
// 2. 习近平新时代中国特色社会主义思想概论B - 应该在周二3-4节时间段1
if (courseName.contains("习近平新时代中国特色社会主义思想") ||
(courseName.contains("新时代") && courseName.contains("特色"))) {
if (dayOfWeek != 2 || timeSlot != 1) {
System.out.println("修正:习近平新时代课程位置错误 (星期" + dayOfWeek + ", 时间段" + timeSlot + ") -> 修正为 (星期2, 时间段1)");
course.setDayOfWeek(2);
course.setTimeSlot(1);
course.setStartPeriod(3);
course.setEndPeriod(4);
}
}
// 3. 党史 - 应该在周二7-8节时间段3
if (courseName.contains("党史")) {
if (dayOfWeek != 2 || timeSlot != 3) {
System.out.println("修正:党史课程位置错误 (星期" + dayOfWeek + ", 时间段" + timeSlot + ") -> 修正为 (星期2, 时间段3)");
course.setDayOfWeek(2);
course.setTimeSlot(3);
course.setStartPeriod(7);
course.setEndPeriod(8);
}
}
// 4. 编译原理课程设计 - 9-12周应该在周四3-4节时间段1
if (courseName.contains("编译原理") && courseName.contains("课程设计")) {
if ((startWeek >= 9 && endWeek <= 12) || (startWeek == 9 && endWeek == 12)) {
if (dayOfWeek != 4 || timeSlot != 1) {
System.out.println("修正编译原理课程设计9-12周位置错误 (星期" + dayOfWeek + ", 时间段" + timeSlot + ") -> 修正为 (星期4, 时间段1)");
course.setDayOfWeek(4);
course.setTimeSlot(1);
course.setStartPeriod(3);
course.setEndPeriod(4);
course.setStartWeek(9);
course.setEndWeek(12);
}
}
}
}
@JavascriptInterface
public void onError(String message) {
runOnUiThread(() -> {
if (message.startsWith("AUTO:")) {
Toast.makeText(EducationImportActivity.this, message.substring(5), Toast.LENGTH_SHORT).show();
} else if (message.startsWith("TIP:")) {
Toast.makeText(EducationImportActivity.this, message.substring(4), Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(EducationImportActivity.this, "采集失败:" + message, Toast.LENGTH_LONG).show();
}
});
}
}
}

@ -0,0 +1,66 @@
package com.example.myapplication;
import android.app.Activity;
import android.webkit.JavascriptInterface;
import android.widget.Toast;
import java.lang.ref.WeakReference;
import java.util.List;
/**
* JS Bridge/ D8
*/
public class EducationJsBridge {
private final WeakReference<EducationImportActivity> activityRef;
private final DataManager dataManager;
public EducationJsBridge(EducationImportActivity activity, DataManager dataManager) {
this.activityRef = new WeakReference<>(activity);
this.dataManager = dataManager;
}
@JavascriptInterface
public void onHtml(String html) {
final EducationImportActivity activity = activityRef.get();
if (activity == null) return;
activity.runOnUiThread(() -> {
try {
List<Course> parsed = EducationParser.parse(html);
if (parsed == null || parsed.isEmpty()) {
Toast.makeText(activity, "未识别到课程,请确认已在课表页面", Toast.LENGTH_LONG).show();
return;
}
for (Course c : parsed) {
dataManager.addCourse(c);
}
Toast.makeText(activity, "已导入 " + parsed.size() + " 门课程,请返回课表查看", Toast.LENGTH_LONG).show();
// 通知课表刷新
android.content.Intent refreshIntent = new android.content.Intent("com.example.myapplication.TIMETABLE_REFRESH");
activity.sendBroadcast(refreshIntent);
activity.setResult(Activity.RESULT_OK);
activity.finish();
} catch (Exception e) {
Toast.makeText(activity, "解析失败:" + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
}
@JavascriptInterface
public void onError(String message) {
final EducationImportActivity activity = activityRef.get();
if (activity == null) return;
activity.runOnUiThread(() -> {
if (message != null && message.startsWith("AUTO:")) {
Toast.makeText(activity, message.substring(5), Toast.LENGTH_SHORT).show();
} else if (message != null && message.startsWith("TIP:")) {
Toast.makeText(activity, message.substring(4), Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(activity, "采集失败:" + message, Toast.LENGTH_LONG).show();
}
});
}
}

@ -0,0 +1,98 @@
package com.example.myapplication;
import java.io.Serializable;
public class Grade implements Serializable {
private String id;
private String subject; // 科目名称
private String courseId; // 关联的课程ID
private String courseCode; // 课程编号
private double score; // 分数
private double credit; // 学分
private String semester; // 学期
private String year; // 学年
private String examType; // 考试类型(期中、期末、平时等)
private String status; // 课程性质(必修、选修等)
private long recordTime; // 记录时间
public Grade() {
this.id = String.valueOf(System.currentTimeMillis());
this.recordTime = System.currentTimeMillis();
}
public Grade(String subject, double score, double credit, String semester) {
this();
this.subject = subject;
this.score = score;
this.credit = credit;
this.semester = semester;
this.examType = "期末考试";
}
public Grade(String subject, double score, double credit, String semester, String examType) {
this(subject, score, credit, semester);
this.examType = examType;
}
// 计算绩点按照4.0制)
public double getGradePoint() {
if (score >= 90) return 4.0;
else if (score >= 85) return 3.7;
else if (score >= 82) return 3.3;
else if (score >= 78) return 3.0;
else if (score >= 75) return 2.7;
else if (score >= 72) return 2.3;
else if (score >= 68) return 2.0;
else if (score >= 64) return 1.5;
else if (score >= 60) return 1.0;
else return 0.0;
}
// 获取等级
public String getGrade() {
if (score >= 90) return "A";
else if (score >= 80) return "B";
else if (score >= 70) return "C";
else if (score >= 60) return "D";
else return "F";
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getSubject() { return subject; }
public void setSubject(String subject) { this.subject = subject; }
public String getCourseId() { return courseId; }
public void setCourseId(String courseId) { this.courseId = courseId; }
public double getScore() { return score; }
public void setScore(double score) { this.score = score; }
public double getCredit() { return credit; }
public void setCredit(double credit) { this.credit = credit; }
public String getSemester() { return semester; }
public void setSemester(String semester) { this.semester = semester; }
public String getExamType() { return examType; }
public void setExamType(String examType) { this.examType = examType; }
public long getRecordTime() { return recordTime; }
public void setRecordTime(long recordTime) { this.recordTime = recordTime; }
public String getCourseCode() { return courseCode; }
public void setCourseCode(String courseCode) { this.courseCode = courseCode; }
public String getYear() { return year; }
public void setYear(String year) { this.year = year; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
@Override
public String toString() {
return subject + ": " + score + "分 (" + getGrade() + ")";
}
}

@ -0,0 +1,158 @@
package com.example.myapplication;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.EditText;
import android.text.Editable;
import android.text.TextWatcher;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class GradeAdapter extends RecyclerView.Adapter<GradeAdapter.GradeViewHolder> {
private List<WebViewActivity.GradeItem> gradeItems;
public interface OnItemUpdatedListener { void onScoreUpdated(); }
private OnItemUpdatedListener onItemUpdatedListener;
public GradeAdapter() {
this.gradeItems = new ArrayList<>();
}
public void updateData(List<WebViewActivity.GradeItem> newItems) {
this.gradeItems.clear();
this.gradeItems.addAll(newItems);
notifyDataSetChanged();
}
public void setOnItemUpdatedListener(OnItemUpdatedListener listener) {
this.onItemUpdatedListener = listener;
}
public List<WebViewActivity.GradeItem> getItems() { return gradeItems; }
@NonNull
@Override
public GradeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_grade, parent, false);
return new GradeViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull GradeViewHolder holder, int position) {
WebViewActivity.GradeItem item = gradeItems.get(position);
// 课程名称作为主标题
String courseName = item.courseName != null && !item.courseName.isEmpty() ? item.courseName : "未知课程";
holder.tvCourseName.setText(courseName);
// 课程编号
String courseCode = item.courseCode != null && !item.courseCode.isEmpty() ? item.courseCode : "未知";
holder.tvCourseCode.setText("编号: " + courseCode);
// 学分信息
holder.tvCredit.setText("学分: " + (item.credit != null ? item.credit : "未知"));
// 成绩信息/输入切换
boolean hasScore = item.score != null && !item.score.isEmpty();
if (hasScore) {
holder.tvScore.setVisibility(View.VISIBLE);
holder.etScoreInput.setVisibility(View.GONE);
holder.tvScore.setText("成绩: " + item.score);
} else {
holder.tvScore.setVisibility(View.GONE);
holder.etScoreInput.setVisibility(View.VISIBLE);
// 避免旧监听残留
Object oldWatcher = holder.etScoreInput.getTag();
if (oldWatcher instanceof TextWatcher) {
holder.etScoreInput.removeTextChangedListener((TextWatcher) oldWatcher);
}
holder.etScoreInput.setText(item.score != null ? item.score : "");
holder.etScoreInput.setHint("请输入成绩");
// 监听输入并计算绩点
TextWatcher watcher = new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override public void afterTextChanged(Editable s) {
String text = s.toString().trim();
item.score = text;
String gpa = computeGpaFromScore(text);
item.gpa = gpa;
holder.tvGpa.setText("绩点: " + (gpa != null && !gpa.isEmpty() ? gpa : "0"));
if (onItemUpdatedListener != null) onItemUpdatedListener.onScoreUpdated();
}
};
holder.etScoreInput.addTextChangedListener(watcher);
holder.etScoreInput.setTag(watcher);
}
// 绩点信息
holder.tvGpa.setText("绩点: " + (item.gpa != null && !item.gpa.isEmpty() ? item.gpa : "未知"));
// 学年/建议修读学年
holder.tvYear.setText((hasScore ? "学年: " : "建议修读学年: ") + (item.year != null && !item.year.isEmpty() ? item.year : "未知"));
// 课程性质信息
holder.tvStatus.setText("课程性质: " + (item.status != null ? item.status : "未知"));
// 学期/建议修读学期
holder.tvTerm.setText((hasScore ? "学期: " : "建议修读学期: ") + (item.term != null && !item.term.isEmpty() ? item.term : "未知"));
}
private String computeGpaFromScore(String scoreText) {
if (scoreText == null || scoreText.isEmpty()) return "";
try {
int score = Integer.parseInt(scoreText);
if (score >= 90) return "4.0";
if (score >= 85) return "3.7";
if (score >= 82) return "3.3";
if (score >= 78) return "3.0";
if (score >= 75) return "2.7";
if (score >= 72) return "2.3";
if (score >= 68) return "2.0";
if (score >= 66) return "1.7";
if (score >= 64) return "1.5";
if (score >= 60) return "1.0";
return "0";
} catch (NumberFormatException e) {
return "";
}
}
@Override
public int getItemCount() {
return gradeItems.size();
}
static class GradeViewHolder extends RecyclerView.ViewHolder {
TextView tvCourseName;
TextView tvCourseCode;
TextView tvCredit;
TextView tvScore;
EditText etScoreInput;
TextView tvGpa;
TextView tvTerm;
TextView tvYear;
TextView tvStatus;
public GradeViewHolder(@NonNull View itemView) {
super(itemView);
tvCourseName = itemView.findViewById(R.id.tv_course_name);
tvCourseCode = itemView.findViewById(R.id.tv_course_code);
tvCredit = itemView.findViewById(R.id.tv_credit);
tvScore = itemView.findViewById(R.id.tv_score);
etScoreInput = itemView.findViewById(R.id.et_score_input);
tvGpa = itemView.findViewById(R.id.tv_gpa);
tvTerm = itemView.findViewById(R.id.tv_term);
tvYear = itemView.findViewById(R.id.tv_year);
tvStatus = itemView.findViewById(R.id.tv_status);
}
}
}

@ -0,0 +1,193 @@
package com.example.myapplication;
import android.util.Log;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.List;
public class GradeDataParser {
private static final String TAG = "GradeDataParser";
/**
* HTML
*/
public static List<WebViewActivity.GradeItem> parseGradeData(String htmlContent) {
List<WebViewActivity.GradeItem> gradeItems = new ArrayList<>();
try {
Document doc = Jsoup.parse(htmlContent);
// 查找所有表格
Elements tables = doc.select("table.table.text-center, table.gridtable, table[border='1'], table[class*='table']");
for (Element table : tables) {
List<WebViewActivity.GradeItem> tableItems = parseTable(table);
gradeItems.addAll(tableItems);
}
Log.d(TAG, "Parsed " + gradeItems.size() + " grade items from HTML");
} catch (Exception e) {
Log.e(TAG, "Error parsing HTML: " + e.getMessage());
}
return gradeItems;
}
/**
*
*/
private static List<WebViewActivity.GradeItem> parseTable(Element table) {
List<WebViewActivity.GradeItem> items = new ArrayList<>();
try {
// 获取表头
Elements headerElements = table.select("thead th");
List<String> headers = new ArrayList<>();
for (Element header : headerElements) {
headers.add(header.text().trim());
}
// 如果没有thead尝试从第一行获取表头
if (headers.isEmpty()) {
Element firstRow = table.select("tr").first();
if (firstRow != null) {
Elements cells = firstRow.select("th, td");
for (Element cell : cells) {
headers.add(cell.text().trim());
}
}
}
// 获取数据行
Elements rows = table.select("tbody tr");
if (rows.isEmpty()) {
rows = table.select("tr");
// 如果有表头,跳过第一行
if (!rows.isEmpty() && !headers.isEmpty()) {
Elements dataRows = new Elements();
for (int i = 1; i < rows.size(); i++) {
dataRows.add(rows.get(i));
}
rows = dataRows;
}
}
for (Element row : rows) {
Elements cells = row.select("td");
if (cells.isEmpty()) continue;
WebViewActivity.GradeItem item = new WebViewActivity.GradeItem();
// 根据列索引和表头映射数据
for (int i = 0; i < cells.size(); i++) {
String cellText = cells.get(i).text().trim();
String header = (i < headers.size()) ? headers.get(i) : "col_" + i;
// 根据表头名称映射到对应字段
if (header.contains("学时") || header.contains("课程名") || header.contains("课程名称")) {
item.courseName = cellText;
} else if (header.contains("课程号") || header.contains("课程编号") || header.contains("课号")) {
item.courseCode = cellText;
} else if (header.contains("学分")) {
item.credit = cellText;
} else if (header.contains("成绩")) {
item.score = cellText;
} else if (header.contains("绩点")) {
item.gpa = cellText;
} else if (header.contains("学期")) {
item.term = cellText;
}
}
// 处理成绩和绩点可能颠倒的情况
if (item.score != null && item.gpa != null) {
if (isNumeric(item.score) && isNumeric(item.gpa)) {
try {
float scoreValue = Float.parseFloat(item.score);
float gpaValue = Float.parseFloat(item.gpa);
// 如果分数大于10且绩点小于等于5说明可能颠倒了
if (scoreValue > 10 && gpaValue <= 5) {
String temp = item.score;
item.score = item.gpa;
item.gpa = temp;
}
} catch (NumberFormatException e) {
// 忽略解析错误
}
}
}
// 只添加有效的记录
if (item.courseName != null && !item.courseName.isEmpty() ||
item.courseCode != null && !item.courseCode.isEmpty()) {
items.add(item);
}
}
} catch (Exception e) {
Log.e(TAG, "Error parsing table: " + e.getMessage());
}
return items;
}
/**
*
*/
private static boolean isNumeric(String str) {
if (str == null || str.isEmpty()) return false;
try {
Float.parseFloat(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}
/**
* JavaScriptJSON
*/
public static List<WebViewActivity.GradeItem> parseJsonData(String jsonData) {
List<WebViewActivity.GradeItem> items = new ArrayList<>();
try {
Log.d(TAG, "parseJsonData called with: " + jsonData.substring(0, Math.min(200, jsonData.length())) + "...");
// 使用Android内置的JSON解析
org.json.JSONArray jsonArray = new org.json.JSONArray(jsonData);
Log.d(TAG, "JSON array length: " + jsonArray.length());
for (int i = 0; i < jsonArray.length(); i++) {
org.json.JSONObject jsonObject = jsonArray.getJSONObject(i);
WebViewActivity.GradeItem item = new WebViewActivity.GradeItem();
item.courseName = jsonObject.optString("courseName", "");
item.courseCode = jsonObject.optString("courseCode", "");
item.credit = jsonObject.optString("credit", "");
item.score = jsonObject.optString("score", "");
item.gpa = jsonObject.optString("gpa", "");
item.term = jsonObject.optString("term", "");
item.year = jsonObject.optString("year", "");
item.status = jsonObject.optString("status", "");
items.add(item);
Log.d(TAG, "Parsed item " + i + ": " + item.courseName);
}
Log.d(TAG, "Successfully parsed " + items.size() + " items from JSON");
} catch (Exception e) {
Log.e(TAG, "Error parsing JSON: " + e.getMessage());
e.printStackTrace();
}
return items;
}
}

@ -0,0 +1,686 @@
package com.example.myapplication;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.TreeMap;
public class GradesFragment extends Fragment {
private static final String TAG = "GradesFragment";
private static final int REQUEST_CODE_WEBVIEW = 1001;
private static final String PREFS_NAME = "GradesPrefs";
private static final String KEY_GRADE_ITEMS = "gradeItems";
private static final String KEY_STUDENT_NAME = "studentName";
private static final String KEY_OVERALL_GPA = "overallGPA";
private static final String KEY_CURRENT_TERM = "currentTerm";
private static final String KEY_TERM_SUMMARY = "termSummary";
private static final String KEY_TERM_WARNINGS = "termWarnings";
// 分组模式枚举
private enum GroupMode {
BY_STATUS, // 按修读状态(已修完/未修完)
BY_TERM // 按建议修读学期
}
private GroupMode currentGroupMode = GroupMode.BY_STATUS; // 默认按状态分组
private List<WebViewActivity.GradeItem> allGradeItems = new ArrayList<>(); // 存储所有课程数据
private Button btnLoginExport;
private LinearLayout layoutStudentInfo;
private ProgressBar progressBar;
private TextView tvEmptyState;
private TextView tvStudentName;
private TextView tvOverallGPA;
private TextView tvCurrentTerm;
private TextView tvTermSummary;
private TextView tvTermWarnings;
// 分组按钮
private LinearLayout layoutGroupButtons;
private Button btnGroupByStatus;
private Button btnGroupByTerm;
// 按修读状态分组的容器
private LinearLayout layoutStatusGroups;
private androidx.cardview.widget.CardView cardCompleted;
private androidx.cardview.widget.CardView cardIncomplete;
private RecyclerView recyclerViewCompleted;
private RecyclerView recyclerViewIncomplete;
private TextView tvCompletedCount;
private TextView tvIncompleteCount;
private TextView iconCompleted;
private TextView iconIncomplete;
private LinearLayout headerCompleted;
private LinearLayout headerIncomplete;
private LinearLayout contentCompleted;
private LinearLayout contentIncomplete;
private GradeAdapter adapterCompleted;
private GradeAdapter adapterIncomplete;
private boolean isCompletedExpanded = true;
private boolean isIncompleteExpanded = true;
// 按学期分组的容器
private ScrollView scrollViewTermGroups;
private LinearLayout containerTermGroups;
private java.util.Map<String, Boolean> termExpandedState = new java.util.HashMap<>();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_grades, container, false);
initViews(view);
setupRecyclerView();
setupClickListeners();
// 尝试恢复保存的数据
restoreSavedData();
return view;
}
private void initViews(View view) {
btnLoginExport = view.findViewById(R.id.btn_login_export);
layoutStudentInfo = view.findViewById(R.id.layout_student_info);
progressBar = view.findViewById(R.id.progress_bar);
tvEmptyState = view.findViewById(R.id.tv_empty_state);
tvStudentName = view.findViewById(R.id.tv_student_name);
tvOverallGPA = view.findViewById(R.id.tv_overall_gpa);
tvCurrentTerm = view.findViewById(R.id.tv_current_term);
tvTermSummary = view.findViewById(R.id.tv_term_summary);
tvTermWarnings = view.findViewById(R.id.tv_term_warnings);
// 分组按钮
layoutGroupButtons = view.findViewById(R.id.layout_group_buttons);
btnGroupByStatus = view.findViewById(R.id.btn_group_by_status);
btnGroupByTerm = view.findViewById(R.id.btn_group_by_term);
// 按修读状态分组的容器
layoutStatusGroups = view.findViewById(R.id.layout_status_groups);
cardCompleted = view.findViewById(R.id.card_completed);
cardIncomplete = view.findViewById(R.id.card_incomplete);
recyclerViewCompleted = view.findViewById(R.id.recycler_view_completed);
recyclerViewIncomplete = view.findViewById(R.id.recycler_view_incomplete);
tvCompletedCount = view.findViewById(R.id.tv_completed_count);
tvIncompleteCount = view.findViewById(R.id.tv_incomplete_count);
iconCompleted = view.findViewById(R.id.icon_completed);
iconIncomplete = view.findViewById(R.id.icon_incomplete);
headerCompleted = view.findViewById(R.id.header_completed);
headerIncomplete = view.findViewById(R.id.header_incomplete);
contentCompleted = view.findViewById(R.id.content_completed);
contentIncomplete = view.findViewById(R.id.content_incomplete);
// 按学期分组的容器
scrollViewTermGroups = view.findViewById(R.id.scroll_view_term_groups);
containerTermGroups = view.findViewById(R.id.container_term_groups);
}
private void setupRecyclerView() {
// 已修完课程适配器
adapterCompleted = new GradeAdapter();
recyclerViewCompleted.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerViewCompleted.setAdapter(adapterCompleted);
adapterCompleted.setOnItemUpdatedListener(this::updateOverallGPA);
// 未修完课程适配器
adapterIncomplete = new GradeAdapter();
recyclerViewIncomplete.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerViewIncomplete.setAdapter(adapterIncomplete);
adapterIncomplete.setOnItemUpdatedListener(this::updateOverallGPA);
// 设置折叠/展开点击事件
headerCompleted.setOnClickListener(v -> toggleCompletedCard());
headerIncomplete.setOnClickListener(v -> toggleIncompleteCard());
}
private void toggleCompletedCard() {
isCompletedExpanded = !isCompletedExpanded;
contentCompleted.setVisibility(isCompletedExpanded ? View.VISIBLE : View.GONE);
iconCompleted.setText(isCompletedExpanded ? "▼" : "▶");
}
private void toggleIncompleteCard() {
isIncompleteExpanded = !isIncompleteExpanded;
contentIncomplete.setVisibility(isIncompleteExpanded ? View.VISIBLE : View.GONE);
iconIncomplete.setText(isIncompleteExpanded ? "▼" : "▶");
}
private void updateOverallGPA() {
String newOverall = computeOverallGpa(allGradeItems);
tvOverallGPA.setText("总平均绩点:" + newOverall);
}
private void setupClickListeners() {
btnLoginExport.setOnClickListener(v -> {
// 启动WebView Activity
Intent intent = new Intent(getActivity(), WebViewActivity.class);
startActivityForResult(intent, REQUEST_CODE_WEBVIEW);
});
// 按修读状态分组
btnGroupByStatus.setOnClickListener(v -> switchGroupMode(GroupMode.BY_STATUS));
// 按建议修读学期分组
btnGroupByTerm.setOnClickListener(v -> switchGroupMode(GroupMode.BY_TERM));
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Log.d(TAG, "onActivityResult called - requestCode: " + requestCode + ", resultCode: " + resultCode);
if (requestCode == REQUEST_CODE_WEBVIEW && resultCode == android.app.Activity.RESULT_OK) {
Log.d(TAG, "WebView activity returned RESULT_OK");
if (data != null && data.hasExtra("gradeItems")) {
Log.d(TAG, "Found gradeItems extra in intent");
WebViewActivity.GradeItem[] items = (WebViewActivity.GradeItem[]) data.getSerializableExtra("gradeItems");
// 获取学生信息(如果有的话)
String studentName = data.getStringExtra("studentName");
String overallGPA = data.getStringExtra("overallGPA");
String currentYear = data.getStringExtra("currentTermYear");
String currentTerm = data.getStringExtra("currentTermNumber");
String currentTermText = "";
if (currentYear != null && !currentYear.isEmpty() && currentTerm != null && !currentTerm.isEmpty()) {
currentTermText = "当前为" + currentYear + "学年 第" + currentTerm + "学期";
}
ArrayList<String> termWarnings = data.getStringArrayListExtra("termWarnings");
String electiveSummary = data.getStringExtra("electiveSummary");
String electiveInProgressCount = data.getStringExtra("electiveInProgressCount");
String electiveInProgressCredits = data.getStringExtra("electiveInProgressCredits");
if (items != null && items.length > 0) {
Log.d(TAG, "Successfully received " + items.length + " grade items");
// 保存所有数据
allGradeItems.clear();
for (WebViewActivity.GradeItem item : items) {
allGradeItems.add(item);
}
// 根据当前分组模式显示数据
updateGradeData();
showDataView(studentName, overallGPA, currentTermText, electiveSummary, electiveInProgressCount, electiveInProgressCredits, termWarnings);
// 保存数据到SharedPreferences
saveData(studentName, overallGPA, currentTermText, electiveSummary, termWarnings);
Toast.makeText(getContext(), "成功获取 " + items.length + " 条学业数据", Toast.LENGTH_SHORT).show();
} else {
Log.d(TAG, "No grade items found in data");
Toast.makeText(getContext(), "未获取到学业数据", Toast.LENGTH_SHORT).show();
}
} else {
Log.d(TAG, "No gradeItems extra found in intent");
}
} else if (requestCode == REQUEST_CODE_WEBVIEW && resultCode == android.app.Activity.RESULT_CANCELED) {
Log.d(TAG, "WebView activity returned RESULT_CANCELED");
Toast.makeText(getContext(), "数据解析失败", Toast.LENGTH_SHORT).show();
} else {
Log.d(TAG, "Unexpected result - requestCode: " + requestCode + ", resultCode: " + resultCode);
}
}
private void showDataView(String studentName, String overallGPA, String currentTermText, String electiveSummary, String electiveInProgressCount, String electiveInProgressCredits, ArrayList<String> termWarnings) {
// 隐藏按钮,显示学生信息和分组按钮
btnLoginExport.setVisibility(View.GONE);
layoutStudentInfo.setVisibility(View.VISIBLE);
layoutGroupButtons.setVisibility(View.VISIBLE);
// 动态设置学生姓名和GPA如果有的话
if (studentName != null && !studentName.isEmpty()) {
tvStudentName.setText("学生姓名:" + studentName);
}
String initialOverall = (overallGPA != null && !overallGPA.isEmpty()) ? overallGPA : computeOverallGpa(allGradeItems);
tvOverallGPA.setText("总平均绩点:" + initialOverall);
if (currentTermText != null && !currentTermText.isEmpty()) {
tvCurrentTerm.setText(currentTermText);
tvCurrentTerm.setVisibility(View.VISIBLE);
} else {
tvCurrentTerm.setText("");
tvCurrentTerm.setVisibility(View.GONE);
}
if (electiveSummary != null && !electiveSummary.isEmpty()) {
String formattedSummary = formatElectiveSummary(electiveSummary);
if (electiveInProgressCount != null && !electiveInProgressCount.isEmpty()) {
StringBuilder sb = new StringBuilder(formattedSummary);
sb.append("\n在修").append(electiveInProgressCount).append("门");
if (electiveInProgressCredits != null && !electiveInProgressCredits.isEmpty()) {
sb.append(",共计在修学分").append(electiveInProgressCredits);
}
formattedSummary = sb.toString();
}
tvTermSummary.setText(formattedSummary);
tvTermSummary.setVisibility(View.VISIBLE);
} else {
tvTermSummary.setText("");
tvTermSummary.setVisibility(View.GONE);
}
if (termWarnings != null && !termWarnings.isEmpty()) {
SpannableStringBuilder builder = new SpannableStringBuilder();
String safeMessage = "本学期的必修课已检查确定无漏选";
for (int i = 0; i < termWarnings.size(); i++) {
String warning = termWarnings.get(i);
int start = builder.length();
builder.append(warning);
int end = builder.length();
int color = warning.equals(safeMessage)
? ContextCompat.getColor(requireContext(), android.R.color.holo_green_dark)
: ContextCompat.getColor(requireContext(), android.R.color.holo_red_dark);
builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (i < termWarnings.size() - 1) {
builder.append("\n");
}
}
tvTermWarnings.setText(builder);
tvTermWarnings.setVisibility(View.VISIBLE);
} else {
tvTermWarnings.setText("");
tvTermWarnings.setVisibility(View.GONE);
}
// 显示数据列表
updateCardVisibility();
tvEmptyState.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
}
/**
*
*/
private void updateGradeData() {
if (currentGroupMode == GroupMode.BY_STATUS) {
displayByStatus();
} else {
displayByTermGroups();
}
}
/**
* /
*/
private void displayByStatus() {
List<WebViewActivity.GradeItem> completed = new ArrayList<>();
List<WebViewActivity.GradeItem> incomplete = new ArrayList<>();
for (WebViewActivity.GradeItem item : allGradeItems) {
if (item.score != null && !item.score.trim().isEmpty()) {
completed.add(item);
} else {
incomplete.add(item);
}
}
adapterCompleted.updateData(completed);
adapterIncomplete.updateData(incomplete);
tvCompletedCount.setText("(" + completed.size() + ")");
tvIncompleteCount.setText("(" + incomplete.size() + ")");
// 显示状态容器,隐藏学期容器
layoutStatusGroups.setVisibility(View.VISIBLE);
scrollViewTermGroups.setVisibility(View.GONE);
updateCardVisibility();
}
/**
*
*/
private void updateCardVisibility() {
boolean hasCompleted = adapterCompleted.getItemCount() > 0;
boolean hasIncomplete = adapterIncomplete.getItemCount() > 0;
cardCompleted.setVisibility(hasCompleted ? View.VISIBLE : View.GONE);
cardIncomplete.setVisibility(hasIncomplete ? View.VISIBLE : View.GONE);
if (hasCompleted) {
contentCompleted.setVisibility(isCompletedExpanded ? View.VISIBLE : View.GONE);
iconCompleted.setText(isCompletedExpanded ? "▼" : "▶");
}
if (hasIncomplete) {
contentIncomplete.setVisibility(isIncompleteExpanded ? View.VISIBLE : View.GONE);
iconIncomplete.setText(isIncompleteExpanded ? "▼" : "▶");
}
}
/**
*
*/
private void switchGroupMode(GroupMode newMode) {
if (currentGroupMode == newMode) return;
currentGroupMode = newMode;
// 更新按钮样式
if (currentGroupMode == GroupMode.BY_STATUS) {
btnGroupByStatus.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getResources().getColor(android.R.color.black)));
btnGroupByTerm.setBackgroundTintList(android.content.res.ColorStateList.valueOf(0xFF424242));
} else {
btnGroupByStatus.setBackgroundTintList(android.content.res.ColorStateList.valueOf(0xFF424242));
btnGroupByTerm.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getResources().getColor(android.R.color.black)));
}
// 重新显示数据
if (!allGradeItems.isEmpty()) {
updateGradeData();
}
}
/**
*
*/
private void displayByTermGroups() {
// 隐藏状态容器,显示学期容器
layoutStatusGroups.setVisibility(View.GONE);
scrollViewTermGroups.setVisibility(View.VISIBLE);
containerTermGroups.removeAllViews();
// 按学期分组使用TreeMap自动排序
java.util.Map<String, List<WebViewActivity.GradeItem>> termGroups = new java.util.TreeMap<>();
for (WebViewActivity.GradeItem item : allGradeItems) {
String termKey = item.year + " 第" + item.term + "学期";
if (item.year == null || item.year.isEmpty()) {
termKey = "未知学期";
}
if (!termGroups.containsKey(termKey)) {
termGroups.put(termKey, new ArrayList<>());
}
termGroups.get(termKey).add(item);
}
// 为每个学期创建卡片
LayoutInflater inflater = LayoutInflater.from(getContext());
for (java.util.Map.Entry<String, List<WebViewActivity.GradeItem>> entry : termGroups.entrySet()) {
String termName = entry.getKey();
List<WebViewActivity.GradeItem> termItems = entry.getValue();
createTermCard(termName, termItems);
}
}
/**
*
*/
private void createTermCard(String termName, List<WebViewActivity.GradeItem> termItems) {
// 创建卡片
androidx.cardview.widget.CardView termCard = new androidx.cardview.widget.CardView(getContext());
LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
cardParams.setMargins(0, 0, 0, 24);
termCard.setLayoutParams(cardParams);
termCard.setRadius(16);
termCard.setCardElevation(8);
// 卡片内容容器
LinearLayout cardContent = new LinearLayout(getContext());
cardContent.setOrientation(LinearLayout.VERTICAL);
cardContent.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
// 标题区域(可点击折叠/展开)
LinearLayout header = new LinearLayout(getContext());
header.setOrientation(LinearLayout.HORIZONTAL);
LinearLayout.LayoutParams headerParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
headerParams.setMargins(32, 32, 32, 32);
header.setLayoutParams(headerParams);
header.setClickable(true);
header.setFocusable(true);
header.setBackgroundResource(android.R.drawable.list_selector_background);
// 学期名称
TextView termTitle = new TextView(getContext());
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1.0f
);
termTitle.setLayoutParams(titleParams);
termTitle.setText(termName);
termTitle.setTextSize(18);
termTitle.setTextColor(getResources().getColor(android.R.color.black));
termTitle.setTypeface(null, android.graphics.Typeface.BOLD);
// 课程数量
TextView countText = new TextView(getContext());
countText.setText("(" + termItems.size() + ")");
countText.setTextSize(16);
countText.setTextColor(getResources().getColor(android.R.color.darker_gray));
LinearLayout.LayoutParams countParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
countParams.setMargins(16, 0, 16, 0);
countText.setLayoutParams(countParams);
// 折叠图标
TextView icon = new TextView(getContext());
icon.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
boolean isExpanded = termExpandedState.getOrDefault(termName, true);
icon.setText(isExpanded ? "▼" : "▶");
icon.setTextSize(14);
icon.setTextColor(getResources().getColor(android.R.color.darker_gray));
header.addView(termTitle);
header.addView(countText);
header.addView(icon);
// RecyclerView容器
LinearLayout contentContainer = new LinearLayout(getContext());
contentContainer.setOrientation(LinearLayout.VERTICAL);
contentContainer.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
contentContainer.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
// 创建RecyclerView
RecyclerView recyclerView = new RecyclerView(getContext());
LinearLayout.LayoutParams recyclerParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
recyclerParams.setMargins(16, 0, 16, 16);
recyclerView.setLayoutParams(recyclerParams);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
GradeAdapter termAdapter = new GradeAdapter();
termAdapter.updateData(termItems);
termAdapter.setOnItemUpdatedListener(this::updateOverallGPA);
recyclerView.setAdapter(termAdapter);
contentContainer.addView(recyclerView);
// 设置点击事件
header.setOnClickListener(v -> {
boolean expanded = contentContainer.getVisibility() == View.VISIBLE;
contentContainer.setVisibility(expanded ? View.GONE : View.VISIBLE);
icon.setText(expanded ? "▶" : "▼");
termExpandedState.put(termName, !expanded);
});
cardContent.addView(header);
cardContent.addView(contentContainer);
termCard.addView(cardContent);
containerTermGroups.addView(termCard);
}
private String formatElectiveSummary(String summary) {
if (summary == null) return "";
return summary
.replace("\u00A0", " ")
.replace("&nbsp;", " ")
.trim();
}
private String computeOverallGpa(List<WebViewActivity.GradeItem> items) {
if (items == null || items.isEmpty()) return "0";
double totalWeighted = 0.0;
double totalCredits = 0.0;
for (WebViewActivity.GradeItem it : items) {
if (it == null) continue;
// 仅统计成绩非空的课程(包括用户手动输入的)
if (it.score != null && !it.score.trim().isEmpty()) {
double credit = 0.0;
double gpa = 0.0;
try { credit = Double.parseDouble(it.credit != null ? it.credit : "0"); } catch (Exception ignored) {}
try { gpa = Double.parseDouble(it.gpa != null && !it.gpa.isEmpty() ? it.gpa : "0"); } catch (Exception ignored) {}
totalWeighted += credit * gpa;
totalCredits += credit;
}
}
if (totalCredits <= 0.0) return "0";
double overall = totalWeighted / totalCredits;
// 保留两位小数
return String.format(java.util.Locale.getDefault(), "%.2f", overall);
}
private void showEmptyState() {
// 显示按钮,隐藏学生信息和分组按钮
btnLoginExport.setVisibility(View.VISIBLE);
layoutStudentInfo.setVisibility(View.GONE);
layoutGroupButtons.setVisibility(View.GONE);
// 隐藏所有容器
layoutStatusGroups.setVisibility(View.GONE);
scrollViewTermGroups.setVisibility(View.GONE);
cardCompleted.setVisibility(View.GONE);
cardIncomplete.setVisibility(View.GONE);
// 显示空状态
tvEmptyState.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
}
private void showLoading() {
layoutStatusGroups.setVisibility(View.GONE);
scrollViewTermGroups.setVisibility(View.GONE);
tvEmptyState.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
}
/**
* SharedPreferences
*/
private void saveData(String studentName, String overallGPA, String currentTerm,
String termSummary, ArrayList<String> termWarnings) {
try {
SharedPreferences prefs = getActivity().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
// 使用Gson保存课程列表
Gson gson = new Gson();
String gradeItemsJson = gson.toJson(allGradeItems);
editor.putString(KEY_GRADE_ITEMS, gradeItemsJson);
// 保存学生信息
editor.putString(KEY_STUDENT_NAME, studentName != null ? studentName : "");
editor.putString(KEY_OVERALL_GPA, overallGPA != null ? overallGPA : "");
editor.putString(KEY_CURRENT_TERM, currentTerm != null ? currentTerm : "");
editor.putString(KEY_TERM_SUMMARY, termSummary != null ? termSummary : "");
// 保存警告列表
if (termWarnings != null && !termWarnings.isEmpty()) {
String warningsJson = gson.toJson(termWarnings);
editor.putString(KEY_TERM_WARNINGS, warningsJson);
} else {
editor.putString(KEY_TERM_WARNINGS, "");
}
editor.apply();
Log.d(TAG, "数据已保存: " + allGradeItems.size() + " 门课程");
} catch (Exception e) {
Log.e(TAG, "保存数据失败: " + e.getMessage());
}
}
/**
* SharedPreferences
*/
private void restoreSavedData() {
try {
SharedPreferences prefs = getActivity().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
// 恢复课程列表
String gradeItemsJson = prefs.getString(KEY_GRADE_ITEMS, "");
if (!gradeItemsJson.isEmpty()) {
Gson gson = new Gson();
Type listType = new TypeToken<List<WebViewActivity.GradeItem>>(){}.getType();
List<WebViewActivity.GradeItem> savedItems = gson.fromJson(gradeItemsJson, listType);
if (savedItems != null && !savedItems.isEmpty()) {
allGradeItems.clear();
allGradeItems.addAll(savedItems);
// 恢复学生信息
String studentName = prefs.getString(KEY_STUDENT_NAME, "");
String overallGPA = prefs.getString(KEY_OVERALL_GPA, "");
String currentTerm = prefs.getString(KEY_CURRENT_TERM, "");
String termSummary = prefs.getString(KEY_TERM_SUMMARY, "");
// 恢复警告列表
String warningsJson = prefs.getString(KEY_TERM_WARNINGS, "");
ArrayList<String> termWarnings = new ArrayList<>();
if (!warningsJson.isEmpty()) {
Type warningsType = new TypeToken<ArrayList<String>>(){}.getType();
termWarnings = gson.fromJson(warningsJson, warningsType);
}
// 显示数据
updateGradeData();
showDataView(studentName, overallGPA, currentTerm, termSummary, "", "", termWarnings);
Log.d(TAG, "数据已恢复: " + allGradeItems.size() + " 门课程");
return;
}
}
} catch (Exception e) {
Log.e(TAG, "恢复数据失败: " + e.getMessage());
}
// 如果没有保存的数据,显示空状态
showEmptyState();
}
}

@ -0,0 +1,62 @@
package com.example.myapplication;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView;
public class HomeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
// 获取四个功能卡片
CardView cardTimetable = findViewById(R.id.card_timetable);
CardView cardNotes = findViewById(R.id.card_notes);
CardView cardGrades = findViewById(R.id.card_grades);
CardView cardCampus = findViewById(R.id.card_campus);
// 课表按钮点击事件
cardTimetable.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(HomeActivity.this, MainActivity.class);
intent.putExtra("fragment", "timetable");
startActivity(intent);
}
});
// 笔记按钮点击事件
cardNotes.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(HomeActivity.this, MainActivity.class);
intent.putExtra("fragment", "notes");
startActivity(intent);
}
});
// 成绩按钮点击事件
cardGrades.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(HomeActivity.this, MainActivity.class);
intent.putExtra("fragment", "grades");
startActivity(intent);
}
});
// 校园导航按钮点击事件
cardCampus.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(HomeActivity.this, MainActivity.class);
intent.putExtra("fragment", "campus");
startActivity(intent);
}
});
}
}

@ -0,0 +1,92 @@
package com.example.myapplication;
import java.io.Serializable;
public class Location implements Serializable {
private String name; // 位置名称
private String description; // 位置描述
private double latitude; // 纬度
private double longitude; // 经度
private String category; // 分类(教学楼、图书馆、食堂等)
private String building; // 建筑名称
private String floor; // 楼层
private String room; // 房间号
public Location() {}
public Location(String name, String description, String category) {
this.name = name;
this.description = description;
this.category = category;
// 模拟坐标(实际应用中应该是真实坐标)
this.latitude = 39.9042 + (Math.random() - 0.5) * 0.01;
this.longitude = 116.4074 + (Math.random() - 0.5) * 0.01;
}
public Location(String name, String description, String category, String building) {
this(name, description, category);
this.building = building;
}
// 计算与另一个位置的距离(简化版本,实际应使用地理计算)
public double calculateDistance(Location other) {
if (other == null) return 0;
double deltaLat = this.latitude - other.latitude;
double deltaLng = this.longitude - other.longitude;
// 简化的距离计算实际应使用Haversine公式
double distance = Math.sqrt(deltaLat * deltaLat + deltaLng * deltaLng) * 111000; // 转换为米
return Math.round(distance * 10) / 10.0; // 保留一位小数
}
// 估算步行时间(分钟)
public int calculateWalkTime(Location other) {
double distance = calculateDistance(other);
// 假设步行速度为5km/h = 83.33m/min
return (int) Math.ceil(distance / 83.33);
}
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public double getLatitude() { return latitude; }
public void setLatitude(double latitude) { this.latitude = latitude; }
public double getLongitude() { return longitude; }
public void setLongitude(double longitude) { this.longitude = longitude; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getBuilding() { return building; }
public void setBuilding(String building) { this.building = building; }
public String getFloor() { return floor; }
public void setFloor(String floor) { this.floor = floor; }
public String getRoom() { return room; }
public void setRoom(String room) { this.room = room; }
@Override
public String toString() {
return name + (building != null ? " (" + building + ")" : "");
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Location location = (Location) obj;
return name != null ? name.equals(location.name) : location.name == null;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
}

@ -0,0 +1,69 @@
package com.example.myapplication;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
/**
*
*
*/
public class LocationDetailsActivity extends AppCompatActivity {
private TextView tvLocationName, tvLocationDescription, tvLocationCoords, tvNearbyInfo;
private Button btnNavigateHere, btnAddToFavorites, btnBack;
private AmapLocation location;
private CampusLocationManager locationManager;
private MapHelper mapHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(createLocationDetailsLayout());
// 从Intent获取位置信息
Intent intent = getIntent();
String locationName = intent.getStringExtra("location_name");
initServices();
loadLocationData(locationName);
setupViews();
setupListeners();
}
private int createLocationDetailsLayout() {
// 由于我们没有专门的布局文件,使用程序化布局
// 在实际项目中应该创建专门的XML布局文件
return android.R.layout.activity_list_item;
}
private void initServices() {
locationManager = new CampusLocationManager(this);
mapHelper = new MapHelper(this);
}
private void loadLocationData(String locationName) {
location = locationManager.findLocationByName(locationName);
if (location == null) {
Toast.makeText(this, "位置信息未找到", Toast.LENGTH_SHORT).show();
finish();
}
}
private void setupViews() {
// 初始化视图组件
// 在真实项目中这些应该通过findViewById获取
if (location != null) {
setTitle(location.getName());
}
}
private void setupListeners() {
// 设置按钮监听器
}
}

@ -0,0 +1,61 @@
package com.example.myapplication;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
// Configure window for better OpenGL rendering
Window window = getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
setContentView(R.layout.activity_main);
// 从Intent获取要显示的页面
String fragmentType = getIntent().getStringExtra("fragment");
if (savedInstanceState == null) {
Fragment fragment = null;
if ("notes".equals(fragmentType)) {
fragment = new NotesFragment();
} else if ("grades".equals(fragmentType)) {
fragment = new GradesFragment();
} else if ("campus".equals(fragmentType)) {
fragment = new CampusNavFragmentSimplified();
} else {
// 默认显示课表页面
fragment = new TimetableFragment();
}
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit();
}
} catch (Exception e) {
Toast.makeText(this, "应用初始化失败", Toast.LENGTH_LONG).show();
e.printStackTrace();
}
}
@Override
public void onBackPressed() {
// 返回主页面
finish();
}
}

@ -0,0 +1,196 @@
package com.example.myapplication;
import android.content.Context;
import android.util.Log;
/**
*
*
*/
public class MapHelper {
private static final String TAG = "MapHelper";
private Context context;
private boolean isMapAvailable = false;
public MapHelper(Context context) {
this.context = context;
initializeMapCapabilities();
}
/**
*
*/
private void initializeMapCapabilities() {
try {
// 检查高德地图SDK是否可用
Class.forName("com.amap.api.maps.AMap");
isMapAvailable = true;
Log.i(TAG, "高德地图SDK可用");
} catch (ClassNotFoundException e) {
isMapAvailable = false;
Log.w(TAG, "高德地图SDK不可用使用简化模式");
}
}
/**
*
*/
public boolean isMapAvailable() {
return isMapAvailable;
}
/**
*
*/
public String getMapStatus() {
if (isMapAvailable) {
return "🗺️ 完整地图功能已启用";
} else {
return "📱 简化导航模式 (地图功能集成中...)";
}
}
/**
*
*/
public String[] getLocationSuggestions(String query) {
if (query == null || query.trim().isEmpty()) {
return new String[]{
"🏫 试试搜索:图书馆",
"🍽️ 试试搜索:食堂",
"🏠 试试搜索:宿舍",
"🔬 试试搜索:实验楼"
};
}
// 基础的搜索建议逻辑
if (query.contains("图书")) {
return new String[]{"主图书馆", "理工图书馆", "电子阅览室"};
} else if (query.contains("食堂")) {
return new String[]{"第一食堂", "第二食堂", "清真食堂", "教工食堂"};
} else if (query.contains("宿舍")) {
return new String[]{"男生1号楼", "女生1号楼", "研究生公寓"};
} else {
return new String[]{"搜索校园位置...", "使用分类按钮浏览"};
}
}
/**
* GPS
*/
public LocationInfo getCurrentLocation() {
return new LocationInfo(
"校门口",
"学校正门入口处",
39.906016,
116.395312,
System.currentTimeMillis()
);
}
/**
*
*/
public static class LocationInfo {
private String name;
private String description;
private double latitude;
private double longitude;
private long timestamp;
public LocationInfo(String name, String description, double latitude, double longitude, long timestamp) {
this.name = name;
this.description = description;
this.latitude = latitude;
this.longitude = longitude;
this.timestamp = timestamp;
}
// Getters
public String getName() { return name; }
public String getDescription() { return description; }
public double getLatitude() { return latitude; }
public double getLongitude() { return longitude; }
public long getTimestamp() { return timestamp; }
public String getFormattedLocation() {
return String.format("📍 %s\n📝 %s\n🌍 %.6f, %.6f",
name, description, latitude, longitude);
}
}
/**
*
*/
public String generateNavigationText(AmapLocation from, AmapLocation to) {
if (from == null || to == null) {
return "⚠️ 位置信息不完整";
}
double distance = from.calculateDistance(to);
int walkTime = from.calculateWalkTime(to);
StringBuilder guide = new StringBuilder();
guide.append("🚶 步行导航指引\n\n");
guide.append(String.format("📍 起点:%s\n", from.getName()));
guide.append(String.format("🎯 终点:%s\n", to.getName()));
guide.append(String.format("📏 距离:约 %.0f 米\n", distance));
guide.append(String.format("⏰ 预计:约 %d 分钟\n\n", walkTime));
// 根据距离生成简单的导航建议
if (distance < 100) {
guide.append("💡 距离很近,步行即可到达");
} else if (distance < 500) {
guide.append("💡 距离适中,建议步行前往");
} else if (distance < 1000) {
guide.append("💡 距离较远,可考虑骑行或步行");
} else {
guide.append("💡 距离较远,建议选择合适的交通方式");
}
return guide.toString();
}
/**
*
*/
public String getLocationTypeIcon(String category) {
switch (category) {
case "图书馆": return "📚";
case "食堂": return "🍽️";
case "宿舍": return "🏠";
case "教学楼": return "🏫";
case "实验楼": return "🔬";
case "体育设施": return "🏃";
case "行政办公": return "🏢";
case "生活服务": return "🛒";
case "休闲娱乐": return "🎭";
default: return "📍";
}
}
/**
*
*/
public String[] getRequiredPermissions() {
return new String[]{
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.INTERNET"
};
}
/**
*
*/
public String getIntegrationProgress() {
return "🚀 地图集成进度:\n" +
"✅ 基础框架 (100%)\n" +
"✅ 位置数据库 (100%)\n" +
"✅ 搜索功能 (100%)\n" +
"🔄 地图显示 (准备中...)\n" +
"⏳ GPS定位 (开发中...)\n" +
"⏳ 实时导航 (计划中...)";
}
}

@ -0,0 +1,95 @@
package com.example.myapplication;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class Note implements Serializable {
private String id;
private String title; // 笔记标题
private String content; // 笔记内容
private String courseId; // 关联的课程ID
private String courseName; // 关联的课程名称
private long createTime; // 创建时间
private long modifyTime; // 修改时间
private List<String> imagePaths; // 图片路径列表
public Note() {
this.id = String.valueOf(System.currentTimeMillis());
this.createTime = System.currentTimeMillis();
this.modifyTime = System.currentTimeMillis();
this.imagePaths = new ArrayList<>();
}
public Note(String title, String content) {
this();
this.title = title;
this.content = content;
}
public Note(String title, String content, String courseId, String courseName) {
this(title, content);
this.courseId = courseId;
this.courseName = courseName;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) {
this.title = title;
this.modifyTime = System.currentTimeMillis();
}
public String getContent() { return content; }
public void setContent(String content) {
this.content = content;
this.modifyTime = System.currentTimeMillis();
}
public String getCourseId() { return courseId; }
public void setCourseId(String courseId) {
this.courseId = courseId;
this.modifyTime = System.currentTimeMillis();
}
public String getCourseName() { return courseName; }
public void setCourseName(String courseName) {
this.courseName = courseName;
this.modifyTime = System.currentTimeMillis();
}
public long getCreateTime() { return createTime; }
public void setCreateTime(long createTime) { this.createTime = createTime; }
public long getModifyTime() { return modifyTime; }
public void setModifyTime(long modifyTime) { this.modifyTime = modifyTime; }
public List<String> getImagePaths() { return imagePaths; }
public void setImagePaths(List<String> imagePaths) {
this.imagePaths = imagePaths != null ? imagePaths : new ArrayList<>();
this.modifyTime = System.currentTimeMillis();
}
public void addImagePath(String imagePath) {
if (this.imagePaths == null) {
this.imagePaths = new ArrayList<>();
}
this.imagePaths.add(imagePath);
this.modifyTime = System.currentTimeMillis();
}
public void removeImagePath(String imagePath) {
if (this.imagePaths != null) {
this.imagePaths.remove(imagePath);
this.modifyTime = System.currentTimeMillis();
}
}
@Override
public String toString() {
return title + (courseName != null ? " (" + courseName + ")" : "");
}
}

@ -0,0 +1,451 @@
package com.example.myapplication;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class NoteEditorActivity extends AppCompatActivity {
public static final String EXTRA_NOTE_ID = "noteId";
private static final String TAG = "NoteEditorActivity";
private static final int REQUEST_CODE_PICK_IMAGE = 1001;
private static final int REQUEST_CODE_TAKE_PHOTO = 1002;
private static final int REQUEST_CODE_PERMISSION = 1003;
private EditText etTitle;
private WebView webViewContent;
private Spinner spinnerCourse;
private Button btnSave;
private Button btnCancel;
private Button btnInsertImage;
private DataManager dataManager;
private Note editingNote;
private List<Course> courses;
private File currentPhotoFile;
private ValueCallback<Uri> imageCallback;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_note_editor);
dataManager = new DataManager(this);
initViews();
setupWebView();
loadCourses();
loadNoteFromIntent();
setupListeners();
}
private void initViews() {
etTitle = findViewById(R.id.et_title);
webViewContent = findViewById(R.id.webview_content);
spinnerCourse = findViewById(R.id.spinner_course);
btnSave = findViewById(R.id.btn_save);
btnCancel = findViewById(R.id.btn_cancel);
btnInsertImage = findViewById(R.id.btn_insert_image);
TextView btnClose = findViewById(R.id.btn_close);
btnClose.setOnClickListener(v -> finish());
}
@SuppressLint("SetJavaScriptEnabled")
private void setupWebView() {
WebSettings settings = webViewContent.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
webViewContent.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// 页面加载完成后,加载笔记内容
loadContentToWebView();
}
});
webViewContent.setWebChromeClient(new WebChromeClient());
// 添加JavaScript接口
webViewContent.addJavascriptInterface(new WebAppInterface(), "Android");
// 加载富文本编辑器HTML
loadEditorHtml();
}
private void loadEditorHtml() {
String html = "<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
"<style>\n" +
"body { margin: 16px; font-family: sans-serif; }\n" +
"#editor { min-height: 300px; border: 1px solid #ddd; padding: 12px; outline: none; background: white; }\n" +
"img { max-width: 100%; height: auto; margin: 8px 0; display: block; }\n" +
"</style>\n" +
"</head>\n" +
"<body>\n" +
"<div id=\"editor\" contenteditable=\"true\"></div>\n" +
"<script>\n" +
"function insertImage(imagePath) {\n" +
" var editor = document.getElementById('editor');\n" +
" var img = document.createElement('img');\n" +
" img.src = 'file://' + imagePath;\n" +
" img.style.maxWidth = '100%';\n" +
" img.style.height = 'auto';\n" +
" img.style.margin = '8px 0';\n" +
" img.style.display = 'block';\n" +
" editor.appendChild(img);\n" +
" editor.appendChild(document.createElement('br'));\n" +
"}\n" +
"function getContent() {\n" +
" return document.getElementById('editor').innerHTML;\n" +
"}\n" +
"function setContent(html) {\n" +
" document.getElementById('editor').innerHTML = html || '';\n" +
"}\n" +
"function execCommand(command, value) {\n" +
" document.execCommand(command, false, value);\n" +
" document.getElementById('editor').focus();\n" +
"}\n" +
"</script>\n" +
"</body>\n" +
"</html>";
webViewContent.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null);
}
private void loadContentToWebView() {
if (editingNote != null) {
String content = editingNote.getContent() != null ? editingNote.getContent() : "";
// 如果内容为空但图片路径存在构建HTML
if (content.isEmpty() && editingNote.getImagePaths() != null && !editingNote.getImagePaths().isEmpty()) {
StringBuilder htmlContent = new StringBuilder();
for (String imagePath : editingNote.getImagePaths()) {
htmlContent.append("<img src=\"file://").append(imagePath).append("\" style=\"max-width: 100%; height: auto; margin: 8px 0; display: block;\" /><br>");
}
content = htmlContent.toString();
}
// 创建final副本用于lambda表达式
final String finalContent = content;
// 延迟加载内容确保WebView已准备好
webViewContent.postDelayed(() -> {
if (!finalContent.isEmpty()) {
// 使用JSON转义
String jsonContent = org.json.JSONObject.quote(finalContent);
webViewContent.evaluateJavascript("setContent(" + jsonContent + ");", null);
}
}, 300);
}
}
private void loadCourses() {
courses = dataManager.getCourses();
List<String> courseNames = new ArrayList<>();
courseNames.add("不关联课程");
for (Course c : courses) {
courseNames.add(c.getName());
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, courseNames);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerCourse.setAdapter(adapter);
}
private void loadNoteFromIntent() {
String noteId = getIntent().getStringExtra(EXTRA_NOTE_ID);
if (noteId != null && !noteId.isEmpty()) {
List<Note> all = dataManager.getNotes();
for (Note n : all) {
if (noteId.equals(n.getId())) {
editingNote = n;
break;
}
}
}
if (editingNote == null) {
editingNote = new Note();
}
etTitle.setText(editingNote.getTitle() != null ? editingNote.getTitle() : "");
}
private void setupListeners() {
btnSave.setOnClickListener(v -> doSave());
btnCancel.setOnClickListener(v -> finish());
btnInsertImage.setOnClickListener(v -> showImageSourceDialog());
}
private void showImageSourceDialog() {
String[] options = {"从相册选择", "拍照"};
new AlertDialog.Builder(this)
.setTitle("选择图片")
.setItems(options, (dialog, which) -> {
if (which == 0) {
pickImageFromGallery();
} else if (which == 1) {
takePhoto();
}
})
.show();
}
private void pickImageFromGallery() {
if (checkReadPermission()) {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
}
}
private void takePhoto() {
if (checkCameraPermission()) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(getPackageManager()) != null) {
try {
currentPhotoFile = createImageFile();
if (currentPhotoFile != null) {
Uri photoURI = FileProvider.getUriForFile(this,
getApplicationContext().getPackageName() + ".fileprovider",
currentPhotoFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO);
}
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "创建图片文件失败", Toast.LENGTH_SHORT).show();
}
}
}
}
private boolean checkReadPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_MEDIA_IMAGES},
REQUEST_CODE_PERMISSION);
return false;
}
} else {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_CODE_PERMISSION);
return false;
}
}
return true;
}
private boolean checkCameraPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA},
REQUEST_CODE_PERMISSION);
return false;
}
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限已授予
} else {
Toast.makeText(this, "需要权限才能插入图片", Toast.LENGTH_SHORT).show();
}
}
}
private File createImageFile() throws IOException {
String imageFileName = "NOTE_IMG_" + System.currentTimeMillis();
File storageDir = new File(getFilesDir(), "note_images");
if (!storageDir.exists()) {
storageDir.mkdirs();
}
return File.createTempFile(imageFileName, ".jpg", storageDir);
}
private String saveImageFromUri(Uri uri) {
try {
InputStream inputStream = getContentResolver().openInputStream(uri);
if (inputStream != null) {
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
inputStream.close();
if (bitmap != null) {
File imageFile = createImageFile();
FileOutputStream outputStream = new FileOutputStream(imageFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
outputStream.close();
return imageFile.getAbsolutePath();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_CODE_PICK_IMAGE && data != null) {
Uri selectedImage = data.getData();
if (selectedImage != null) {
String imagePath = saveImageFromUri(selectedImage);
if (imagePath != null) {
editingNote.addImagePath(imagePath);
// 插入图片到编辑器
String jsCode = "insertImage(" + org.json.JSONObject.quote(imagePath) + ");";
webViewContent.evaluateJavascript(jsCode, null);
} else {
Toast.makeText(this, "保存图片失败", Toast.LENGTH_SHORT).show();
}
}
} else if (requestCode == REQUEST_CODE_TAKE_PHOTO && currentPhotoFile != null) {
if (currentPhotoFile.exists()) {
editingNote.addImagePath(currentPhotoFile.getAbsolutePath());
// 插入图片到编辑器
String jsCode = "insertImage(" + org.json.JSONObject.quote(currentPhotoFile.getAbsolutePath()) + ");";
webViewContent.evaluateJavascript(jsCode, null);
} else {
Toast.makeText(this, "拍照失败", Toast.LENGTH_SHORT).show();
}
}
}
}
private void doSave() {
String title = etTitle.getText().toString().trim();
// 从WebView获取内容
webViewContent.evaluateJavascript("(function() { return document.getElementById('editor').innerHTML; })();",
new ValueCallback<String>() {
@Override
public void onReceiveValue(String htmlContent) {
// 移除JSON字符串的引号和转义
if (htmlContent != null && htmlContent.length() > 2) {
htmlContent = htmlContent.substring(1, htmlContent.length() - 1);
htmlContent = htmlContent.replace("\\\"", "\"");
htmlContent = htmlContent.replace("\\n", "\n");
htmlContent = htmlContent.replace("\\/", "/");
// 还原 WebView 返回的 Unicode 转义(例如 \u003C -> <
htmlContent = htmlContent
.replace("\\u003C", "<").replace("\\u003c", "<")
.replace("\\u003E", ">").replace("\\u003e", ">")
.replace("\\u0026", "&");
}
// 提取图片路径
List<String> imagePaths = new ArrayList<>();
if (htmlContent != null && htmlContent.contains("<img")) {
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("file://([^\"]+)");
java.util.regex.Matcher matcher = pattern.matcher(htmlContent);
while (matcher.find()) {
String path = matcher.group(1);
if (path != null && !path.isEmpty()) {
imagePaths.add(path);
}
}
}
if (title.isEmpty() && (htmlContent == null || htmlContent.trim().isEmpty()) && imagePaths.isEmpty()) {
runOnUiThread(() -> Toast.makeText(NoteEditorActivity.this,
"请输入标题、内容或插入图片", Toast.LENGTH_SHORT).show());
return;
}
editingNote.setTitle(title);
editingNote.setContent(htmlContent != null ? htmlContent : "");
editingNote.setImagePaths(imagePaths);
int pos = spinnerCourse.getSelectedItemPosition();
if (pos <= 0) {
editingNote.setCourseId("");
editingNote.setCourseName("");
} else {
Course c = courses.get(pos - 1);
editingNote.setCourseId(c.getId());
editingNote.setCourseName(c.getName());
}
// 检查是新建还是更新
List<Note> allNotes = dataManager.getNotes();
boolean exists = false;
for (Note n : allNotes) {
if (n.getId().equals(editingNote.getId())) {
exists = true;
break;
}
}
if (exists) {
dataManager.updateNote(editingNote);
} else {
dataManager.addNote(editingNote);
}
runOnUiThread(() -> {
Toast.makeText(NoteEditorActivity.this, "笔记已保存", Toast.LENGTH_SHORT).show();
finish();
});
}
});
}
public class WebAppInterface {
@JavascriptInterface
public void onContentChanged(String content) {
// 内容变化时的回调
}
}
}

@ -0,0 +1,177 @@
package com.example.myapplication;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
public class NoteReaderActivity extends AppCompatActivity {
public static final String EXTRA_NOTE_ID = "noteId";
private WebView webView;
private Note note;
private DataManager dataManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_note_reader);
dataManager = new DataManager(this);
loadNote();
initViews();
displayNote();
}
private void loadNote() {
String noteId = getIntent().getStringExtra(EXTRA_NOTE_ID);
if (noteId != null && !noteId.isEmpty()) {
for (Note n : dataManager.getNotes()) {
if (noteId.equals(n.getId())) {
note = n;
break;
}
}
}
}
private void initViews() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(note != null ? note.getTitle() : "笔记");
}
webView = findViewById(R.id.webview);
android.webkit.WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
settings.setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
webView.setWebViewClient(new WebViewClient());
}
private void displayNote() {
if (note == null) return;
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html><html><head>");
html.append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
html.append("<style>");
html.append("body { font-family: sans-serif; padding: 16px; line-height: 1.6; background: #fff; }");
html.append("h1 { color: #333; margin-bottom: 16px; font-size: 24px; }");
html.append("p { color: #666; margin-bottom: 12px; }");
html.append("img { max-width: 100%; height: auto; margin: 12px 0; border-radius: 8px; display: block; }");
html.append(".course-info { color: #999; font-size: 14px; margin-bottom: 16px; }");
html.append("</style></head><body>");
if (note.getTitle() != null && !note.getTitle().isEmpty()) {
html.append("<h1>").append(escapeHtml(note.getTitle())).append("</h1>");
}
if (note.getCourseName() != null && !note.getCourseName().isEmpty()) {
html.append("<p class=\"course-info\">课程: ").append(escapeHtml(note.getCourseName())).append("</p>");
}
// 显示内容HTML格式
String content = note.getContent() != null ? note.getContent() : "";
// 兼容历史数据:还原 Unicode 转义(例如 \\u003C -> <
content = content
.replace("\\u003C", "<").replace("\\u003c", "<")
.replace("\\u003E", ">").replace("\\u003e", ">")
.replace("\\u0026", "&");
if (!content.isEmpty()) {
// 如果内容包含HTML直接使用否则转换为HTML
if (!content.contains("<")) {
content = content.replace("\n", "<br>");
}
html.append("<div>").append(content).append("</div>");
}
// 显示图片(如果内容中没有图片,则单独显示)
if (note.getImagePaths() != null && !note.getImagePaths().isEmpty()) {
boolean hasImagesInContent = content != null && content.contains("<img");
if (!hasImagesInContent) {
for (String imagePath : note.getImagePaths()) {
java.io.File imageFile = new java.io.File(imagePath);
if (imageFile.exists()) {
html.append("<img src=\"file://").append(imagePath).append("\" />");
}
}
}
}
html.append("</body></html>");
String baseUrl = "file://" + getFilesDir().getAbsolutePath() + "/";
webView.loadDataWithBaseURL(baseUrl, html.toString(), "text/html", "UTF-8", null);
}
private String escapeHtml(String text) {
if (text == null) return "";
return text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_note_reader, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void editNote() {
if (note == null) return;
Intent intent = new Intent(this, NoteEditorActivity.class);
intent.putExtra(NoteEditorActivity.EXTRA_NOTE_ID, note.getId());
startActivity(intent);
finish();
}
private void deleteNote() {
if (note == null) return;
new android.app.AlertDialog.Builder(this)
.setTitle("删除笔记")
.setMessage("确定要删除笔记《" + note.getTitle() + "》吗?")
.setPositiveButton("删除", (dialog, which) -> {
dataManager.deleteNote(note.getId());
// 删除图片文件
if (note.getImagePaths() != null) {
for (String imagePath : note.getImagePaths()) {
try {
java.io.File file = new java.io.File(imagePath);
if (file.exists()) {
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
finish();
})
.setNegativeButton("取消", null)
.show();
}
}

@ -0,0 +1,290 @@
package com.example.myapplication;
import android.content.Context;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class NotesAdapter extends RecyclerView.Adapter<NotesAdapter.NoteViewHolder> {
private List<Note> notes;
private Context context;
private OnNoteClickListener listener;
private NoteViewHolder currentSwipedHolder; // 当前滑动的ViewHolder
public interface OnNoteClickListener {
void onNoteClick(Note note);
void onNoteLongClick(Note note);
void onEditClick(Note note);
void onDeleteClick(Note note);
void onResetOtherItems(); // 重置其他项的滑动状态
}
public NotesAdapter(Context context, List<Note> notes) {
this.context = context;
this.notes = notes;
}
public void setOnNoteClickListener(OnNoteClickListener listener) {
this.listener = listener;
}
@NonNull
@Override
public NoteViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.item_note, parent, false);
return new NoteViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull NoteViewHolder holder, int position) {
Note note = notes.get(position);
holder.bind(note);
// 重置滑动状态
holder.resetSwipeState();
}
@Override
public int getItemCount() {
return notes.size();
}
public void updateNotes(List<Note> newNotes) {
this.notes = newNotes;
notifyDataSetChanged();
}
class NoteViewHolder extends RecyclerView.ViewHolder {
TextView tvNoteTitle, tvNoteContent, tvCourseName, tvModifyTime;
CardView cardContent;
LinearLayout layoutSwipeActions;
ImageButton btnEdit, btnDelete;
private float startX = 0;
private boolean isSwiped = false;
private boolean isDragging = false; // 标记是否正在拖动
private final float revealPx; // 需要左滑露出的宽度px
public NoteViewHolder(@NonNull View itemView) {
super(itemView);
tvNoteTitle = itemView.findViewById(R.id.tv_note_title);
tvNoteContent = itemView.findViewById(R.id.tv_note_content);
tvCourseName = itemView.findViewById(R.id.tv_course_name);
tvModifyTime = itemView.findViewById(R.id.tv_modify_time);
cardContent = itemView.findViewById(R.id.card_content);
layoutSwipeActions = itemView.findViewById(R.id.layout_swipe_actions);
btnEdit = itemView.findViewById(R.id.btn_edit);
btnDelete = itemView.findViewById(R.id.btn_delete);
// 以 96dp 作为露出宽度,转换为 px
float density = itemView.getResources().getDisplayMetrics().density;
revealPx = 96f * density;
// 编辑按钮
btnEdit.setOnClickListener(v -> {
if (listener != null) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onEditClick(notes.get(position));
resetSwipeState();
}
}
});
// 删除按钮
btnDelete.setOnClickListener(v -> {
if (listener != null) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onDeleteClick(notes.get(position));
resetSwipeState();
}
}
});
// 使用 GestureDetector 处理点击和长按
final GestureDetector gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
// 单击事件
android.util.Log.d("NotesAdapter", "Card tapped");
if (listener != null && !isSwiped) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION && position < notes.size()) {
listener.onNoteClick(notes.get(position));
}
} else if (isSwiped) {
// 如果已滑动,点击恢复
resetSwipeState();
}
return true;
}
@Override
public void onLongPress(MotionEvent e) {
// 长按不做任何操作(禁用长按菜单)
}
});
// 触摸事件处理滑动
cardContent.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// 先让 GestureDetector 处理
gestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = event.getRawX();
isDragging = false;
return true; // 消费事件以接收后续事件
case MotionEvent.ACTION_MOVE:
float deltaX = event.getRawX() - startX;
// 如果横向移动超过阈值,认为是滑动
if (Math.abs(deltaX) > 20) {
if (!isDragging) {
isDragging = true;
// 请求父视图不拦截触摸事件
ViewParent parent = v.getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
// 实时跟随手指移动
if (deltaX < 0 && !isSwiped) { // 向左滑动
float translationX = Math.max(deltaX, -revealPx);
cardContent.setTranslationX(translationX);
if (translationX < -revealPx / 2f) {
layoutSwipeActions.setVisibility(View.VISIBLE);
}
} else if (deltaX > 0 && isSwiped) { // 向右滑动恢复
float translationX = Math.min(deltaX, 0f);
cardContent.setTranslationX(translationX - revealPx);
if (translationX > -revealPx / 2f) {
layoutSwipeActions.setVisibility(View.GONE);
}
}
}
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
ViewParent parent = v.getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(false);
}
if (isDragging) {
float finalDeltaX = event.getRawX() - startX;
if (finalDeltaX < -revealPx * 0.5f && !isSwiped) {
// 滑动足够距离,显示按钮
showSwipeActions();
} else if (!isSwiped) {
// 滑动距离不够,恢复
resetSwipeState();
} else if (finalDeltaX > revealPx * 0.3f && isSwiped) {
// 向右滑动恢复
resetSwipeState();
} else if (isSwiped) {
// 保持滑动状态
showSwipeActions();
}
isDragging = false;
}
return true;
}
return true;
}
});
// 点击空白区域恢复滑动状态
itemView.setOnClickListener(v -> {
if (isSwiped) {
resetSwipeState();
}
});
}
private void resetOtherItems() {
// 重置其他ViewHolder的滑动状态
if (listener != null) {
listener.onResetOtherItems();
}
}
private void showSwipeActions() {
// 重置其他项的滑动状态
if (currentSwipedHolder != null && currentSwipedHolder != NoteViewHolder.this) {
currentSwipedHolder.resetSwipeState();
}
currentSwipedHolder = NoteViewHolder.this;
isSwiped = true;
layoutSwipeActions.setVisibility(View.VISIBLE);
// 动画滑动到最终位置
cardContent.animate()
.translationX(-revealPx)
.setDuration(200)
.start();
}
public void resetSwipeState() {
isSwiped = false;
layoutSwipeActions.setVisibility(View.GONE);
cardContent.animate()
.translationX(0f)
.setDuration(200)
.start();
if (currentSwipedHolder == NoteViewHolder.this) {
currentSwipedHolder = null;
}
}
public void bind(Note note) {
tvNoteTitle.setText(note.getTitle() != null ? note.getTitle() : "");
// 显示内容预览去除HTML标签
String content = note.getContent() != null ? note.getContent() : "";
// 兼容历史数据:还原 Unicode 转义再做预览(例如 \\u003C -> <
content = content
.replace("\\u003C", "<").replace("\\u003c", "<")
.replace("\\u003E", ">").replace("\\u003e", ">")
.replace("\\u0026", "&");
// 简单去除HTML标签
content = content.replaceAll("<[^>]+>", "").trim();
if (content.isEmpty() && note.getImagePaths() != null && !note.getImagePaths().isEmpty()) {
content = "[包含 " + note.getImagePaths().size() + " 张图片]";
}
tvNoteContent.setText(content);
// 显示关联课程
if (note.getCourseName() != null && !note.getCourseName().isEmpty()) {
tvCourseName.setText(note.getCourseName());
tvCourseName.setVisibility(View.VISIBLE);
} else {
tvCourseName.setVisibility(View.GONE);
}
// 格式化修改时间
SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm", Locale.getDefault());
tvModifyTime.setText(sdf.format(new Date(note.getModifyTime())));
}
}
}

@ -0,0 +1,368 @@
package com.example.myapplication;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
public class NotesFragment extends Fragment {
private RecyclerView rvNotes;
private FloatingActionButton fabAddNote;
private Spinner spinnerCourseFilter;
private LinearLayout layoutEmpty;
private com.google.android.material.textfield.TextInputLayout tilNoteSearch;
private android.widget.EditText etNoteSearch;
private DataManager dataManager;
private List<Note> allNotes;
private List<Note> filteredNotes;
private List<Course> courses;
private NotesAdapter notesAdapter;
private String selectedCourseId = "";
private String searchQuery = "";
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_notes, container, false);
initViews(view);
setupData();
setupListeners();
setupRecyclerView();
setupCourseFilter();
loadNotes();
return view;
}
private void initViews(View view) {
rvNotes = view.findViewById(R.id.rv_notes);
fabAddNote = view.findViewById(R.id.fab_add_note);
spinnerCourseFilter = view.findViewById(R.id.spinner_course_filter);
layoutEmpty = view.findViewById(R.id.layout_empty);
tilNoteSearch = view.findViewById(R.id.til_note_search);
etNoteSearch = view.findViewById(R.id.et_note_search);
}
private void setupData() {
dataManager = new DataManager(getContext());
allNotes = new ArrayList<>();
filteredNotes = new ArrayList<>();
courses = dataManager.getCourses();
}
private void setupListeners() {
fabAddNote.setOnClickListener(v -> {
android.content.Intent intent = new android.content.Intent(getContext(), NoteEditorActivity.class);
startActivity(intent);
});
if (tilNoteSearch != null) {
tilNoteSearch.setEndIconOnClickListener(v -> filterNotes());
}
if (etNoteSearch != null) {
etNoteSearch.addTextChangedListener(new android.text.TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
searchQuery = s.toString().trim();
filterNotes();
}
@Override public void afterTextChanged(android.text.Editable s) {}
});
}
}
private void setupRecyclerView() {
notesAdapter = new NotesAdapter(getContext(), filteredNotes);
notesAdapter.setOnNoteClickListener(new NotesAdapter.OnNoteClickListener() {
@Override
public void onNoteClick(Note note) {
// 打开阅读模式
android.content.Intent intent = new android.content.Intent(getContext(), NoteReaderActivity.class);
intent.putExtra(NoteReaderActivity.EXTRA_NOTE_ID, note.getId());
startActivity(intent);
}
@Override
public void onNoteLongClick(Note note) {
showNoteOptions(note);
}
@Override
public void onEditClick(Note note) {
// 打开编辑模式
android.content.Intent intent = new android.content.Intent(getContext(), NoteEditorActivity.class);
intent.putExtra(NoteEditorActivity.EXTRA_NOTE_ID, note.getId());
startActivity(intent);
}
@Override
public void onDeleteClick(Note note) {
deleteNote(note);
}
@Override
public void onResetOtherItems() {
// 重置所有滑动状态
notesAdapter.notifyDataSetChanged();
}
});
rvNotes.setLayoutManager(new LinearLayoutManager(getContext()));
rvNotes.setAdapter(notesAdapter);
}
private void setupCourseFilter() {
List<String> courseNames = new ArrayList<>();
courseNames.add("全部课程");
List<String> courseIds = new ArrayList<>();
courseIds.add("");
for (Course course : courses) {
courseNames.add(course.getName());
courseIds.add(course.getId());
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(getContext(),
android.R.layout.simple_spinner_item, courseNames);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerCourseFilter.setAdapter(adapter);
spinnerCourseFilter.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
selectedCourseId = courseIds.get(position);
filterNotes();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
}
private void loadNotes() {
allNotes = dataManager.getNotes();
filterNotes();
}
private void filterNotes() {
filteredNotes.clear();
String q = searchQuery == null ? "" : searchQuery.toLowerCase();
for (Note note : allNotes) {
boolean courseOk = selectedCourseId.isEmpty() || (note.getCourseId() != null && note.getCourseId().equals(selectedCourseId));
if (!courseOk) continue;
if (q.isEmpty()) {
filteredNotes.add(note);
} else {
String title = note.getTitle() == null ? "" : note.getTitle().toLowerCase();
String content = note.getContent() == null ? "" : note.getContent().toLowerCase();
String course = note.getCourseName() == null ? "" : note.getCourseName().toLowerCase();
if (title.contains(q) || content.contains(q) || course.contains(q)) {
filteredNotes.add(note);
}
}
}
// 按修改时间倒序
java.util.Collections.sort(filteredNotes, (a, b) -> Long.compare(b.getModifyTime(), a.getModifyTime()));
notesAdapter.updateNotes(filteredNotes);
updateEmptyView();
}
private void updateEmptyView() {
if (filteredNotes.isEmpty()) {
rvNotes.setVisibility(View.GONE);
layoutEmpty.setVisibility(View.VISIBLE);
} else {
rvNotes.setVisibility(View.VISIBLE);
layoutEmpty.setVisibility(View.GONE);
}
}
private void showAddNoteDialog(Note editNote) {
View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.dialog_add_note, null);
TextView tvDialogTitle = dialogView.findViewById(R.id.tv_dialog_title);
EditText etNoteTitle = dialogView.findViewById(R.id.et_note_title);
EditText etNoteContent = dialogView.findViewById(R.id.et_note_content);
Spinner spinnerCourse = dialogView.findViewById(R.id.spinner_course);
// 设置标题
tvDialogTitle.setText(editNote == null ? "添加笔记" : "编辑笔记");
// 设置课程选择器
List<String> courseOptions = new ArrayList<>();
courseOptions.add("无关联课程");
List<String> courseIdOptions = new ArrayList<>();
courseIdOptions.add("");
for (Course course : courses) {
courseOptions.add(course.getName());
courseIdOptions.add(course.getId());
}
ArrayAdapter<String> courseAdapter = new ArrayAdapter<>(getContext(),
android.R.layout.simple_spinner_item, courseOptions);
courseAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerCourse.setAdapter(courseAdapter);
// 如果是编辑模式,填充现有数据
if (editNote != null) {
etNoteTitle.setText(editNote.getTitle());
etNoteContent.setText(editNote.getContent());
// 设置课程选择
if (editNote.getCourseId() != null) {
for (int i = 0; i < courseIdOptions.size(); i++) {
if (courseIdOptions.get(i).equals(editNote.getCourseId())) {
spinnerCourse.setSelection(i);
break;
}
}
}
}
AlertDialog dialog = new AlertDialog.Builder(getContext())
.setView(dialogView)
.create();
dialogView.findViewById(R.id.btn_cancel).setOnClickListener(v -> dialog.dismiss());
dialogView.findViewById(R.id.btn_save).setOnClickListener(v -> {
String title = etNoteTitle.getText().toString().trim();
String content = etNoteContent.getText().toString().trim();
if (title.isEmpty()) {
Toast.makeText(getContext(), "请输入笔记标题", Toast.LENGTH_SHORT).show();
return;
}
if (content.isEmpty()) {
Toast.makeText(getContext(), "请输入笔记内容", Toast.LENGTH_SHORT).show();
return;
}
String selectedCourseId = courseIdOptions.get(spinnerCourse.getSelectedItemPosition());
String selectedCourseName = "";
if (!selectedCourseId.isEmpty()) {
selectedCourseName = courseOptions.get(spinnerCourse.getSelectedItemPosition());
}
if (editNote == null) {
// 添加新笔记
Note newNote = new Note(title, content, selectedCourseId, selectedCourseName);
dataManager.addNote(newNote);
allNotes.add(newNote);
Toast.makeText(getContext(), "笔记添加成功", Toast.LENGTH_SHORT).show();
} else {
// 编辑现有笔记
editNote.setTitle(title);
editNote.setContent(content);
editNote.setCourseId(selectedCourseId);
editNote.setCourseName(selectedCourseName);
dataManager.saveNotes(allNotes);
Toast.makeText(getContext(), "笔记修改成功", Toast.LENGTH_SHORT).show();
}
filterNotes();
dialog.dismiss();
});
dialog.show();
}
private void deleteNote(Note note) {
new AlertDialog.Builder(getContext())
.setTitle("删除笔记")
.setMessage("确定要删除笔记《" + note.getTitle() + "》吗?")
.setPositiveButton("删除", (dialog, which) -> {
dataManager.deleteNote(note.getId());
// 删除图片文件
if (note.getImagePaths() != null) {
for (String imagePath : note.getImagePaths()) {
try {
java.io.File file = new java.io.File(imagePath);
if (file.exists()) {
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
allNotes.remove(note);
filterNotes();
Toast.makeText(getContext(), "笔记已删除", Toast.LENGTH_SHORT).show();
})
.setNegativeButton("取消", null)
.show();
}
private void showNoteDetails(Note note) {
String details = note.getContent();
String title = note.getTitle();
if (note.getCourseName() != null && !note.getCourseName().isEmpty()) {
title += " (" + note.getCourseName() + ")";
}
new AlertDialog.Builder(getContext())
.setTitle(title)
.setMessage(details)
.setPositiveButton("确定", null)
.setNeutralButton("编辑", (dialog, which) -> showAddNoteDialog(note))
.show();
}
private void showNoteOptions(Note note) {
String[] options = {"查看详情", "编辑笔记", "删除笔记"};
new AlertDialog.Builder(getContext())
.setTitle(note.getTitle())
.setItems(options, (dialog, which) -> {
switch (which) {
case 0:
showNoteDetails(note);
break;
case 1:
showAddNoteDialog(note);
break;
case 2:
deleteNote(note);
break;
}
})
.show();
}
@Override
public void onResume() {
super.onResume();
// 重新加载课程数据(可能有新增的课程)
courses = dataManager.getCourses();
setupCourseFilter();
// 返回时刷新笔记列表
loadNotes();
// 重置所有滑动状态
if (notesAdapter != null) {
notesAdapter.notifyDataSetChanged();
}
}
}

@ -0,0 +1,80 @@
package com.example.myapplication;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
public class ReminderAlertActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true);
setTurnScreenOn(true);
}
getWindow().addFlags(
android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
);
setFinishOnTouchOutside(false);
String courseName = getIntent().getStringExtra(ReminderReceiver.EXTRA_COURSE_NAME);
String location = getIntent().getStringExtra(ReminderReceiver.EXTRA_COURSE_LOCATION);
String teacher = getIntent().getStringExtra(ReminderReceiver.EXTRA_COURSE_TEACHER);
StringBuilder message = new StringBuilder();
if (location != null && !location.isEmpty()) {
message.append("上课地点:").append(location);
}
if (teacher != null && !teacher.isEmpty()) {
if (message.length() > 0) {
message.append("\n");
}
message.append("任课教师:").append(teacher);
}
if (message.length() > 0) {
message.append("\n");
}
message.append("请准时到达教室,祝您上课顺利!");
new AlertDialog.Builder(this)
.setTitle(courseName == null || courseName.isEmpty() ? "上课提醒" : "上课提醒:" + courseName)
.setMessage(message.toString())
.setCancelable(false)
.setPositiveButton("知道了", (dialog, which) -> finish())
.setNegativeButton("稍后提醒", (dialog, which) -> {
// 五分钟后再次提醒
Course course = findCourseById(getIntent().getStringExtra(ReminderReceiver.EXTRA_COURSE_ID));
if (course != null) {
ReminderScheduler.scheduleReminderInMinutes(this, course, 5);
}
finish();
})
.setOnCancelListener(dialog -> finish())
.show();
}
private Course findCourseById(String courseId) {
if (courseId == null || courseId.isEmpty()) {
return null;
}
DataManager dataManager = new DataManager(this);
for (Course course : dataManager.getCourses()) {
if (course != null && courseId.equals(course.getId())) {
return course;
}
}
return null;
}
}

@ -0,0 +1,254 @@
package com.example.myapplication;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.text.TextUtils;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.util.List;
public class ReminderReceiver extends BroadcastReceiver {
public static final String ACTION_SHOW_REMINDER = "com.example.myapplication.action.SHOW_REMINDER";
public static final String EXTRA_COURSE_ID = "extra_course_id";
public static final String EXTRA_COURSE_NAME = "extra_course_name";
public static final String EXTRA_COURSE_LOCATION = "extra_course_location";
public static final String EXTRA_COURSE_TEACHER = "extra_course_teacher";
public static final String EXTRA_REMINDER_MINUTES = "extra_reminder_minutes";
public static final String EXTRA_REMINDER_SECONDS = "extra_reminder_seconds";
public static final String EXTRA_TIME_SLOT = "extra_time_slot";
public static final String EXTRA_DAY_OF_WEEK = "extra_day_of_week";
public static final String EXTRA_DISPLAY_TIME = "extra_display_time";
public static final String ACTION_INAPP_REMINDER = "com.example.myapplication.action.INAPP_REMINDER";
private static final String CHANNEL_ID = "course_reminder_channel";
private static final String CHANNEL_NAME = "课程提醒";
private static final int NOTIFICATION_ID_BASE = 10000;
@Override
public void onReceive(Context context, Intent intent) {
android.util.Log.d("ReminderReceiver", "onReceive 被调用");
if (context == null || intent == null) {
android.util.Log.w("ReminderReceiver", "context 或 intent 为空");
return;
}
String courseId = intent.getStringExtra(EXTRA_COURSE_ID);
if (TextUtils.isEmpty(courseId)) {
android.util.Log.w("ReminderReceiver", "courseId 为空");
return;
}
android.util.Log.d("ReminderReceiver", "收到提醒广播课程ID: " + courseId);
DataManager dataManager = new DataManager(context);
List<Course> courses = dataManager.getCourses();
Course course = null;
for (Course c : courses) {
if (c != null && courseId.equals(c.getId())) {
course = c;
break;
}
}
if (course == null) {
return;
}
// 移除防重复逻辑,允许用户设置多个提醒时间
// 例如提前10分钟一次提前5分钟一次
long now = System.currentTimeMillis();
SharedPreferences oncePrefs = context.getSharedPreferences("reminder_once", Context.MODE_PRIVATE);
String onceKey = "last_" + courseId;
long lastTime = oncePrefs.getLong(onceKey, 0L);
// 只在10秒内防止重复避免系统bug导致的重复触发
if (now - lastTime < 10000L) {
android.util.Log.d("ReminderReceiver", "忽略10秒内重复提醒课程ID: " + courseId);
return;
}
oncePrefs.edit().putLong(onceKey, now).apply();
// 只显示通知栏提醒,不打断用户当前操作
showNotification(context, course);
// 移除全屏弹窗避免打断用户使用app
// showAlertPopup(context, course);
showAlertPopup(context, course);
// 重新调度下一次提醒
// 注意对于5秒测试提醒如果已经触发过了应该调度到下一周的课程时间
// ReminderScheduler.scheduleReminder 内部会处理,确保不会立即触发
if (course.getReminderAdvanceSeconds() == 5) {
// 5秒测试提醒触发后不再重新调度避免循环
// 如果需要继续提醒,应该在课程正常时间重新调度
// 这里暂时不重新调度,避免立即触发
} else {
// 正常提醒:重新调度到下一周的课程时间
ReminderScheduler.scheduleReminder(context, course);
}
}
private void showNotification(Context context, Course course) {
createChannelIfNeeded(context);
// 移除点击通知打开Activity的行为避免打断用户
int requestCode = course.getId() != null ? course.getId().hashCode() : course.hashCode();
int notificationId = NOTIFICATION_ID_BASE + Math.abs(requestCode);
int delaySeconds = course.getReminderAdvanceSeconds();
String title;
String message;
if (delaySeconds == 5) {
title = "课程提醒(测试)";
message = "《" + safeText(course.getName()) + "》提醒测试";
} else if (delaySeconds < 60) {
title = "课程提醒";
message = "《" + safeText(course.getName()) + "》还有" + delaySeconds + "秒开始";
} else if (delaySeconds % 3600 == 0) {
title = "课程提醒";
message = "《" + safeText(course.getName()) + "》还有" + (delaySeconds / 3600) + "小时开始";
} else {
title = "课程提醒";
message = "《" + safeText(course.getName()) + "》还有" + (delaySeconds / 60) + "分钟开始";
}
String detail = buildContentText(course);
String fullContent = message + "\n" + detail;
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title)
.setContentText(message)
.setStyle(new NotificationCompat.BigTextStyle().bigText(fullContent))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_REMINDER)
.setAutoCancel(true)
.setColor(context.getResources().getColor(R.color.primary, null))
// 不需要震动/声音,仅以消息提示
.setVibrate(new long[]{0L})
.setSound(null)
.setOnlyAlertOnce(true)
.setOngoing(false);
NotificationManagerCompat.from(context).notify(notificationId, builder.build());
// 同时发一条本地广播,让前台界面显示顶部提示
notifyInAppBanner(context, course);
}
private void showAlertPopup(Context context, Course course) {
Intent intent = new Intent(context, ReminderAlertActivity.class);
intent.putExtra(EXTRA_COURSE_ID, course.getId());
intent.putExtra(EXTRA_COURSE_NAME, course.getName());
intent.putExtra(EXTRA_COURSE_LOCATION, course.getLocation());
intent.putExtra(EXTRA_COURSE_TEACHER, course.getTeacher());
intent.putExtra(EXTRA_TIME_SLOT, course.getTimeSlot());
intent.putExtra(EXTRA_DAY_OF_WEEK, course.getDayOfWeek());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
context.startActivity(intent);
}
private void createChannelIfNeeded(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager manager = context.getSystemService(NotificationManager.class);
if (manager == null) {
return;
}
NotificationChannel channel = manager.getNotificationChannel(CHANNEL_ID);
if (channel == null) {
channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
channel.enableLights(false);
channel.enableVibration(false);
channel.setSound(null, null);
channel.setDescription("课程开始前的消息提醒");
manager.createNotificationChannel(channel);
}
}
}
private String buildContentText(Course course) {
StringBuilder builder = new StringBuilder();
builder.append("地点:").append(safeText(course.getLocation()));
String teacher = safeText(course.getTeacher());
if (!TextUtils.isEmpty(teacher)) {
builder.append(" | 教师:").append(teacher);
}
builder.append(" | 时间:").append(ReminderScheduler.formatDisplayTime(course));
return builder.toString();
}
private String safeText(String text) {
return text == null ? "" : text;
}
private void notifyInAppBanner(Context context, Course course) {
try {
// 检查应用是否在前台运行
boolean isAppInForeground = isAppInForeground(context);
android.util.Log.d("ReminderReceiver", "应用是否在前台: " + isAppInForeground);
Intent intent = new Intent(ACTION_INAPP_REMINDER);
intent.putExtra(EXTRA_COURSE_NAME, safeText(course.getName()));
intent.putExtra(EXTRA_COURSE_LOCATION, safeText(course.getLocation()));
intent.putExtra(EXTRA_COURSE_TEACHER, safeText(course.getTeacher()));
intent.putExtra(EXTRA_DAY_OF_WEEK, course.getDayOfWeek());
intent.putExtra(EXTRA_TIME_SLOT, course.getTimeSlot());
intent.putExtra(EXTRA_DISPLAY_TIME, ReminderScheduler.formatDisplayTime(course));
// 发送本地广播
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
android.util.Log.d("ReminderReceiver", "已发送应用内横幅广播: " + course.getName());
// 如果应用不在前台,尝试启动 MainActivity 来显示横幅
if (!isAppInForeground) {
try {
Intent mainIntent = new Intent(context, MainActivity.class);
mainIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
mainIntent.putExtra(ACTION_INAPP_REMINDER, true);
mainIntent.putExtra(EXTRA_COURSE_NAME, safeText(course.getName()));
mainIntent.putExtra(EXTRA_COURSE_LOCATION, safeText(course.getLocation()));
mainIntent.putExtra(EXTRA_DISPLAY_TIME, ReminderScheduler.formatDisplayTime(course));
context.startActivity(mainIntent);
android.util.Log.d("ReminderReceiver", "应用在后台,尝试启动 MainActivity");
} catch (Exception e) {
android.util.Log.e("ReminderReceiver", "启动 MainActivity 失败", e);
}
}
} catch (Exception e) {
android.util.Log.e("ReminderReceiver", "发送应用内横幅广播失败", e);
}
}
private boolean isAppInForeground(Context context) {
try {
android.app.ActivityManager activityManager = (android.app.ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
return false;
}
java.util.List<android.app.ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
if (appProcesses == null) {
return false;
}
String packageName = context.getPackageName();
for (android.app.ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess.importance == android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
&& appProcess.processName.equals(packageName)) {
return true;
}
}
} catch (Exception e) {
android.util.Log.e("ReminderReceiver", "检查应用前台状态失败", e);
}
return false;
}
}

@ -0,0 +1,253 @@
package com.example.myapplication;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.text.TextUtils;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
/**
*
*/
public class ReminderScheduler {
private static final int[][] TIME_SLOT_START_TIMES = new int[][]{
{8, 0}, // 第1-2节 08:00
{10, 5}, // 第3-4节 10:05
{13, 30}, // 第5-6节 13:30
{15, 15}, // 第7-8节 15:15
{18, 30}, // 第9-10节 18:30
{20, 35} // 第11-12节 20:35
};
private ReminderScheduler() {
// no instance
}
public static void setupAllReminders(Context context, List<Course> courses) {
if (context == null || courses == null) {
return;
}
for (Course course : courses) {
updateReminder(context, course);
}
}
public static void updateReminder(Context context, Course course) {
if (context == null || course == null || TextUtils.isEmpty(course.getId())) {
return;
}
cancelReminder(context, course);
if (course.isReminderEnabled()) {
scheduleReminder(context, course);
}
}
public static void scheduleReminder(Context context, Course course) {
if (context == null || course == null || !course.isReminderEnabled()) {
android.util.Log.w("ReminderScheduler", "scheduleReminder: 参数无效或提醒未启用");
return;
}
if (course.getReminderAdvanceSeconds() <= 0) {
android.util.Log.w("ReminderScheduler", "scheduleReminder: 提醒秒数为0");
return;
}
long triggerAtMillis;
long now = System.currentTimeMillis();
if (course.getReminderAdvanceSeconds() == 5) {
// 测试提醒保存后5秒触发仅用于测试
// 对于5秒测试提醒直接使用当前时间+5秒不受最小延迟限制
triggerAtMillis = now + 5000L;
android.util.Log.d("ReminderScheduler", "5秒测试提醒: 当前时间=" + now +
", 测试触发时间=" + triggerAtMillis +
", 延迟约=" + ((triggerAtMillis - now) / 1000) + "秒");
} else {
triggerAtMillis = calculateNextTriggerTime(course);
android.util.Log.d("ReminderScheduler", "正常提醒: 触发时间=" + triggerAtMillis);
}
if (triggerAtMillis <= 0) {
android.util.Log.e("ReminderScheduler", "计算出的触发时间无效: " + triggerAtMillis);
return;
}
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager == null) {
android.util.Log.e("ReminderScheduler", "无法获取 AlarmManager");
return;
}
PendingIntent pendingIntent = buildPendingIntent(context, course);
if (pendingIntent == null) {
android.util.Log.e("ReminderScheduler", "无法创建 PendingIntent");
return;
}
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
}
long delaySeconds = (triggerAtMillis - now) / 1000;
android.util.Log.d("ReminderScheduler", "提醒已调度: 课程=" + course.getName() +
", 触发时间=" + triggerAtMillis +
", 延迟=" + delaySeconds + "秒");
} catch (SecurityException e) {
android.util.Log.e("ReminderScheduler", "调度提醒失败: 权限不足", e);
} catch (Exception e) {
android.util.Log.e("ReminderScheduler", "调度提醒失败", e);
}
}
public static void cancelReminder(Context context, Course course) {
if (context == null || course == null || TextUtils.isEmpty(course.getId())) {
return;
}
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager == null) {
return;
}
PendingIntent pendingIntent = buildPendingIntent(context, course, PendingIntent.FLAG_NO_CREATE);
if (pendingIntent != null) {
alarmManager.cancel(pendingIntent);
pendingIntent.cancel();
}
}
private static PendingIntent buildPendingIntent(Context context, Course course) {
return buildPendingIntent(context, course, PendingIntent.FLAG_UPDATE_CURRENT);
}
private static PendingIntent buildPendingIntent(Context context, Course course, int flag) {
Intent intent = new Intent(context, ReminderReceiver.class);
intent.setAction(ReminderReceiver.ACTION_SHOW_REMINDER);
intent.putExtra(ReminderReceiver.EXTRA_COURSE_ID, course.getId());
intent.putExtra(ReminderReceiver.EXTRA_COURSE_NAME, course.getName());
intent.putExtra(ReminderReceiver.EXTRA_COURSE_LOCATION, course.getLocation());
intent.putExtra(ReminderReceiver.EXTRA_COURSE_TEACHER, course.getTeacher());
intent.putExtra(ReminderReceiver.EXTRA_REMINDER_SECONDS, course.getReminderAdvanceSeconds());
intent.putExtra(ReminderReceiver.EXTRA_REMINDER_MINUTES, Math.max(0, course.getReminderAdvanceSeconds() / 60));
intent.putExtra(ReminderReceiver.EXTRA_TIME_SLOT, course.getTimeSlot());
intent.putExtra(ReminderReceiver.EXTRA_DAY_OF_WEEK, course.getDayOfWeek());
int requestCode = Objects.hash(course.getId());
int flags = flag;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
}
return PendingIntent.getBroadcast(context, requestCode, intent, flags);
}
private static long calculateNextTriggerTime(Course course) {
if (course.getDayOfWeek() < 1 || course.getDayOfWeek() > 7) {
return -1;
}
int delaySeconds = course.getReminderAdvanceSeconds();
if (delaySeconds <= 0) {
return -1;
}
Calendar now = Calendar.getInstance();
Calendar courseStartTime = Calendar.getInstance();
int currentDow = convertToCourseDow(now.get(Calendar.DAY_OF_WEEK));
int targetDow = course.getDayOfWeek();
// 计算到目标星期几的天数差
int daysDiff = targetDow - currentDow;
if (daysDiff < 0) {
daysDiff += 7;
}
// 设置课程开始时间(本周的课程时间)
courseStartTime.add(Calendar.DAY_OF_YEAR, daysDiff);
int[] time = extractStartTime(course.getTimeSlot());
courseStartTime.set(Calendar.HOUR_OF_DAY, time[0]);
courseStartTime.set(Calendar.MINUTE, time[1]);
courseStartTime.set(Calendar.SECOND, 0);
courseStartTime.set(Calendar.MILLISECOND, 0);
// 如果本周的课程开始时间已过,推迟到下一周
if (courseStartTime.getTimeInMillis() <= now.getTimeInMillis()) {
courseStartTime.add(Calendar.DAY_OF_YEAR, 7);
}
// 计算提醒时间:课程开始时间 + 延迟时间
Calendar reminderTime = (Calendar) courseStartTime.clone();
// 提前提醒:在课程开始前 delaySeconds 触发
if (delaySeconds != 0) {
reminderTime.add(Calendar.SECOND, -delaySeconds);
}
// 如果提醒时间已过或距离当前时间太近,推迟到下一周
long nowMillis = now.getTimeInMillis();
long minDelay = 60000; // 最小延迟60秒1分钟
while (reminderTime.getTimeInMillis() - nowMillis < minDelay) {
reminderTime.add(Calendar.DAY_OF_YEAR, 7);
}
return reminderTime.getTimeInMillis();
}
public static void scheduleReminderInMinutes(Context context, Course course, int minutes) {
if (context == null || course == null || minutes <= 0 || !course.isReminderEnabled()) {
return;
}
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager == null) {
return;
}
PendingIntent pendingIntent = buildPendingIntent(context, course);
if (pendingIntent == null) {
return;
}
long triggerAt = System.currentTimeMillis() + minutes * 60L * 1000L;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent);
}
}
private static int[] extractStartTime(int timeSlot) {
if (timeSlot >= 0 && timeSlot < TIME_SLOT_START_TIMES.length) {
return TIME_SLOT_START_TIMES[timeSlot];
}
// 默认08:00
return new int[]{8, 0};
}
private static int convertToCourseDow(int calendarDow) {
if (calendarDow == Calendar.SUNDAY) {
return 7;
}
return calendarDow - 1;
}
public static String formatDisplayTime(Course course) {
int[] start = extractStartTime(course.getTimeSlot());
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, start[0]);
calendar.set(Calendar.MINUTE, start[1]);
return String.format(Locale.getDefault(), "%02d:%02d", start[0], start[1]);
}
}

@ -0,0 +1,265 @@
package com.example.myapplication;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
/**
*
*
*/
public class SimpleMapView extends View {
private Paint paintBackground;
private Paint paintGrid;
private Paint paintCurrentLocation;
private Paint paintDestination;
private Paint paintPath;
private Paint paintText;
private List<LocationPoint> locations;
private LocationPoint currentLocation;
private LocationPoint destination;
private float centerX = 400f;
private float centerY = 400f;
private float scale = 100000f; // 地图缩放比例
public interface OnLocationClickListener {
void onLocationClicked(LocationPoint point);
}
private OnLocationClickListener onLocationClickListener;
public SimpleMapView(Context context) {
super(context);
init();
}
public SimpleMapView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
initPaints();
initData();
}
private void initPaints() {
paintBackground = new Paint();
paintBackground.setColor(getResources().getColor(R.color.accent_green_bg, null));
paintBackground.setStyle(Paint.Style.FILL);
paintGrid = new Paint();
paintGrid.setColor(getResources().getColor(R.color.divider, null));
paintGrid.setStrokeWidth(1);
paintGrid.setAlpha(100);
paintCurrentLocation = new Paint();
paintCurrentLocation.setColor(getResources().getColor(R.color.primary, null));
paintCurrentLocation.setStyle(Paint.Style.FILL);
paintDestination = new Paint();
paintDestination.setColor(getResources().getColor(R.color.error, null));
paintDestination.setStyle(Paint.Style.FILL);
paintPath = new Paint();
paintPath.setColor(getResources().getColor(R.color.accent_orange_dark, null));
paintPath.setStrokeWidth(6);
paintPath.setStyle(Paint.Style.STROKE);
paintText = new Paint();
paintText.setColor(Color.BLACK);
paintText.setTextSize(24);
paintText.setAntiAlias(true);
}
private void initData() {
locations = new ArrayList<>();
// 添加一些示例位置点(使用相对坐标)
locations.add(new LocationPoint("图书馆", 0, 100, "📚"));
locations.add(new LocationPoint("第一食堂", 150, 50, "🍽️"));
locations.add(new LocationPoint("第二食堂", -100, 80, "🍽️"));
locations.add(new LocationPoint("教学楼A", 80, -50, "🏫"));
locations.add(new LocationPoint("教学楼B", 120, -80, "🏫"));
locations.add(new LocationPoint("实验楼", -80, -100, "🔬"));
locations.add(new LocationPoint("体育馆", -150, 20, "🏃"));
locations.add(new LocationPoint("宿舍区", 50, 150, "🏠"));
// 设置默认当前位置
currentLocation = new LocationPoint("校门口", 0, 0, "📍");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
// 绘制背景
canvas.drawRect(0, 0, width, height, paintBackground);
// 绘制网格
drawGrid(canvas, width, height);
// 绘制路径
if (currentLocation != null && destination != null) {
drawPath(canvas);
}
// 绘制位置点
for (LocationPoint point : locations) {
drawLocationPoint(canvas, point, paintCurrentLocation);
}
// 绘制当前位置
if (currentLocation != null) {
drawLocationPoint(canvas, currentLocation, paintCurrentLocation);
}
// 绘制目标位置
if (destination != null) {
drawLocationPoint(canvas, destination, paintDestination);
}
// 绘制图例
drawLegend(canvas, width, height);
}
private void drawGrid(Canvas canvas, int width, int height) {
int gridSize = 50;
// 垂直线
for (int x = 0; x < width; x += gridSize) {
canvas.drawLine(x, 0, x, height, paintGrid);
}
// 水平线
for (int y = 0; y < height; y += gridSize) {
canvas.drawLine(0, y, width, y, paintGrid);
}
}
private void drawLocationPoint(Canvas canvas, LocationPoint point, Paint paint) {
float screenX = centerX + point.x;
float screenY = centerY - point.y; // Y轴翻转
// 绘制圆点
canvas.drawCircle(screenX, screenY, 15, paint);
// 绘制标签
String label = point.icon + " " + point.name;
canvas.drawText(label, screenX - 50, screenY - 25, paintText);
}
private void drawPath(Canvas canvas) {
if (currentLocation == null || destination == null) return;
float startX = centerX + currentLocation.x;
float startY = centerY - currentLocation.y;
float endX = centerX + destination.x;
float endY = centerY - destination.y;
canvas.drawLine(startX, startY, endX, endY, paintPath);
}
private void drawLegend(Canvas canvas, int width, int height) {
int legendX = 20;
int legendY = height - 120;
// 绘制图例背景
RectF legendRect = new RectF(legendX - 10, legendY - 10, legendX + 200, legendY + 80);
Paint legendBg = new Paint();
legendBg.setColor(Color.WHITE);
legendBg.setAlpha(200);
canvas.drawRect(legendRect, legendBg);
// 绘制图例内容
canvas.drawText("📍 当前位置", legendX, legendY, paintText);
canvas.drawText("🎯 目标位置", legendX, legendY + 25, paintText);
canvas.drawText("📚🍽️🏫 校园位置", legendX, legendY + 50, paintText);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
float touchX = event.getX();
float touchY = event.getY();
// 检查是否点击了某个位置点
LocationPoint clickedPoint = findClickedLocation(touchX, touchY);
if (clickedPoint != null && onLocationClickListener != null) {
onLocationClickListener.onLocationClicked(clickedPoint);
return true;
}
}
return super.onTouchEvent(event);
}
private LocationPoint findClickedLocation(float touchX, float touchY) {
for (LocationPoint point : locations) {
float screenX = centerX + point.x;
float screenY = centerY - point.y;
float distance = (float) Math.sqrt(
Math.pow(touchX - screenX, 2) + Math.pow(touchY - screenY, 2)
);
if (distance <= 30) { // 点击范围
return point;
}
}
return null;
}
public void setCurrentLocation(String name, float x, float y) {
currentLocation = new LocationPoint(name, x, y, "📍");
invalidate();
}
public void setDestination(String name, float x, float y) {
destination = new LocationPoint(name, x, y, "🎯");
invalidate();
}
public void setOnLocationClickListener(OnLocationClickListener listener) {
this.onLocationClickListener = listener;
}
public void addLocation(String name, float x, float y, String icon) {
locations.add(new LocationPoint(name, x, y, icon));
invalidate();
}
public void clearDestination() {
destination = null;
invalidate();
}
/**
*
*/
public static class LocationPoint {
public String name;
public float x, y;
public String icon;
public LocationPoint(String name, float x, float y, String icon) {
this.name = name;
this.x = x;
this.y = y;
this.icon = icon;
}
}
}

@ -0,0 +1,130 @@
package com.example.myapplication;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.ArrayList;
import java.util.List;
/**
*
*
*/
public class UserManager {
private static final String PREFS_NAME = "UserManager";
private static final String KEY_CURRENT_USER_ID = "current_user_id";
private static final String KEY_CURRENT_USER_NAME = "current_user_name";
private SharedPreferences prefs;
private static UserManager instance;
private Context context; // 保存context用于清除数据
private UserManager(Context context) {
this.context = context.getApplicationContext();
prefs = this.context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}
public static synchronized UserManager getInstance(Context context) {
if (instance == null) {
instance = new UserManager(context.getApplicationContext());
}
return instance;
}
/**
*
* @param userId ID
* @param userName
*/
public void setCurrentUser(String userId, String userName) {
if (userId == null || userId.trim().isEmpty()) {
return;
}
prefs.edit()
.putString(KEY_CURRENT_USER_ID, userId.trim())
.putString(KEY_CURRENT_USER_NAME, userName != null ? userName.trim() : "")
.apply();
}
/**
* ID
* @return IDnull
*/
public String getCurrentUserId() {
String userId = prefs.getString(KEY_CURRENT_USER_ID, null);
return (userId != null && !userId.trim().isEmpty()) ? userId.trim() : null;
}
/**
*
* @return null
*/
public String getCurrentUserName() {
String userName = prefs.getString(KEY_CURRENT_USER_NAME, null);
return (userName != null && !userName.trim().isEmpty()) ? userName.trim() : null;
}
/**
*
* @return truefalse
*/
public boolean isUserLoggedIn() {
return getCurrentUserId() != null;
}
/**
* 退
*/
public void logout() {
// 在清除用户信息之前先获取当前用户ID用于清除该用户的数据
String currentUserId = getCurrentUserId();
if (currentUserId != null && context != null) {
System.out.println("UserManager.logout: 开始退出登录用户ID: " + currentUserId);
// 清除该用户的所有课程数据
DataManager dataManager = new DataManager(context);
List<Course> courses = dataManager.getCourses();
System.out.println("UserManager.logout: 清除用户课程数据,共 " + courses.size() + " 门课程");
// 清除所有课程
dataManager.saveCourses(new ArrayList<>());
System.out.println("UserManager.logout: 已清除所有课程数据");
}
// 清除用户信息
prefs.edit()
.remove(KEY_CURRENT_USER_ID)
.remove(KEY_CURRENT_USER_NAME)
.apply();
System.out.println("UserManager.logout: 已清除用户信息");
}
/**
*
* @param userId ID
* @param userName
*/
public void switchUser(String userId, String userName) {
if (userId == null || userId.trim().isEmpty()) {
System.out.println("UserManager.switchUser: 用户ID为空无法切换");
return;
}
String trimmedUserId = userId.trim();
System.out.println("UserManager.switchUser: 切换到用户 " + trimmedUserId);
// 使用commit()确保同步保存,避免异步导致的延迟
prefs.edit()
.putString(KEY_CURRENT_USER_ID, trimmedUserId)
.putString(KEY_CURRENT_USER_NAME, userName != null ? userName.trim() : "")
.commit(); // 使用commit()而不是apply(),确保立即保存
// 验证切换是否成功
String savedUserId = getCurrentUserId();
System.out.println("UserManager.switchUser: 切换后验证当前用户ID: " + savedUserId);
if (!trimmedUserId.equals(savedUserId)) {
System.out.println("UserManager.switchUser: 警告 - 用户切换可能失败!");
}
}
}

@ -0,0 +1,543 @@
package com.example.myapplication;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.JavascriptInterface;
import android.widget.Toast;
import android.widget.TextView;
import android.widget.ProgressBar;
import android.widget.LinearLayout;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
import java.util.List;
public class WebViewActivity extends AppCompatActivity {
private static final String TAG = "WebViewActivity";
private static final String LOGIN_URL = "http://jwgl.cauc.edu.cn/xtgl/login_slogin.html";
private static final String TARGET_URL = "http://jwgl.cauc.edu.cn/xsxy/xsxyqk_cxXsxyqkIndex.html?gnmkdm=N105515&layout=default";
private WebView webView;
private List<GradeItem> gradeItems = new ArrayList<>();
private boolean hasNavigated = false; // 防止重复导航
private boolean hasParsed = false; // 防止重复解析
// 解析状态显示组件
private LinearLayout layoutParsingStatus;
private TextView tvParsingStatus;
private TextView tvParsingCount;
private ProgressBar progressParsing;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
Log.d(TAG, "WebViewActivity onCreate called");
initViews();
setupWebView();
loadLoginPage();
}
private void initViews() {
webView = findViewById(R.id.webview);
layoutParsingStatus = findViewById(R.id.layout_parsing_status);
tvParsingStatus = findViewById(R.id.tv_parsing_status);
tvParsingCount = findViewById(R.id.tv_parsing_count);
progressParsing = findViewById(R.id.progress_parsing);
// 设置关闭按钮
findViewById(R.id.btn_close).setOnClickListener(v -> finish());
// 设置手动操作按钮
findViewById(R.id.btn_navigate_to_grades).setOnClickListener(v -> {
hasNavigated = false; // 重置导航状态
hasParsed = false; // 重置解析状态
navigateToTargetPage();
});
findViewById(R.id.btn_parse_data).setOnClickListener(v -> {
hasParsed = false; // 重置解析状态
parseGradeData();
});
}
@SuppressLint("SetJavaScriptEnabled")
private void setupWebView() {
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.d(TAG, "Page finished loading: " + url);
// 检查是否在目标页面且未解析过
if (url.contains("xsxy") || url.contains("学业") || url.contains("课程修读情况")) {
if (!hasParsed) {
Log.d(TAG, "Detected target page, starting parsing...");
parseGradeData();
}
} else if (url.contains("login") || url.contains("xtgl")) {
// 在登录页面,检查是否已登录成功
Log.d(TAG, "On login page, checking login status...");
checkLoginStatus();
}
}
});
webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
Log.d(TAG, "Console: " + consoleMessage.message());
return super.onConsoleMessage(consoleMessage);
}
});
// 添加JavaScript接口
webView.addJavascriptInterface(new WebAppInterface(), "Android");
}
private void loadLoginPage() {
Log.d(TAG, "Loading login page: " + LOGIN_URL);
webView.loadUrl(LOGIN_URL);
}
private void navigateToTargetPage() {
Log.d(TAG, "Navigating to target page: " + TARGET_URL);
hasNavigated = true;
webView.loadUrl(TARGET_URL);
}
private void checkLoginStatus() {
// 检查页面是否包含登录成功的标识
String jsCode =
"javascript:(function() {" +
" console.log('=== 检查登录状态 ===');" +
" console.log('页面标题: ' + document.title);" +
" console.log('页面URL: ' + window.location.href);" +
" " +
" var userInfo = document.querySelector('.user-info, .user-name, .user');" +
" var menu = document.querySelector('.menu, .nav, .sidebar');" +
" var loginForm = document.querySelector('form[action*=\"login\"], .login-form');" +
" " +
" console.log('用户信息元素: ' + (userInfo ? 'found' : 'not found'));" +
" console.log('菜单元素: ' + (menu ? 'found' : 'not found'));" +
" console.log('登录表单: ' + (loginForm ? 'found' : 'not found'));" +
" " +
" if (userInfo || menu) {" +
" console.log('登录成功,准备跳转');" +
" Android.onLoginSuccess();" +
" } else if (loginForm) {" +
" console.log('仍在登录页面,等待用户登录');" +
" } else {" +
" console.log('页面状态未知,尝试跳转');" +
" Android.onLoginSuccess();" +
" }" +
"})();";
webView.evaluateJavascript(jsCode, null);
}
private void parseGradeData() {
Log.d(TAG, "Starting data parsing...");
Log.d(TAG, "Current URL: " + webView.getUrl());
showParsingStatus("正在解析数据...", 0);
// 完整版解析逻辑
String jsCode = "setTimeout(function() {" +
"console.log('=== 开始解析 ===');" +
"var studentName = '';" +
"var overallGPA = '';" +
"var alertBox = document.getElementById('alertBox');" +
"if (alertBox) {" +
"var nameText = alertBox.textContent || alertBox.innerText;" +
"var nameMatch = nameText.match(/([^&\\s]+)\\s*同学/);" +
"if (nameMatch && nameMatch[1]) {" +
"studentName = nameMatch[1].trim();" +
"console.log('提取到学生姓名: ' + studentName);" +
"}" +
"}" +
"var gpaElement = document.querySelector('a[name=\\\"showGpa\\\"]');" +
"if (gpaElement) {" +
"var parentFont = gpaElement.closest('font');" +
"if (parentFont) {" +
"var gpaFont = parentFont.querySelector('font[style*=\\\"color: red\\\"]');" +
"if (gpaFont) {" +
"var gpaText = gpaFont.textContent || gpaFont.innerText;" +
"var gpaMatch = gpaText.match(/([0-9.]+)/);" +
"if (gpaMatch && gpaMatch[1]) {" +
"overallGPA = gpaMatch[1].trim();" +
"console.log('提取到GPA: ' + overallGPA);" +
"}" +
"}" +
"}" +
"}" +
"var tables = document.querySelectorAll('table');" +
"console.log('找到 ' + tables.length + ' 个表格');" +
"var items = [];" +
"var seenFirstCol = new Set();" +
"var currentYearForPolicy = '';" +
"var currentTermForPolicy = '';" +
"var missedCourseWarnings = [];" +
"var hasCurrentTermMissing = false;" +
"var hasNonElectiveMissing = false;" +
"var electiveSummary = '';" +
"var electiveInProgressCount = 0;" +
"var electiveInProgressCredits = 0;" +
"var formatCreditValue = function(value) {" +
"if (value === null || value === undefined || value === '') { return ''; }" +
"var num = parseFloat(value);" +
"if (isNaN(num)) { return value; }" +
"return num % 1 === 0 ? num.toFixed(1) : num.toString();" +
"};" +
"var electiveTitleElements = document.querySelectorAll('p.title1');" +
"for (var i = 0; i < electiveTitleElements.length; i++) {" +
"var textContent = electiveTitleElements[i].textContent || '';" +
"var normalizedText = textContent.replace(/\\s+/g, ' ').trim();" +
"if (normalizedText.indexOf('专业选修课') !== -1 && normalizedText.indexOf('要求学分') !== -1) {" +
"var requiredCreditsAttr = electiveTitleElements[i].getAttribute('yqzdxf');" +
"var earnedCreditsAttr = electiveTitleElements[i].getAttribute('yxxf');" +
"var requiredCredits = formatCreditValue(requiredCreditsAttr);" +
"var earnedCredits = formatCreditValue(earnedCreditsAttr);" +
"var missingCredits = '';" +
"if (requiredCreditsAttr && earnedCreditsAttr) {" +
"var diff = parseFloat(requiredCreditsAttr) - parseFloat(earnedCreditsAttr);" +
"if (!isNaN(diff)) {" +
"missingCredits = formatCreditValue(diff);" +
"}" +
"} else {" +
"var missingMatch = normalizedText.match(/未获得学分[:]\\s*([0-9.]+)/);" +
"if (missingMatch) {" +
"missingCredits = missingMatch[1];" +
"}" +
"}" +
"var summaryParts = ['专业选修课'];" +
"if (requiredCredits) summaryParts.push(' 要求学分:' + requiredCredits);" +
"if (earnedCredits) summaryParts.push(' 获得学分:' + earnedCredits);" +
"if (missingCredits) summaryParts.push(' 未获得学分:' + missingCredits);" +
"electiveSummary = summaryParts.join('');" +
"break;" +
"}" +
"}" +
"for (var t = 0; t < tables.length; t++) {" +
"var table = tables[t];" +
"var rows = table.querySelectorAll('tr');" +
"for (var r = 0; r < rows.length; r++) {" +
"var row = rows[r];" +
"var cells = row.querySelectorAll('td');" +
"if (cells.length > 0) {" +
"var rowData = [];" +
"for (var c = 0; c < cells.length; c++) {" +
"var cell = cells[c];" +
"var cellText = (cell.textContent || '').trim();" +
"if (c === 0) {" +
"var titleNode = cell.querySelector('[title]');" +
"if (titleNode && titleNode.getAttribute('title')) {" +
"cellText = titleNode.getAttribute('title').trim();" +
"} else {" +
"var iconNode = cell.querySelector('.png_ico_tjxk, .zt3, i, span');" +
"if (iconNode) {" +
"var iconTitle = iconNode.getAttribute('title');" +
"if (iconTitle) {" +
"cellText = iconTitle.trim();" +
"} else {" +
"var className = iconNode.className || '';" +
"if (className.indexOf('tjxk4') !== -1) {" +
"cellText = '已修';" +
"} else if (className.indexOf('tjxk1') !== -1) {" +
"cellText = '在修';" +
"} else if (className.indexOf('tjxk3') !== -1) {" +
"cellText = '未修';" +
"} else if (className.indexOf('tjxk2') !== -1) {" +
"cellText = '未过';" +
"} else if (className.indexOf('zt33') !== -1) {" +
"cellText = '学分已满';" +
"} else if (className.indexOf('zt32') !== -1) {" +
"cellText = '学分超出';" +
"} else if (className.indexOf('zt31') !== -1) {" +
"cellText = '学分未满';" +
"}" +
"}" +
"}" +
"}" +
"}" +
"rowData.push(cellText);" +
"}" +
"seenFirstCol.add(rowData[0] || '');" +
"if (rowData[7] === '专业选修课' && rowData[0] === '在修') {" +
"electiveInProgressCount++;" +
"var creditVal = parseFloat(rowData[8]);" +
"if (!isNaN(creditVal)) {" +
"electiveInProgressCredits += creditVal;" +
"}" +
"}" +
"var hasYearTermCols = rowData.length > 16;" +
"if (!currentYearForPolicy && hasYearTermCols && rowData[0] === '在修' && rowData[5] && rowData[5].indexOf('形势与政策') !== -1) {" +
"currentYearForPolicy = rowData[15] || '';" +
"currentTermForPolicy = rowData[16] || '';" +
"console.log('找到形势与政策在修行: 年=' + currentYearForPolicy + ', 学期=' + currentTermForPolicy);" +
"}" +
"if (currentYearForPolicy && currentTermForPolicy && hasYearTermCols && rowData[0] === '未修' && rowData[5] && rowData[15] === currentYearForPolicy && rowData[16] === currentTermForPolicy) {" +
"var courseNameWarn = '\"' + rowData[5] + '\"';" +
"var courseTypeWarn = rowData[7] || '';" +
"hasCurrentTermMissing = true;" +
"if (courseTypeWarn === '专业选修课') {" +
"missedCourseWarnings.push('本学期专业选修课' + courseNameWarn + '未修,请结合自身学分情况考虑是否选修');" +
"} else {" +
"missedCourseWarnings.push('你漏选了本学期的必修课' + courseNameWarn + ',请快速处理');" +
"hasNonElectiveMissing = true;" +
"}" +
"}" +
"if (rowData.length > 2) {" +
"var scoreValue = (rowData[12] === '缓') ? ((rowData[10] || '') + '(缓)') : (rowData[12] || '');" +
"var yearValue = (scoreValue && scoreValue.length > 0) ? (rowData[1] || '') : (rowData[15] || '');" +
"var termValue = (scoreValue && scoreValue.length > 0) ? (rowData[2] || '') : (rowData[16] || '');" +
"var item = {" +
"courseName: rowData[5] || ''," +
"courseCode: rowData[3] || ''," +
"credit: rowData[8] || ''," +
"score: scoreValue," +
"gpa: rowData[11] || ''," +
"term: termValue," +
"year: yearValue," +
"status: rowData[7] || ''" +
"};" +
"if (item.courseName && item.courseName.length > 0) {" +
"items.push(item);" +
"console.log('添加课程: ' + item.courseName);" +
"}" +
"}" +
"}" +
"}" +
"}" +
"console.log('总共找到 ' + items.length + ' 个课程');" +
"console.log('第一列去重取值:' + JSON.stringify(Array.from(seenFirstCol)));" +
"if (currentYearForPolicy && currentTermForPolicy && hasCurrentTermMissing && !hasNonElectiveMissing) {" +
"missedCourseWarnings.push('本学期的必修课已检查确定无漏选');" +
"}" +
"console.log('学生姓名: ' + studentName + ', GPA: ' + overallGPA);" +
"var result = {" +
"studentName: studentName," +
"overallGPA: overallGPA," +
"courses: items," +
"currentYear: currentYearForPolicy," +
"currentTerm: currentTermForPolicy," +
"missedCourses: missedCourseWarnings," +
"electiveSummary: electiveSummary," +
"electiveInProgressCount: electiveInProgressCount," +
"electiveInProgressCredits: formatCreditValue(electiveInProgressCredits)" +
"};" +
"try {" +
"Android.onDataParsed(JSON.stringify(result));" +
"console.log('Android.onDataParsed调用成功');" +
"} catch(e) {" +
"console.log('Android.onDataParsed调用失败: ' + e.message);" +
"}" +
"}, 3000);";
webView.evaluateJavascript(jsCode, null);
}
private void showParsingStatus(String status, int count) {
runOnUiThread(() -> {
layoutParsingStatus.setVisibility(LinearLayout.VISIBLE);
tvParsingStatus.setText(status);
tvParsingCount.setText("已解析: " + count + " 条数据");
});
}
private void hideParsingStatus() {
runOnUiThread(() -> {
layoutParsingStatus.setVisibility(LinearLayout.GONE);
});
}
// 课程数据类
public static class GradeItem implements java.io.Serializable {
public String courseName;
public String courseCode;
public String credit;
public String score;
public String gpa;
public String term;
public String year;
public String status;
public GradeItem() {}
public GradeItem(String courseName, String courseCode, String credit, String score, String gpa, String term) {
this.courseName = courseName;
this.courseCode = courseCode;
this.credit = credit;
this.score = score;
this.gpa = gpa;
this.term = term;
}
@Override
public String toString() {
return "课程名称: " + courseName + ", 课程号: " + courseCode + ", 学分: " + credit +
", 成绩: " + score + ", 绩点: " + gpa + ", 学期: " + term +
", 学年: " + year + ", 课程性质: " + status;
}
}
public class WebAppInterface {
@JavascriptInterface
public void onDataParsed(String jsonData) {
Log.d(TAG, "=== onDataParsed CALLED ===");
Log.d(TAG, "JavaScript parsing completed. Raw JSON: " + jsonData);
try {
// 尝试解析新的数据结构(包含学生信息)
org.json.JSONObject result = new org.json.JSONObject(jsonData);
String studentName = result.optString("studentName", "");
String overallGPA = result.optString("overallGPA", "");
String currentYear = result.optString("currentYear", "");
String currentTerm = result.optString("currentTerm", "");
String electiveSummary = result.optString("electiveSummary", "");
String electiveInProgressCount = result.optString("electiveInProgressCount", "");
String electiveInProgressCredits = result.optString("electiveInProgressCredits", "");
org.json.JSONArray missedCoursesArray = result.optJSONArray("missedCourses");
ArrayList<String> missedCourseWarnings = new ArrayList<>();
if (missedCoursesArray != null) {
for (int i = 0; i < missedCoursesArray.length(); i++) {
String warning = missedCoursesArray.optString(i, "");
if (!warning.isEmpty()) {
missedCourseWarnings.add(warning);
}
}
}
org.json.JSONArray coursesArray = result.optJSONArray("courses");
Log.d(TAG, "学生姓名: " + studentName + ", GPA: " + overallGPA);
List<GradeItem> parsedItems;
if (coursesArray != null && coursesArray.length() > 0) {
// 新格式解析courses数组
Log.d(TAG, "使用新格式解析courses数组长度: " + coursesArray.length());
parsedItems = new ArrayList<>();
for (int i = 0; i < coursesArray.length(); i++) {
org.json.JSONObject courseObj = coursesArray.getJSONObject(i);
GradeItem item = new GradeItem();
item.courseName = courseObj.optString("courseName", "");
item.courseCode = courseObj.optString("courseCode", "");
item.credit = courseObj.optString("credit", "");
item.score = courseObj.optString("score", "");
item.gpa = courseObj.optString("gpa", "");
item.term = courseObj.optString("term", "");
item.year = courseObj.optString("year", "");
item.status = courseObj.optString("status", "");
parsedItems.add(item);
Log.d(TAG, "解析课程 " + i + ": " + item.courseName);
}
} else {
// 旧格式使用GradeDataParser解析
Log.d(TAG, "使用旧格式解析coursesArray为null或空");
parsedItems = GradeDataParser.parseJsonData(jsonData);
}
if (!parsedItems.isEmpty()) {
gradeItems = parsedItems;
hasParsed = true;
Log.d(TAG, "JavaScript parsing successful, found " + gradeItems.size() + " items");
runOnUiThread(() -> {
showParsingStatus("JavaScript解析完成", gradeItems.size());
Toast.makeText(WebViewActivity.this, "成功解析 " + gradeItems.size() + " 条数据", Toast.LENGTH_SHORT).show();
// 延迟2秒后返回结果
webView.postDelayed(() -> {
Log.d(TAG, "Preparing to return " + gradeItems.size() + " items to GradesFragment");
Intent resultIntent = new Intent();
resultIntent.putExtra("gradeItems", gradeItems.toArray(new GradeItem[0]));
// 如果有学生信息,也传递过去
if (!studentName.isEmpty()) {
resultIntent.putExtra("studentName", studentName);
}
if (!overallGPA.isEmpty()) {
resultIntent.putExtra("overallGPA", overallGPA);
}
if (!currentYear.isEmpty()) {
resultIntent.putExtra("currentTermYear", currentYear);
}
if (!currentTerm.isEmpty()) {
resultIntent.putExtra("currentTermNumber", currentTerm);
}
if (!missedCourseWarnings.isEmpty()) {
resultIntent.putStringArrayListExtra("termWarnings", missedCourseWarnings);
}
if (!electiveSummary.isEmpty()) {
resultIntent.putExtra("electiveSummary", electiveSummary);
}
if (!electiveInProgressCount.isEmpty()) {
resultIntent.putExtra("electiveInProgressCount", electiveInProgressCount);
}
if (!electiveInProgressCredits.isEmpty()) {
resultIntent.putExtra("electiveInProgressCredits", electiveInProgressCredits);
}
setResult(Activity.RESULT_OK, resultIntent);
Log.d(TAG, "Set result and calling finish()");
finish(); // 关闭WebViewActivity
}, 2000);
});
} else {
Log.d(TAG, "JavaScript parsing found no items.");
runOnUiThread(() -> {
showParsingStatus("JavaScript解析未找到数据", 0);
Toast.makeText(WebViewActivity.this, "未找到数据", Toast.LENGTH_SHORT).show();
webView.postDelayed(() -> {
setResult(Activity.RESULT_CANCELED);
finish();
}, 2000);
});
}
} catch (Exception e) {
Log.e(TAG, "Error processing parsed JSON data: " + e.getMessage());
runOnUiThread(() -> {
showParsingStatus("解析数据出错: " + e.getMessage(), 0);
Toast.makeText(WebViewActivity.this, "解析出错", Toast.LENGTH_SHORT).show();
webView.postDelayed(() -> {
setResult(Activity.RESULT_CANCELED);
finish();
}, 2000);
});
}
}
@JavascriptInterface
public void onLoginSuccess() {
runOnUiThread(() -> {
Log.d(TAG, "Login successful, navigating to target page...");
if (!hasNavigated) {
navigateToTargetPage();
}
});
}
@JavascriptInterface
public void testConnection() {
Log.d(TAG, "=== JavaScript接口测试成功 ===");
Log.d(TAG, "testConnection方法被调用");
runOnUiThread(() -> {
Toast.makeText(WebViewActivity.this, "JavaScript接口工作正常", Toast.LENGTH_SHORT).show();
});
}
}
}

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#80000000" />
<size
android:width="48dp"
android:height="48dp" />
</shape>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#EEEEEE" />
</shape>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="56dp" android:height="56dp" />
<solid android:color="@color/error" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="56dp" android:height="56dp" />
<solid android:color="@color/primary" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19.14,12.94c0.04,-0.31 0.06,-0.63 0.06,-0.94s-0.02,-0.63 -0.06,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.4 0.12,-0.61l-1.92,-3.32c-0.11,-0.21 -0.36,-0.3 -0.58,-0.22l-2.39,0.96c-0.5,-0.38 -1.04,-0.7 -1.64,-0.94L14.5,2.5c-0.02,-0.23 -0.22,-0.41 -0.45,-0.41h-3.1c-0.23,0 -0.43,0.18 -0.45,0.41L10,4.33c-0.59,0.24 -1.14,0.56 -1.64,0.94L5.97,4.31c-0.22,-0.09 -0.47,0.01 -0.58,0.22L3.47,7.85c-0.11,0.21 -0.06,0.47 0.12,0.61l2.03,1.58c-0.04,0.31 -0.06,0.63 -0.06,0.94s0.02,0.63 0.06,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.4 -0.12,0.61l1.92,3.32c0.11,0.21 0.36,0.3 0.58,0.22l2.39,-0.96c0.5,0.38 1.04,0.7 1.64,0.94l0.5,1.83c0.02,0.23 0.22,0.41 0.45,0.41h3.1c0.23,0 0.43,-0.18 0.45,-0.41l0.5,-1.83c0.59,-0.24 1.14,-0.56 1.64,-0.94l2.39,0.96c0.22,0.09 0.47,-0.01 0.58,-0.22l1.92,-3.32c0.11,-0.21 0.06,-0.47 -0.12,-0.61l-2.03,-1.58zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
</vector>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#4CAF50" />
<corners android:radius="8dp" />
</shape>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
<corners android:radius="16dp" />
<stroke
android:width="1dp"
android:color="#E0E0E0" />
</shape>

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="24dp"
android:background="@drawable/reminder_dialog_background"
android:orientation="vertical"
android:padding="0dp">
<!-- 顶部关闭按钮 -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/reminder_header"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="📚 课程提醒"
android:textColor="@color/text_white"
android:textSize="18sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/btn_close"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="4dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:tint="@color/text_white" />
</RelativeLayout>
<!-- 内容区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<!-- 提醒时间 -->
<TextView
android:id="@+id/tv_reminder_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:background="@color/reminder_time_bg"
android:paddingLeft="16dp"
android:paddingTop="6dp"
android:paddingRight="16dp"
android:paddingBottom="6dp"
android:text="10分钟后上课"
android:textColor="@color/reminder_time_text"
android:textSize="14sp"
android:textStyle="bold" />
<!-- 课程名称 -->
<TextView
android:id="@+id/tv_course_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:gravity="center"
android:text="高等数学"
android:textColor="@color/text_primary"
android:textSize="24sp"
android:textStyle="bold" />
<!-- 课程信息 -->
<TextView
android:id="@+id/tv_course_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:text="地点教学楼A101\n时间08:00-09:40"
android:textColor="@color/text_secondary"
android:textSize="16sp" />
<!-- 确认按钮 -->
<Button
android:id="@+id/btn_confirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/reminder_button_background"
android:text="我知道了"
android:textColor="@color/text_white"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_secondary">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:orientation="horizontal">
<EditText
android:id="@+id/et_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="输入教务系统网址"/>
<Button
android:id="@+id/btn_go"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="前往"
android:textSize="14sp"
android:backgroundTint="@color/button_secondary"
android:textColor="@color/text_white"
android:padding="8dp"/>
</LinearLayout>
<ProgressBar
android:id="@+id/progress_bar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="3dp"
android:max="100"
android:progress="0"
android:visibility="gone"/>
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<Button
android:id="@+id/btn_start_import"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="我已在课表页,开始导入"
android:textSize="16sp"
android:backgroundTint="@color/button_primary"
android:textColor="@color/text_white"
android:padding="12dp"/>
<Button
android:id="@+id/btn_locate_timetable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="自动定位课表"
android:textSize="14sp"
android:backgroundTint="@color/button_secondary"
android:textColor="@color/text_white"
android:padding="10dp"/>
<Space
android:layout_width="match_parent"
android:layout_height="8dp"/>
</LinearLayout>

@ -0,0 +1,283 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_primary"
android:padding="20dp">
<!-- 标题区域 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="校园助手"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:gravity="start"
android:layout_marginTop="24dp"
android:layout_marginBottom="6dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="一站式校园生活"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginBottom="18dp" />
<!-- 功能按钮网格 -->
<GridLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:columnCount="2"
android:rowCount="2"
android:alignmentMode="alignMargins"
android:columnOrderPreserved="false"
android:useDefaultMargins="true">
<!-- 课表按钮 -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_timetable"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_row="0"
android:layout_column="0"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="16dp"
app:cardElevation="2dp"
app:strokeWidth="1dp"
app:strokeColor="@color/border_light"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="18dp">
<FrameLayout
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/bg_circle"
android:backgroundTint="@color/primary_bg">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center"
android:tint="@color/primary_dark"
android:src="@android:drawable/ic_menu_month" />
</FrameLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:layout_marginTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="课程表"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="查看今日课程安排"
android:textSize="13sp"
android:textColor="@color/text_secondary" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 笔记按钮 -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_notes"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_row="0"
android:layout_column="1"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="16dp"
app:cardElevation="2dp"
app:strokeWidth="1dp"
app:strokeColor="@color/border_light"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="18dp">
<FrameLayout
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/bg_circle"
android:backgroundTint="@color/accent_green_bg">
<ImageView
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_gravity="center"
android:tint="@color/accent_green_dark"
android:src="@android:drawable/ic_menu_edit" />
</FrameLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:layout_marginTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="课程笔记"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="记录课堂灵感与作业"
android:textSize="13sp"
android:textColor="@color/text_secondary" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 成绩按钮 -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_grades"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_row="1"
android:layout_column="0"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="16dp"
app:cardElevation="2dp"
app:strokeWidth="1dp"
app:strokeColor="@color/border_light"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="18dp">
<FrameLayout
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/bg_circle"
android:backgroundTint="@color/accent_orange_bg">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center"
android:tint="@color/accent_orange_dark"
android:src="@android:drawable/ic_menu_sort_by_size" />
</FrameLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:layout_marginTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="成绩查询"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="导入并统计GPA"
android:textSize="13sp"
android:textColor="@color/text_secondary" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 校园导航按钮 -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_campus"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_row="1"
android:layout_column="1"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="16dp"
app:cardElevation="2dp"
app:strokeWidth="1dp"
app:strokeColor="@color/border_light"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="18dp">
<FrameLayout
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/bg_circle"
android:backgroundTint="@color/accent_purple_bg">
<ImageView
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_gravity="center"
android:tint="@color/accent_purple_dark"
android:src="@android:drawable/ic_menu_mylocation" />
</FrameLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:layout_marginTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="校园导航"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="找地点与路线指引"
android:textSize="13sp"
android:textColor="@color/text_secondary" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</GridLayout>
</LinearLayout>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<!-- 顶部栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<TextView
android:id="@+id/btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="←"
android:textSize="22sp"
android:padding="4dp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="编辑笔记"
android:textStyle="bold"
android:textSize="18sp"
android:gravity="center" />
</LinearLayout>
<!-- 标题 -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="标题"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxBackgroundMode="outline">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 关联课程 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="课程:"
android:layout_marginEnd="8dp" />
<Spinner
android:id="@+id/spinner_course"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
<!-- 内容 - 使用WebView实现富文本编辑 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginTop="8dp">
<WebView
android:id="@+id/webview_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- 工具栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:background="@color/background_secondary">
<Button
android:id="@+id/btn_insert_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="插入图片"
android:textColor="@color/primary"
app:strokeColor="@color/primary"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
</LinearLayout>
</LinearLayout>
<!-- 操作按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:layout_marginTop="12dp">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="取消" />
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="保存"
android:layout_marginStart="12dp" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Light"
app:titleTextColor="@color/text_primary"
app:subtitleTextColor="@color/text_secondary"
app:navigationIconTint="@color/text_primary"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 标题栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:backgroundTint="@color/primary">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="教务系统登录"
android:textSize="18sp"
android:textColor="@color/text_white"
android:textStyle="bold" />
<Button
android:id="@+id/btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="关闭"
android:textColor="@color/text_white"
android:background="@android:color/transparent" />
</LinearLayout>
<!-- WebView -->
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- 解析状态显示 -->
<LinearLayout
android:id="@+id/layout_parsing_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
android:background="@color/primary_light"
android:visibility="gone">
<TextView
android:id="@+id/tv_parsing_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="正在解析数据..."
android:textSize="14sp"
android:textColor="@color/primary"
android:gravity="center" />
<TextView
android:id="@+id/tv_parsing_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="已解析: 0 条数据"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:gravity="center"
android:layout_marginTop="4dp" />
<ProgressBar
android:id="@+id/progress_parsing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp" />
</LinearLayout>
<!-- 底部操作栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:background="@color/background_secondary">
<Button
android:id="@+id/btn_navigate_to_grades"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="跳转到学业页面"
android:textSize="12sp"
android:padding="8dp"
android:layout_marginEnd="4dp" />
<Button
android:id="@+id/btn_parse_data"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="解析当前页面"
android:textSize="12sp"
android:padding="8dp"
android:layout_marginStart="4dp" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,224 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_course"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"
android:gravity="center" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:hint="@string/course_name">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_course_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tv_error_course_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/error"
android:textSize="12sp"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:hint="@string/course_teacher">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_course_teacher"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tv_error_course_teacher"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/error"
android:textSize="12sp"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:hint="@string/course_location">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_course_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tv_error_course_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/error"
android:textSize="12sp"
android:layout_marginBottom="12dp"
android:visibility="gone" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/week_day"
android:textSize="16sp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" />
<Spinner
android:id="@+id/spinner_day_of_week"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/course_time_slot"
android:textSize="16sp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" />
<Spinner
android:id="@+id/spinner_time_slot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="周次范围"
android:textSize="16sp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="第"
android:textSize="16sp"
android:gravity="center_vertical"
android:layout_marginEnd="8dp" />
<Spinner
android:id="@+id/spinner_start_week"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="周 至 第"
android:textSize="16sp"
android:gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
<Spinner
android:id="@+id/spinner_end_week"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="周"
android:textSize="16sp"
android:gravity="center_vertical"
android:layout_marginStart="8dp" />
</LinearLayout>
<Button
android:id="@+id/btn_reminder_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="设置提醒"
android:textSize="15sp"
android:backgroundTint="@color/button_secondary"
android:textColor="@color/text_white"
android:padding="10dp"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/tv_reminder_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未设置提醒"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginBottom="16dp"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:layout_marginTop="16dp">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:textSize="14sp"
android:backgroundTint="@color/button_secondary"
android:textColor="@color/text_white"
android:padding="8dp"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/save"
android:textSize="14sp"
android:backgroundTint="@color/button_primary"
android:textColor="@color/text_white"
android:padding="8dp" />
</LinearLayout>
</LinearLayout>
</ScrollView>

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_dialog_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_grade"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"
android:gravity="center" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:hint="@string/grade_subject">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_subject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:hint="@string/grade_score">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_score"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:hint="学分">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_credit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="学期"
android:textSize="16sp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" />
<Spinner
android:id="@+id/spinner_semester"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="考试类型"
android:textSize="16sp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" />
<Spinner
android:id="@+id/spinner_exam_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:layout_marginTop="16dp">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:textSize="16sp"
android:layout_marginEnd="8dp"
style="@style/Widget.Material3.Button.OutlinedButton" />
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/save"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
</ScrollView>

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_dialog_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_note"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"
android:gravity="center" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:hint="@string/note_title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_note_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/link_to_course"
android:textSize="16sp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" />
<Spinner
android:id="@+id/spinner_course"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="@string/note_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_note_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:lines="6"
android:gravity="top|start"
android:scrollbars="vertical" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:layout_marginTop="16dp">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:textSize="16sp"
android:layout_marginEnd="8dp"
style="@style/Widget.Material3.Button.OutlinedButton" />
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/save"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
</ScrollView>

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="教务系统导入"
android:textSize="24sp"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="16dp" />
<!-- 学号输入 -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:hint="学号">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_student_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tv_error_student_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/error"
android:textSize="12sp"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<!-- 姓名输入 -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:hint="姓名">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_student_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tv_error_student_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/error"
android:textSize="12sp"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<!-- 验证码输入 -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:hint="验证码">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_verification_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tv_error_verification_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/error"
android:textSize="12sp"
android:layout_marginBottom="16dp"
android:visibility="gone" />
<!-- 按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_cancel_import"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="取消"
android:textSize="16sp"
android:layout_marginEnd="8dp"
style="@style/Widget.Material3.Button.OutlinedButton" />
<Button
android:id="@+id/btn_confirm_import"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="确认导入"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="课表提醒设置"
android:textSize="24sp"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="设置课前提醒时间"
android:textSize="16sp"
android:gravity="center"
android:layout_marginBottom="16dp" />
<!-- 滚动选择器NumberPicker替代单选按钮 -->
<NumberPicker
android:id="@+id/np_reminder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="系统将在上课前指定时间提醒您"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_cancel_reminder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="取消"
android:textSize="16sp"
android:background="?attr/selectableItemBackgroundBorderless"
android:textColor="@color/error" />
<Button
android:id="@+id/btn_save_reminder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="保存设置"
android:textSize="16sp"
android:background="?attr/selectableItemBackgroundBorderless"
android:textColor="@color/primary" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,520 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 全屏地图 -->
<com.amap.api.maps.MapView
android:id="@+id/amap_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layerType="hardware"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true" />
<!-- 定位按钮(右下角,避免遮挡,位于底部卡片上方) -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_my_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_marginBottom="80dp"
android:layout_gravity="bottom|end"
android:src="@android:drawable/ic_menu_mylocation"
android:contentDescription="我的位置"
app:fabSize="normal"
app:backgroundTint="@color/white"
app:tint="@color/amap_primary"
app:elevation="6dp" />
<!-- 顶部卡片容器(可滚动) -->
<androidx.core.widget.NestedScrollView
android:id="@+id/nested_scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_marginTop="16dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@android:color/transparent"
android:scrollbars="none"
android:fillViewport="false"
android:maxHeight="320dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 搜索框卡片 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="8dp"
app:cardBackgroundColor="@color/amap_card_background"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<!-- 搜索框 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/search_location"
app:endIconMode="custom"
app:endIconDrawable="@android:drawable/ic_menu_search"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxBackgroundMode="outline"
app:boxCornerRadiusTopStart="8dp"
app:boxCornerRadiusTopEnd="8dp"
app:boxCornerRadiusBottomStart="8dp"
app:boxCornerRadiusBottomEnd="8dp"
app:boxStrokeColor="@color/amap_primary"
app:boxStrokeWidth="1.5dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_search_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:singleLine="true"
android:maxLines="1"
android:imeOptions="actionSearch"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- 搜索结果列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_location_results"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:nestedScrollingEnabled="false"
android:maxHeight="200dp"
android:layout_marginTop="8dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 今日课程卡片(可折叠) -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_today_courses"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="8dp"
app:cardBackgroundColor="@color/amap_card_background"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="今日课程"
android:textSize="16sp"
android:textColor="@color/amap_text_primary"
android:textStyle="bold" />
<ImageButton
android:id="@+id/btn_settings"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="6dp"
android:background="@drawable/bg_circle"
android:backgroundTint="@color/background_secondary"
android:contentDescription="设置"
android:src="@drawable/ic_settings_gear"
app:tint="@color/amap_primary" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_today_courses"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"
android:nestedScrollingEnabled="false"
android:maxHeight="120dp"
android:minHeight="32dp"
android:layout_marginTop="4dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- 收藏位置和最近搜索(隐藏的浮层) -->
<LinearLayout
android:id="@+id/layout_search_results"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_marginTop="450dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:orientation="vertical"
android:visibility="gone"
android:background="@android:color/transparent">
<!-- 收藏位置 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="4dp">
<TextView
android:id="@+id/tv_favorites_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="收藏位置"
android:textSize="14sp"
android:textColor="@color/text_white"
android:textStyle="bold"
android:visibility="gone"
android:background="@color/background_overlay"
android:padding="4dp" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="2dp"
android:scrollbars="none">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group_favorites"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:singleLine="true"
app:singleSelection="false" />
</HorizontalScrollView>
</LinearLayout>
<!-- 最近搜索 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_recent_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="最近搜索"
android:textSize="14sp"
android:textColor="@color/text_white"
android:textStyle="bold"
android:visibility="gone"
android:background="@color/background_overlay"
android:padding="4dp" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="2dp"
android:scrollbars="none">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group_recent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:singleLine="true"
app:singleSelection="false" />
</HorizontalScrollView>
</LinearLayout>
</LinearLayout>
<!-- 底部信息卡片(统一显示位置信息、路线信息和导航控制) -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_route_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
app:cardCornerRadius="12dp"
app:cardElevation="8dp"
app:cardBackgroundColor="@color/amap_card_background"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<!-- 位置信息区域 -->
<TextView
android:id="@+id/tv_current_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="当前位置:未知位置"
android:textSize="13sp"
android:textColor="@color/amap_text_secondary"
android:layout_marginBottom="2dp" />
<TextView
android:id="@+id/tv_destination"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="目标位置:请选择"
android:textSize="13sp"
android:textColor="@color/amap_text_secondary"
android:layout_marginBottom="8dp" />
<!-- 路线信息区域(选择目的地后显示) -->
<LinearLayout
android:id="@+id/layout_route_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📍 路线信息"
android:textSize="14sp"
android:textColor="@color/amap_text_primary"
android:textStyle="bold"
android:layout_marginBottom="6dp" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/toggle_transport_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
app:singleSelection="true"
app:selectionRequired="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_mode_walk"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="步行"
android:textAllCaps="false"
android:checkable="true"
android:checked="true" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_mode_bike"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="自行车"
android:textAllCaps="false"
android:checkable="true" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<TextView
android:id="@+id/tv_route_distance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="距离约500米"
android:textSize="14sp"
android:textColor="@color/amap_text_secondary" />
<TextView
android:id="@+id/tv_route_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="步行时间约5分钟"
android:textSize="14sp"
android:textColor="@color/amap_text_secondary"
android:layout_marginTop="2dp" />
<TextView
android:id="@+id/tv_arrival_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="到达时间:--:--"
android:textSize="14sp"
android:textColor="@color/amap_text_secondary"
android:layout_marginTop="2dp" />
<TextView
android:id="@+id/tv_instruction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="导航未开始"
android:textSize="14sp"
android:textColor="@color/amap_primary"
android:layout_marginTop="6dp" />
<!-- 导航控制按钮(导航时显示) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="6dp"
android:visibility="gone"
android:id="@+id/layout_navigation_controls">
<Button
android:id="@+id/btn_pause_resume"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="暂停"
android:textSize="13sp" />
<CheckBox
android:id="@+id/cb_mute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="静音"
android:layout_marginStart="12dp" />
<CheckBox
android:id="@+id/cb_follow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="跟随"
android:layout_marginStart="12dp" />
<Button
android:id="@+id/btn_overview"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="总览"
android:textSize="13sp"
android:layout_marginStart="12dp" />
</LinearLayout>
</LinearLayout>
<!-- 底部按钮区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_start_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="开始导航"
android:textSize="14sp"
android:enabled="false"
android:layout_marginEnd="8dp"
android:backgroundTint="@color/amap_primary"
android:textColor="@color/white"
style="@style/Widget.MaterialComponents.Button"
app:cornerRadius="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_stop_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="停止导航"
android:textSize="14sp"
android:textColor="@color/amap_route_current"
android:backgroundTint="@color/white"
android:visibility="gone"
android:layout_marginEnd="8dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
app:strokeColor="@color/amap_route_current"
app:cornerRadius="8dp" />
<Button
android:id="@+id/btn_clear_destination"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="清除"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 机头式指引卡片(顶部浮层) -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_maneuver"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginTop="8dp"
android:visibility="gone"
android:layout_gravity="top|center_horizontal"
app:cardCornerRadius="12dp"
app:cardElevation="8dp"
app:cardBackgroundColor="@color/amap_maneuver_background"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="10dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/tv_maneuver_distance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="--米"
android:textStyle="bold"
android:textSize="24sp"
android:textColor="@color/amap_maneuver_text" />
<ImageView
android:id="@+id/iv_maneuver_arrow"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginStart="12dp"
android:src="@android:drawable/arrow_up_float"
app:tint="@color/amap_maneuver_text" />
<TextView
android:id="@+id/tv_maneuver_road"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:text="进入 --"
android:textSize="16sp"
android:textColor="@color/amap_text_primary" />
<TextView
android:id="@+id/tv_speed_bubble"
android:layout_width="44dp"
android:layout_height="44dp"
android:gravity="center"
android:background="@drawable/bg_circle"
android:text="--\nkm/h"
android:textSize="12sp"
android:textColor="@color/amap_primary"
android:backgroundTint="@color/background_secondary"
android:visibility="gone" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>

@ -0,0 +1,330 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<!-- 标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="成绩管理"
android:textSize="24sp"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="16dp" />
<!-- 登录导出按钮 -->
<Button
android:id="@+id/btn_login_export"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="登录一键导出学业情况"
android:textSize="16sp"
android:backgroundTint="@color/button_primary"
android:textColor="@color/text_white"
android:padding="12dp"
android:layout_marginBottom="16dp" />
<!-- 分组方式切换按钮 -->
<LinearLayout
android:id="@+id/layout_group_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp"
android:visibility="gone">
<Button
android:id="@+id/btn_group_by_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="按修读状态"
android:textSize="14sp"
android:padding="8dp"
android:layout_marginEnd="8dp"
android:backgroundTint="@color/button_secondary"
android:textColor="@color/text_white" />
<Button
android:id="@+id/btn_group_by_term"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="按建议学期"
android:textSize="14sp"
android:padding="8dp"
android:backgroundTint="@color/button_secondary"
android:textColor="@color/text_white" />
</LinearLayout>
<!-- 学生信息 -->
<LinearLayout
android:id="@+id/layout_student_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/background_secondary"
android:padding="12dp"
android:layout_marginBottom="16dp"
android:gravity="center_vertical"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tv_student_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="学生姓名:邱以恒"
android:textSize="16sp"
android:textColor="@color/text_primary"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_overall_gpa"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="总平均绩点3.26"
android:textSize="16sp"
android:textColor="@color/grade_good"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/tv_current_term"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="当前为2024-2025学年 第2学期"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="6dp"
android:visibility="gone" />
<TextView
android:id="@+id/tv_term_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="专业选修课 要求学分:8.0 获得学分:2.0 未获得学分:6.0"
android:textSize="14sp"
android:textColor="@color/grade_danger"
android:layout_marginTop="4dp"
android:visibility="gone" />
<TextView
android:id="@+id/tv_term_warnings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="本学期专业选修课形势与政策未修,请结合自身学分情况考虑是否选修"
android:textSize="14sp"
android:textColor="@color/grade_danger"
android:layout_marginTop="4dp"
android:visibility="gone" />
</LinearLayout>
<!-- 加载进度条 -->
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
<!-- 按修读状态分组的容器 -->
<LinearLayout
android:id="@+id/layout_status_groups"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:visibility="gone">
<!-- 已修完课程卡片 -->
<androidx.cardview.widget.CardView
android:id="@+id/card_completed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:visibility="gone"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 卡片标题(可点击) -->
<LinearLayout
android:id="@+id/header_completed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="已修完课程"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:id="@+id/tv_completed_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="(0)"
android:textSize="16sp"
android:textColor="@color/text_secondary"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/icon_completed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="▼"
android:textSize="14sp"
android:textColor="@color/text_secondary" />
</LinearLayout>
<!-- 折叠内容 -->
<LinearLayout
android:id="@+id/content_completed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="visible">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_completed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- 未修完课程卡片 -->
<androidx.cardview.widget.CardView
android:id="@+id/card_incomplete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:visibility="gone"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 卡片标题(可点击) -->
<LinearLayout
android:id="@+id/header_incomplete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="未修完课程"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:id="@+id/tv_incomplete_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="(0)"
android:textSize="16sp"
android:textColor="@color/text_secondary"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/icon_incomplete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="▼"
android:textSize="14sp"
android:textColor="@color/text_secondary" />
</LinearLayout>
<!-- 折叠内容 -->
<LinearLayout
android:id="@+id/content_incomplete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="visible">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_incomplete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
<!-- 按学期分组的容器ScrollView包裹动态生成的卡片 -->
<ScrollView
android:id="@+id/scroll_view_term_groups"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="gone">
<LinearLayout
android:id="@+id/container_term_groups"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</ScrollView>
<!-- 空状态提示 -->
<TextView
android:id="@+id/tv_empty_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="暂无学业数据"
android:textSize="16sp"
android:gravity="center"
android:layout_marginTop="32dp" />
</LinearLayout>

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:paddingTop="16dp">
<!-- 标题栏和筛选 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/notes_title"
android:textSize="24sp"
android:textStyle="bold" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_input_add"
android:backgroundTint="@color/primary"
app:tint="@color/text_white"
android:contentDescription="@string/add_note" />
</LinearLayout>
<!-- 课程筛选 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="筛选课程:"
android:textSize="16sp"
android:layout_marginEnd="8dp" />
<Spinner
android:id="@+id/spinner_course_filter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
<!-- 搜索笔记 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_note_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="搜索笔记"
app:startIconDrawable="@android:drawable/ic_menu_search"
app:endIconMode="clear_text"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxBackgroundMode="outline"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
android:layout_marginBottom="12dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_note_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:maxLines="1"
android:imeOptions="actionSearch" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 笔记列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_notes"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- 空状态提示 -->
<LinearLayout
android:id="@+id/layout_empty"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="暂无笔记"
android:textSize="18sp"
android:textColor="@color/text_secondary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击右上角 + 号添加笔记"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="8dp" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/background_secondary">
<!-- 课表管理标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/timetable_title"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<!-- 标题栏:三个操作按钮等宽、样式统一 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<Button
android:id="@+id/btn_import_courses"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/import_from_education"
android:textSize="14sp"
android:backgroundTint="@color/button_secondary"
android:textColor="@color/text_white"
android:padding="8dp"
android:maxLines="1"
android:ellipsize="end"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btn_refresh"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="刷新"
android:textSize="14sp"
android:backgroundTint="@color/button_secondary"
android:textColor="@color/text_white"
android:padding="8dp"
android:maxLines="1"
android:ellipsize="end"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btn_clear_all"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="清空"
android:textSize="14sp"
android:backgroundTint="@color/button_danger"
android:textColor="@color/text_white"
android:padding="8dp"
android:maxLines="1"
android:ellipsize="end"
android:layout_marginEnd="8dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_course"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_menu_add"
android:backgroundTint="@color/button_secondary"
android:tint="@color/text_white"
android:contentDescription="@string/add_course" />
</LinearLayout>
<!-- 周视图切换 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginBottom="16dp">
<Button
android:id="@+id/btn_prev_week"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/prev_week"
android:textSize="14sp"
android:backgroundTint="@color/button_secondary"
android:textColor="@color/text_white"
android:padding="8dp" />
<TextView
android:id="@+id/tv_current_week"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/current_week"
android:textAlignment="center"
android:textSize="18sp"
android:layout_margin="8dp" />
<Button
android:id="@+id/btn_next_week"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next_week"
android:textSize="14sp"
android:backgroundTint="@color/button_secondary"
android:textColor="@color/text_white"
android:padding="8dp" />
</LinearLayout>
<!-- 课表容器(包含星期标题和课程内容,同步滚动) -->
<HorizontalScrollView
android:id="@+id/horizontal_scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true"
android:scrollbars="none">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 周标题行(周一~周日) -->
<LinearLayout
android:id="@+id/layout_week_header"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/tv_time_header"
android:layout_width="56dp"
android:layout_height="match_parent"
android:gravity="center"
android:textStyle="bold"
android:text="" />
<LinearLayout
android:id="@+id/week_days_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical" />
</LinearLayout>
<!-- 课表网格与课程块 -->
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fillViewport="false"
android:contentDescription="@string/timetable_scroll_description"
tools:ignore="NestedScrolling">
<RelativeLayout
android:id="@+id/timetable_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</ScrollView>
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
android:background="@color/background_card"
android:layout_margin="4dp"
android:elevation="2dp">
<!-- 课程名称作为主标题 -->
<TextView
android:id="@+id/tv_course_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="课程名称"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<!-- 课程信息行 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- 课程编号 -->
<TextView
android:id="@+id/tv_course_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="编号: 课程编号"
android:textSize="12sp"
android:textColor="@color/text_secondary" />
<!-- 学分 -->
<TextView
android:id="@+id/tv_credit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="学分: 学分"
android:textSize="12sp"
android:textColor="@color/text_secondary" />
</LinearLayout>
<!-- 成绩信息行 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp">
<!-- 成绩 -->
<TextView
android:id="@+id/tv_score"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="成绩: 94"
android:textSize="14sp"
android:textColor="@color/grade_good"
android:textStyle="bold" />
<!-- 成绩输入(当成绩为空时显示) -->
<EditText
android:id="@+id/et_score_input"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="number"
android:hint="请输入成绩"
android:maxLength="3"
android:visibility="gone" />
<!-- 绩点 -->
<TextView
android:id="@+id/tv_gpa"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="绩点: 4.0"
android:textSize="14sp"
android:textColor="@color/grade_excellent"
android:textStyle="bold" />
</LinearLayout>
<!-- 课程性质信息 -->
<TextView
android:id="@+id/tv_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="课程性质: 必修课"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp" />
<!-- 学年和学期信息 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp">
<!-- 学年 -->
<TextView
android:id="@+id/tv_year"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="学年: 2023-2024"
android:textSize="12sp"
android:textColor="@color/text_secondary" />
<!-- 学期 -->
<TextView
android:id="@+id/tv_term"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="学期: 1"
android:textSize="12sp"
android:textColor="@color/text_secondary" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 左滑操作按钮(置于背景层,垂直堆叠圆形按钮) -->
<LinearLayout
android:id="@+id/layout_swipe_actions"
android:layout_width="96dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:orientation="vertical"
android:gravity="center_vertical|end"
android:background="@color/background_secondary"
android:visibility="gone">
<ImageButton
android:id="@+id/btn_edit"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginBottom="12dp"
android:src="@android:drawable/ic_menu_edit"
android:tint="@color/text_white"
android:background="@drawable/bg_circle_primary"
android:backgroundTint="@null"
android:contentDescription="编辑" />
<ImageButton
android:id="@+id/btn_delete"
android:layout_width="56dp"
android:layout_height="56dp"
android:src="@android:drawable/ic_menu_delete"
android:tint="@color/text_white"
android:background="@drawable/bg_circle_danger"
android:backgroundTint="@null"
android:contentDescription="删除" />
</LinearLayout>
<!-- 主内容(置于前景层) -->
<androidx.cardview.widget.CardView
android:id="@+id/card_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tv_note_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="笔记标题"
android:textSize="18sp"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end" />
<TextView
android:id="@+id/tv_course_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="高等数学"
android:textSize="14sp"
android:textColor="@color/text_white"
android:background="@color/accent_green_light"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:layout_marginStart="8dp" />
</LinearLayout>
<TextView
android:id="@+id/tv_note_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="笔记内容预览..."
android:textSize="16sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="8dp"
android:maxLines="3"
android:ellipsize="end" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/tv_modify_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="修改时间"
android:textSize="14sp"
android:textColor="@color/text_secondary" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_timetable"
android:title="课表"
android:icon="@mipmap/ic_launcher" />
<item
android:id="@+id/nav_notes"
android:title="笔记"
android:icon="@mipmap/ic_launcher" />
<item
android:id="@+id/nav_grades"
android:title="成绩"
android:icon="@mipmap/ic_launcher" />
<item
android:id="@+id/nav_campus"
android:title="导航"
android:icon="@mipmap/ic_launcher" />
</menu>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- 阅读界面不再提供编辑/删除按钮,菜单留空 -->
</menu>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save