网络资源模板--基于Android Studio 实现的喝水提醒App

发布于:2025-07-07 ⋅ 阅读:(23) ⋅ 点赞:(0)

目录

一、测试环境说明

二、项目简介

三、项目演示

四、部设计详情(部分)

注册页面

首页

统计页

五、项目源码 


一、测试环境说明

二、项目简介

本应用采用经典的 MVC(Model - View - Controller)架构,将数据模型(Model)、视图(View)和控制器(Controller)分离,提高代码的可维护性和可扩展性。

Model:负责数据的存储和处理,包括用户信息、饮水记录等。使用 Room 数据库来实现数据的持久化存储。

View:负责界面的展示,包括登录界面、主界面、统计界面等。使用 Android 的布局文件和视图组件来构建界面。

Controller:负责处理用户的交互事件和业务逻辑,协调 Model 和 View 之间的通信。使用 Android 的 Activity 和 Fragment 来实现控制器。

三、项目演示

网络资源模板--基于Android studio喝水提醒App

四、部设计详情(部分)

注册页面

package com.example.waterreminder;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Patterns;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import com.example.waterreminder.utils.UserManager;

public class RegisterActivity extends AppCompatActivity {

    private TextInputLayout tilUsername, tilEmail, tilPassword, tilConfirmPassword;
    private TextInputEditText etUsername, etEmail, etPassword, etConfirmPassword;
    private MaterialButton btnRegister, btnBack;
    private View registerCard;
    private UserManager userManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_register);

        userManager = UserManager.getInstance(this);
        initViews();
        setupListeners();
    }

    private void initViews() {
        tilUsername = findViewById(R.id.tilUsername);
        tilEmail = findViewById(R.id.tilEmail);
        tilPassword = findViewById(R.id.tilPassword);
        tilConfirmPassword = findViewById(R.id.tilConfirmPassword);
        
        etUsername = findViewById(R.id.etUsername);
        etEmail = findViewById(R.id.etEmail);
        etPassword = findViewById(R.id.etPassword);
        etConfirmPassword = findViewById(R.id.etConfirmPassword);
        
        btnRegister = findViewById(R.id.btnRegister);
        btnBack = findViewById(R.id.btnBack);
        registerCard = findViewById(R.id.registerCard);
    }

    private void setupListeners() {
        btnRegister.setOnClickListener(v -> attemptRegister());
        btnBack.setOnClickListener(v -> finish());

        // 添加焦点变化监听器,清除错误提示
        setupFocusChangeListener(etUsername, tilUsername);
        setupFocusChangeListener(etEmail, tilEmail);
        setupFocusChangeListener(etPassword, tilPassword);
        setupFocusChangeListener(etConfirmPassword, tilConfirmPassword);
    }

    private void setupFocusChangeListener(TextInputEditText editText, TextInputLayout inputLayout) {
        editText.setOnFocusChangeListener((v, hasFocus) -> {
            if (hasFocus) {
                inputLayout.setError(null);
            }
        });
    }

    private void attemptRegister() {
        // 重置错误提示
        tilUsername.setError(null);
        tilEmail.setError(null);
        tilPassword.setError(null);
        tilConfirmPassword.setError(null);

        String username = etUsername.getText().toString().trim();
        String email = etEmail.getText().toString().trim();
        String password = etPassword.getText().toString();
        String confirmPassword = etConfirmPassword.getText().toString();

        boolean cancel = false;
        View focusView = null;

        // 检查确认密码
        if (TextUtils.isEmpty(confirmPassword)) {
            tilConfirmPassword.setError("请确认密码");
            focusView = etConfirmPassword;
            cancel = true;
            shakeView(tilConfirmPassword);
        } else if (!password.equals(confirmPassword)) {
            tilConfirmPassword.setError("两次输入的密码不一致");
            focusView = etConfirmPassword;
            cancel = true;
            shakeView(tilConfirmPassword);
        }

        // 检查密码
        if (TextUtils.isEmpty(password)) {
            tilPassword.setError("请输入密码");
            focusView = etPassword;
            cancel = true;
            shakeView(tilPassword);
        } else if (password.length() < 6) {
            tilPassword.setError("密码长度至少为6位");
            focusView = etPassword;
            cancel = true;
            shakeView(tilPassword);
        }

        // 检查邮箱
        if (TextUtils.isEmpty(email)) {
            tilEmail.setError("请输入邮箱");
            focusView = etEmail;
            cancel = true;
            shakeView(tilEmail);
        } else if (!email.toLowerCase().contains("@qq.com")) {
            tilEmail.setError("请输入有效的QQ邮箱地址");
            focusView = etEmail;
            cancel = true;
            shakeView(tilEmail);
        }

        // 检查用户名
        if (TextUtils.isEmpty(username)) {
            tilUsername.setError("请输入用户名");
            focusView = etUsername;
            cancel = true;
            shakeView(tilUsername);
        }

        if (cancel) {
            focusView.requestFocus();
        } else {
            // 尝试注册
            if (userManager.register(username, password, email)) {
                showSuccessAnimation();
            } else {
                showRegisterError();
            }
        }
    }

    private void shakeView(View view) {
        Animation shake = AnimationUtils.loadAnimation(this, R.anim.shake);
        view.startAnimation(shake);
    }

    private void showSuccessAnimation() {
        // 禁用所有输入
        setInputsEnabled(false);

        // 创建圆形收缩动画
        int cx = registerCard.getWidth() / 2;
        int cy = registerCard.getHeight() / 2;
        float finalRadius = (float) Math.hypot(cx, cy);

        Animator anim = ViewAnimationUtils.createCircularReveal(registerCard, cx, cy, finalRadius, 0);
        anim.setDuration(500);

        // 动画结束后关闭活动
        anim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                registerCard.setVisibility(View.INVISIBLE);
                Toast.makeText(RegisterActivity.this, "注册成功", Toast.LENGTH_SHORT).show();
                finish();
                overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
            }
        });

        anim.start();
    }

    private void showRegisterError() {
        tilUsername.setError("用户名已存在");
        shakeView(tilUsername);
    }

    private void setInputsEnabled(boolean enabled) {
        etUsername.setEnabled(enabled);
        etEmail.setEnabled(enabled);
        etPassword.setEnabled(enabled);
        etConfirmPassword.setEnabled(enabled);
        btnRegister.setEnabled(enabled);
        btnBack.setEnabled(enabled);
    }
} 

