Browse Source

wuzhen update

wuzhen 1 year ago
parent
commit
1d5e62b7f6

+ 148 - 0
mobile/src/api/visit.js

@@ -0,0 +1,148 @@
+/**
+ * 班主任走访管理API接口
+ * 提供走访记录增删改查、学生查询等功能
+ */
+
+import { request } from '@/common/request.js';
+
+/**
+ * 查询走访记录列表
+ * @param {Object} query 查询参数
+ * @returns Promise
+ */
+export const listVisit = (query) => {
+    return request({
+        url: '/zsjy/internshipVisit/list',
+        method: 'GET',
+        params: query
+    });
+};
+
+/**
+ * 获取走访记录详情
+ * @param {string|number} id 走访记录ID
+ * @returns Promise
+ */
+export const getVisit = (id) => {
+    return request({
+        url: `/zsjy/internshipVisit/${id}`,
+        method: 'GET'
+    });
+};
+
+/**
+ * 新增走访记录
+ * @param {Object} data 走访记录数据
+ * @returns Promise
+ */
+export const addVisit = (data) => {
+    return request({
+        url: '/zsjy/internshipVisit',
+        method: 'POST',
+        data
+    });
+};
+
+/**
+ * 修改走访记录
+ * @param {Object} data 走访记录数据
+ * @returns Promise
+ */
+export const updateVisit = (data) => {
+    return request({
+        url: '/zsjy/internshipVisit',
+        method: 'PUT',
+        data
+    });
+};
+
+/**
+ * 删除走访记录
+ * @param {string|number|Array} ids 走访记录ID(支持批量删除)
+ * @returns Promise
+ */
+export const delVisit = (ids) => {
+    return request({
+        url: `/zsjy/internshipVisit/${ids}`,
+        method: 'DELETE'
+    });
+};
+
+/**
+ * 导出走访记录
+ * @param {Object} query 查询参数
+ * @returns Promise
+ */
+export const exportVisit = (query) => {
+    return request({
+        url: '/zsjy/internshipVisit/export',
+        method: 'POST',
+        data: query
+    });
+};
+
+/**
+ * 获取班主任管理的学生实习信息列表(用于学生选择)
+ * @param {Object} params 查询参数,包含classTeacherId
+ * @returns Promise
+ */
+export const getStudentInternshipList = (params) => {
+    return request({
+        url: '/zsjy/internship/list',
+        method: 'GET',
+        params
+    });
+};
+
+/**
+ * 根据关键字搜索学生
+ * @param {string} keyword 搜索关键字(学生姓名或学号)
+ * @param {string|number} classTeacherId 班主任ID
+ * @returns Promise
+ */
+export const searchStudents = (keyword, classTeacherId) => {
+    const params = {
+        classTeacherId,
+        pageNum: 1,
+        pageSize: 50 // 搜索结果限制50条
+    };
+    
+    // 判断关键词类型,优化搜索参数
+    if (keyword && keyword.trim()) {
+        // 如果是纯数字,优先按学号搜索
+        if (/^\d+$/.test(keyword.trim())) {
+            params.studentNo = keyword.trim();
+        } else {
+            // 否则按姓名搜索
+            params.studentName = keyword.trim();
+        }
+    }
+    
+    return request({
+        url: '/zsjy/internship/list',
+        method: 'GET',
+        params
+    });
+};
+
+/**
+ * 获取走访状态字典数据
+ * @returns Promise
+ */
+export const getVisitStatusDict = () => {
+    return request({
+        url: '/system/dict/data/type/zsjy_visit_status',
+        method: 'GET'
+    });
+};
+
+/**
+ * 获取走访方式字典数据
+ * @returns Promise
+ */
+export const getVisitTypeDict = () => {
+    return request({
+        url: '/system/dict/data/type/zsjy_visit_type',
+        method: 'GET'
+    });
+}; 

+ 891 - 0
mobile/src/pages/visit/add.vue

