当前位置: 首页 > news >正文

微信小程序-智慧社区项目开发完整技术文档(上)

微信小程序-智慧社区项目开发完整技术文档(上)

学习项目(仅作参考)

一、项目概述

1.1 项目背景与目标

智慧社区项目旨在通过数字化手段提升社区管理效率和服务质量。该项目采用微信小程序作为前端载体,结合Django后端框架,为社区居民和网格员提供便捷的信息采集、人脸识别、语音识别等功能。

1.2 核心功能架构

┌─ 欢迎页面 (动态广告页)
├─ 首页 (轮播图 + 公告栏 + 功能入口)
├─ 信息采集模块
│  ├─ 采集列表
│  ├─ 表单填写
│  ├─ 拍照功能
│  └─ 数据统计
├─ 人脸识别模块
│  ├─ 人脸注册
│  ├─ 人脸搜索
│  └─ 识别记录
├─ 语音识别模块
│  ├─ 录音功能
│  └─ 语音转文字
├─ 社区活动
├─ 积分商城
└─ 个人中心

1.3 技术栈详解

  • 前端技术栈:

    • 微信小程序原生框架
    • Vant Weapp UI组件库
    • WXML/WXSS/JavaScript
    • 微信原生API(相机、录音等)
  • 后端技术栈:

    • Django 3.2+ Web框架
    • Django REST Framework (DRF)
    • SQLite/MySQL数据库
    • SimpleUI后台管理
    • Pillow图像处理库
    • 百度AI SDK

二、环境搭建与项目初始化

2.1 小程序端详细配置

项目结构规划
smart-community/
├── pages/
│   ├── welcome/
│   ├── index/
│   ├── collection/
│   ├── form/
│   ├── camera/
│   ├── face/
│   ├── voice/
│   └── statistics/
├── components/
├── static/
│   ├── images/
│   ├── css/
│   └── icons/
├── config/
├── utils/
└── app.js
详细的app.json配置
{"pages": ["pages/welcome/welcome","pages/index/index","pages/collection/collection","pages/form/form","pages/camera/camera","pages/face/face","pages/voice/voice","pages/statistics/statistics","pages/activity/activity","pages/goods/goods","pages/profile/profile"],"tabBar": {"color": "#999999","selectedColor": "#007AFF","backgroundColor": "#ffffff","borderStyle": "black","list": [{"pagePath": "pages/index/index","text": "首页","iconPath": "static/images/icon/home.png","selectedIconPath": "static/images/icon/home-active.png"},{"pagePath": "pages/activity/activity","text": "活动","iconPath": "static/images/icon/activity.png","selectedIconPath": "static/images/icon/activity-active.png"},{"pagePath": "pages/profile/profile","text": "我的","iconPath": "static/images/icon/profile.png","selectedIconPath": "static/images/icon/profile-active.png"}]},"window": {"backgroundTextStyle": "light","navigationBarBackgroundColor": "#007AFF","navigationBarTitleText": "智慧社区","navigationBarTextStyle": "white","enablePullDownRefresh": true},"permission": {"scope.camera": {"desc": "需要访问相机进行拍照和人脸识别"},"scope.record": {"desc": "需要访问麦克风进行语音识别"}},"requiredPrivateInfos": ["camera","record"]
}
项目配置文件优化
// project.config.json
{"description": "项目配置文件","packOptions": {"ignore": [{"type": "file","value": ".eslintrc.js"}]},"setting": {"urlCheck": false,"es6": true,"enhance": true,"postcss": true,"preloadBackgroundData": false,"minified": true,"newFeature": false,"coverView": true,"nodeModules": false,"autoAudits": false,"showShadowRootInWxmlPanel": true,"scopeDataCheck": false,"uglifyFileName": false,"checkInvalidKey": true,"checkSiteMap": true,"uploadWithSourceMap": true,"compileHotReLoad": false,"lazyloadPlaceholderEnable": false,"useMultiFrameRuntime": true,"useApiHook": true,"useApiHostProcess": true,"babelSetting": {"ignore": [],"disablePlugins": [],"outputPath": ""},"enableEngineNative": false,"useIsolateContext": true,"userConfirmedBundleSwitch": false,"packNpmManually": true,"packNpmRelationList": [{"packageJsonPath": "./package.json","miniprogramNpmDistDir": "./miniprogram/"}],"minifyWXSS": true,"showES6CompileOption": false,"minifyWXML": true},"compileType": "miniprogram","libVersion": "2.19.4","appid": "你的小程序AppID","projectname": "smart-community","debugOptions": {"hidedInDevtools": []},"scripts": {},"staticServerOptions": {"baseURL": "","servePath": ""},"isGameTourist": false,"condition": {"search": {"list": []},"conversation": {"list": []},"game": {"list": []},"plugin": {"list": []},"gamePlugin": {"list": []},"miniprogram": {"list": []}}
}

2.2 后端Django项目详细搭建

项目结构设计
smart-backend/
├── manage.py
├── smart_backend/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── smart/
│   ├── migrations/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── serializers.py
│   ├── views.py
│   ├── urls.py
│   └── libs/
│       └── baidu_ai.py
├── media/
│   ├── welcome/
│   ├── banner/
│   ├── collection/
│   └── notice/
└── requirements.txt
详细的settings.py配置
"""
Django settings for smart_backend project.
"""
import os
from pathlib import Path
from datetime import timedelta# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-your-secret-key-here'# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = TrueALLOWED_HOSTS = ['*']# Application definition
INSTALLED_APPS = ['simpleui','django.contrib.admin','django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles','rest_framework','corsheaders','django_filters','smart.apps.SmartConfig',
]MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware','django.middleware.security.SecurityMiddleware','django.contrib.sessions.middleware.SessionMiddleware','django.middleware.common.CommonMiddleware','django.middleware.csrf.CsrfViewMiddleware','django.contrib.auth.middleware.AuthenticationMiddleware','django.contrib.messages.middleware.MessageMiddleware','django.middleware.clickjacking.XFrameOptionsMiddleware',
]ROOT_URLCONF = 'smart_backend.urls'TEMPLATES = [{'BACKEND': 'django.template.backends.django.DjangoTemplates','DIRS': [BASE_DIR / 'templates'],'APP_DIRS': True,'OPTIONS': {'context_processors': ['django.template.context_processors.debug','django.template.context_processors.request','django.contrib.auth.context_processors.auth','django.contrib.messages.context_processors.messages',],},},
]WSGI_APPLICATION = 'smart_backend.wsgi.application'# Database
DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3','NAME': BASE_DIR / 'db.sqlite3',}
}# Password validation
AUTH_PASSWORD_VALIDATORS = [{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',},{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',},{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',},{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',},
]# Internationalization
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = True# Static files (CSS, JavaScript, Images)
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'# REST Framework configuration
REST_FRAMEWORK = {'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.AllowAny',],'DEFAULT_PARSER_CLASSES': ['rest_framework.parsers.JSONParser','rest_framework.parsers.MultiPartParser','rest_framework.parsers.FormParser',],'DEFAULT_RENDERER_CLASSES': ['rest_framework.renderers.JSONRenderer',],'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend',],'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination','PAGE_SIZE': 20
}# CORS settings
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_METHODS = ['DELETE','GET','OPTIONS','PATCH','POST','PUT',
]CORS_ALLOW_HEADERS = ['accept','accept-encoding','authorization','content-type','dnt','origin','user-agent','x-csrftoken','x-requested-with',
]# Baidu AI configuration
BAIDU_AI_APP_ID = 'your_app_id'
BAIDU_AI_API_KEY = 'your_api_key'
BAIDU_AI_SECRET_KEY = 'your_secret_key'# SimpleUI configuration
SIMPLEUI_HOME_INFO = False
SIMPLEUI_ANALYSIS = False
SIMPLEUI_DEFAULT_THEME = 'admin.lte.css'
详细的URL配置
# smart_backend/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import staticurlpatterns = [path('admin/', admin.site.urls),path('smart/', include('smart.urls')),
]# 开发环境下的媒体文件服务
if settings.DEBUG:urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

2.3 Vant Weapp深度集成