一、用户界面与交互设计

该注册界面采用Material Design组件构建,包含用户名、邮箱、密码和确认密码四个输入字段。

界面交互设计具有以下特点:

1. 错误处理机制:每个输入字段都配备了实时错误提示功能,当用户输入不符合要求时,会在对应字段下方显示错误信息并触发抖动动画(shake动画),增强用户感知。

2. 焦点管理:为所有输入字段设置了焦点变化监听器,当用户点击某个输入框时,自动清除该字段之前的错误提示,避免干扰当前输入。

3. 动画反馈:注册成功时采用圆形收缩动画(CircularReveal)作为视觉反馈,配合渐隐过渡效果,提供流畅的用户体验。

二、输入验证逻辑

注册表单实现了严格的客户端验证逻辑,验证顺序从下往上(确认密码→密码→邮箱→用户名),确保及时发现所有问题:

1. 用户名验证:检查非空,确保用户必须输入用户名。

2. 邮箱验证:检查非空且必须包含"@qq.com"(不区分大小写),专门针对QQ邮箱进行验证。

3. 密码验证:检查非空且长度至少6位,符合基本密码安全要求。

4. 确认密码验证:检查非空且必须与密码字段完全一致,防止用户输入错误。

三、业务逻辑实现

1. 用户管理:通过UserManager单例处理核心注册逻辑,封装了用户数据的持久化操作。

2. 线程处理:注册操作在主线程执行,适合轻量级的本地用户管理。如需网络请求应考虑异步处理。

3. 状态管理:注册过程中禁用所有输入控件,防止重复提交;注册成功后立即关闭当前活动。

四、动画与视觉效果

1. 错误提示动画:使用R.anim.shake资源实现输入错误的抖动效果,增强用户注意力。

2. 成功动画:采用ViewAnimationUtils.createCircularReveal创建圆形收缩动画,视觉上"收起"注册卡片,暗示注册完成。

3. 过渡动画:活动关闭时使用系统提供的淡入淡出效果(fade_in/fade_out),保持界面切换流畅性。

五、代码组织特点

