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

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

学习项目(仅作参考)

一、项目概述

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配置
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"
  ]
}
项目配置文件优化
javascript 复制代码
// 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配置
python 复制代码
"""
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 = True

ALLOWED_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配置
python 复制代码
# 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 static

urlpatterns = [
    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配置和构建
json 复制代码
// 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": {}
}
组件全局注册
javascript 复制代码
// 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/'
  }
})
css 复制代码
/* 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结构优化
xml 复制代码
<!-- 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样式深度优化
css 复制代码
/* 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逻辑完整实现
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.countdown
    const 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
    }
  }
})
json 复制代码
// pages/welcome/welcome.json
{
  "navigationStyle": "custom",
  "disableScroll": true
}

3.2 后端数据模型详细设计

欢迎页模型扩展
python 复制代码
# smart/models.py
from django.db import models
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator

class 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_name
        ordering = ['-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}"
    
    @property
    def is_valid(self):
        """检查图片是否在有效期内"""
        now = timezone.now()
        if not self.is_active or self.is_delete:
            return False
        if self.start_time and self.start_time > now:
            return False
        if self.end_time and self.end_time < now:
            return False
        return True
    
    def 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.url
        return None
管理后台配置
python 复制代码
# 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_valid
    is_valid.boolean = True
    is_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 = True
        obj.is_active = False
        obj.save()
    
    def delete_queryset(self, request, queryset):
        """批量软删除"""
        queryset.update(is_delete=True, is_active=False)

3.3 后端API接口完整实现

序列化器
python 复制代码
# smart/serializers.py
from rest_framework import serializers
from .models import Welcome

class WelcomeSerializer(serializers.ModelSerializer):
    image_url = serializers.SerializerMethodField()
    is_valid = serializers.ReadOnlyField()
    
    class Meta:
        model = Welcome
        fields = [
            '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)
视图集实现
python 复制代码
# 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 WelcomeSerializer

class 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路由配置
python 复制代码
# smart/urls.py
from django.urls import path
from .views import WelcomeAPIView, WelcomeListAPIView

urlpatterns = [
    path('welcome/', WelcomeAPIView.as_view(), name='welcome'),
    path('welcome/list/', WelcomeListAPIView.as_view(), name='welcome-list'),
]

3.4 配置管理深度优化

环境配置管理
javascript 复制代码
// 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配置管理
javascript 复制代码
// 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 = url
  Object.keys(params).forEach(key => {
    builtUrl = builtUrl.replace(`{${key}}`, params[key])
  })
  return builtUrl
}

module.exports = {
  ...apiConfig,
  buildUrl,
  environment
}
工具函数封装
javascript 复制代码
// utils/request.js
const { environment, buildUrl } = require('../config/api')

/**
 * 统一的网络请求封装
 */
class Request {
  constructor() {
    this.baseConfig = {
      timeout: 10000,
      header: {
        'Content-Type': 'application/json'
      }
    }
  }

  // 请求拦截器
  interceptorsRequest(config) {
    // 添加认证token
    const 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 } = response
    
    if (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
javascript 复制代码
// 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 timeout
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout)
      func(...args)
    }
    clearTimeout(timeout)
    timeout = setTimeout(later, wait)
  }
}

// 节流函数
const throttle = (func, limit = 300) => {
  let inThrottle
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}

// 深拷贝
const deepClone = (obj) => {
  if (obj === null || typeof obj !== 'object') return obj
  if (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结构优化
xml 复制代码
<!-- 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样式深度优化
css 复制代码
/* 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逻辑完整实现
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.dataset
    if (url) {
      // 跳转到指定页面或网页
      wx.navigateTo({
        url: `/pages/webview/webview?url=${encodeURIComponent(url)}`
      })
    }
    
    // 记录点击统计(可选)
    this.recordBannerClick(id)
  },

  onNoticeClick() {
    const { notice } = this.data
    if (notice.id) {
      wx.navigateTo({
        url: `/pages/notice/detail/detail?id=${notice.id}`
      })
    }
  },

  navigateToPage(e) {
    const { page } = e.currentTarget.dataset
    
    if (!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.dataset
    wx.navigateTo({
      url: `/pages/news/detail/detail?id=${id}`
    })
  },

  recordBannerClick(bannerId) {
    // 记录banner点击统计,可选实现
    request.post(buildUrl('banner.click'), { banner_id: bannerId })
      .catch(console.error)
  }
})
json 复制代码
// 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 后端数据模型详细设计

轮播图模型扩展
python 复制代码
# 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_name
        ordering = ['-order', '-create_time']
        indexes = [
            models.Index(fields=['position', 'is_active', 'is_delete']),
        ]
    
    def __str__(self):
        return self.title or f"轮播图-{self.id}"
    
    @property
    def is_valid(self):
        """检查轮播图是否在有效期内"""
        now = timezone.now()
        if not self.is_active or self.is_delete:
            return False
        if self.start_time and self.start_time > now:
            return False
        if self.end_time and self.end_time < now:
            return False
        return True
    
    def 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.url
        return None
新闻动态模型
python 复制代码
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_name
        ordering = ['-is_top', '-publish_time', '-create_time']
        indexes = [
            models.Index(fields=['category', 'is_active']),
        ]
    
    def __str__(self):
        return self.title
    
    def 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.url
        return None
    
    def increase_view_count(self):
        """增加浏览数"""
        self.view_count += 1
        self.save(update_fields=['view_count'])

4.3 后端API接口完整实现

序列化器
python 复制代码
# smart/serializers.py
class BannerSerializer(serializers.ModelSerializer):
    image_url = serializers.SerializerMethodField()
    is_valid = serializers.ReadOnlyField()
    
    class Meta:
        model = Banner
        fields = [
            '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 = News
        fields = [
            '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 = Notice
        fields = ['id', 'title', 'content', 'create_time']

class HomeDataSerializer(serializers.Serializer):
    """首页数据序列化器"""
    banners = BannerSerializer(many=True)
    notice = NoticeSerializer()
    news = NewsSerializer(many=True)
视图集实现
python 复制代码
# 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 None
            news_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路由配置
python 复制代码
# smart/urls.py
urlpatterns = [
    # ... 其他路由
    path('banner/', BannerListAPIView.as_view(), name='banner-list'),
    path('home/', HomeAPIView.as_view(), name='home'),
]

4.4 管理后台配置

python 复制代码
# 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.content
        super().save_model(request, obj, form, change)

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

5.1 数据模型详细设计

用户和区域模型
python 复制代码
# 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_name
    
    def __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_name
        ordering = ['level', 'code']
    
    def __str__(self):
        return f"{self.name}({self.code})"
    
    def get_full_path(self):
        """获取完整区域路径"""
        path = []
        current = self
        while current:
            path.append(current.name)
            current = current.parent
        return ' - '.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_name
        ordering = ['-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.url
        return None
    
    def 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_pinyin
            return ''.join(lazy_pinyin(name))
        except ImportError:
            # 如果没有安装pypinyin,使用简单实现
            return name

5.2 采集列表页面深度实现

前端页面优化
xml 复制代码
<!-- 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>
采集列表样式优化
css 复制代码
/* 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;
}
采集列表逻辑实现
javascript 复制代码
// 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) return
    
    this.setData({ page: this.data.page + 1 })
    await this.loadCollectionList(false)
  },

  async loadCollectionList(refresh = false) {
    if (this.data.isLoading) return
    
    this.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.data
        const 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.dataset
    wx.navigateTo({
      url: `/pages/collection/detail/detail?id=${id}`
    })
  },

  doDeleteRow(e) {
    const { nid } = e.currentTarget.dataset
    const item = this.data.collectionList.find(i => i.id === nid)
    
    if (!item) return
    
    wx.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接口完整实现

序列化器
python 复制代码
# smart/serializers.py
class UserInfoSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserInfo
        fields = ['id', 'nickname', 'avatar', 'role', 'phone']

class AreaSerializer(serializers.ModelSerializer):
    full_path = serializers.ReadOnlyField()
    
    class Meta:
        model = Area
        fields = ['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 = Collection
        fields = [
            '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 value
    
    def validate_phone(self, value):
        """验证手机号格式"""
        if value and not re.match(r'^1[3-9]\d{9}$', value):
            raise serializers.ValidationError("手机号格式不正确")
        return value

class CollectionListSerializer(serializers.ModelSerializer):
    """用于列表显示的简化序列化器"""
    area = AreaSerializer(read_only=True)
    avatar_url = serializers.SerializerMethodField()
    
    class Meta:
        model = Collection
        fields = [
            '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()
视图集实现
python 复制代码
# 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 = AreaSerializer
    pagination_class = None
    
    def get_queryset(self):
        queryset = super().get_queryset()
        # 可以根据需要添加权限过滤
        return queryset

class CollectionViewSet(ModelViewSet):
    """信息采集视图集"""
    queryset = Collection.objects.all()
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['area', 'collector']
    
    def get_serializer_class(self):
        if self.action == 'list':
            return CollectionListSerializer
        return CollectionSerializer
    
    def 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 = True
        collection.save()
        
        return Response({
            'code': 200,
            'message': '核验成功',
            'data': None
        })
URL路由配置
python 复制代码
# smart/urls.py
from rest_framework.routers import DefaultRouter
from .views import AreaViewSet, CollectionViewSet

router = DefaultRouter()
router.register(r'area', AreaViewSet)
router.register(r'collection', CollectionViewSet)

urlpatterns = [
    # ... 其他路由
] + router.urls
相关推荐
從南走到北5 小时前
JAVA国际版任务悬赏发布接单系统源码支持IOS+Android+H5
android·java·ios·微信·微信小程序·小程序
游戏开发爱好者87 小时前
iOS 开发推送功能全流程详解 从 APNs 配置到上架发布的完整实践(含跨平台上传方案)
android·macos·ios·小程序·uni-app·cocoa·iphone
汤姆yu7 小时前
基于微信小程序的博物馆文创系统
微信小程序·小程序·博物馆
云起SAAS1 天前
ai公司起名取名抖音快手微信小程序看广告流量主开源
微信小程序·小程序·ai编程·看广告变现轻·ai公司起名取名
黑马源码库miui520861 天前
JAVA购物返利商品比价系统源码支持微信小程序
微信·微信小程序·小程序·1024程序员节
学会煎墙1 天前
使用uniapp——实现微信小程序的拖拽排序(vue3+ts)
微信小程序·uni-app·vue·ts
淡淡蓝蓝1 天前
uni-app小程序往飞书多维表格写入内容(包含图片)
小程序·uni-app·飞书
2501_915921431 天前
iOS混淆与IPA加固全流程(iOS混淆 IPA加固 Ipa Guard实战)
android·ios·小程序·https·uni-app·iphone·webview
游戏开发爱好者81 天前
iOS 26 App 开发阶段性能优化 从多工具协作到数据驱动的实战体系
android·ios·小程序·uni-app·iphone·webview·1024程序员节