详细的npm配置和构建
// package.json
{"name": "smart-community","version": "1.0.0","description": "智慧社区微信小程序","main": "app.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"keywords": ["微信小程序", "智慧社区"],"author": "Your Name","license": "MIT","dependencies": {"@vant/weapp": "^1.10.0"},"devDependencies": {}
}
组件全局注册
// app.js
App({onLaunch() {// 展示本地存储能力const logs = wx.getStorageSync('logs') || []logs.unshift(Date.now())wx.setStorageSync('logs', logs)// 登录wx.login({success: res => {console.log('登录成功', res.code)// 发送 res.code 到后台换取 openId, sessionKey, unionId}})},globalData: {userInfo: null,baseUrl: 'http://192.168.1.100:8000/smart/',uploadUrl: 'http://192.168.1.100:8000/smart/upload/'}
})
/* app.wxss */
page {background-color: #f7f7f7;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'Segoe UI', Arial, Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
}.container {padding: 20rpx;box-sizing: border-box;
}/* 引入Vant Weapp样式 */
@import '/miniprogram_npm/@vant/weapp/common/index.wxss';/* 引入图标字体 */
@import '/static/css/iconfont.wxss';

三、欢迎页面深度开发

3.1 前端完整实现

WXML结构优化
<!-- pages/welcome/welcome.wxml -->
<view class="welcome-container"><!-- 背景图片 --><image src="{{splashImage}}" mode="aspectFill" class="splash-image"bindload="onImageLoad"binderror="onImageError"></image><!-- 遮罩层 --><view class="overlay"><!-- 应用Logo --><image src="/static/images/logo.png" class="app-logo"></image><!-- 应用名称 --><text class="app-name">智慧社区</text><!-- 应用标语 --><text class="app-slogan">数字化社区管理解决方案</text></view><!-- 跳过按钮 --><view class="skip-container"><view class="skip-btn" bindtap="skipWelcome"><text class="skip-text">跳过</text><view class="countdown-badge">{{countdown}}s</view></view></view><!-- 加载状态 --><view class="loading-container" wx:if="{{isLoading}}"><view class="loading-spinner"></view><text class="loading-text">加载中...</text></view>
</view>
WXSS样式深度优化
/* pages/welcome/welcome.wxss */
.welcome-container {position: relative;width: 100vw;height: 100vh;overflow: hidden;
}.splash-image {width: 100%;height: 100%;transition: opacity 0.5s ease;
}.overlay {position: absolute;top: 0;left: 0;width: 100%;height: 100%;background: linear-gradient(to bottom,rgba(0, 0, 0, 0.3) 0%,rgba(0, 0, 0, 0.1) 50%,rgba(0, 0, 0, 0.3) 100%);display: flex;flex-direction: column;justify-content: center;align-items: center;padding: 60rpx;box-sizing: border-box;
}.app-logo {width: 120rpx;height: 120rpx;border-radius: 24rpx;margin-bottom: 40rpx;box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
}.app-name {font-size: 48rpx;font-weight: bold;color: #ffffff;margin-bottom: 20rpx;text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.5);
}.app-slogan {font-size: 28rpx;color: rgba(255, 255, 255, 0.9);text-align: center;line-height: 1.5;
}.skip-container {position: absolute;top: 120rpx;right: 40rpx;z-index: 10;
}.skip-btn {display: flex;align-items: center;background: rgba(0, 0, 0, 0.4);border-radius: 40rpx;padding: 16rpx 32rpx;backdrop-filter: blur(10rpx);border: 1rpx solid rgba(255, 255, 255, 0.2);
}.skip-text {font-size: 28rpx;color: #ffffff;margin-right: 16rpx;
}.countdown-badge {background: #ff3b30;color: #ffffff;font-size: 24rpx;width: 48rpx;height: 48rpx;border-radius: 50%;display: flex;align-items: center;justify-content: center;font-weight: bold;
}.loading-container {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);display: flex;flex-direction: column;align-items: center;background: rgba(0, 0, 0, 0.7);padding: 40rpx;border-radius: 20rpx;backdrop-filter: blur(20rpx);
}.loading-spinner {width: 60rpx;height: 60rpx;border: 4rpx solid rgba(255, 255, 255, 0.3);border-top: 4rpx solid #ffffff;border-radius: 50%;animation: spin 1s linear infinite;margin-bottom: 20rpx;
}.loading-text {font-size: 28rpx;color: #ffffff;
}@keyframes spin {0% { transform: rotate(0deg); }100% { transform: rotate(360deg); }
}/* 响应式适配 */
@media (max-width: 375px) {.app-name {font-size: 42rpx;}.app-slogan {font-size: 26rpx;}
}
JavaScript逻辑完整实现
// pages/welcome/welcome.js
const app = getApp()Page({data: {splashImage: '/static/images/default-splash.jpg',countdown: 5,isLoading: true,hasError: false,timer: null},onLoad() {this.initWelcomePage()},onUnload() {// 清理定时器if (this.data.timer) {clearInterval(this.data.timer)}},async initWelcomePage() {try {// 并行执行加载任务await Promise.all([this.loadSplashImage(),this.preloadHomeData()])this.startCountdown()} catch (error) {console.error('初始化欢迎页失败:', error)this.handleInitError()}},async loadSplashImage() {return new Promise((resolve, reject) => {wx.request({url: app.globalData.baseUrl + 'welcome/',method: 'GET',timeout: 10000,success: (res) => {if (res.statusCode === 200 && res.data.code === 200) {this.setData({splashImage: res.data.data.image,isLoading: false})resolve(res.data)} else {reject(new Error('获取欢迎页图片失败'))}},fail: (error) => {console.warn('使用默认欢迎图片:', error)this.setData({ isLoading: false })resolve() // 使用默认图片,不阻断流程}})})},async preloadHomeData() {// 预加载首页数据return new Promise((resolve) => {wx.request({url: app.globalData.baseUrl + 'home/',method: 'GET',success: () => {console.log('首页数据预加载成功')resolve()},fail: () => {console.warn('首页数据预加载失败')resolve() // 预加载失败不影响主流程}})})},startCountdown() {let countdown = this.data.countdownconst timer = setInterval(() => {countdown--this.setData({ countdown })if (countdown <= 0) {clearInterval(timer)this.navigateToHome()}}, 1000)this.setData({ timer })},skipWelcome() {if (this.data.timer) {clearInterval(this.data.timer)}this.navigateToHome()},navigateToHome() {wx.switchTab({url: '/pages/index/index',success: () => {console.log('跳转到首页成功')},fail: (error) => {console.error('跳转到首页失败:', error)wx.showToast({title: '跳转失败',icon: 'error'})}})},onImageLoad(e) {console.log('欢迎页图片加载成功')this.setData({ isLoading: false })},onImageError(e) {console.error('欢迎页图片加载失败:', e.detail)this.setData({splashImage: '/static/images/default-splash.jpg',isLoading: false,hasError: true})},handleInitError() {this.setData({isLoading: false,hasError: true})wx.showToast({title: '加载失败,请重试',icon: 'error',duration: 2000})// 3秒后自动跳转setTimeout(() => {this.navigateToHome()}, 3000)},onShareAppMessage() {return {title: '智慧社区 - 数字化社区管理',path: '/pages/welcome/welcome',imageUrl: this.data.splashImage}}
})
// pages/welcome/welcome.json
{"navigationStyle": "custom","disableScroll": true
}

3.2 后端数据模型详细设计

欢迎页模型扩展
# smart/models.py
from django.db import models
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidatorclass Welcome(models.Model):"""欢迎页图片模型"""IMAGE_TYPES = (('splash', '启动页'),('ad', '广告页'),('promotion', '推广页'),)title = models.CharField(max_length=100, verbose_name="图片标题", blank=True)image = models.ImageField(upload_to='welcome/%Y/%m/%d/', default='welcome/default-splash.png',verbose_name="欢迎页图片")image_type = models.CharField(max_length=20, choices=IMAGE_TYPES, default='splash',verbose_name="图片类型")order = models.IntegerField(default=0,validators=[MinValueValidator(0), MaxValueValidator(999)],verbose_name="排序权重",help_text="数字越大排序越靠前")start_time = models.DateTimeField(default=timezone.now,verbose_name="开始时间",help_text="图片开始显示的时间")end_time = models.DateTimeField(null=True, blank=True,verbose_name="结束时间",help_text="图片结束显示的时间,为空表示长期有效")is_active = models.BooleanField(default=True, verbose_name="是否启用")is_delete = models.BooleanField(default=False, verbose_name="逻辑删除")click_url = models.URLField(blank=True, verbose_name="点击链接",help_text="点击图片跳转的链接")description = models.TextField(blank=True, verbose_name="图片描述")create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")class Meta:db_table = 'smart_welcome'verbose_name = '欢迎页图片'verbose_name_plural = verbose_nameordering = ['-order', '-create_time']indexes = [models.Index(fields=['is_active', 'is_delete', 'image_type']),models.Index(fields=['start_time', 'end_time']),]def __str__(self):return self.title or f"欢迎页图片-{self.id}"@propertydef is_valid(self):"""检查图片是否在有效期内"""now = timezone.now()if not self.is_active or self.is_delete:return Falseif self.start_time and self.start_time > now:return Falseif self.end_time and self.end_time < now:return Falsereturn Truedef get_absolute_image_url(self, request=None):"""获取完整的图片URL"""if self.image and hasattr(self.image, 'url'):if request:return request.build_absolute_uri(self.image.url)return self.image.urlreturn None
管理后台配置
# smart/admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Welcome@admin.register(Welcome)
class WelcomeAdmin(admin.ModelAdmin):list_display = ['id', 'image_preview', 'title', 'image_type', 'order', 'is_active', 'is_valid', 'create_time']list_filter = ['image_type', 'is_active', 'is_delete', 'create_time']search_fields = ['title', 'description']list_editable = ['order', 'is_active']readonly_fields = ['create_time', 'update_time', 'image_preview']fieldsets = (('基本信息', {'fields': ('title', 'image', 'image_preview', 'image_type', 'description')}),('时间设置', {'fields': ('start_time', 'end_time')}),('配置设置', {'fields': ('order', 'click_url', 'is_active')}),('系统信息', {'fields': ('create_time', 'update_time'),'classes': ('collapse',)}),)def image_preview(self, obj):if obj.image:return format_html('<img src="{}" style="max-height: 50px; max-width: 100px;" />',obj.image.url)return "-"image_preview.short_description = '图片预览'def is_valid(self, obj):return obj.is_validis_valid.boolean = Trueis_valid.short_description = '是否有效'def get_queryset(self, request):return super().get_queryset(request).filter(is_delete=False)def delete_model(self, request, obj):"""软删除"""obj.is_delete = Trueobj.is_active = Falseobj.save()def delete_queryset(self, request, queryset):"""批量软删除"""queryset.update(is_delete=True, is_active=False)

3.3 后端API接口完整实现

序列化器
# smart/serializers.py
from rest_framework import serializers
from .models import Welcomeclass WelcomeSerializer(serializers.ModelSerializer):image_url = serializers.SerializerMethodField()is_valid = serializers.ReadOnlyField()class Meta:model = Welcomefields = ['id', 'title', 'image_url', 'image_type', 'order','click_url', 'description', 'is_valid', 'create_time']read_only_fields = ['is_valid']def get_image_url(self, obj):request = self.context.get('request')return obj.get_absolute_image_url(request)
视图集实现
# smart/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.utils import timezone
from django.db.models import Q
from .models import Welcome
from .serializers import WelcomeSerializerclass WelcomeAPIView(APIView):"""欢迎页图片API支持获取当前有效的欢迎页图片"""def get(self, request):try:now = timezone.now()# 查询当前有效的欢迎页图片welcome_images = Welcome.objects.filter(is_active=True,is_delete=False,start_time__lte=now).filter(Q(end_time__isnull=True) | Q(end_time__gte=now)).order_by('-order', '-create_time')if not welcome_images.exists():return Response({'code': 404,'message': '未找到有效的欢迎页图片','data': None}, status=status.HTTP_404_NOT_FOUND)# 获取排序最高的图片welcome = welcome_images.first()serializer = WelcomeSerializer(welcome, context={'request': request})return Response({'code': 200,'message': 'success','data': serializer.data})except Exception as e:return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)class WelcomeListAPIView(APIView):"""欢迎页图片列表API用于管理端查看所有图片"""def get(self, request):try:# 获取查询参数image_type = request.GET.get('type')is_active = request.GET.get('is_active')queryset = Welcome.objects.filter(is_delete=False)# 过滤条件if image_type:queryset = queryset.filter(image_type=image_type)if is_active is not None:queryset = queryset.filter(is_active=is_active.lower() == 'true')queryset = queryset.order_by('-order', '-create_time')serializer = WelcomeSerializer(queryset, many=True, context={'request': request})return Response({'code': 200,'message': 'success','data': serializer.data})except Exception as e:return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
URL路由配置
# smart/urls.py
from django.urls import path
from .views import WelcomeAPIView, WelcomeListAPIViewurlpatterns = [path('welcome/', WelcomeAPIView.as_view(), name='welcome'),path('welcome/list/', WelcomeListAPIView.as_view(), name='welcome-list'),
]

3.4 配置管理深度优化

环境配置管理
// config/environment.js
// 环境配置管理
const environments = {development: {name: '开发环境',baseUrl: 'http://192.168.1.100:8000/smart/',uploadUrl: 'http://192.168.1.100:8000/smart/upload/',debug: true,logLevel: 'debug'},test: {name: '测试环境',baseUrl: 'https://test-api.yourdomain.com/smart/',uploadUrl: 'https://test-api.yourdomain.com/smart/upload/',debug: true,logLevel: 'info'},production: {name: '生产环境',baseUrl: 'https://api.yourdomain.com/smart/',uploadUrl: 'https://api.yourdomain.com/smart/upload/',debug: false,logLevel: 'warn'}
}// 自动检测环境
const getEnvironment = () => {// 微信小程序开发工具if (typeof __wxConfig !== 'undefined') {const accountInfo = wx.getAccountInfoSync()if (accountInfo.miniProgram.envVersion === 'develop') {return 'development'} else if (accountInfo.miniProgram.envVersion === 'trial') {return 'test'} else if (accountInfo.miniProgram.envVersion === 'release') {return 'production'}}// 默认开发环境return 'development'
}const currentEnv = getEnvironment()
const config = environments[currentEnv]console.log(`当前环境: ${config.name}`)module.exports = config
API配置管理
// config/api.js
const environment = require('./environment')const apiConfig = {// 欢迎页welcome: {get: environment.baseUrl + 'welcome/',list: environment.baseUrl + 'welcome/list/'},// 首页home: {get: environment.baseUrl + 'home/'},// 轮播图banner: {list: environment.baseUrl + 'banner/'},// 公告notice: {list: environment.baseUrl + 'notice/',detail: environment.baseUrl + 'notice/{id}/'},// 信息采集collection: {list: environment.baseUrl + 'collection/',detail: environment.baseUrl + 'collection/{id}/',create: environment.uploadUrl + 'collection/',delete: environment.baseUrl + 'collection/{id}/'},// 网格区域area: {list: environment.baseUrl + 'area/'},// 人脸识别face: {detect: environment.uploadUrl + 'face/detect/'},// 语音识别speech: {recognize: environment.uploadUrl + 'speech/'},// 数据统计statistics: {collection: environment.baseUrl + 'statistics/collection/'}
}// API URL构建工具
const buildUrl = (url, params = {}) => {let builtUrl = urlObject.keys(params).forEach(key => {builtUrl = builtUrl.replace(`{${key}}`, params[key])})return builtUrl
}module.exports = {...apiConfig,buildUrl,environment
}
工具函数封装
// utils/request.js
const { environment, buildUrl } = require('../config/api')/*** 统一的网络请求封装*/
class Request {constructor() {this.baseConfig = {timeout: 10000,header: {'Content-Type': 'application/json'}}}// 请求拦截器interceptorsRequest(config) {// 添加认证tokenconst token = wx.getStorageSync('token')if (token) {config.header['Authorization'] = `Bearer ${token}`}// 添加时间戳防止缓存if (config.method === 'GET') {config.url += (config.url.includes('?') ? '&' : '?') + `_t=${Date.now()}`}if (environment.debug) {console.log('🚀 [Request]', config)}return config}// 响应拦截器interceptorsResponse(response) {if (environment.debug) {console.log('📨 [Response]', response)}const { statusCode, data } = responseif (statusCode === 200) {return Promise.resolve(data)} else if (statusCode === 401) {// token过期,跳转到登录页wx.removeStorageSync('token')wx.showToast({title: '登录已过期',icon: 'error'})return Promise.reject(new Error('认证失败'))} else if (statusCode >= 500) {return Promise.reject(new Error('服务器错误'))} else {return Promise.reject(new Error(`网络错误: ${statusCode}`))}}// 统一请求方法request(config) {config = { ...this.baseConfig, ...config }config = this.interceptorsRequest(config)return new Promise((resolve, reject) => {wx.request({...config,success: (res) => {this.interceptorsResponse(res).then(resolve).catch(reject)},fail: (error) => {console.error('请求失败:', error)wx.showToast({title: '网络连接失败',icon: 'error'})reject(error)}})})}// GET请求get(url, data = {}, config = {}) {return this.request({url,data,method: 'GET',...config})}// POST请求post(url, data = {}, config = {}) {return this.request({url,data,method: 'POST',...config})}// PUT请求put(url, data = {}, config = {}) {return this.request({url,data,method: 'PUT',...config})}// DELETE请求delete(url, data = {}, config = {}) {return this.request({url,data,method: 'DELETE',...config})}// 文件上传upload(url, filePath, formData = {}, config = {}) {return new Promise((resolve, reject) => {const uploadTask = wx.uploadFile({url,filePath,name: 'file',formData,...config,success: (res) => {if (res.statusCode === 200) {try {const data = JSON.parse(res.data)resolve(data)} catch (error) {reject(new Error('解析响应数据失败'))}} else {reject(new Error(`上传失败: ${res.statusCode}`))}},fail: reject})// 上传进度监听uploadTask.onProgressUpdate((res) => {if (config.onProgress) {config.onProgress(res)}})})}
}// 创建请求实例
const request = new Request()module.exports = request
// utils/util.js
/*** 通用工具函数*/// 格式化时间
const formatTime = (date, format = 'YYYY-MM-DD HH:mm:ss') => {if (!date) return ''const d = new Date(date)const year = d.getFullYear()const month = String(d.getMonth() + 1).padStart(2, '0')const day = String(d.getDate()).padStart(2, '0')const hour = String(d.getHours()).padStart(2, '0')const minute = String(d.getMinutes()).padStart(2, '0')const second = String(d.getSeconds()).padStart(2, '0')return format.replace('YYYY', year).replace('MM', month).replace('DD', day).replace('HH', hour).replace('mm', minute).replace('ss', second)
}// 防抖函数
const debounce = (func, wait = 300) => {let timeoutreturn function executedFunction(...args) {const later = () => {clearTimeout(timeout)func(...args)}clearTimeout(timeout)timeout = setTimeout(later, wait)}
}// 节流函数
const throttle = (func, limit = 300) => {let inThrottlereturn function(...args) {if (!inThrottle) {func.apply(this, args)inThrottle = truesetTimeout(() => inThrottle = false, limit)}}
}// 深拷贝
const deepClone = (obj) => {if (obj === null || typeof obj !== 'object') return objif (obj instanceof Date) return new Date(obj)if (obj instanceof Array) return obj.map(item => deepClone(item))if (obj instanceof Object) {const clonedObj = {}Object.keys(obj).forEach(key => {clonedObj[key] = deepClone(obj[key])})return clonedObj}
}// 显示加载提示
const showLoading = (title = '加载中...') => {wx.showLoading({title,mask: true})
}// 隐藏加载提示
const hideLoading = () => {wx.hideLoading()
}// 显示成功提示
const showSuccess = (title = '操作成功') => {wx.showToast({title,icon: 'success',duration: 2000})
}// 显示错误提示
const showError = (title = '操作失败') => {wx.showToast({title,icon: 'error',duration: 2000})
}// 验证手机号
const validatePhone = (phone) => {const reg = /^1[3-9]\d{9}$/return reg.test(phone)
}// 验证身份证号
const validateIDCard = (idCard) => {const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/return reg.test(idCard)
}// 生成随机字符串
const generateRandomString = (length = 8) => {const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'let result = ''for (let i = 0; i < length; i++) {result += chars.charAt(Math.floor(Math.random() * chars.length))}return result
}module.exports = {formatTime,debounce,throttle,deepClone,showLoading,hideLoading,showSuccess,showError,validatePhone,validateIDCard,generateRandomString
}

四、首页(Index)深度开发

4.1 前端完整实现

WXML结构优化
<!-- pages/index/index.wxml -->
<view class="index-container"><!-- 轮播图 --><view class="banner-section"><van-swiper class="banner-swiper" autoplay="{{3000}}" indicator-dots="{{true}}"indicator-color="rgba(255,255,255,0.5)"indicator-active-color="#ffffff"circular="{{true}}"><van-swiper-item wx:for="{{bannerList}}" wx:key="id"><image src="{{item.image_url}}" mode="aspectFill" class="banner-image"bindtap="onBannerClick"data-url="{{item.click_url}}"data-id="{{item.id}}"/></van-swiper-item></van-swiper></view><!-- 公告栏 --><view class="notice-section"><van-notice-bar left-icon="volume-o"color="#333333"background="#fff9e6"text="{{notice.content || '暂无公告'}}"scrollable="{{true}}"speed="50"bind:click="onNoticeClick"/></view><!-- 功能网格 --><view class="grid-section"><van-grid column-num="3" clickable="{{true}}"border="{{false}}"><van-grid-item wx:for="{{gridItems}}" wx:key="id"icon="{{item.icon}}" text="{{item.text}}"bind:click="navigateToPage"data-page="{{item.page}}"/></van-grid></view><!-- 社区动态 --><view class="news-section" wx:if="{{newsList.length > 0}}"><view class="section-header"><text class="section-title">社区动态</text><text class="section-more" bindtap="viewMoreNews">更多</text></view><view class="news-list"><view class="news-item" wx:for="{{newsList}}" wx:key="id"bindtap="viewNewsDetail"data-id="{{item.id}}"><image class="news-image" src="{{item.image_url}}" mode="aspectFill" /><view class="news-content"><text class="news-title">{{item.title}}</text><text class="news-time">{{item.create_time}}</text></view></view></view></view><!-- 加载状态 --><view class="loading-container" wx:if="{{isLoading}}"><van-loading size="24px" vertical>加载中...</van-loading></view><!-- 错误状态 --><view class="error-container" wx:if="{{hasError}}"><view class="error-content"><image src="/static/images/error.png" class="error-image" /><text class="error-text">加载失败,请重试</text><button class="retry-btn" bindtap="loadHomeData">重新加载</button></view></view>
</view>
WXSS样式深度优化
/* pages/index/index.wxss */
.index-container {min-height: 100vh;background: #f7f7f7;
}.banner-section {padding: 20rpx;padding-bottom: 0;
}.banner-swiper {height: 300rpx;border-radius: 20rpx;overflow: hidden;box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}.banner-image {width: 100%;height: 100%;
}.notice-section {margin: 20rpx;border-radius: 12rpx;overflow: hidden;
}.grid-section {margin: 30rpx 20rpx;background: #ffffff;border-radius: 20rpx;padding: 20rpx 0;box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}.news-section {margin: 30rpx 20rpx;background: #ffffff;border-radius: 20rpx;padding: 30rpx;box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}.section-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 30rpx;
}.section-title {font-size: 32rpx;font-weight: bold;color: #333333;
}.section-more {font-size: 26rpx;color: #999999;
}.news-list {display: flex;flex-direction: column;gap: 30rpx;
}.news-item {display: flex;gap: 20rpx;
}.news-image {width: 160rpx;height: 120rpx;border-radius: 12rpx;flex-shrink: 0;
}.news-content {flex: 1;display: flex;flex-direction: column;justify-content: space-between;
}.news-title {font-size: 28rpx;color: #333333;line-height: 1.4;display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 2;overflow: hidden;
}.news-time {font-size: 24rpx;color: #999999;
}.loading-container,
.error-container {display: flex;justify-content: center;align-items: center;padding: 100rpx 0;
}.error-content {display: flex;flex-direction: column;align-items: center;gap: 20rpx;
}.error-image {width: 120rpx;height: 120rpx;
}.error-text {font-size: 28rpx;color: #999999;
}.retry-btn {background: #007AFF;color: #ffffff;border: none;border-radius: 40rpx;padding: 16rpx 40rpx;font-size: 28rpx;
}/* 响应式适配 */
@media (max-width: 375px) {.banner-swiper {height: 250rpx;}.news-image {width: 140rpx;height: 100rpx;}
}
JavaScript逻辑完整实现
// pages/index/index.js
const { request, buildUrl } = require('../../utils/request')
const { showError, showLoading, hideLoading } = require('../../utils/util')Page({data: {bannerList: [],notice: {},newsList: [],gridItems: [{id: 1,icon: 'user-circle-o',text: '信息采集',page: 'collection'},{id: 2,icon: 'calender-o',text: '社区活动',page: 'activity'},{id: 3,icon: 'user-o',text: '人脸检测',page: 'face'},{id: 4,icon: 'volume-o',text: '语音识别',page: 'voice'},{id: 5,icon: 'heart-o',text: '心率检测',page: 'heart'},{id: 6,icon: 'shopping-cart-o',text: '积分商城',page: 'goods'}],isLoading: true,hasError: false},onLoad() {this.loadHomeData()},onPullDownRefresh() {this.loadHomeData().finally(() => {wx.stopPullDownRefresh()})},onShareAppMessage() {return {title: '智慧社区 - 首页',path: '/pages/index/index'}},async loadHomeData() {this.setData({ isLoading: true, hasError: false })try {// 并行请求首页数据const [bannerRes, homeRes] = await Promise.all([request.get(buildUrl('banner.list')),request.get(buildUrl('home.get'))])const bannerList = bannerRes.code === 200 ? bannerRes.data : []const homeData = homeRes.code === 200 ? homeRes.data : {}this.setData({bannerList,notice: homeData.notice || {},newsList: homeData.news || [],isLoading: false})} catch (error) {console.error('加载首页数据失败:', error)this.setData({ hasError: true, isLoading: false })showError('加载失败')}},onBannerClick(e) {const { url, id } = e.currentTarget.datasetif (url) {// 跳转到指定页面或网页wx.navigateTo({url: `/pages/webview/webview?url=${encodeURIComponent(url)}`})}// 记录点击统计(可选)this.recordBannerClick(id)},onNoticeClick() {const { notice } = this.dataif (notice.id) {wx.navigateTo({url: `/pages/notice/detail/detail?id=${notice.id}`})}},navigateToPage(e) {const { page } = e.currentTarget.datasetif (!page) {showError('功能开发中')return}const pageMap = {collection: '/pages/collection/collection',activity: '/pages/activity/activity',face: '/pages/face/face',voice: '/pages/voice/voice',heart: '/pages/heart/heart',goods: '/pages/goods/goods'}const url = pageMap[page]if (url) {if (page === 'activity' || page === 'goods') {wx.switchTab({ url })} else {wx.navigateTo({ url })}} else {showError('功能暂未开放')}},viewMoreNews() {wx.navigateTo({url: '/pages/news/list/list'})},viewNewsDetail(e) {const { id } = e.currentTarget.datasetwx.navigateTo({url: `/pages/news/detail/detail?id=${id}`})},recordBannerClick(bannerId) {// 记录banner点击统计,可选实现request.post(buildUrl('banner.click'), { banner_id: bannerId }).catch(console.error)}
})
// pages/index/index.json
{"usingComponents": {"van-swiper": "@vant/weapp/swiper/index","van-swiper-item": "@vant/weapp/swiper-item/index","van-notice-bar": "@vant/weapp/notice-bar/index","van-grid": "@vant/weapp/grid/index","van-grid-item": "@vant/weapp/grid-item/index","van-loading": "@vant/weapp/loading/index"},"enablePullDownRefresh": true,"backgroundTextStyle": "dark"
}

4.2 后端数据模型详细设计

轮播图模型扩展
# smart/models.py
class Banner(models.Model):"""轮播图模型"""POSITION_CHOICES = (('home', '首页'),('activity', '活动页'),('mall', '商城页'),)title = models.CharField(max_length=100, verbose_name="标题", blank=True)image = models.ImageField(upload_to='banner/%Y/%m/%d/', verbose_name="轮播图片")position = models.CharField(max_length=20, choices=POSITION_CHOICES, default='home',verbose_name="显示位置")order = models.IntegerField(default=0,validators=[MinValueValidator(0), MaxValueValidator(999)],verbose_name="排序权重")click_url = models.URLField(blank=True, verbose_name="点击链接")start_time = models.DateTimeField(default=timezone.now,verbose_name="开始时间")end_time = models.DateTimeField(null=True, blank=True,verbose_name="结束时间")is_active = models.BooleanField(default=True, verbose_name="是否启用")is_delete = models.BooleanField(default=False, verbose_name="逻辑删除")description = models.TextField(blank=True, verbose_name="描述")create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")class Meta:db_table = 'smart_banner'verbose_name = '轮播图'verbose_name_plural = verbose_nameordering = ['-order', '-create_time']indexes = [models.Index(fields=['position', 'is_active', 'is_delete']),]def __str__(self):return self.title or f"轮播图-{self.id}"@propertydef is_valid(self):"""检查轮播图是否在有效期内"""now = timezone.now()if not self.is_active or self.is_delete:return Falseif self.start_time and self.start_time > now:return Falseif self.end_time and self.end_time < now:return Falsereturn Truedef get_absolute_image_url(self, request=None):"""获取完整的图片URL"""if self.image and hasattr(self.image, 'url'):if request:return request.build_absolute_uri(self.image.url)return self.image.urlreturn None
新闻动态模型
class News(models.Model):"""新闻动态模型"""CATEGORY_CHOICES = (('news', '社区新闻'),('notice', '公告通知'),('activity', '活动预告'),)title = models.CharField(max_length=200, verbose_name="标题")content = models.TextField(verbose_name="内容")summary = models.TextField(blank=True, verbose_name="摘要")image = models.ImageField(upload_to='news/%Y/%m/%d/', blank=True, null=True,verbose_name="封面图片")category = models.CharField(max_length: '20', choices=CATEGORY_CHOICES, default='news',verbose_name="分类")is_top = models.BooleanField(default=False, verbose_name="是否置顶")is_active = models.BooleanField(default=True, verbose_name="是否发布")view_count = models.PositiveIntegerField(default=0, verbose_name="浏览数")publish_time = models.DateTimeField(default=timezone.now,verbose_name="发布时间")create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")class Meta:db_table = 'smart_news'verbose_name = '新闻动态'verbose_name_plural = verbose_nameordering = ['-is_top', '-publish_time', '-create_time']indexes = [models.Index(fields=['category', 'is_active']),]def __str__(self):return self.titledef get_absolute_image_url(self, request=None):"""获取完整的图片URL"""if self.image and hasattr(self.image, 'url'):if request:return request.build_absolute_uri(self.image.url)return self.image.urlreturn Nonedef increase_view_count(self):"""增加浏览数"""self.view_count += 1self.save(update_fields=['view_count'])

4.3 后端API接口完整实现

序列化器
# smart/serializers.py
class BannerSerializer(serializers.ModelSerializer):image_url = serializers.SerializerMethodField()is_valid = serializers.ReadOnlyField()class Meta:model = Bannerfields = ['id', 'title', 'image_url', 'position', 'order','click_url', 'description', 'is_valid', 'create_time']read_only_fields = ['is_valid']def get_image_url(self, obj):request = self.context.get('request')return obj.get_absolute_image_url(request)class NewsSerializer(serializers.ModelSerializer):image_url = serializers.SerializerMethodField()create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M')class Meta:model = Newsfields = ['id', 'title', 'summary', 'image_url', 'category','is_top', 'view_count', 'publish_time', 'create_time']def get_image_url(self, obj):request = self.context.get('request')return obj.get_absolute_image_url(request)class NoticeSerializer(serializers.ModelSerializer):create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M')class Meta:model = Noticefields = ['id', 'title', 'content', 'create_time']class HomeDataSerializer(serializers.Serializer):"""首页数据序列化器"""banners = BannerSerializer(many=True)notice = NoticeSerializer()news = NewsSerializer(many=True)
视图集实现
# smart/views.py
class BannerListAPIView(APIView):"""轮播图列表API"""def get(self, request):try:position = request.GET.get('position', 'home')banners = Banner.objects.filter(position=position,is_active=True,is_delete=False).filter(Q(start_time__lte=timezone.now()) &(Q(end_time__isnull=True) | Q(end_time__gte=timezone.now()))).order_by('-order', '-create_time')serializer = BannerSerializer(banners, many=True, context={'request': request})return Response({'code': 200,'message': 'success','data': serializer.data})except Exception as e:return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)class HomeAPIView(APIView):"""首页聚合数据API"""def get(self, request):try:# 获取轮播图banners = Banner.objects.filter(position='home',is_active=True,is_delete=False).filter(Q(start_time__lte=timezone.now()) &(Q(end_time__isnull=True) | Q(end_time__gte=timezone.now()))).order_by('-order', '-create_time')[:5]# 获取最新公告notice = Notice.objects.filter(is_active=True).order_by('-create_time').first()# 获取最新新闻news = News.objects.filter(is_active=True).order_by('-is_top', '-publish_time')[:3]banner_serializer = BannerSerializer(banners, many=True, context={'request': request})notice_serializer = NoticeSerializer(notice, context={'request': request}) if notice else Nonenews_serializer = NewsSerializer(news, many=True, context={'request': request})data = {'banners': banner_serializer.data,'notice': notice_serializer.data if notice_serializer else {},'news': news_serializer.data}return Response({'code': 200,'message': 'success','data': data})except Exception as e:return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
URL路由配置
# smart/urls.py
urlpatterns = [# ... 其他路由path('banner/', BannerListAPIView.as_view(), name='banner-list'),path('home/', HomeAPIView.as_view(), name='home'),
]

4.4 管理后台配置

# smart/admin.py
@admin.register(Banner)
class BannerAdmin(admin.ModelAdmin):list_display = ['id', 'title', 'position', 'order', 'is_active', 'is_valid', 'start_time', 'end_time', 'create_time']list_filter = ['position', 'is_active', 'create_time']search_fields = ['title', 'description']list_editable = ['order', 'is_active']readonly_fields = ['create_time', 'update_time']fieldsets = (('基本信息', {'fields': ('title', 'image', 'position', 'description')}),('链接设置', {'fields': ('click_url',)}),('时间设置', {'fields': ('start_time', 'end_time')}),('配置设置', {'fields': ('order', 'is_active')}),)@admin.register(News)
class NewsAdmin(admin.ModelAdmin):list_display = ['id', 'title', 'category', 'is_top', 'is_active','view_count', 'publish_time', 'create_time']list_filter = ['category', 'is_top', 'is_active', 'publish_time']search_fields = ['title', 'content']list_editable = ['is_top', 'is_active']readonly_fields = ['view_count', 'create_time', 'update_time']fieldsets = (('基本信息', {'fields': ('title', 'summary', 'content', 'image', 'category')}),('发布设置', {'fields': ('is_top', 'is_active', 'publish_time')}),)def save_model(self, request, obj, form, change):if not obj.summary and obj.content:# 自动生成摘要obj.summary = obj.content[:100] + '...' if len(obj.content) > 100 else obj.contentsuper().save_model(request, obj, form, change)

五、信息采集模块深度开发

5.1 数据模型详细设计

用户和区域模型
# smart/models.py
class UserInfo(models.Model):"""用户表(网格员)"""ROLE_CHOICES = (('admin', '管理员'),('grid_member', '网格员'),('resident', '居民'),)openid = models.CharField(max_length=100, unique=True, verbose_name="微信OpenID")nickname = models.CharField(max_length=50, verbose_name="昵称")avatar = models.URLField(blank=True, verbose_name="头像")phone = models.CharField(max_length=20, blank=True, verbose_name="手机号")role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='grid_member',verbose_name="角色")is_active = models.BooleanField(default=True, verbose_name="是否启用")last_login = models.DateTimeField(null=True, blank=True, verbose_name="最后登录时间")create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")class Meta:db_table = 'smart_user'verbose_name = '用户信息'verbose_name_plural = verbose_namedef __str__(self):return f"{self.nickname}({self.role})"class Area(models.Model):"""网格区域表"""name = models.CharField(max_length=50, verbose_name="区域名称")code = models.CharField(max_length=20, unique=True, verbose_name="区域编码")manager = models.ManyToManyField(UserInfo, related_name='managed_areas', verbose_name="负责人",limit_choices_to={'role': 'grid_member'})parent = models.ForeignKey('self',null=True,blank=True,on_delete=models.CASCADE,related_name='children',verbose_name="上级区域")level = models.IntegerField(default=1, verbose_name="区域层级")population = models.IntegerField(default=0, verbose_name="人口数量")address = models.TextField(blank=True, verbose_name="详细地址")is_active = models.BooleanField(default=True, verbose_name="是否启用")create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")class Meta:db_table = 'smart_area'verbose_name = '网格区域'verbose_name_plural = verbose_nameordering = ['level', 'code']def __str__(self):return f"{self.name}({self.code})"def get_full_path(self):"""获取完整区域路径"""path = []current = selfwhile current:path.append(current.name)current = current.parentreturn ' - '.join(reversed(path))class Collection(models.Model):"""信息采集表"""GENDER_CHOICES = (('M', '男'),('F', '女'),('U', '未知'),)name = models.CharField(max_length=20, verbose_name="姓名")name_pinyin = models.CharField(max_length=100, verbose_name="姓名拼音")gender = models.CharField(max_length=1, choices=GENDER_CHOICES, default='U',verbose_name="性别")id_card = models.CharField(max_length=18, blank=True, verbose_name="身份证号")phone = models.CharField(max_length=20, blank=True, verbose_name="手机号")avatar = models.ImageField(upload_to='collection/%Y/%m/%d/', verbose_name="头像")area = models.ForeignKey(Area, on_delete=models.CASCADE, verbose_name="所属网格")address = models.TextField(blank=True, verbose_name="详细地址")emergency_contact = models.CharField(max_length=20, blank=True, verbose_name="紧急联系人")emergency_phone = models.CharField(max_length=20, blank=True, verbose_name="紧急联系电话")health_status = models.TextField(blank=True, verbose_name="健康状况")special_needs = models.TextField(blank=True, verbose_name="特殊需求")create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")face_token = models.CharField(max_length=100, blank=True, verbose_name="人脸Token")collector = models.ForeignKey(UserInfo, on_delete=models.CASCADE, verbose_name="采集员")is_verified = models.BooleanField(default=False, verbose_name="是否核验")class Meta:db_table = 'smart_collection'verbose_name = '信息采集'verbose_name_plural = verbose_nameordering = ['-create_time']indexes = [models.Index(fields=['name', 'area']),models.Index(fields=['create_time']),]def __str__(self):return f"{self.name}-{self.area.name}"def get_absolute_avatar_url(self, request=None):"""获取完整的头像URL"""if self.avatar and hasattr(self.avatar, 'url'):if request:return request.build_absolute_uri(self.avatar.url)return self.avatar.urlreturn Nonedef save(self, *args, **kwargs):# 自动生成姓名拼音if self.name and not self.name_pinyin:self.name_pinyin = self.generate_pinyin(self.name)super().save(*args, **kwargs)def generate_pinyin(self, name):"""生成姓名拼音"""try:from pypinyin import lazy_pinyinreturn ''.join(lazy_pinyin(name))except ImportError:# 如果没有安装pypinyin,使用简单实现return name

5.2 采集列表页面深度实现

前端页面优化
<!-- pages/collection/collection.wxml -->
<view class="collection-container"><!-- 统计卡片 --><view class="stats-cards"><view class="stats-card today"><view class="stats-icon"><text class="iconfont icon-user"></text></view><view class="stats-content"><text class="stats-value">{{todayCount}}</text><text class="stats-label">今日采集</text></view></view><view class="stats-card total"><view class="stats-icon"><text class="iconfont icon-list"></text></view><view class="stats-content"><text class="stats-value">{{totalCount}}</text><text class="stats-label">总采集数</text></view></view></view><!-- 操作按钮 --><view class="action-buttons"><button class="btn primary" bindtap="navigateToForm"><text class="iconfont icon-plus"></text>新采集</button><button class="btn secondary" bindtap="navigateToStatistics"><text class="iconfont icon-chart"></text>数据统计</button><button class="btn outline" bindtap="showFilterPanel"><text class="iconfont icon-filter"></text>筛选</button></view><!-- 筛选面板 --><view class="filter-panel" wx:if="{{showFilter}}"><view class="filter-content"><view class="filter-item"><text class="filter-label">网格区域</text><picker range="{{areaOptions}}" range-key="name"value="{{filterAreaIndex}}"bindchange="onAreaFilterChange"><view class="filter-value">{{areaOptions[filterAreaIndex].name || '全部区域'}}</view></picker></view><view class="filter-item"><text class="filter-label">采集时间</text><picker mode="date" value="{{filterDate}}"bindchange="onDateFilterChange"><view class="filter-value">{{filterDate || '全部时间'}}</view></picker></view><view class="filter-actions"><button class="btn outline" bindtap="resetFilter">重置</button><button class="btn primary" bindtap="applyFilter">应用</button></view></view></view><!-- 采集列表 --><view class="collection-list"><view class="list-header"><text class="header-title">采集记录</text><text class="header-count">共 {{collectionList.length}} 条</text></view><view class="list-item" wx:for="{{collectionList}}" wx:key="id"bindtap="viewCollectionDetail"data-id="{{item.id}}"><image class="item-avatar" src="{{item.avatar_url}}" mode="aspectFill" /><view class="item-content"><view class="item-header"><text class="item-name">{{item.name}}</text><text class="item-gender">{{item.gender_display}}</text></view><view class="item-info"><text class="item-area">{{item.area.name}}</text><text class="item-time">{{item.create_time}}</text></view><view class="item-tags"><text class="tag verified" wx:if="{{item.is_verified}}">已核验</text><text class="tag face" wx:if="{{item.face_token}}">已录入人脸</text></view></view><view class="item-actions"><text class="iconfont icon-shanchu" bindtap="doDeleteRow"data-nid="{{item.id}}"></text></view></view><!-- 空状态 --><view class="empty-state" wx:if="{{collectionList.length === 0 && !isLoading}}"><image src="/static/images/empty.png" class="empty-image" /><text class="empty-text">暂无采集记录</text><button class="btn primary" bindtap="navigateToForm">开始采集</button></view><!-- 加载更多 --><view class="load-more" wx:if="{{hasMore && !isLoading}}"><text class="load-more-text" bindtap="loadMore">加载更多</text></view><view class="load-more" wx:if="{{!hasMore && collectionList.length > 0}}"><text class="load-more-text">没有更多数据了</text></view></view><!-- 加载状态 --><view class="loading-container" wx:if="{{isLoading}}"><van-loading size="24px" vertical>加载中...</van-loading></view>
</view>
采集列表样式优化
/* pages/collection/collection.wxss */
.collection-container {min-height: 100vh;background: #f7f7f7;padding: 20rpx;
}.stats-cards {display: flex;gap: 20rpx;margin-bottom: 30rpx;
}.stats-card {flex: 1;background: #ffffff;border-radius: 16rpx;padding: 30rpx;display: flex;align-items: center;gap: 20rpx;box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}.stats-card.today {border-left: 6rpx solid #007AFF;
}.stats-card.total {border-left: 6rpx solid #34C759;
}.stats-icon {width: 60rpx;height: 60rpx;border-radius: 12rpx;display: flex;align-items: center;justify-content: center;font-size: 32rpx;color: #ffffff;
}.stats-card.today .stats-icon {background: #007AFF;
}.stats-card.total .stats-icon {background: #34C759;
}.stats-content {flex: 1;
}.stats-value {display: block;font-size: 36rpx;font-weight: bold;color: #333333;line-height: 1.2;
}.stats-label {display: block;font-size: 24rpx;color: #999999;margin-top: 8rpx;
}.action-buttons {display: flex;gap: 20rpx;margin-bottom: 30rpx;
}.btn {flex: 1;display: flex;align-items: center;justify-content: center;gap: 10rpx;padding: 20rpx;border-radius: 12rpx;font-size: 28rpx;border: none;
}.btn.primary {background: #007AFF;color: #ffffff;
}.btn.secondary {background: #34C759;color: #ffffff;
}.btn.outline {background: transparent;border: 2rpx solid #007AFF;color: #007AFF;
}.filter-panel {background: rgba(0, 0, 0, 0.5);position: fixed;top: 0;left: 0;right: 0;bottom: 0;z-index: 1000;display: flex;align-items: flex-end;
}.filter-content {background: #ffffff;width: 100%;border-radius: 20rpx 20rpx 0 0;padding: 40rpx;box-sizing: border-box;
}.filter-item {display: flex;align-items: center;justify-content: space-between;padding: 30rpx 0;border-bottom: 1rpx solid #f0f0f0;
}.filter-label {font-size: 28rpx;color: #333333;
}.filter-value {font-size: 28rpx;color: #007AFF;padding: 16rpx 30rpx;background: #f8f8f8;border-radius: 8rpx;
}.filter-actions {display: flex;gap: 20rpx;margin-top: 40rpx;
}.collection-list {background: #ffffff;border-radius: 16rpx;overflow: hidden;box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}.list-header {display: flex;justify-content: space-between;align-items: center;padding: 30rpx;border-bottom: 1rpx solid #f0f0f0;
}.header-title {font-size: 32rpx;font-weight: bold;color: #333333;
}.header-count {font-size: 26rpx;color: #999999;
}.list-item {display: flex;align-items: center;padding: 30rpx;border-bottom: 1rpx solid #f0f0f0;transition: background-color 0.3s;
}.list-item:active {background: #f8f8f8;
}.item-avatar {width: 80rpx;height: 80rpx;border-radius: 12rpx;margin-right: 20rpx;flex-shrink: 0;
}.item-content {flex: 1;min-width: 0;
}.item-header {display: flex;align-items: center;gap: 20rpx;margin-bottom: 12rpx;
}.item-name {font-size: 32rpx;font-weight: bold;color: #333333;
}.item-gender {font-size: 24rpx;color: #999999;background: #f8f8f8;padding: 4rpx 12rpx;border-radius: 6rpx;
}.item-info {display: flex;justify-content: space-between;align-items: center;margin-bottom: 12rpx;
}.item-area {font-size: 26rpx;color: #666666;
}.item-time {font-size: 24rpx;color: #999999;
}.item-tags {display: flex;gap: 10rpx;
}.tag {font-size: 20rpx;padding: 4rpx 12rpx;border-radius: 6rpx;
}.tag.verified {background: #e6f7ff;color: #1890ff;
}.tag.face {background: #f6ffed;color: #52c41a;
}.item-actions {margin-left: 20rpx;
}.item-actions .iconfont {font-size: 32rpx;color: #ff4d4f;padding: 20rpx;
}.empty-state {display: flex;flex-direction: column;align-items: center;padding: 80rpx 40rpx;
}.empty-image {width: 200rpx;height: 200rpx;margin-bottom: 40rpx;opacity: 0.6;
}.empty-text {font-size: 28rpx;color: #999999;margin-bottom: 40rpx;
}.load-more {text-align: center;padding: 40rpx;
}.load-more-text {font-size: 28rpx;color: #999999;
}.loading-container {padding: 80rpx 0;
}
采集列表逻辑实现
// pages/collection/collection.js
const { request, buildUrl } = require('../../utils/request')
const { showLoading, hideLoading, showError, showSuccess, formatTime } = require('../../utils/util')Page({data: {collectionList: [],areaOptions: [{ id: 0, name: '全部区域' }],filterAreaIndex: 0,filterDate: '',showFilter: false,isLoading: false,hasMore: true,page: 1,pageSize: 10,todayCount: 0,totalCount: 0},onLoad() {this.initPage()},onShow() {this.refreshList()},onPullDownRefresh() {this.refreshList().finally(() => {wx.stopPullDownRefresh()})},onReachBottom() {if (this.data.hasMore && !this.data.isLoading) {this.loadMore()}},async initPage() {await Promise.all([this.loadAreaOptions(),this.loadStatistics()])},async loadAreaOptions() {try {const res = await request.get(buildUrl('area.list'))if (res.code === 200) {const areaOptions = [{ id: 0, name: '全部区域' }, ...res.data]this.setData({ areaOptions })}} catch (error) {console.error('加载区域选项失败:', error)}},async loadStatistics() {try {const res = await request.get(buildUrl('collection.statistics'))if (res.code === 200) {this.setData({todayCount: res.data.today_count || 0,totalCount: res.data.total_count || 0})}} catch (error) {console.error('加载统计信息失败:', error)}},async refreshList() {this.setData({ page: 1, hasMore: true })await this.loadCollectionList(true)},async loadMore() {if (!this.data.hasMore) returnthis.setData({ page: this.data.page + 1 })await this.loadCollectionList(false)},async loadCollectionList(refresh = false) {if (this.data.isLoading) returnthis.setData({ isLoading: true })try {const params = {page: this.data.page,page_size: this.data.pageSize}// 添加筛选条件if (this.data.filterAreaIndex > 0) {const selectedArea = this.data.areaOptions[this.data.filterAreaIndex]params.area_id = selectedArea.id}if (this.data.filterDate) {params.date = this.data.filterDate}const res = await request.get(buildUrl('collection.list'), params)if (res.code === 200) {const { list, pagination } = res.dataconst formattedList = list.map(item => ({...item,create_time: formatTime(item.create_time, 'MM-DD HH:mm'),gender_display: this.getGenderDisplay(item.gender)}))if (refresh) {this.setData({ collectionList: formattedList })} else {this.setData({ collectionList: [...this.data.collectionList, ...formattedList]})}this.setData({ hasMore: pagination.has_next,isLoading: false})} else {throw new Error(res.message)}} catch (error) {console.error('加载采集列表失败:', error)this.setData({ isLoading: false })if (refresh) {showError('加载失败')}}},getGenderDisplay(gender) {const genderMap = {'M': '男','F': '女','U': '未知'}return genderMap[gender] || '未知'},navigateToForm() {wx.navigateTo({url: '/pages/form/form'})},navigateToStatistics() {wx.navigateTo({url: '/pages/statistics/statistics'})},showFilterPanel() {this.setData({ showFilter: true })},hideFilterPanel() {this.setData({ showFilter: false })},onAreaFilterChange(e) {this.setData({ filterAreaIndex: e.detail.value })},onDateFilterChange(e) {this.setData({ filterDate: e.detail.value })},resetFilter() {this.setData({filterAreaIndex: 0,filterDate: ''})},applyFilter() {this.hideFilterPanel()this.refreshList()},viewCollectionDetail(e) {const { id } = e.currentTarget.datasetwx.navigateTo({url: `/pages/collection/detail/detail?id=${id}`})},doDeleteRow(e) {const { nid } = e.currentTarget.datasetconst item = this.data.collectionList.find(i => i.id === nid)if (!item) returnwx.showModal({title: '确认删除',content: `确定要删除 ${item.name} 的采集信息吗?此操作不可恢复。`,confirmColor: '#ff4d4f',success: async (res) => {if (res.confirm) {await this.deleteCollection(nid)}}})// 阻止事件冒泡e.stopPropagation()},async deleteCollection(id) {try {showLoading('删除中...')const res = await request.delete(buildUrl('collection.detail', { id }))if (res.code === 200) {showSuccess('删除成功')// 从列表中移除this.setData({collectionList: this.data.collectionList.filter(item => item.id !== id)})// 重新加载统计信息this.loadStatistics()} else {throw new Error(res.message)}} catch (error) {console.error('删除失败:', error)showError('删除失败')} finally {hideLoading()}}
})

5.3 后端API接口完整实现

序列化器
# smart/serializers.py
class UserInfoSerializer(serializers.ModelSerializer):class Meta:model = UserInfofields = ['id', 'nickname', 'avatar', 'role', 'phone']class AreaSerializer(serializers.ModelSerializer):full_path = serializers.ReadOnlyField()class Meta:model = Areafields = ['id', 'name', 'code', 'full_path', 'level', 'population', 'address']class CollectionSerializer(serializers.ModelSerializer):area = AreaSerializer(read_only=True)collector = UserInfoSerializer(read_only=True)avatar_url = serializers.SerializerMethodField()area_id = serializers.IntegerField(write_only=True)class Meta:model = Collectionfields = ['id', 'name', 'name_pinyin', 'gender', 'id_card', 'phone','avatar', 'avatar_url', 'area', 'area_id', 'address','emergency_contact', 'emergency_phone', 'health_status','special_needs', 'create_time', 'update_time', 'face_token','collector', 'is_verified']read_only_fields = ['name_pinyin', 'face_token', 'collector', 'create_time', 'update_time']def get_avatar_url(self, obj):request = self.context.get('request')return obj.get_absolute_avatar_url(request)def validate_id_card(self, value):"""验证身份证号格式"""if value and len(value) not in [15, 18]:raise serializers.ValidationError("身份证号格式不正确")return valuedef validate_phone(self, value):"""验证手机号格式"""if value and not re.match(r'^1[3-9]\d{9}$', value):raise serializers.ValidationError("手机号格式不正确")return valueclass CollectionListSerializer(serializers.ModelSerializer):"""用于列表显示的简化序列化器"""area = AreaSerializer(read_only=True)avatar_url = serializers.SerializerMethodField()class Meta:model = Collectionfields = ['id', 'name', 'gender', 'avatar_url', 'area','create_time', 'face_token', 'is_verified']def get_avatar_url(self, obj):request = self.context.get('request')return obj.get_absolute_avatar_url(request)class CollectionStatisticsSerializer(serializers.Serializer):"""采集统计序列化器"""today_count = serializers.IntegerField()total_count = serializers.IntegerField()verified_count = serializers.IntegerField()face_registered_count = serializers.IntegerField()
视图集实现
# smart/views.py
from rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count, Q
from django.utils import timezone
from datetime import datetime, timedelta
from .models import Collection, Area
from .serializers import (CollectionSerializer, CollectionListSerializer,CollectionStatisticsSerializer,AreaSerializer
)class AreaViewSet(ModelViewSet):"""网格区域视图集"""queryset = Area.objects.filter(is_active=True)serializer_class = AreaSerializerpagination_class = Nonedef get_queryset(self):queryset = super().get_queryset()# 可以根据需要添加权限过滤return querysetclass CollectionViewSet(ModelViewSet):"""信息采集视图集"""queryset = Collection.objects.all()filter_backends = [DjangoFilterBackend]filterset_fields = ['area', 'collector']def get_serializer_class(self):if self.action == 'list':return CollectionListSerializerreturn CollectionSerializerdef get_queryset(self):queryset = super().get_queryset()# 日期过滤date = self.request.GET.get('date')if date:try:filter_date = datetime.strptime(date, '%Y-%m-%d').date()queryset = queryset.filter(create_time__date=filter_date)except ValueError:pass# 区域过滤area_id = self.request.GET.get('area_id')if area_id and area_id != '0':queryset = queryset.filter(area_id=area_id)return queryset.select_related('area', 'collector').order_by('-create_time')def perform_create(self, serializer):# 自动设置采集员为当前用户# 这里需要先集成用户认证系统# serializer.save(collector=self.request.user)serializer.save()def list(self, request, *args, **kwargs):"""重写list方法以支持分页和自定义响应格式"""queryset = self.filter_queryset(self.get_queryset())page = self.paginate_queryset(queryset)if page is not None:serializer = self.get_serializer(page, many=True)return self.get_paginated_response(serializer.data)serializer = self.get_serializer(queryset, many=True)return Response({'code': 200,'message': 'success','data': {'list': serializer.data,'pagination': {'total': queryset.count(),'page': int(request.GET.get('page', 1)),'page_size': int(request.GET.get('page_size', 10)),'has_next': False  # 简化实现}}})@action(detail=False, methods=['get'])def statistics(self, request):"""采集统计信息"""today = timezone.now().date()# 今日采集数today_count = Collection.objects.filter(create_time__date=today).count()# 总采集数total_count = Collection.objects.count()# 已核验数量verified_count = Collection.objects.filter(is_verified=True).count()# 已录入人脸数量face_registered_count = Collection.objects.exclude(face_token='').count()data = {'today_count': today_count,'total_count': total_count,'verified_count': verified_count,'face_registered_count': face_registered_count}serializer = CollectionStatisticsSerializer(data)return Response({'code': 200,'message': 'success','data': serializer.data})@action(detail=True, methods=['post'])def verify(self, request, pk=None):"""核验采集信息"""collection = self.get_object()collection.is_verified = Truecollection.save()return Response({'code': 200,'message': '核验成功','data': None})
URL路由配置
# smart/urls.py
from rest_framework.routers import DefaultRouter
from .views import AreaViewSet, CollectionViewSetrouter = DefaultRouter()
router.register(r'area', AreaViewSet)
router.register(r'collection', CollectionViewSet)urlpatterns = [# ... 其他路由
] + router.urls
http://www.dtcms.com/a/537231.html

相关文章:

  • 2025年10月主流工程项目管理软件推荐
  • 设计模版网站一级a做爰片365网站
  • 计算机网络自顶向下方法7——应用层 HTTP概述及其连接方式
  • 网站建设贵不贵wordpress站文章显示时分秒
  • 【编译原理笔记】3.4 Tokens Recognization
  • day19_添加修改删除
  • 【Linux】ps -ef 和 ps -aux的区别
  • OpenFeign与Sentinel集成的原理
  • window系统下利用anaconda安装labelImag
  • Windows开机启动命令
  • LocalDream 2.1.2 |在手机设备上运行SD模型,支持文本到图像生成和图像重绘,无任何限制
  • 招聘网站建设维护求个网站直接能看的
  • 辽宁pc网站建设开发网站建设登记表
  • 网站 目录结构想注册一家公司怎么注册
  • 玩转前端图标系统:从零搭建一套完整的图标选择器组件
  • 卡尔费休滴定法微量水分测定仪:高精度水分分析的核心技术解析
  • 【重庆政务服务网-注册_登录安全分析报告】
  • 大型网站开发的主流语言网站的标题优化怎么做
  • 3.Xposed框架入门指南:深入解析Hook内部类与匿名类的实现技巧
  • 南皮做网站网站开发 放大图片
  • 【开源负载测试工具Locust的并发测试优势】
  • 历史上的今天 网站如何做影视动画设计专业
  • 网站搭建需要多少钱?嵌入式培训班多少钱
  • JavaScript学习第八天:对象
  • 数据重构!按一级科目拆分序时账,批量生成明细账
  • 适合权重小的网站做的专题西宁市网站建设
  • 清远网站开发sohu电商网站 收费与免费
  • UE5关卡蓝图视图恢复方法
  • JS 自定义事件:从 CustomEvent 到 dispatchEvent!
  • gpt-5和gpt-5-codex到底用哪个好?