1. 职责分离:将视图初始化、事件监听设置和业务逻辑分离到不同方法中,保持代码清晰。

2. 重用性:通过setupFocusChangeListener方法统一处理所有输入框的焦点变化逻辑,避免重复代码。

3. 可扩展性:验证逻辑模块化设计,便于后续添加更多验证规则或修改现有规则。

首页

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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:padding="16dp">

    <TextView
        android:id="@+id/tv_today_count"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="今日已喝水"
        android:textSize="24sp"
        android:textColor="@color/colorPrimary"
        android:layout_marginTop="32dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <com.example.waterreminder.ui.custom.WaterGlassView
        android:id="@+id/water_glass_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginHorizontal="32dp"
        android:layout_marginVertical="32dp"
        app:layout_constraintDimensionRatio="1:1.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toTopOf="@id/drinkOptionsLayout"/>

    <LinearLayout
        android:id="@+id/drinkOptionsLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="16dp"
        android:gravity="center"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="16dp">

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_water"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_margin="8dp"
            android:text="白水"
            app:icon="@drawable/ic_water"
            style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_coffee"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_margin="8dp"
            android:text="咖啡"
            app:icon="@drawable/ic_coffee"
            style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_tea"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_margin="8dp"
            android:text="茶"
            app:icon="@drawable/ic_tea"
            style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout> 

一、基本架构与数据管理

该代码实现了一个饮水记录应用的主界面功能模块,采用标准的Android开发架构。

核心数据管理通过Room数据库实现,创建了DrinkDatabase单例来管理饮水记录数据的持久化存储。

数据库操作采用异步线程处理,确保数据库访问不会阻塞UI线程,同时使用runOnUiThread回调实现线程安全的UI更新。

二、用户界面交互设计

界面包含三个主要交互元素:白水、咖啡和茶的记录按钮。其中白水按钮触发自定义输入对话框,允许用户输入任意饮水量;咖啡和茶按钮则直接记录预设值(200ml和250ml)。

自定义对话框dialog_custom_amount采用Material Design组件构建,包含输入验证逻辑,确保用户只能输入有效的正整数值。

饮水进度通过自定义视图WaterGlassView可视化展示,以水位上升动画直观反映当前饮水进度与每日目标的比例关系。

三、业务逻辑实现

每日饮水统计功能基于时间范围查询实现,利用StatsUtils.getTodayStartMillis()获取当日0点时间戳作为查询起点。

总饮水量与预设的DAILY_GOAL(2000ml)比较计算完成百分比,驱动水位视图的动画效果。

记录添加操作采用"插入后刷新"模式,每次新增记录后自动触发今日统计数据的重新计算和界面更新,保证数据的实时一致性。

四、代码组织特点

该Fragment遵循了清晰的职责分离原则:视图初始化在onViewCreated中完成,业务逻辑封装在独立方法中,数据库操作全部在后台线程执行。采用lambda表达式简化事件监听器的实现,使代码更加简洁。错误处理方面,对用户输入进行了基本的验证和友好的Toast提示,提升了用户体验。

统计页

package com.example.waterreminder.ui.statistics;

import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.ProgressBar;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.example.waterreminder.DrinkDatabase;
import com.example.waterreminder.R;
import com.example.waterreminder.StatsUtils;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.formatter.ValueFormatter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executors;
import com.example.waterreminder.DrinkRecord;

public class StatisticsFragment extends Fragment {

    private DrinkDatabase db;
    private TextView tvTotalDrinks, tvAvgInterval, tvConsecutiveDays;
    private ViewGroup chartContainer;
    private SimpleDateFormat dateFormat;
    private TextView tvWaterAmount, tvCoffeeAmount, tvTeaAmount;
    private ProgressBar progressWater, progressCoffee, progressTea;

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                           @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_statistics, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        db = DrinkDatabase.getInstance(requireContext());
        dateFormat = new SimpleDateFormat("MM-dd", Locale.getDefault());
        