@@ -0,0 +1,891 @@
+<template>
+    <view class="visit-add-container">
+        <!-- 表单内容 -->
+        <u-form
+            ref="formRef"
+            :model="form"
+            :rules="rules"
+            label-width="180rpx"
+            label-position="left"
+        >
+            <!-- 学生选择 -->
+            <view class="form-section">
+                <view class="section-title">学生信息</view>
+                
+                <u-form-item label="选择学生" prop="studentId" required>
+                    <view @click="handleStudentSelect" style="width: 100%">
+                        <u-input
+                            v-model="studentDisplayText"
+                            placeholder="请选择学生"
+                            readonly
+                            border="none"
+                            suffix-icon="arrow-down"
+                            suffix-icon-style="color: #c8c9cc;"
+                            style="pointer-events: none"
+                        />
+                    </view>
+                </u-form-item>
+                
+                <!-- 学生基础信息展示 -->
+                <view class="student-info-section" v-if="selectedStudent">
+                    <view class="info-row">
+                        <text class="info-label">学号:</text>
+                        <text class="info-value">{{ selectedStudent.studentNo || '-' }}</text>
+                    </view>
+                    <view class="info-row">
+                        <text class="info-label">班级:</text>
+                        <text class="info-value">{{ selectedStudent.className || '-' }}</text>
+                    </view>
+                    <view class="info-row">
+                        <text class="info-label">专业:</text>
+                        <text class="info-value">{{ selectedStudent.majorName || '-' }}</text>
+                    </view>
+                    <view class="info-row">
+                        <text class="info-label">实习单位:</text>
+                        <text class="info-value">{{ selectedStudent.companyName || '-' }}</text>
+                    </view>
+                </view>
+            </view>
+
+            <!-- 走访基本信息 -->
+            <view class="form-section">
+                <view class="section-title">走访信息</view>
+                
+                <u-form-item label="走访日期" prop="visitDate" required>
+                    <view @click="openVisitDatePicker" style="width: 100%">
+                        <u-input
+                            v-model="form.visitDate"
+                            placeholder="请选择走访日期"
+                            readonly
+                            border="none"
+                            suffix-icon="calendar"
+                            suffix-icon-style="color: #c8c9cc;"
+                        />
+                    </view>
+                </u-form-item>
+                
+                <u-form-item label="走访人员" prop="visitorName" required>
+                    <u-input
+                        v-model="form.visitorName"
+                        placeholder="请输入走访人员姓名"
+                        border="none"
+                        clearable
+                    />
+                </u-form-item>
+                
+                <u-form-item label="走访人职务" prop="visitorPosition">
+                    <u-input
+                        v-model="form.visitorPosition"
+                        placeholder="请输入走访人员职务"
+                        border="none"
+                        clearable
+                    />
+                </u-form-item>
+                
+                <u-form-item label="走访目的" prop="visitPurpose" required>
+                    <u-textarea
+                        v-model="form.visitPurpose"
+                        placeholder="请输入走访目的"
+                        :maxlength="500"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+            </view>
+
+            <!-- 走访内容 -->
+            <view class="form-section">
+                <view class="section-title">走访内容</view>
+                
+                <u-form-item label="企业情况" prop="companySituation">
+                    <u-textarea
+                        v-model="form.companySituation"
+                        placeholder="请描述企业基本情况"
+                        :maxlength="1000"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+                
+                <u-form-item label="学生表现" prop="studentPerformance">
+                    <u-textarea
+                        v-model="form.studentPerformance"
+                        placeholder="请描述学生实习表现"
+                        :maxlength="1000"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+                
+                <u-form-item label="发现问题" prop="problemsFound">
+                    <u-textarea
+                        v-model="form.problemsFound"
+                        placeholder="请描述发现的问题"
+                        :maxlength="1000"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+                
+                <u-form-item label="改进建议" prop="suggestions">
+                    <u-textarea
+                        v-model="form.suggestions"
+                        placeholder="请提出改进建议"
+                        :maxlength="1000"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+                
+                <u-form-item label="走访总结" prop="visitSummary">
+                    <u-textarea
+                        v-model="form.visitSummary"
+                        placeholder="请输入走访总结"
+                        :maxlength="1000"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+            </view>
+
+            <!-- 现场照片 -->
+            <view class="form-section">
+                <view class="section-title">现场照片</view>
+                
+                <view class="upload-section">
+                    <view class="image-list">
+                        <view 
+                            class="image-item" 
+                            v-for="(image, index) in imageList" 
+                            :key="index"
+                        >
+                            <image 
+                                :src="image.url" 
+                                mode="aspectFill" 
+                                @click="previewImage(index)"
+                            />
+                            <view class="delete-btn" @click="removeImage(index)">
+                                <u-icon name="close" size="12" color="#ffffff" />
+                            </view>
+                        </view>
+                        
+                        <view 
+                            class="upload-btn" 
+                            @click="chooseImage"
+                            v-if="imageList.length < maxImageCount"
+                        >
+                            <u-icon name="camera" size="24" color="#c8c9cc" />
+                            <text class="upload-text">添加照片</text>
+                        </view>
+                    </view>
+                    <text class="upload-tip">最多上传{{ maxImageCount }}张照片</text>
+                </view>
+            </view>
+
+            <!-- 备注信息 -->
+            <view class="form-section">
+                <view class="section-title">备注信息</view>
+                
+                <u-form-item label="备注" prop="remark">
+                    <u-textarea
+                        v-model="form.remark"
+                        placeholder="请输入备注信息(可选)"
+                        :maxlength="500"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+            </view>
+        </u-form>
+
+        <!-- 底部操作按钮 -->
+        <view class="bottom-actions">
+            <u-button 
+                type="primary" 
+                @click="submitForm"
+                :loading="submitLoading"
+                style="width: 100%;"
+            >
+                提交走访记录
+            </u-button>
+        </view>
+
+        <!-- 学生选择弹窗 -->
+        <u-popup 
+            :show="showStudentSelector" 
+            mode="bottom" 
+            height="80%"
+            :round="10"
+            :closeable="false"
+            @close="showStudentSelector = false"
+        >
+            <view class="student-selector">
+                <view class="selector-header">
+                    <text class="selector-title">选择学生</text>
+                    <u-icon 
+                        name="close" 
+                        size="20" 
+                        @click="showStudentSelector = false"
+                    />
+                </view>
+                
+                <view class="search-section">
+                    <u-search 
+                        v-model="studentSearchKeyword" 
+                        placeholder="搜索学生姓名或学号"
+                        @search="searchStudents"
+                        @custom="searchStudents"
+                        height="72"
+                        shape="round"
+                    />
+                </view>
+                
+                <view class="student-list">
+                    <view 
+                        class="student-item" 
+                        v-for="student in studentList" 
+                        :key="student.studentId"
+                        @click="selectStudent(student)"
+                    >
+                        <view class="student-main">
+                            <text class="student-name">{{ student.studentName }}</text>
+                            <text class="student-no">{{ student.studentNo }}</text>
+                        </view>
+                        <view class="student-detail">
+                            <text class="detail-text">{{ student.className || '-' }}</text>
+                            <text class="detail-text">{{ student.companyName || '暂无实习信息' }}</text>
+                        </view>
+                        <u-icon 
+                            name="checkmark" 
+                            size="16" 
+                            color="#007AFF"
+                            v-if="form.studentId === student.studentId"
+                        />
+                    </view>
+                    
+                    <view class="student-empty" v-if="studentList.length === 0 && !studentLoading">
+                        <u-empty 
+                            mode="search" 
+                            text="暂无学生数据"
+                            textColor="#c8c9cc"
+                            iconColor="#c8c9cc"
+                        />
+                    </view>
+                    
+                    <view class="student-loading" v-if="studentLoading">
+                        <u-loading-icon mode="circle" />
+                        <text style="margin-left: 10rpx; color: #909399;">加载中...</text>
+                    </view>
+                </view>
+            </view>
+        </u-popup>
+
+        <!-- 日期选择器 -->
+        <u-datetime-picker
+            :show="showVisitDatePicker"
+            v-model="visitDateValue"
+            mode="date"
+            :min-date="minDate"
+            :max-date="maxDate"
+            @confirm="handleVisitDateConfirm"
+            @cancel="showVisitDatePicker = false"
+        />
+    </view>
+</template>
+
+<script>
+import { addVisit, getStudentInternshipList, searchStudents } from '@/api/visit.js';
+import { userStorage } from '@/common/storage.js';
+
+export default {
+    name: 'VisitAdd',
+    data() {
+        return {
+            // 表单数据
+            form: {
+                studentId: '',
+                internshipId: '',
+                companyName: '',
+                companyAddress: '',
+                visitDate: '',
+                visitorName: '',
+                visitorPosition: '',
+                visitPurpose: '',
+                companySituation: '',
+                studentPerformance: '',
+                problemsFound: '',
+                suggestions: '',
+                visitSummary: '',
+                visitImages: '',
+                remark: '',
+                status: 'planned' // 默认状态为计划中
+            },
+            
+            // 表单验证规则
+            rules: {
+                studentId: [
+                    { required: true, message: '请选择学生', trigger: ['blur', 'change'] }
+                ],
+                visitDate: [
+                    { required: true, message: '请选择走访日期', trigger: ['blur', 'change'] }
+                ],
+                visitorName: [
+                    { required: true, message: '请输入走访人员姓名', trigger: ['blur', 'change'] }
+                ],
+                visitPurpose: [
+                    { required: true, message: '请输入走访目的', trigger: ['blur', 'change'] }
+                ]
+            },
+            
+            // 学生选择
+            showStudentSelector: false,
+            studentList: [],
+            selectedStudent: null,
+            studentDisplayText: '',
+            studentSearchKeyword: '',
+            studentLoading: false,
+            
+            // 日期选择
+            showVisitDatePicker: false,
+            visitDateValue: Date.now(),
+            minDate: Date.now() - 30 * 24 * 60 * 60 * 1000, // 最小日期为30天前
+            maxDate: Date.now() + 365 * 24 * 60 * 60 * 1000, // 最大日期为一年后
+            
+            // 图片上传
+            imageList: [],
+            maxImageCount: 10,
+            
+            // 加载状态
+            submitLoading: false
+        };
+    },
+    
+    onLoad() {
+        this.initForm();
+    },
+    
+    methods: {
+        // 初始化表单
+        async initForm() {
+            // 获取当前登录用户信息,自动填入走访人
+            try {
+                const userInfo = userStorage.getUserInfo();
+                // 修复:用户信息结构为 {user: {userId, nickName, ...}, permissions, roles}
+                const user = userInfo?.user;
+                if (user && user.nickName) {
+                    this.form.visitorName = user.nickName;
+                }
+                if (user && user.position) {
+                    this.form.visitorPosition = user.position;
+                }
+            } catch (error) {
+                console.warn('获取用户信息失败:', error);
+            }
+            
+            // 加载学生列表
+            this.loadStudentList();
+        },
+        
+        // 加载学生列表
+        async loadStudentList() {
+            if (this.studentLoading) return;
+            
+            this.studentLoading = true;
+            try {
+                // 获取当前登录用户信息,获取班主任ID
+                const userInfo = userStorage.getUserInfo();
+                // 修复:从正确的路径获取用户ID
+                const classTeacherId = userInfo?.user?.userId;
+                
+                if (!classTeacherId) {
+                    uni.showToast({ title: '无法获取班主任信息', icon: 'none' });
+                    return;
+                }
+                
+                const params = {
+                    classTeacherId,
+                    pageNum: 1,
+                    pageSize: 100 // 获取较多数据供选择
+                };
+                
+                const response = await getStudentInternshipList(params);
+                
+                if (response.code === 200) {
+                    // 实习信息接口返回TableDataInfo格式
+                    this.studentList = response.rows || [];
+                } else {
+                    uni.showToast({ title: '获取学生列表失败', icon: 'none' });
+                }
+            } catch (error) {
+                console.error('加载学生列表失败:', error);
+                uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
+            } finally {
+                this.studentLoading = false;
+            }
+        },
+        
+        // 搜索学生
+        async searchStudents() {
+            if (!this.studentSearchKeyword.trim()) {
+                this.loadStudentList();
+                return;
+            }
+            
+            if (this.studentLoading) return;
+            
+            this.studentLoading = true;
+            try {
+                const userInfo = userStorage.getUserInfo();
+                // 修复:从正确的路径获取用户ID
+                const classTeacherId = userInfo?.user?.userId;
+                
+                const response = await searchStudents(this.studentSearchKeyword, classTeacherId);
+                
+                if (response.code === 200) {
+                    // 实习信息接口返回TableDataInfo格式
+                    this.studentList = response.rows || [];
+                } else {
+                    this.studentList = [];
+                }
+            } catch (error) {
+                console.error('搜索学生失败:', error);
+                this.studentList = [];
+            } finally {
+                this.studentLoading = false;
+            }
+        },
+        
+        // 显示学生选择器
+        handleStudentSelect() {
+            this.showStudentSelector = true;
+        },
+        
+        // 选择学生
+        selectStudent(student) {
+            this.selectedStudent = student;
+            this.form.studentId = student.studentId;
+            this.form.internshipId = student.internshipId;
+            this.form.companyName = student.companyName;
+            this.form.companyAddress = student.companyAddress;
+            this.studentDisplayText = `${student.studentName}(${student.studentNo})`;
+            this.showStudentSelector = false;
+        },
+        
+        // 打开走访日期选择器
+        openVisitDatePicker() {
+            console.log('打开日期选择器,当前表单日期:', this.form.visitDate);
+            console.log('当前visitDateValue:', this.visitDateValue);
+            
+            // 如果表单中已有日期,使用该日期初始化选择器
+            if (this.form.visitDate) {
+                try {
+                    const existingDate = new Date(this.form.visitDate);
+                    if (!isNaN(existingDate.getTime())) {
+                        this.visitDateValue = existingDate.getTime();
+                        console.log('使用现有日期初始化:', this.visitDateValue);
+                    }
+                } catch (error) {
+                    console.log('解析现有日期失败:', error);
+                }
+            }
+            
+            this.showVisitDatePicker = true;
+        },
+        
+        // 走访日期确认
+        handleVisitDateConfirm(value) {
+            console.log('=== 走访日期确认开始 ===');
+            console.log('接收到的value:', value);
+            console.log('value类型:', typeof value);
+            console.log('value是否为null/undefined:', value == null);
+            
+            if (value == null) {
+                console.error('接收到空值');
+                uni.showToast({ title: '请选择有效日期', icon: 'none' });
+                this.showVisitDatePicker = false;
+                return;
+            }
+            
+            try {
+                let timestamp;
+                
+                // 处理uview-ui返回的对象格式 {value: timestamp, mode: 'date'}
+                if (typeof value === 'object' && value.value !== undefined) {
+                    timestamp = value.value;
+                    console.log('从对象中提取timestamp:', timestamp);
+                } else if (typeof value === 'number') {
+                    timestamp = value;
+                    console.log('直接使用number类型timestamp:', timestamp);
+                } else if (typeof value === 'string') {
+                    timestamp = parseInt(value);
+                    console.log('从string转换timestamp:', timestamp);
+                } else {
+                    throw new Error('无法识别的value格式');
+                }
+                
+                // 创建Date对象
+                const date = new Date(timestamp);
+                console.log('创建的Date对象:', date);
+                console.log('Date.getTime():', date.getTime());
+                console.log('是否为有效日期:', !isNaN(date.getTime()));
+                
+                if (isNaN(date.getTime())) {
+                    throw new Error('无效的日期值');
+                }
+                
+                const formattedDate = this.formatDate(date);
+                console.log('格式化后的日期:', formattedDate);
+                
+                if (!formattedDate) {
+                    throw new Error('日期格式化失败');
+                }
+                
+                this.form.visitDate = formattedDate;
+                this.visitDateValue = timestamp; // 保存时间戳而不是原始对象
+                
+                console.log('最终form.visitDate:', this.form.visitDate);
+                console.log('最终visitDateValue:', this.visitDateValue);
+                console.log('日期设置成功');
+                
+            } catch (error) {
+                console.error('日期处理异常:', error.message);
+                console.error('异常详情 - value:', value, 'type:', typeof value);
+                uni.showToast({ title: '日期选择失败,请重试', icon: 'none' });
+            }
+            
+            this.showVisitDatePicker = false;
+            console.log('=== 走访日期确认结束 ===');
+        },
+        
+        // 格式化日期
+        formatDate(date) {
+            console.log('formatDate - 输入的date:', date);
+            console.log('formatDate - date类型:', typeof date);
+            
+            if (!date) {
+                console.log('formatDate - date为空');
+                return '';
+            }
+            
+            if (isNaN(date.getTime())) {
+                console.log('formatDate - date.getTime()为NaN');
+                return '';
+            }
+            
+            try {
+                const year = date.getFullYear();
+                const month = String(date.getMonth() + 1).padStart(2, '0');
+                const day = String(date.getDate()).padStart(2, '0');
+                const result = `${year}-${month}-${day}`;
+                
+                console.log('formatDate - 格式化结果:', result);
+                return result;
+            } catch (error) {
+                console.error('formatDate - 格式化异常:', error);
+                return '';
+            }
+        },
+        
+        // 选择图片
+        chooseImage() {
+            const remainCount = this.maxImageCount - this.imageList.length;
+            uni.chooseImage({
+                count: remainCount,
+                sizeType: ['compressed'],
+                sourceType: ['album', 'camera'],
+                success: (res) => {
+                    this.handleImageUpload(res.tempFilePaths);
+                }
+            });
+        },
+        
+        // 处理图片上传
+        async handleImageUpload(filePaths) {
+            uni.showLoading({ title: '上传中...' });
+            
+            try {
+                for (const filePath of filePaths) {
+                    // 这里应该调用具体的文件上传接口
+                    // 暂时使用本地路径作为示例
+                    this.imageList.push({
+                        url: filePath,
+                        path: filePath
+                    });
+                }
+                
+                // 更新表单中的图片数据
+                this.form.visitImages = JSON.stringify(this.imageList);
+            } catch (error) {
+                console.error('上传图片失败:', error);
+                uni.showToast({ title: '上传图片失败', icon: 'none' });
+            } finally {
+                uni.hideLoading();
+            }
+        },
+        
+        // 预览图片
+        previewImage(index) {
+            const urls = this.imageList.map(item => item.url);
+            uni.previewImage({
+                urls,
+                current: index
+            });
+        },
+        
+        // 删除图片
+        removeImage(index) {
+            this.imageList.splice(index, 1);
+            this.form.visitImages = JSON.stringify(this.imageList);
+        },
+        
+
+        
+        // 提交表单
+        async submitForm() {
+            // 表单验证
+            try {
+                await this.$refs.formRef.validate();
+            } catch (error) {
+                console.log('表单验证失败:', error);
+                return;
+            }
+            
+            this.submitLoading = true;
+            try {
+                const formData = {
+                    ...this.form,
+                    status: '1' // 已提交状态 (0-草稿 1-已提交 2-已审核)
+                };
+                
+                const response = await addVisit(formData);
+                
+                if (response.code === 200) {
+                    uni.showToast({ title: '提交成功', icon: 'success' });
+                    setTimeout(() => {
+                        uni.navigateBack();
+                    }, 1500);
+                } else {
+                    uni.showToast({ title: response.msg || '提交失败', icon: 'none' });
+                }
+            } catch (error) {
+                console.error('提交走访记录失败:', error);
+                uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
+            } finally {
+                this.submitLoading = false;
+            }
+        }
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+.visit-add-container {
+    min-height: 100vh;
+    background-color: #f8f8f8;
+    padding-bottom: 120rpx; // 为底部按钮留出空间
+}
+
+.form-section {
+    background: #ffffff;
+    margin-bottom: 20rpx;
+    padding: 0 20rpx;
+    
+    .section-title {
+        padding: 24rpx 0 16rpx;
+        font-size: 32rpx;
+        font-weight: 600;
+        color: #303133;
+        border-bottom: 1rpx solid #ebeef5;
+        margin-bottom: 20rpx;
+    }
+}
+
+.student-info-section {
+    background: #f8f9fa;
+    border-radius: 8rpx;
+    padding: 20rpx;
+    margin-top: 16rpx;
+    
+    .info-row {
+        display: flex;
+        margin-bottom: 12rpx;
+        
+        &:last-child {
+            margin-bottom: 0;
+        }
+        
+        .info-label {
+            font-size: 28rpx;
+            color: #606266;
+            width: 120rpx;
+        }
+        
+        .info-value {
+            font-size: 28rpx;
+            color: #303133;
+            flex: 1;
+        }
+    }
+}
+
+.upload-section {
+    .image-list {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 16rpx;
+        margin-bottom: 16rpx;
+        
+        .image-item {
+            position: relative;
+            width: 160rpx;
+            height: 160rpx;
+            border-radius: 8rpx;
+            overflow: hidden;
+            
+            image {
+                width: 100%;
+                height: 100%;
+            }
+            
+            .delete-btn {
+                position: absolute;
+                top: 8rpx;
+                right: 8rpx;
+                width: 32rpx;
+                height: 32rpx;
+                border-radius: 50%;
+                background: rgba(0, 0, 0, 0.6);
+                display: flex;
+                align-items: center;
+                justify-content: center;
+            }
+        }
+        
+        .upload-btn {
+            width: 160rpx;
+            height: 160rpx;
+            border: 2rpx dashed #c8c9cc;
+            border-radius: 8rpx;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            
+            .upload-text {
+                font-size: 24rpx;
+                color: #c8c9cc;
+                margin-top: 8rpx;
+            }
+        }
+    }
+    
+    .upload-tip {
+        font-size: 24rpx;
+        color: #909399;
+    }
+}
+
+.bottom-actions {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    background: #ffffff;
+    padding: 20rpx;
+    border-top: 1rpx solid #ebeef5;
+    display: flex;
+    align-items: center;
+    z-index: 100;
+}
+
+.student-selector {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+    
+    .selector-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 24rpx 20rpx;
+        border-bottom: 1rpx solid #ebeef5;
+        
+        .selector-title {
+            font-size: 32rpx;
+            font-weight: 600;
+            color: #303133;
+        }
+    }
+    
+    .search-section {
+        padding: 20rpx;
+        border-bottom: 1rpx solid #ebeef5;
+    }
+    
+    .student-list {
+        height: 600rpx; /* 固定高度,确保可以滚动 */
+        padding: 0 20rpx;
+        overflow-y: scroll;
+        -webkit-overflow-scrolling: touch;
+        position: relative;
+        border: 1px solid #eee; /* 添加边框便于调试 */
+        box-sizing: border-box;
+        
+        .student-item {
+            display: flex;
+            align-items: center;
+            padding: 24rpx 0;
+            border-bottom: 1rpx solid #ebeef5;
+            
+            .student-main {
+                flex: 1;
+                
+                .student-name {
+                    font-size: 32rpx;
+                    font-weight: 600;
+                    color: #303133;
+                    margin-right: 16rpx;
+                }
+                
+                .student-no {
+                    font-size: 28rpx;
+                    color: #909399;
+                }
+            }
+            
+            .student-detail {
+                flex: 2;
+                margin-left: 20rpx;
+                
+                .detail-text {
+                    font-size: 26rpx;
+                    color: #606266;
+                    display: block;
+                    margin-bottom: 4rpx;
+                    
+                    &:last-child {
+                        margin-bottom: 0;
+                    }
+                }
+            }
+        }
+        
+        .student-empty, 
+        .student-loading {
+            padding: 80rpx 20rpx;
+            text-align: center;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+    }
+}
+</style> 

+ 424 - 0
mobile/src/pages/visit/detail.vue

@@ -0,0 +1,424 @@
+<template>
+    <view class="visit-detail-container">
+        <!-- 学生信息 -->
+        <view class="info-section">
+            <view class="section-title">学生信息</view>
+            <view class="section-content">
+                <view class="info-item">
+                    <text class="info-label">学生姓名:</text>
+                    <text class="info-value">{{ visitInfo.studentName || '-' }}</text>
+                </view>
+                <view class="info-item">
+                    <text class="info-label">学号:</text>
+                    <text class="info-value">{{ visitInfo.studentNo || '-' }}</text>
+                </view>
+                <view class="info-item">
+                    <text class="info-label">班级:</text>
+                    <text class="info-value">{{ visitInfo.className || '-' }}</text>
+                </view>
+                <view class="info-item">
+                    <text class="info-label">专业:</text>
+                    <text class="info-value">{{ visitInfo.majorName || '-' }}</text>
+                </view>
+                <view class="info-item">
+                    <text class="info-label">学院:</text>
+                    <text class="info-value">{{ visitInfo.collegeName || '-' }}</text>
+                </view>
+            </view>
+        </view>
+
+        <!-- 企业信息 -->
+        <view class="info-section">
+            <view class="section-title">企业信息</view>
+            <view class="section-content">
+                <view class="info-item">
+                    <text class="info-label">企业名称:</text>
+                    <text class="info-value">{{ visitInfo.companyName || '-' }}</text>
+                </view>
+                <view class="info-item">
+                    <text class="info-label">企业地址:</text>
+                    <text class="info-value">{{ visitInfo.companyAddress || '-' }}</text>
+                </view>
+            </view>
+        </view>
+
+        <!-- 走访基本信息 -->
+        <view class="info-section">
+            <view class="section-title">走访基本信息</view>
+            <view class="section-content">
+                <view class="info-item">
+                    <text class="info-label">走访日期:</text>
+                    <text class="info-value">{{ visitInfo.visitDate || '-' }}</text>
+                </view>
+                <view class="info-item">
+                    <text class="info-label">走访人员:</text>
+                    <text class="info-value">{{ visitInfo.visitorName || '-' }}</text>
+                </view>
+                <view class="info-item" v-if="visitInfo.visitorPosition">
+                    <text class="info-label">走访人职务:</text>
+                    <text class="info-value">{{ visitInfo.visitorPosition }}</text>
+                </view>
+                <view class="info-item">
+                    <text class="info-label">走访状态:</text>
+                    <u-tag 
+                        :text="getStatusText(visitInfo.status)" 
+                        :type="getStatusType(visitInfo.status)"
+                        size="mini"
+                    />
+                </view>
+                <view class="info-item info-item-full" v-if="visitInfo.visitPurpose">
+                    <text class="info-label">走访目的:</text>
+                    <text class="info-value info-value-multiline">{{ visitInfo.visitPurpose }}</text>
+                </view>
+            </view>
+        </view>
+
+        <!-- 走访内容 -->
+        <view class="info-section">
+            <view class="section-title">走访内容</view>
+            <view class="section-content">
+                <view class="info-item info-item-full" v-if="visitInfo.companySituation">
+                    <text class="info-label">企业情况:</text>
+                    <text class="info-value info-value-multiline">{{ visitInfo.companySituation }}</text>
+                </view>
+                <view class="info-item info-item-full" v-if="visitInfo.studentPerformance">
+                    <text class="info-label">学生表现:</text>
+                    <text class="info-value info-value-multiline">{{ visitInfo.studentPerformance }}</text>
+                </view>
+                <view class="info-item info-item-full" v-if="visitInfo.problemsFound">
+                    <text class="info-label">发现问题:</text>
+                    <text class="info-value info-value-multiline">{{ visitInfo.problemsFound }}</text>
+                </view>
+                <view class="info-item info-item-full" v-if="visitInfo.suggestions">
+                    <text class="info-label">改进建议:</text>
+                    <text class="info-value info-value-multiline">{{ visitInfo.suggestions }}</text>
+                </view>
+                <view class="info-item info-item-full" v-if="visitInfo.visitSummary">
+                    <text class="info-label">走访总结:</text>
+                    <text class="info-value info-value-multiline">{{ visitInfo.visitSummary }}</text>
+                </view>
+            </view>
+        </view>
+
+        <!-- 现场照片 -->
+        <view class="info-section" v-if="imageList.length > 0">
+            <view class="section-title">现场照片</view>
+            <view class="section-content">
+                <view class="image-grid">
+                    <view 
+                        class="image-item" 
+                        v-for="(image, index) in imageList" 
+                        :key="index"
+                        @click="previewImage(index)"
+                    >
+                        <image :src="image.url" mode="aspectFill" />
+                    </view>
+                </view>
+            </view>
+        </view>
+
+        <!-- 备注信息 -->
+        <view class="info-section" v-if="visitInfo.remark">
+            <view class="section-title">备注信息</view>
+            <view class="section-content">
+                <view class="info-item info-item-full">
+                    <text class="info-value info-value-multiline">{{ visitInfo.remark }}</text>
+                </view>
+            </view>
+        </view>
+
+        <!-- 系统信息 -->
+        <view class="info-section">
+            <view class="section-title">系统信息</view>
+            <view class="section-content">
+                <view class="info-item">
+                    <text class="info-label">创建时间:</text>
+                    <text class="info-value">{{ visitInfo.createTime || '-' }}</text>
+                </view>
+                <view class="info-item" v-if="visitInfo.updateTime">
+                    <text class="info-label">更新时间:</text>
+                    <text class="info-value">{{ visitInfo.updateTime }}</text>
+                </view>
+            </view>
+        </view>
+
+        <!-- 操作按钮 -->
+        <view class="action-section" v-if="canEdit">
+            <u-button 
+                type="primary" 
+                @click="handleEdit"
+                :loading="loading"
+                style="margin-right: 20rpx;"
+            >
+                编辑走访记录
+            </u-button>
+            <u-button 
+                type="error" 
+                @click="handleDelete"
+                :loading="loading"
+                plain
+            >
+                删除记录
+            </u-button>
+        </view>
+
+        <!-- 加载遮罩 -->
+        <u-loading-page 
+            :loading="pageLoading" 
+            loading-text="加载中..."
+            loading-mode="circle"
+        />
+    </view>
+</template>
+
+<script>
+import { getVisit, delVisit } from '@/api/visit.js';
+
+export default {
+    name: 'VisitDetail',
+    data() {
+        return {
+            // 页面参数
+            visitId: '',
+            
+            // 页面状态
+            pageLoading: true,
+            loading: false,
+            
+            // 走访信息
+            visitInfo: {},
+            
+            // 图片数据
+            imageList: []
+        };
+    },
+    
+    computed: {
+        // 是否可以编辑
+        canEdit() {
+            // 只有计划中或草稿状态的记录可以编辑
+            return this.visitInfo.status === 'planned' || 
+                   this.visitInfo.status === '0' ||
+                   this.visitInfo.status === 'draft';
+        }
+    },
+    
+    onLoad(options) {
+        if (options.id) {
+            this.visitId = options.id;
+            this.loadVisitDetail();
+        } else {
+            uni.showToast({ title: '参数错误', icon: 'none' });
+            uni.navigateBack();
+        }
+    },
+    
+    methods: {
+        // 加载走访记录详情
+        async loadVisitDetail() {
+            this.pageLoading = true;
+            try {
+                const response = await getVisit(this.visitId);
+                
+                if (response.code === 200 && response.data) {
+                    this.visitInfo = response.data;
+                    
+                    // 处理图片数据
+                    if (response.data.visitImages) {
+                        try {
+                            this.imageList = JSON.parse(response.data.visitImages) || [];
+                        } catch (e) {
+                            console.warn('解析图片数据失败:', e);
+                            this.imageList = [];
+                        }
+                    }
+                    
+                } else {
+                    uni.showToast({ title: response.msg || '获取数据失败', icon: 'none' });
+                    uni.navigateBack();
+                }
+            } catch (error) {
+                console.error('加载走访记录详情失败:', error);
+                uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
+                uni.navigateBack();
+            } finally {
+                this.pageLoading = false;
+            }
+        },
+        
+        // 获取状态文本
+        getStatusText(status) {
+            switch (status) {
+                case 'planned':
+                case '0':
+                    return '计划中';
+                case 'completed':
+                case '1':
+                    return '已完成';
+                case 'cancelled':
+                case '2':
+                    return '已取消';
+                default:
+                    return status || '未知';
+            }
+        },
+        
+        // 获取状态类型
+        getStatusType(status) {
+            switch (status) {
+                case 'planned':
+                case '0':
+                    return 'primary';
+                case 'completed':
+                case '1':
+                    return 'success';
+                case 'cancelled':
+                case '2':
+                    return 'error';
+                default:
+                    return 'default';
+            }
+        },
+        
+        // 预览图片
+        previewImage(index) {
+            const urls = this.imageList.map(item => item.url);
+            uni.previewImage({
+                urls,
+                current: index
+            });
+        },
+        
+        // 编辑走访记录
+        handleEdit() {
+            uni.navigateTo({
+                url: `/pages/visit/edit?id=${this.visitId}`
+            });
+        },
+        
+        // 删除走访记录
+        handleDelete() {
+            uni.showModal({
+                title: '确认删除',
+                content: '确定要删除这条走访记录吗?删除后无法恢复。',
+                success: async (res) => {
+                    if (res.confirm) {
+                        await this.deleteVisit();
+                    }
+                }
+            });
+        },
+        
+        // 执行删除
+        async deleteVisit() {
+            this.loading = true;
+            try {
+                const response = await delVisit(this.visitId);
+                
+                if (response.code === 200) {
+                    uni.showToast({ title: '删除成功', icon: 'success' });
+                    setTimeout(() => {
+                        uni.navigateBack();
+                    }, 1500);
+                } else {
+                    uni.showToast({ title: response.msg || '删除失败', icon: 'none' });
+                }
+            } catch (error) {
+                console.error('删除走访记录失败:', error);
+                uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
+            } finally {
+                this.loading = false;
+            }
+        }
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+.visit-detail-container {
+    min-height: 100vh;
+    background-color: #f8f8f8;
+    padding: 20rpx;
+    padding-bottom: 40rpx;
+}
+
+.info-section {
+    background: #ffffff;
+    border-radius: 12rpx;
+    margin-bottom: 20rpx;
+    overflow: hidden;
+    
+    .section-title {
+        padding: 24rpx 20rpx 16rpx;
+        font-size: 32rpx;
+        font-weight: 600;
+        color: #303133;
+        border-bottom: 1rpx solid #ebeef5;
+        background: #f8f9fa;
+    }
+    
+    .section-content {
+        padding: 20rpx;
+    }
+}
+
+.info-item {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: 16rpx;
+    
+    &:last-child {
+        margin-bottom: 0;
+    }
+    
+    &.info-item-full {
+        flex-direction: column;
+        align-items: stretch;
+    }
+    
+    .info-label {
+        font-size: 28rpx;
+        color: #606266;
+        width: 160rpx;
+        flex-shrink: 0;
+        margin-bottom: 8rpx;
+    }
+    
+    .info-value {
+        font-size: 28rpx;
+        color: #303133;
+        flex: 1;
+        
+        &.info-value-multiline {
+            line-height: 1.6;
+            word-break: break-all;
+        }
+    }
+}
+
+.image-grid {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 16rpx;
+    
+    .image-item {
+        width: 200rpx;
+        height: 200rpx;
+        border-radius: 8rpx;
+        overflow: hidden;
+        
+        image {
+            width: 100%;
+            height: 100%;
+        }
+    }
+}
+
+.action-section {
+    display: flex;
+    padding: 20rpx;
+    background: #ffffff;
+    border-radius: 12rpx;
+    margin-bottom: 20rpx;
+}
+</style> 

+ 622 - 0
mobile/src/pages/visit/edit.vue

@@ -0,0 +1,622 @@
+<template>
+    <view class="visit-edit-container">
+        <!-- 表单内容 -->
+        <u-form
+            ref="formRef"
+            :model="form"
+            :rules="rules"
+            label-width="180rpx"
+            label-position="left"
+        >
+            <!-- 学生信息展示 -->
+            <view class="form-section">
+                <view class="section-title">学生信息</view>
+                
+                <view class="student-info-section">
+                    <view class="info-row">
+                        <text class="info-label">学生姓名:</text>
+                        <text class="info-value">{{ visitInfo.studentName || '-' }}</text>
+                    </view>
+                    <view class="info-row">
+                        <text class="info-label">学号:</text>
+                        <text class="info-value">{{ visitInfo.studentNo || '-' }}</text>
+                    </view>
+                    <view class="info-row">
+                        <text class="info-label">班级:</text>
+                        <text class="info-value">{{ visitInfo.className || '-' }}</text>
+                    </view>
+                    <view class="info-row">
+                        <text class="info-label">专业:</text>
+                        <text class="info-value">{{ visitInfo.majorName || '-' }}</text>
+                    </view>
+                    <view class="info-row">
+                        <text class="info-label">实习单位:</text>
+                        <text class="info-value">{{ visitInfo.companyName || '-' }}</text>
+                    </view>
+                </view>
+            </view>
+
+            <!-- 走访基本信息 -->
+            <view class="form-section">
+                <view class="section-title">走访信息</view>
+                
+                <u-form-item label="走访日期" prop="visitDate" required>
+                    <view @click="showVisitDatePicker = true" style="width: 100%">
+                        <u-input
+                            v-model="form.visitDate"
+                            placeholder="请选择走访日期"
+                            readonly
+                            border="none"
+                            suffix-icon="calendar"
+                            suffix-icon-style="color: #c8c9cc;"
+                            style="pointer-events: none"
+                        />
+                    </view>
+                </u-form-item>
+                
+                <u-form-item label="走访人员" prop="visitorName" required>
+                    <u-input
+                        v-model="form.visitorName"
+                        placeholder="请输入走访人员姓名"
+                        border="none"
+                        clearable
+                    />
+                </u-form-item>
+                
+                <u-form-item label="走访人职务" prop="visitorPosition">
+                    <u-input
+                        v-model="form.visitorPosition"
+                        placeholder="请输入走访人员职务"
+                        border="none"
+                        clearable
+                    />
+                </u-form-item>
+                
+                <u-form-item label="走访目的" prop="visitPurpose" required>
+                    <u-textarea
+                        v-model="form.visitPurpose"
+                        placeholder="请输入走访目的"
+                        :maxlength="500"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+            </view>
+
+            <!-- 走访内容 -->
+            <view class="form-section">
+                <view class="section-title">走访内容</view>
+                
+                <u-form-item label="企业情况" prop="companySituation">
+                    <u-textarea
+                        v-model="form.companySituation"
+                        placeholder="请描述企业基本情况"
+                        :maxlength="1000"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+                
+                <u-form-item label="学生表现" prop="studentPerformance">
+                    <u-textarea
+                        v-model="form.studentPerformance"
+                        placeholder="请描述学生实习表现"
+                        :maxlength="1000"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+                
+                <u-form-item label="发现问题" prop="problemsFound">
+                    <u-textarea
+                        v-model="form.problemsFound"
+                        placeholder="请描述发现的问题"
+                        :maxlength="1000"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+                
+                <u-form-item label="改进建议" prop="suggestions">
+                    <u-textarea
+                        v-model="form.suggestions"
+                        placeholder="请提出改进建议"
+                        :maxlength="1000"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+                
+                <u-form-item label="走访总结" prop="visitSummary">
+                    <u-textarea
+                        v-model="form.visitSummary"
+                        placeholder="请输入走访总结"
+                        :maxlength="1000"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+            </view>
+
+            <!-- 现场照片 -->
+            <view class="form-section">
+                <view class="section-title">现场照片</view>
+                
+                <view class="upload-section">
+                    <view class="image-list">
+                        <view 
+                            class="image-item" 
+                            v-for="(image, index) in imageList" 
+                            :key="index"
+                        >
+                            <image 
+                                :src="image.url" 
+                                mode="aspectFill" 
+                                @click="previewImage(index)"
+                            />
+                            <view class="delete-btn" @click="removeImage(index)">
+                                <u-icon name="close" size="12" color="#ffffff" />
+                            </view>
+                        </view>
+                        
+                        <view 
+                            class="upload-btn" 
+                            @click="chooseImage"
+                            v-if="imageList.length < maxImageCount"
+                        >
+                            <u-icon name="camera" size="24" color="#c8c9cc" />
+                            <text class="upload-text">添加照片</text>
+                        </view>
+                    </view>
+                    <text class="upload-tip">最多上传{{ maxImageCount }}张照片</text>
+                </view>
+            </view>
+
+            <!-- 备注信息 -->
+            <view class="form-section">
+                <view class="section-title">备注信息</view>
+                
+                <u-form-item label="备注" prop="remark">
+                    <u-textarea
+                        v-model="form.remark"
+                        placeholder="请输入备注信息(可选)"
+                        :maxlength="500"
+                        show-confirm-bar
+                        height="120"
+                        auto-height
+                    />
+                </u-form-item>
+            </view>
+        </u-form>
+
+        <!-- 底部操作按钮 -->
+        <view class="bottom-actions">
+            <u-button 
+                type="default" 
+                @click="saveDraft"
+                :loading="draftLoading"
+                style="margin-right: 20rpx;"
+            >
+                保存草稿
+            </u-button>
+            <u-button 
+                type="primary" 
+                @click="submitForm"
+                :loading="submitLoading"
+                style="flex: 1;"
+            >
+                更新走访记录
+            </u-button>
+        </view>
+
+        <!-- 日期选择器 -->
+        <u-datetime-picker
+            :show="showVisitDatePicker"
+            v-model="visitDateValue"
+            mode="date"
+            :maxDate="maxDate"
+            @confirm="handleVisitDateConfirm"
+            @cancel="showVisitDatePicker = false"
+        />
+
+        <!-- 加载遮罩 -->
+        <u-loading-page 
+            :loading="pageLoading" 
+            loading-text="加载中..."
+            loading-mode="circle"
+        />
+    </view>
+</template>
+
+<script>
+import { getVisit, updateVisit } from '@/api/visit.js';
+
+export default {
+    name: 'VisitEdit',
+    data() {
+        return {
+            // 页面参数
+            visitId: '',
+            
+            // 页面状态
+            pageLoading: true,
+            
+            // 走访信息
+            visitInfo: {},
+            
+            // 表单数据
+            form: {
+                visitId: '',
+                studentId: '',
+                internshipId: '',
+                companyName: '',
+                companyAddress: '',
+                visitDate: '',
+                visitorName: '',
+                visitorPosition: '',
+                visitPurpose: '',
+                companySituation: '',
+                studentPerformance: '',
+                problemsFound: '',
+                suggestions: '',
+                visitSummary: '',
+                visitImages: '',
+                remark: '',
+                status: ''
+            },
+            
+            // 表单验证规则
+            rules: {
+                visitDate: [
+                    { required: true, message: '请选择走访日期', trigger: ['blur', 'change'] }
+                ],
+                visitorName: [
+                    { required: true, message: '请输入走访人员姓名', trigger: ['blur', 'change'] }
+                ],
+                visitPurpose: [
+                    { required: true, message: '请输入走访目的', trigger: ['blur', 'change'] }
+                ]
+            },
+            
+            // 日期选择
+            showVisitDatePicker: false,
+            visitDateValue: Date.now(),
+            maxDate: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30天后
+            
+            // 图片上传
+            imageList: [],
+            maxImageCount: 10,
+            
+            // 加载状态
+            submitLoading: false,
+            draftLoading: false
+        };
+    },
+    
+    onLoad(options) {
+        if (options.id) {
+            this.visitId = options.id;
+            this.loadVisitDetail();
+        } else {
+            uni.showToast({ title: '参数错误', icon: 'none' });
+            uni.navigateBack();
+        }
+    },
+    
+    methods: {
+        // 加载走访记录详情
+        async loadVisitDetail() {
+            this.pageLoading = true;
+            try {
+                const response = await getVisit(this.visitId);
+                
+                if (response.code === 200 && response.data) {
+                    this.visitInfo = response.data;
+                    
+                    // 填充表单数据
+                    this.form = {
+                        visitId: response.data.visitId,
+                        studentId: response.data.studentId,
+                        internshipId: response.data.internshipId,
+                        companyName: response.data.companyName,
+                        companyAddress: response.data.companyAddress,
+                        visitDate: response.data.visitDate,
+                        visitorName: response.data.visitorName,
+                        visitorPosition: response.data.visitorPosition,
+                        visitPurpose: response.data.visitPurpose,
+                        companySituation: response.data.companySituation,
+                        studentPerformance: response.data.studentPerformance,
+                        problemsFound: response.data.problemsFound,
+                        suggestions: response.data.suggestions,
+                        visitSummary: response.data.visitSummary,
+                        visitImages: response.data.visitImages,
+                        remark: response.data.remark,
+                        status: response.data.status
+                    };
+                    
+                    // 处理图片数据
+                    if (response.data.visitImages) {
+                        try {
+                            this.imageList = JSON.parse(response.data.visitImages) || [];
+                        } catch (e) {
+                            console.warn('解析图片数据失败:', e);
+                            this.imageList = [];
+                        }
+                    }
+                    
+                    // 设置日期选择器的值
+                    if (response.data.visitDate) {
+                        this.visitDateValue = new Date(response.data.visitDate).getTime();
+                    }
+                    
+                } else {
+                    uni.showToast({ title: response.msg || '获取数据失败', icon: 'none' });
+                    uni.navigateBack();
+                }
+            } catch (error) {
+                console.error('加载走访记录详情失败:', error);
+                uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
+                uni.navigateBack();
+            } finally {
+                this.pageLoading = false;
+            }
+        },
+        
+        // 走访日期确认
+        handleVisitDateConfirm(value) {
+            const date = new Date(value);
+            this.form.visitDate = this.formatDate(date);
+            this.showVisitDatePicker = false;
+        },
+        
+        // 格式化日期
+        formatDate(date) {
+            const year = date.getFullYear();
+            const month = String(date.getMonth() + 1).padStart(2, '0');
+            const day = String(date.getDate()).padStart(2, '0');
+            return `${year}-${month}-${day}`;
+        },
+        
+        // 选择图片
+        chooseImage() {
+            const remainCount = this.maxImageCount - this.imageList.length;
+            uni.chooseImage({
+                count: remainCount,
+                sizeType: ['compressed'],
+                sourceType: ['album', 'camera'],
+                success: (res) => {
+                    this.handleImageUpload(res.tempFilePaths);
+                }
+            });
+        },
+        
+        // 处理图片上传
+        async handleImageUpload(filePaths) {
+            uni.showLoading({ title: '上传中...' });
+            
+            try {
+                for (const filePath of filePaths) {
+                    // 这里应该调用具体的文件上传接口
+                    // 暂时使用本地路径作为示例
+                    this.imageList.push({
+                        url: filePath,
+                        path: filePath
+                    });
+                }
+                
+                // 更新表单中的图片数据
+                this.form.visitImages = JSON.stringify(this.imageList);
+            } catch (error) {
+                console.error('上传图片失败:', error);
+                uni.showToast({ title: '上传图片失败', icon: 'none' });
+            } finally {
+                uni.hideLoading();
+            }
+        },
+        
+        // 预览图片
+        previewImage(index) {
+            const urls = this.imageList.map(item => item.url);
+            uni.previewImage({
+                urls,
+                current: index
+            });
+        },
+        
+        // 删除图片
+        removeImage(index) {
+            this.imageList.splice(index, 1);
+            this.form.visitImages = JSON.stringify(this.imageList);
+        },
+        
+        // 保存草稿
+        async saveDraft() {
+            this.draftLoading = true;
+            try {
+                const formData = {
+                    ...this.form,
+                    status: 'planned' // 草稿状态
+                };
+                
+                const response = await updateVisit(formData);
+                
+                if (response.code === 200) {
+                    uni.showToast({ title: '草稿保存成功', icon: 'success' });
+                    setTimeout(() => {
+                        uni.navigateBack();
+                    }, 1500);
+                } else {
+                    uni.showToast({ title: response.msg || '保存失败', icon: 'none' });
+                }
+            } catch (error) {
+                console.error('保存草稿失败:', error);
+                uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
+            } finally {
+                this.draftLoading = false;
+            }
+        },
+        
+        // 提交表单
+        async submitForm() {
+            // 表单验证
+            try {
+                await this.$refs.formRef.validate();
+            } catch (error) {
+                console.log('表单验证失败:', error);
+                return;
+            }
+            
+            this.submitLoading = true;
+            try {
+                const formData = {
+                    ...this.form,
+                    status: 'completed' // 已完成状态
+                };
+                
+                const response = await updateVisit(formData);
+                
+                if (response.code === 200) {
+                    uni.showToast({ title: '更新成功', icon: 'success' });
+                    setTimeout(() => {
+                        uni.navigateBack();
+                    }, 1500);
+                } else {
+                    uni.showToast({ title: response.msg || '更新失败', icon: 'none' });
+                }
+            } catch (error) {
+                console.error('更新走访记录失败:', error);
+                uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
+            } finally {
+                this.submitLoading = false;
+            }
+        }
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+.visit-edit-container {
+    min-height: 100vh;
+    background-color: #f8f8f8;
+    padding-bottom: 120rpx; // 为底部按钮留出空间
+}
+
+.form-section {
+    background: #ffffff;
+    margin-bottom: 20rpx;
+    padding: 0 20rpx;
+    
+    .section-title {
+        padding: 24rpx 0 16rpx;
+        font-size: 32rpx;
+        font-weight: 600;
+        color: #303133;
+        border-bottom: 1rpx solid #ebeef5;
+        margin-bottom: 20rpx;
+    }
+}
+
+.student-info-section {
+    background: #f8f9fa;
+    border-radius: 8rpx;
+    padding: 20rpx;
+    margin-top: 16rpx;
+    
+    .info-row {
+        display: flex;
+        margin-bottom: 12rpx;
+        
+        &:last-child {
+            margin-bottom: 0;
+        }
+        
+        .info-label {
+            font-size: 28rpx;
+            color: #606266;
+            width: 140rpx;
+        }
+        
+        .info-value {
+            font-size: 28rpx;
+            color: #303133;
+            flex: 1;
+        }
+    }
+}
+
+.upload-section {
+    .image-list {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 16rpx;
+        margin-bottom: 16rpx;
+        
+        .image-item {
+            position: relative;
+            width: 160rpx;
+            height: 160rpx;
+            border-radius: 8rpx;
+            overflow: hidden;
+            
+            image {
+                width: 100%;
+                height: 100%;
+            }
+            
+            .delete-btn {
+                position: absolute;
+                top: 8rpx;
+                right: 8rpx;
+                width: 32rpx;
+                height: 32rpx;
+                border-radius: 50%;
+                background: rgba(0, 0, 0, 0.6);
+                display: flex;
+                align-items: center;
+                justify-content: center;
+            }
+        }
+        
+        .upload-btn {
+            width: 160rpx;
+            height: 160rpx;
+            border: 2rpx dashed #c8c9cc;
+            border-radius: 8rpx;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            
+            .upload-text {
+                font-size: 24rpx;
+                color: #c8c9cc;
+                margin-top: 8rpx;
+            }
+        }
+    }
+    
+    .upload-tip {
+        font-size: 24rpx;
+        color: #909399;
+    }
+}
+
+.bottom-actions {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    background: #ffffff;
+    padding: 20rpx;
+    border-top: 1rpx solid #ebeef5;
+    display: flex;
+    align-items: center;
+    z-index: 100;
+}
+</style> 

+ 590 - 0
mobile/src/pages/visit/index.vue

@@ -0,0 +1,590 @@
+<template>
+    <view class="visit-container">
+        <!-- 搜索栏 -->
+        <view class="search-bar">
+            <u-search 
+                v-model="searchKeyword" 
+                placeholder="搜索学生姓名、学号或企业"
+                :show-action="false"
+                @search="handleSearch"
+                @custom="handleSearch"
+                bg-color="#f6f6f6"
+                height="72"
+                shape="round"
+            />
+        </view>
+
+        <!-- 筛选条件 -->
+        <view class="filter-bar" v-if="showFilter">
+            <view class="filter-item">
+                <u-input
+                    v-model="queryParams.visitorName"
+                    placeholder="走访人姓名"
+                    border="surround"
+                    clearable
+                />
+            </view>
+            <view class="filter-item">
+                <view @click="showStatusPicker = true">
+                    <u-input
+                        v-model="statusText"
+                        placeholder="选择走访状态"
+                        readonly
+                        border="surround"
+                        suffix-icon="arrow-down"
+                    />
+                </view>
+            </view>
+            <view class="filter-item">
+                <view @click="showDatePicker = true">
+                    <u-input
+                        v-model="dateRangeText"
+                        placeholder="选择走访日期"
+                        readonly
+                        border="surround"
+                        suffix-icon="calendar"
+                    />
+                </view>
+            </view>
+            <view class="filter-actions">
+                <u-button 
+                    type="primary" 
+                    size="small" 
+                    @click="handleFilter"
+                    :loading="loading"
+                >
+                    筛选
+                </u-button>
+                <u-button 
+                    type="default" 
+                    size="small" 
+                    @click="resetFilter"
+                    style="margin-left: 10rpx;"
+                >
+                    重置
+                </u-button>
+            </view>
+        </view>
+
+        <!-- 筛选按钮 -->
+        <view class="action-bar">
+            <u-button 
+                type="default" 
+                size="small" 
+                :icon="showFilter ? 'arrow-up' : 'arrow-down'"
+                @click="showFilter = !showFilter"
+            >
+                {{ showFilter ? '收起筛选' : '展开筛选' }}
+            </u-button>
+            <u-button 
+                type="primary" 
+                size="small" 
+                icon="plus"
+                @click="handleAdd"
+            >
+                新增走访
+            </u-button>
+        </view>
+
+        <!-- 走访记录列表 -->
+        <view class="list-container">
+            <view class="list-content" v-if="visitList.length > 0">
+                <view 
+                    class="visit-card" 
+                    v-for="item in visitList" 
+                    :key="item.visitId"
+                    @click="handleView(item)"
+                >
+                    <!-- 学生信息 -->
+                    <view class="card-header">
+                        <view class="student-info">
+                            <text class="student-name">{{ item.studentName }}</text>
+                            <text class="student-no">{{ item.studentNo }}</text>
+                        </view>
+                        <view class="status-badge">
+                            <u-tag 
+                                :text="item.statusName || item.status" 
+                                :type="getStatusType(item.status)"
+                                size="mini"
+                            />
+                        </view>
+                    </view>
+                    
+                    <!-- 企业信息 -->
+                    <view class="card-row">
+                        <u-icon name="home" size="16" color="#909399" />
+                        <text class="info-text">{{ item.companyName || '-' }}</text>
+                    </view>
+                    
+                    <!-- 走访信息 -->
+                    <view class="card-row">
+                        <u-icon name="calendar" size="16" color="#909399" />
+                        <text class="info-text">{{ item.visitDate || '-' }}</text>
+                        <u-icon name="account" size="16" color="#909399" style="margin-left: 20rpx;" />
+                        <text class="info-text">{{ item.visitorName || '-' }}</text>
+                    </view>
+                    
+                    <!-- 班级专业信息 -->
+                    <view class="card-row">
+                        <u-icon name="bookmark" size="16" color="#909399" />
+                        <text class="info-text">{{ item.className || '-' }} / {{ item.majorName || '-' }}</text>
+                    </view>
+                    
+                    <!-- 操作按钮 -->
+                    <view class="card-actions">
+                        <u-button 
+                            type="default" 
+                            size="mini" 
+                            @click.stop="handleEdit(item)"
+                            v-if="item.status === 'planned' || item.status === '0'"
+                        >
+                            编辑
+                        </u-button>
+                        <u-button 
+                            type="error" 
+                            size="mini" 
+                            @click.stop="handleDelete(item)"
+                            style="margin-left: 10rpx;"
+                            v-if="item.status === 'planned' || item.status === '0'"
+                        >
+                            删除
+                        </u-button>
+                    </view>
+                </view>
+            </view>
+            
+            <!-- 空状态 -->
+            <view class="empty-state" v-else-if="!loading">
+                <u-empty 
+                    mode="data" 
+                    text="暂无走访记录"
+                    textColor="#c8c9cc"
+                    iconColor="#c8c9cc"
+                />
+            </view>
+            
+            <!-- 加载状态 -->
+            <view class="loading-state" v-if="loading">
+                <u-loading-icon mode="circle" />
+                <text style="margin-left: 10rpx; color: #909399;">加载中...</text>
+            </view>
+        </view>
+
+        <!-- 上拉加载更多 -->
+        <view class="load-more" v-if="hasMore && visitList.length > 0">
+            <u-loadmore 
+                :status="loadMoreStatus" 
+                @loadmore="loadMore"
+                load-text="加载更多"
+                loading-text="正在加载..."
+                no-more-text="没有更多了"
+            />
+        </view>
+
+        <!-- 状态选择器 -->
+        <u-picker
+            :show="showStatusPicker"
+            :columns="statusOptions"
+            :default-index="[currentStatusIndex]"
+            @confirm="handleStatusConfirm"
+            @cancel="showStatusPicker = false"
+        />
+
+        <!-- 日期范围选择器 -->
+        <u-calendar
+            :show="showDatePicker"
+            mode="range"
+            @confirm="handleDateConfirm"
+            @close="showDatePicker = false"
+        />
+    </view>
+</template>
+
+<script>
+import { listVisit, delVisit, getVisitStatusDict } from '@/api/visit.js';
+
+export default {
+    name: 'VisitList',
+    data() {
+        return {
+            // 搜索和筛选
+            searchKeyword: '',
+            showFilter: false,
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                studentKeyword: '',
+                companyName: '',
+                visitorName: '',
+                status: '',
+                beginVisitDate: '',
+                endVisitDate: ''
+            },
+            
+            // 列表数据
+            visitList: [],
+            total: 0,
+            loading: false,
+            hasMore: true,
+            loadMoreStatus: 'loadmore',
+            
+            // 状态选择
+            showStatusPicker: false,
+            statusOptions: [],
+            currentStatusIndex: -1,
+            statusText: '',
+            statusDict: [],
+            
+            // 日期选择
+            showDatePicker: false,
+            dateRangeText: '',
+            selectedDateRange: []
+        };
+    },
+    
+    onLoad() {
+        this.initPage();
+    },
+    
+    onPullDownRefresh() {
+        this.refreshList();
+    },
+    
+    onReachBottom() {
+        if (this.hasMore && this.loadMoreStatus === 'loadmore') {
+            this.loadMore();
+        }
+    },
+    
+    methods: {
+        // 初始化页面
+        async initPage() {
+            await this.loadStatusDict();
+            this.getList();
+        },
+        
+        // 加载状态字典
+        async loadStatusDict() {
+            try {
+                const response = await getVisitStatusDict();
+                if (response.code === 200 && response.data) {
+                    this.statusDict = response.data;
+                    this.statusOptions = [
+                        [
+                            { text: '全部状态', value: '' },
+                            ...response.data.map(item => ({
+                                text: item.dictLabel,
+                                value: item.dictValue
+                            }))
+                        ]
+                    ];
+                }
+            } catch (error) {
+                console.error('加载状态字典失败:', error);
+                // 设置默认状态选项
+                this.statusOptions = [[
+                    { text: '全部状态', value: '' },
+                    { text: '计划中', value: 'planned' },
+                    { text: '已完成', value: 'completed' },
+                    { text: '已取消', value: 'cancelled' }
+                ]];
+            }
+        },
+        
+        // 获取列表数据
+        async getList(isLoadMore = false) {
+            if (this.loading) return;
+            
+            this.loading = true;
+            if (!isLoadMore) {
+                this.loadMoreStatus = 'loadmore';
+            } else {
+                this.loadMoreStatus = 'loading';
+            }
+            
+            try {
+                const response = await listVisit(this.queryParams);
+                
+                if (response.code === 200) {
+                    // 后端返回的是TableDataInfo格式,直接包含rows和total字段
+                    const { rows, total } = response;
+                    
+                    if (isLoadMore) {
+                        this.visitList = [...this.visitList, ...rows || []];
+                    } else {
+                        this.visitList = rows || [];
+                    }
+                    
+                    this.total = total || 0;
+                    
+                    // 判断是否还有更多数据
+                    const currentTotal = this.visitList.length;
+                    this.hasMore = currentTotal < this.total;
+                    
+                    if (!this.hasMore) {
+                        this.loadMoreStatus = 'nomore';
+                    }
+                } else {
+                    uni.showToast({ title: response.msg || '获取数据失败', icon: 'none' });
+                }
+            } catch (error) {
+                console.error('获取走访记录列表失败:', error);
+                uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
+            } finally {
+                this.loading = false;
+                if (this.queryParams.pageNum === 1) {
+                    uni.stopPullDownRefresh();
+                }
+            }
+        },
+        
+        // 刷新列表
+        refreshList() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        
+        // 加载更多
+        loadMore() {
+            if (this.hasMore && !this.loading) {
+                this.queryParams.pageNum++;
+                this.getList(true);
+            }
+        },
+        
+        // 搜索
+        handleSearch() {
+            this.queryParams.studentKeyword = this.searchKeyword;
+            this.refreshList();
+        },
+        
+        // 筛选
+        handleFilter() {
+            this.refreshList();
+        },
+        
+        // 重置筛选
+        resetFilter() {
+            this.queryParams = {
+                ...this.queryParams,
+                studentKeyword: this.searchKeyword,
+                companyName: '',
+                visitorName: '',
+                status: '',
+                beginVisitDate: '',
+                endVisitDate: ''
+            };
+            this.statusText = '';
+            this.dateRangeText = '';
+            this.selectedDateRange = [];
+            this.currentStatusIndex = -1;
+            this.refreshList();
+        },
+        
+        // 状态选择确认
+        handleStatusConfirm(value) {
+            const selected = value.value[0];
+            this.queryParams.status = selected.value;
+            this.statusText = selected.text;
+            this.currentStatusIndex = value.index[0];
+            this.showStatusPicker = false;
+        },
+        
+        // 日期选择确认
+        handleDateConfirm(value) {
+            this.selectedDateRange = value;
+            if (value && value.length === 2) {
+                this.queryParams.beginVisitDate = value[0];
+                this.queryParams.endVisitDate = value[1];
+                this.dateRangeText = `${value[0]} ~ ${value[1]}`;
+            } else {
+                this.queryParams.beginVisitDate = '';
+                this.queryParams.endVisitDate = '';
+                this.dateRangeText = '';
+            }
+            this.showDatePicker = false;
+        },
+        
+        // 获取状态类型
+        getStatusType(status) {
+            switch (status) {
+                case 'planned':
+                case '0':
+                    return 'primary';
+                case 'completed':
+                case '1':
+                    return 'success';
+                case 'cancelled':
+                case '2':
+                    return 'error';
+                default:
+                    return 'default';
+            }
+        },
+        
+        // 查看详情
+        handleView(item) {
+            uni.navigateTo({
+                url: `/pages/visit/detail?id=${item.visitId}`
+            });
+        },
+        
+        // 新增走访
+        handleAdd() {
+            uni.navigateTo({
+                url: '/pages/visit/add'
+            });
+        },
+        
+        // 编辑走访
+        handleEdit(item) {
+            uni.navigateTo({
+                url: `/pages/visit/edit?id=${item.visitId}`
+            });
+        },
+        
+        // 删除走访
+        handleDelete(item) {
+            uni.showModal({
+                title: '确认删除',
+                content: `确定要删除${item.studentName}的走访记录吗?`,
+                success: async (res) => {
+                    if (res.confirm) {
+                        await this.deleteVisit(item.visitId);
+                    }
+                }
+            });
+        },
+        
+        // 执行删除
+        async deleteVisit(id) {
+            try {
+                uni.showLoading({ title: '删除中...' });
+                const response = await delVisit(id);
+                
+                if (response.code === 200) {
+                    uni.showToast({ title: '删除成功', icon: 'success' });
+                    this.refreshList();
+                } else {
+                    uni.showToast({ title: response.msg || '删除失败', icon: 'none' });
+                }
+            } catch (error) {
+                console.error('删除走访记录失败:', error);
+                uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
+            } finally {
+                uni.hideLoading();
+            }
+        }
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+.visit-container {
+    min-height: 100vh;
+    background-color: #f8f8f8;
+}
+
+.search-bar {
+    padding: 20rpx;
+    background: #ffffff;
+}
+
+.filter-bar {
+    background: #ffffff;
+    padding: 0 20rpx 20rpx;
+    border-bottom: 1rpx solid #ebeef5;
+    
+    .filter-item {
+        margin-bottom: 20rpx;
+    }
+    
+    .filter-actions {
+        display: flex;
+        justify-content: flex-end;
+        gap: 10rpx;
+    }
+}
+
+.action-bar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 20rpx;
+    background: #ffffff;
+    border-bottom: 1rpx solid #ebeef5;
+}
+
+.list-container {
+    padding: 20rpx;
+}
+
+.visit-card {
+    background: #ffffff;
+    border-radius: 12rpx;
+    padding: 24rpx;
+    margin-bottom: 20rpx;
+    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
+    
+    .card-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 16rpx;
+        
+        .student-info {
+            .student-name {
+                font-size: 32rpx;
+                font-weight: 600;
+                color: #303133;
+                margin-right: 16rpx;
+            }
+            
+            .student-no {
+                font-size: 26rpx;
+                color: #909399;
+            }
+        }
+    }
+    
+    .card-row {
+        display: flex;
+        align-items: center;
+        margin-bottom: 12rpx;
+        
+        .info-text {
+            font-size: 28rpx;
+            color: #606266;
+            margin-left: 8rpx;
+        }
+        
+        &:last-of-type {
+            margin-bottom: 16rpx;
+        }
+    }
+    
+    .card-actions {
+        display: flex;
+        justify-content: flex-end;
+        gap: 10rpx;
+        padding-top: 16rpx;
+        border-top: 1rpx solid #ebeef5;
+    }
+}
+
+.empty-state {
+    padding: 80rpx 20rpx;
+    text-align: center;
+}
+
+.loading-state {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 40rpx;
+}
+
+.load-more {
+    padding: 20rpx;
+}
+</style>