        // 初始化视图
        tvTotalDrinks = view.findViewById(R.id.tv_total_drinks);
        tvAvgInterval = view.findViewById(R.id.tv_avg_interval);
        tvConsecutiveDays = view.findViewById(R.id.tv_consecutive_days);
        chartContainer = view.findViewById(R.id.chart_container);
        tvWaterAmount = view.findViewById(R.id.tv_water_amount);
        tvCoffeeAmount = view.findViewById(R.id.tv_coffee_amount);
        tvTeaAmount = view.findViewById(R.id.tv_tea_amount);
        progressWater = view.findViewById(R.id.progress_water);
        progressCoffee = view.findViewById(R.id.progress_coffee);
        progressTea = view.findViewById(R.id.progress_tea);

        // 创建图表
        setupChart();
        
        // 加载数据
        loadStatistics();
        loadDrinkDetails();
    }

    private void setupChart() {
        BarChart chart = new BarChart(requireContext());
        chart.setLayoutParams(new ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT));
        
        // 配置图表
        chart.getDescription().setEnabled(false);
        chart.setDrawGridBackground(false);
        chart.setDrawBarShadow(false);
        chart.setHighlightFullBarEnabled(false);
        
        // 配置X轴
        XAxis xAxis = chart.getXAxis();
        xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
        xAxis.setDrawGridLines(false);
        xAxis.setGranularity(1f);
        xAxis.setValueFormatter(new ValueFormatter() {
            @Override
            public String getFormattedValue(float value) {
                Calendar cal = Calendar.getInstance();
                cal.add(Calendar.DAY_OF_YEAR, (int)value - 6); // 从最近7天开始
                return dateFormat.format(cal.getTime());
            }
        });
        
        // 配置Y轴
        YAxis leftAxis = chart.getAxisLeft();
        leftAxis.setDrawGridLines(true);
        leftAxis.setAxisMinimum(0f);
        leftAxis.setValueFormatter(new ValueFormatter() {
            @Override
            public String getFormattedValue(float value) {
                return String.format(Locale.getDefault(), "%.0f ml", value);
            }
        });
        
        chart.getAxisRight().setEnabled(false);
        
        // 添加到容器
        chartContainer.addView(chart);
        
        // 加载图表数据
        loadChartData(chart);
    }

    private void loadChartData(BarChart chart) {
        Executors.newSingleThreadExecutor().execute(() -> {
            // 获取最近7天的数据
            List<BarEntry> entries = new ArrayList<>();
            Calendar cal = Calendar.getInstance();
            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
            cal.set(Calendar.SECOND, 0);
            cal.set(Calendar.MILLISECOND, 0);
            
            // 获取7天前的开始时间
            cal.add(Calendar.DAY_OF_YEAR, -6);
            
            for (int i = 0; i < 7; i++) {
                long startTime = cal.getTimeInMillis();
                cal.add(Calendar.DAY_OF_YEAR, 1);
                long endTime = cal.getTimeInMillis();
                
                // 获取该天的总饮水量
                int totalAmount = db.drinkDao().getTotalAmountBetween(startTime, endTime);
                entries.add(new BarEntry(i, totalAmount));
            }

            // 创建数据集
            BarDataSet dataSet = new BarDataSet(entries, "每日饮水量(ml)");
            dataSet.setColor(Color.BLUE);
            dataSet.setValueTextColor(Color.BLACK);
            dataSet.setValueTextSize(10f);
            dataSet.setValueFormatter(new ValueFormatter() {
                @Override
                public String getFormattedValue(float value) {
                    return String.format(Locale.getDefault(), "%.0f", value);
                }
            });

            // 设置数据
            BarData barData = new BarData(dataSet);
            barData.setBarWidth(0.7f);
            
            // 在主线程更新UI
            requireActivity().runOnUiThread(() -> {
                chart.setData(barData);
                chart.invalidate();
            });
        });
    }

    private void loadStatistics() {
        Executors.newSingleThreadExecutor().execute(() -> {
            // 获取本月总饮水量
            Calendar cal = Calendar.getInstance();
            cal.set(Calendar.DAY_OF_MONTH, 1);
            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
            cal.set(Calendar.SECOND, 0);
            cal.set(Calendar.MILLISECOND, 0);
            long monthStart = cal.getTimeInMillis();
            
            int monthlyTotal = db.drinkDao().getTotalAmountBetween(
                monthStart,
                System.currentTimeMillis()
            );

            long avgInterval = StatsUtils.getAverageInterval(
                db.drinkDao().getRecordsBetween(
                    StatsUtils.getTodayStartMillis(),
                    System.currentTimeMillis()
                )
            );

            int consecutiveDays = StatsUtils.getConsecutiveGoalDays(
                db.drinkDao(),
                requireContext().getSharedPreferences("settings", 0).getInt("daily_goal", 2000)
            );

            // 更新UI
            requireActivity().runOnUiThread(() -> {
                tvTotalDrinks.setText("本月总计:" + monthlyTotal + " ml");
                tvAvgInterval.setText("平均间隔:" + (avgInterval > 0 ? (avgInterval / 60000) : "暂无数据") + " 分钟");
                tvConsecutiveDays.setText("连续达标:" + consecutiveDays + " 天");
            });
        });
    }

    private void loadDrinkDetails() {
        Executors.newSingleThreadExecutor().execute(() -> {
            // 获取最近7天的数据
            Calendar cal = Calendar.getInstance();
            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
            cal.set(Calendar.SECOND, 0);
            cal.set(Calendar.MILLISECOND, 0);
            
            // 获取7天前的开始时间
            cal.add(Calendar.DAY_OF_YEAR, -6);
            long weekStart = cal.getTimeInMillis();
            long now = System.currentTimeMillis();
            
            java.util.List<DrinkRecord> weekRecords = db.drinkDao().getRecordsBetween(weekStart, now);
            int total = 0, water = 0, coffee = 0, tea = 0;
            
            for (DrinkRecord r : weekRecords) {
                total += r.amount;
                if ("白水".equals(r.type)) water += r.amount;
                else if ("咖啡".equals(r.type)) coffee += r.amount;
                else if ("茶".equals(r.type)) tea += r.amount;
            }
            
            int waterPercent = total > 0 ? (water * 100 / total) : 0;
            int coffeePercent = total > 0 ? (coffee * 100 / total) : 0;
            int teaPercent = total > 0 ? (tea * 100 / total) : 0;
            
            final int fWater = water;
            final int fCoffee = coffee;
            final int fTea = tea;
            final int fWaterPercent = waterPercent;
            final int fCoffeePercent = coffeePercent;
            final int fTeaPercent = teaPercent;
            
            requireActivity().runOnUiThread(() -> {
                tvWaterAmount.setText(fWater + " ml [" + fWaterPercent + "%]");
                progressWater.setProgress(fWaterPercent);
                tvCoffeeAmount.setText(fCoffee + " ml [" + fCoffeePercent + "%]");
                progressCoffee.setProgress(fCoffeePercent);
                tvTeaAmount.setText(fTea + " ml [" + fTeaPercent + "%]");
                progressTea.setProgress(fTeaPercent);
            });
        });
    }
} 

面展示用户的饮水习惯。该模块采用MPAndroidChart库构建七日饮水量柱状图,通过自定义X轴日期标签和带单位的Y轴数值,直观呈现饮水趋势变化。

数据加载采用异步线程处理,通过Executors管理数据库查询任务,确保UI流畅性。

核心统计功能包含三个层次的时间范围分析:当日数据用于计算平均饮水间隔,七日数据支撑图表展示和饮品分类占比,月度数据则统计总饮水量。

统计指标通过Room数据库的高效查询实现,利用@Query注解获取特定时间段的记录,在内存中进行二次聚合计算。

特别设计的连续达标天数算法,结合SharedPreferences存储的每日目标值,动态追踪用户饮水目标的达成情况。

分类统计功能将饮水量按白水、咖啡、茶三种类型细分,不仅显示绝对数值,还通过进度条直观展示各类饮品占比。

数据处理过程中加入严格的空值检查机制,避免零除错误,并对无数据情况显示友好提示。

日期显示统一采用SimpleDateFormat格式化为"MM-dd"格式,保证界面信息的一致性。

性能优化方面,模块采用多线程架构分离数据加载与UI渲染,通过局部变量缓存中间计算结果,有效降低内存占用。

动态创建的图表组件适配不同屏幕尺寸,进度条长度随数据实时调整,形成响应式布局。

整体实现兼顾了数据准确性、视觉表现力和操作流畅度,为用户提供全面的饮水行为分析支持。

五、项目源码 

👇👇👇👇👇快捷方式👇👇👇👇👇


网站公告

今日签到

点亮在社区的每一天
去签到