fix: 显式声明接口参数名并优化移动端若干页面

生产环境未保留方法参数名时 Spring 无法绑定 @RequestParam/@PathVariable,补齐注解名称并在 Maven 编译中强制 -parameters;同时优化走访、勤工助学、请假和宿舍点名页交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-18 11:48:08 +08:00
1 parent a15efa8ef8
commit 1d67506451
187 files changed
+1769 -1295

No files matched your search

+4 -2
View File
@@ -8,13 +8,15 @@ NODE_ENV=production
VUE_APP_API_BASE_URL=/prod-api
# 如果生产环境需要直接访问完整URL,请修改为:
# VUE_APP_API_BASE_URL=https://api.example.com
VUE_APP_DEV_SERVER_URL=https://m.hzdyjsxy.cn:18080
#VUE_APP_DEV_SERVER_URL=https://m.hzdyjsxy.cn:18080
VUE_APP_DEV_SERVER_URL=http://m.hzdyjsxy.cn:18080
VUE_APP_REQUEST_TIMEOUT=300000
VUE_APP_REPEAT_SUBMIT_INTERVAL=500
# 前端域名配置
# 🚨 生产环境部署前必须修改此地址!
VUE_APP_FRONTEND_URL=https://m.hzdyjsxy.cn:18080/login?redirect=/index
#VUE_APP_FRONTEND_URL=https://m.hzdyjsxy.cn:18080/login?redirect=/index
VUE_APP_FRONTEND_URL=http://m.hzdyjsxy.cn:18080/login?redirect=/index
# 加密配置
VUE_APP_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
+214 -113
View File
@@ -9,15 +9,29 @@
<!-- 搜索栏 -->
<view class="search-bar">
<u-search
v-model="searchKeyword"
placeholder="搜索学生姓名、学号或企业"
:show-action="false"
@search="handleSearch"
@custom="handleSearch"
bg-color="#f6f6f6"
shape="round"
/>
<view class="search-row">
<view class="search-input-wrap">
<u-search
v-model="searchKeyword"
placeholder="搜索学生姓名、学号或企业"
:show-action="false"
@search="handleSearch"
@custom="handleSearch"
bg-color="#f6f6f6"
shape="round"
/>
</view>
<view
class="filter-toggle"
@click="showFilter = !showFilter"
>
<u-icon
:name="showFilter ? 'arrow-up' : 'list'"
size="16"
color="#606266"
/>
</view>
</view>
</view>
<!-- 筛选条件 -->
@@ -26,8 +40,12 @@
<u-input
v-model="queryParams.visitorName"
placeholder="走访人姓名"
border="surround"
placeholder-style="color: #c0c4cc; font-size: 14px"
font-size="14px"
shape="circle"
border="none"
clearable
:custom-style="filterInputStyle"
/>
</view>
<view class="filter-item">
@@ -35,9 +53,13 @@
<u-input
v-model="statusText"
placeholder="选择走访状态"
placeholder-style="color: #c0c4cc; font-size: 14px"
font-size="14px"
shape="circle"
readonly
border="surround"
border="none"
suffix-icon="arrow-down"
:custom-style="filterInputStyle"
/>
</view>
</view>
@@ -46,9 +68,13 @@
<u-input
v-model="dateRangeText"
placeholder="选择走访日期"
placeholder-style="color: #c0c4cc; font-size: 14px"
font-size="14px"
shape="circle"
readonly
border="surround"
border="none"
suffix-icon="calendar"
:custom-style="filterInputStyle"
/>
</view>
</view>
@@ -74,22 +100,6 @@
</view>
</view>
<!-- 筛选按钮 -->
<view class="action-bar">
<u-button
type="info"
shape="round"
size="small"
:icon="showFilter ? 'arrow-up' : 'arrow-down'"
@click="showFilter = !showFilter"
>
{{ showFilter ? '收起筛选' : '展开筛选' }}
</u-button>
<u-button type="primary" shape="round" size="small" icon="plus" @click="handleAdd">
新增走访
</u-button>
</view>
<!-- 加载状态 -->
<u-loading-page
:loading="loading && visitList.length === 0"
@@ -109,46 +119,52 @@
: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 class="company-title">
<view class="company-icon">
<u-icon name="home" size="18" color="#3c9cff" />
</view>
<text class="company-name">{{ item.companyName || '未填写企业' }}</text>
</view>
<view class="status-badge">
<u-tag
:text="getStatusLabel(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"
<u-tag
:text="getStatusLabel(item.status)"
:type="getStatusType(item.status)"
size="mini"
shape="circle"
/>
<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 class="card-body">
<view class="info-row">
<u-icon name="account" size="16" color="#909399" />
<text class="info-label">学生</text>
<text class="info-value">
{{ item.studentName || '-' }}
<text class="info-sub" v-if="item.studentNo">
({{ item.studentNo }})
</text>
</text>
</view>
<view class="info-row">
<u-icon name="calendar" size="16" color="#909399" />
<text class="info-label">日期</text>
<text class="info-value">{{ item.visitDate || '-' }}</text>
</view>
<view class="info-row">
<u-icon name="account-fill" size="16" color="#909399" />
<text class="info-label">走访人</text>
<text class="info-value">{{ item.visitorName || '-' }}</text>
</view>
<view class="info-row">
<u-icon name="bookmark" size="16" color="#909399" />
<text class="info-label">班级</text>
<text class="info-value info-value--ellipsis">
{{ item.className || '-' }}
<text v-if="item.majorName"> / {{ item.majorName }}</text>
</text>
</view>
</view>
<!-- 操作按钮 -->
@@ -215,9 +231,11 @@
<u-picker
:show="showStatusPicker"
:columns="statusOptions"
:default-index="[currentStatusIndex]"
keyName="text"
:default-index="currentStatusIndex >= 0 ? [currentStatusIndex] : [0]"
@confirm="handleStatusConfirm"
@cancel="showStatusPicker = false"
@close="showStatusPicker = false"
/>
<!-- 日期范围选择器 -->
@@ -227,6 +245,11 @@
@confirm="handleDateConfirm"
@close="showDatePicker = false"
/>
<!-- 悬浮新增按钮 -->
<view class="fab-button" @click="handleAdd">
<u-icon name="plus" size="24" color="#ffffff" />
</view>
</view>
</template>
@@ -269,6 +292,14 @@ export default {
showDatePicker: false,
dateRangeText: '',
selectedDateRange: [],
// 筛选输入框样式(与搜索框高度一致:u-search 默认 32px)
filterInputStyle: {
backgroundColor: '#f6f6f6',
padding: '0 24rpx',
height: '32px',
boxSizing: 'border-box',
},
};
},
@@ -419,11 +450,14 @@ export default {
},
// 状态选择确认
handleStatusConfirm(value) {
const selected = value.value[0];
this.queryParams.status = selected.value;
this.statusText = selected.text;
this.currentStatusIndex = value.index[0];
handleStatusConfirm(e) {
const index = e.indexs[0];
const selected = this.statusOptions[0][index];
if (selected) {
this.queryParams.status = selected.value;
this.statusText = selected.text;
this.currentStatusIndex = index;
}
this.showStatusPicker = false;
},
@@ -551,12 +585,37 @@ export default {
.search-bar {
padding: 20rpx;
background: #ffffff;
.search-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.search-input-wrap {
flex: 1;
min-width: 0;
}
.filter-toggle {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #f6f6f6;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&:active {
opacity: 0.8;
}
}
}
.filter-bar {
background: #ffffff;
padding: 0 20rpx 20rpx;
border-bottom: 1rpx solid #ebeef5;
.filter-item {
margin-bottom: 20rpx;
@@ -569,17 +628,30 @@ export default {
}
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
background: #ffffff;
border-bottom: 1rpx solid #ebeef5;
}
.content-wrapper {
padding: 20rpx;
padding-bottom: 160rpx;
}
.fab-button {
position: fixed;
right: 20px;
bottom: 30px;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #007aff, #0051d9);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.3);
transition: all 0.3s ease;
z-index: 999;
&:active {
transform: scale(0.95);
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.4);
}
}
.empty-container {
@@ -590,66 +662,95 @@ export default {
.list-content {
.visit-card {
background: #ffffff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border-radius: 16rpx;
padding: 0;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(60, 156, 255, 0.08);
overflow: hidden;
transition: all 0.2s ease;
&:active {
transform: scale(0.99);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
transform: scale(0.985);
box-shadow: 0 2rpx 10rpx rgba(60, 156, 255, 0.06);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
align-items: center;
padding: 24rpx 24rpx 20rpx;
background: linear-gradient(135deg, #f5f9ff 0%, #ffffff 100%);
border-bottom: 1rpx solid #f0f3f8;
.student-info {
.company-title {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
margin-right: 16rpx;
.student-name {
.company-icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 12rpx;
}
.company-name {
font-size: 32rpx;
font-weight: 600;
color: #303133;
margin-right: 16rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.student-no {
font-size: 26rpx;
color: #909399;
}
}
.status-badge {
flex-shrink: 0;
}
}
.card-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.card-body {
padding: 20rpx 24rpx 8rpx;
.info-text {
font-size: 28rpx;
color: #606266;
margin-left: 8rpx;
}
&:last-of-type {
.info-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.info-label {
width: 80rpx;
flex-shrink: 0;
font-size: 24rpx;
color: #909399;
margin-left: 10rpx;
}
.info-value {
flex: 1;
min-width: 0;
font-size: 28rpx;
color: #606266;
.info-sub {
font-size: 24rpx;
color: #909399;
}
&--ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.card-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10rpx;
padding-top: 16rpx;
border-top: 1rpx solid #ebeef5;
padding: 16rpx 24rpx 20rpx;
border-top: 1rpx solid #f0f3f8;
}
}
}
+93 -52
View File
@@ -8,16 +8,38 @@
</u-navbar>
<!-- 搜索栏 -->
<view class="search-container">
<u-search
v-model="searchKeyword"
placeholder="搜索岗位名称或联系老师"
:show-action="false"
@search="handleSearch"
@custom="handleSearch"
bg-color="#f5f6fa"
shape="round"
/>
<view class="search-bar">
<view class="search-row">
<view class="search-input-wrap">
<u-search
v-model="searchKeyword"
placeholder="搜索岗位名称或联系老师"
:show-action="false"
@search="handleSearch"
@custom="handleSearch"
bg-color="#f6f6f6"
shape="round"
/>
</view>
<view
v-if="currentTab === 0"
class="filter-toggle"
@click="showFilter = !showFilter"
>
<u-icon
:name="showFilter ? 'arrow-up' : 'list'"
size="16"
color="#606266"
/>
</view>
<view
v-if="currentTab === 0"
class="filter-toggle"
@click="refreshData"
>
<u-icon name="reload" size="16" color="#606266" />
</view>
</view>
</view>
<!-- 标签页 -->
@@ -43,9 +65,13 @@
<u-input
v-model="scopeText"
placeholder="选择发布范围"
placeholder-style="color: #c0c4cc; font-size: 14px"
font-size="14px"
shape="circle"
readonly
border="surround"
border="none"
suffix-icon="arrow-down"
:custom-style="filterInputStyle"
/>
</view>
</view>
@@ -54,18 +80,29 @@
<u-input
v-model="statusText"
placeholder="选择岗位状态"
placeholder-style="color: #c0c4cc; font-size: 14px"
font-size="14px"
shape="circle"
readonly
border="surround"
border="none"
suffix-icon="arrow-down"
:custom-style="filterInputStyle"
/>
</view>
</view>
<view class="filter-actions">
<u-button type="primary" size="small" @click="handleFilter" :loading="loading">
<u-button
type="primary"
shape="round"
size="small"
@click="handleFilter"
:loading="loading"
>
筛选
</u-button>
<u-button
type="default"
type="info"
shape="round"
size="small"
@click="resetFilter"
style="margin-left: 10rpx"
@@ -75,27 +112,6 @@
</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="reload"
@click="refreshData"
:loading="refreshing"
>
刷新
</u-button>
</view>
<!-- 岗位卡片列表 -->
<view class="list-content" v-if="positionList.length > 0">
<view
@@ -273,6 +289,7 @@
keyName="label"
@confirm="onScopeConfirm"
@cancel="showScopePicker = false"
@close="showScopePicker = false"
/>
<!-- 状态选择器 -->
@@ -282,6 +299,7 @@
keyName="label"
@confirm="onStatusConfirm"
@cancel="showStatusPicker = false"
@close="showStatusPicker = false"
/>
<!-- 加载状态 -->
@@ -316,6 +334,13 @@ export default {
showStatusPicker: false,
scopeText: '',
statusText: '',
// 筛选输入框样式(与搜索框高度一致)
filterInputStyle: {
backgroundColor: '#f6f6f6',
padding: '0 24rpx',
height: '32px',
boxSizing: 'border-box',
},
// 选择器选项(字典)
scopeOptions: [[]],
@@ -919,24 +944,48 @@ export default {
<style scoped>
.work-study-container {
min-height: 100vh;
background-color: #f5f6fa;
background-color: #f8f8f8;
}
.search-container {
.search-bar {
padding: 20rpx;
background-color: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
background: #ffffff;
}
.search-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.search-input-wrap {
flex: 1;
min-width: 0;
}
.filter-toggle {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #f6f6f6;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.filter-toggle:active {
opacity: 0.8;
}
.tabs-container {
background-color: #fff;
border-bottom: 1rpx solid #eee;
border-bottom: 1rpx solid #ebeef5;
}
.filter-bar {
background-color: #fff;
padding: 20rpx;
border-bottom: 1rpx solid #eee;
background: #ffffff;
padding: 20rpx 20rpx 20rpx;
}
.filter-item {
@@ -946,15 +995,7 @@ export default {
.filter-actions {
display: flex;
justify-content: flex-end;
margin-top: 20rpx;
}
.action-bar {
display: flex;
justify-content: space-between;
padding: 20rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
gap: 10rpx;
}
.list-container {
@@ -190,36 +190,39 @@
@cancel="showCancelTimePicker = false"
></u-datetime-picker>
<u-form-item label="销假地点" prop="cancelLocation" required>
<view class="location-row">
<u-button
shape="round"
type="info"
size="mini"
plain
@click="handleGetLocation"
>
<u-icon name="map-fill" size="20" color="#2979ff" />
<text style="margin-left: 8rpx">
{{ isWechatBrowser ? '微信定位' : '定位' }}
</text>
</u-button>
</view>
</u-form-item>
<u-form-item label="坐标">
<view v-if="cancelForm.cancelLocation" class="location-info">
<view class="location-coords-container">
<text class="location-coords">
{{ cancelForm.cancelLocation }}
</text>
<!-- 生产环境公众号 H5(HTTP+自定义端口)定位不可用,临时隐藏;恢复时将 enableLocationFeature 设为 true -->
<template v-if="enableLocationFeature">
<u-form-item label="销假地点" prop="cancelLocation" required>
<view class="location-row">
<u-button
shape="round"
type="info"
size="mini"
plain
@click="handleGetLocation"
>
<u-icon name="map-fill" size="20" color="#2979ff" />
<text style="margin-left: 8rpx">
{{ isWechatBrowser ? '微信定位' : '定位' }}
</text>
</u-button>
</view>
<view class="location-action-container">
<view class="location-icon-btn" @click="openLocationInMap">
<u-icon name="map-fill" size="23" color="#2979ff" />
</u-form-item>
<u-form-item label="坐标">
<view v-if="cancelForm.cancelLocation" class="location-info">
<view class="location-coords-container">
<text class="location-coords">
{{ cancelForm.cancelLocation }}
</text>
</view>
<view class="location-action-container">
<view class="location-icon-btn" @click="openLocationInMap">
<u-icon name="map-fill" size="23" color="#2979ff" />
</view>
</view>
</view>
</view>
</u-form-item>
</u-form-item>
</template>
<u-form-item label="销假备注" prop="cancelRemark">
<u-textarea
@@ -555,7 +558,9 @@ export default {
isWechatBrowser: false,
wechatInitializing: false,
// 定位
// 微信定位相关
// 临时关闭定位:公众号 H5 在 HTTP+自定义端口下无法可靠定位;恢复定位时改为 true
enableLocationFeature: false,
geoLatitude: null,
geoLongitude: null,
@@ -626,6 +631,15 @@ export default {
leaveNonImageFiles() {
return this.leaveAttachmentList.filter((file) => !this.isImageFile(file));
},
rules() {
const base = {
cancelTime: [{ required: true, message: '请选择销假时间' }],
};
if (this.enableLocationFeature) {
base.cancelLocation = [{ required: true, message: '请输入销假地点' }];
}
return base;
},
},
async onLoad(options) {
this.routeParams = options || {};
@@ -836,10 +850,6 @@ export default {
},
// ============== 校验与时间处理 ==============
rules: {
cancelTime: [{ required: true, message: '请选择销假时间' }],
cancelLocation: [{ required: true, message: '请输入销假地点' }],
},
handleCancelTimeClick() {
this.showCancelTimePicker = true;
},
@@ -1449,7 +1459,8 @@ export default {
uni.showToast({ title: '销假时间不能为空', icon: 'none' });
return;
}
if (!this.cancelForm.cancelLocation) {
// 定位功能临时关闭时跳过,允许无坐标保存
if (this.enableLocationFeature && !this.cancelForm.cancelLocation) {
uni.showToast({ title: '销假地点不能为空', icon: 'none' });
return;
}
@@ -1498,14 +1509,17 @@ export default {
return;
}
}
if (needLocationOnCancel) {
if (!this.cancelForm.cancelLocation) {
uni.showToast({
title: '该请假类型要求提供销假定位',
icon: 'none',
});
return;
}
// 定位功能临时关闭时跳过销假定位校验
if (
this.enableLocationFeature &&
needLocationOnCancel &&
!this.cancelForm.cancelLocation
) {
uni.showToast({
title: '该请假类型要求提供销假定位',
icon: 'none',
});
return;
}
}
}
+22 -17
View File
@@ -100,14 +100,11 @@
</view>
<!-- 卡片头部 -->
<view class="card-header">
<view class="leave-info">
<view class="header-top">
<view class="student-info">
<text class="student-name">{{ item.studentName || '-' }}</text>
<text class="student-no">{{ item.studentNo || '-' }}</text>
</view>
<view class="leave-type">{{ item.leaveTypeName || '-' }}</view>
</view>
<view class="right-section">
<view class="status-section">
<u-tag
:text="
@@ -117,6 +114,9 @@
size="mini"
></u-tag>
</view>
</view>
<view class="header-bottom">
<view class="leave-type">{{ item.leaveTypeName || '-' }}</view>
<view class="apply-time">
申请时间:{{ formatTime(item.createTime) }}
</view>
@@ -969,23 +969,33 @@ export default {
// 卡片头部
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-direction: column;
gap: 8rpx;
padding: 24rpx;
padding-left: 80rpx; // 为序号预留左边空间
border-bottom: 1px solid #f0f0f0;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
}
.leave-info {
flex: 1;
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-bottom {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16rpx;
}
.student-info {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 8rpx;
flex: 1;
min-width: 0;
}
.student-name {
@@ -1006,25 +1016,20 @@ export default {
font-size: 28rpx;
color: #606266;
font-weight: 500;
margin-top: 8rpx;
}
.right-section {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
flex-shrink: 0;
}
.apply-time {
font-size: 24rpx;
color: #999999;
text-align: right;
flex-shrink: 0;
}
.status-section {
display: flex;
justify-content: flex-end;
flex-shrink: 0;
}
// 卡片内容
@@ -175,45 +175,48 @@
/>
</u-form-item>
<u-form-item label="请假地点">
<view class="location-row">
<!-- 微信定位按钮单独一行 -->
<u-button
type="info"
shape="round"
size="mini"
plain
@click="handleGetLocation"
>
<u-icon name="map-fill" size="20" color="#2979ff" />
<text style="margin-left: 8rpx">
{{ isWechatBrowser ? '微信定位' : '定位' }}
</text>
</u-button>
</view>
</u-form-item>
<u-form-item label="坐标">
<view v-if="form.leaveLocation" class="location-info">
<!-- 坐标和查看位置按钮在同一行 -->
<view class="location-coords-container">
<text class="location-coords">
经纬度: {{ form.leaveLocation || '未获取' }}
</text>
</view>
<view class="location-action-container">
<!-- 生产环境公众号 H5(HTTP+自定义端口)定位不可用,临时隐藏;恢复时将 enableLocationFeature 设为 true -->
<template v-if="enableLocationFeature">
<u-form-item label="请假地点">
<view class="location-row">
<!-- 微信定位按钮单独一行 -->
<u-button
type="info"
shape="round"
size="mini"
type="info"
plain
@click="openLocationInMap"
@click="handleGetLocation"
>
<!-- <u-icon name="eye" size="16" color="#0051d9" /> -->
<text>查看位置</text>
<u-icon name="map-fill" size="20" color="#2979ff" />
<text style="margin-left: 8rpx">
{{ isWechatBrowser ? '微信定位' : '定位' }}
</text>
</u-button>
</view>
</view>
</u-form-item>
</u-form-item>
<u-form-item label="坐标">
<view v-if="form.leaveLocation" class="location-info">
<!-- 坐标和查看位置按钮在同一行 -->
<view class="location-coords-container">
<text class="location-coords">
经纬度: {{ form.leaveLocation || '未获取' }}
</text>
</view>
<view class="location-action-container">
<u-button
shape="round"
size="mini"
type="info"
plain
@click="openLocationInMap"
>
<!-- <u-icon name="eye" size="16" color="#0051d9" /> -->
<text>查看位置</text>
</u-button>
</view>
</view>
</u-form-item>
</template>
</view>
<!-- 附件上传 -->
@@ -624,9 +627,12 @@ export default {
taskVariables: {},
startTimeValue: new Date().getTime(), // 开始时间选择器的值(默认当前时间)
endTimeValue: new Date().getTime(), // 结束时间选择器的值(默认当前时间)
minDate: '', // 时间选择器的最小日期
maxDate: '', // 时间选择器的最大日期
// u-datetime-picker 的 minDate/maxDate 必须为 Number 时间戳,空字符串会导致 Invalid array length
minDate: new Date(new Date().getFullYear() - 1, 0, 1).getTime(),
maxDate: new Date(new Date().getFullYear() + 1, 11, 31).getTime(),
// 微信定位相关
// 临时关闭定位:公众号 H5 在 HTTP+自定义端口下无法可靠定位;恢复定位时改为 true
enableLocationFeature: false,
isWxInited: false,
wxInitPromise: null,
geoLatitude: null,
@@ -3132,12 +3138,14 @@ export default {
return false;
}
// 2. 校验是否需要上传地理位置
if (leaveType.needLocationOnApply === '1') {
if (!this.form.leaveLocation) {
uni.showToast({ title: '该请假类型需要上传地理位置', icon: 'none' });
return false;
}
// 2. 校验是否需要上传地理位置(定位功能临时关闭时跳过,允许无坐标保存)
if (
this.enableLocationFeature &&
leaveType.needLocationOnApply === '1' &&
!this.form.leaveLocation
) {
uni.showToast({ title: '该请假类型需要上传地理位置', icon: 'none' });
return false;
}
// 3. 校验和设置销假状态
@@ -3180,8 +3188,8 @@ export default {
// 启动工作流
async startWorkflow(data) {
try {
// 获取班主任ID和学院审批人
const [classTeacherResponse, collegeApproversResponse] = await Promise.all([
// 获取班主任ID、学院审批人、二级学院分管领导
const [classTeacherResponse, collegeApproversResponse, collegeLeadersResponse] = await Promise.all([
request({
url: `/studentMsg/studentMsg/getClassTeacherId/${data.studentId}`,
method: 'GET',
@@ -3190,10 +3198,28 @@ export default {
url: `/studentMsg/studentMsg/getCollegeLeaveApprovers/${data.studentId}`,
method: 'GET',
}),
request({
url: `/studentMsg/studentMsg/getCollegeLeaders/${data.studentId}`,
method: 'GET',
}),
]);
const classTeacherId = classTeacherResponse.data;
const collegeApprovers = collegeApproversResponse.data || [];
const collegeLeaders = collegeLeadersResponse.data || [];
if (!classTeacherId) {
uni.showToast({ title: '获取班主任信息失败', icon: 'none' });
return;
}
if (!collegeApprovers.length) {
uni.showToast({ title: '获取学院审批人信息失败', icon: 'none' });
return;
}
if (!collegeLeaders.length) {
uni.showToast({ title: '获取二级学院分管领导信息失败', icon: 'none' });
return;
}
this.taskVariables = {
qjts: data.leaveDuration,
@@ -3202,6 +3228,7 @@ export default {
// sfxy: data.needCancel === '1',
bzrspr: classTeacherId,
ejxyspr: collegeApprovers,
ejxyfgld: collegeLeaders,
};
// 启动工作流
+335 -107
View File
@@ -6,60 +6,113 @@
</view>
</u-navbar>
<u-loading-page :loading="pageLoading" loading-text="加载中..."></u-loading-page>
<view v-if="!pageLoading" class="content-container">
<!-- 顶部操作栏 -->
<view class="action-bar">
<u-button type="primary" icon="plus" @click="handleAdd" style="flex: 1">点名</u-button>
</view>
<!-- 搜索栏 -->
<view class="filter-card">
<u-form label-width="80">
<u-form-item label="批次号">
<u-input v-model="queryParams.batchNo" placeholder="请输入批次号" clearable border="none" @change="handleQuery" />
</u-form-item>
<u-form-item label="宿舍楼">
<view @click="showBuildingPicker = true" style="width: 100%">
<u-input :value="selectedBuildingName" placeholder="请选择宿舍楼" readonly border="none" suffix-icon="arrow-down" suffix-icon-style="color: #c8c9cc;" style="pointer-events: none" />
</view>
<u-picker :show="showBuildingPicker" :columns="buildingColumns" keyName="text" @confirm="handleBuildingConfirm" @cancel="showBuildingPicker = false"></u-picker>
</u-form-item>
</u-form>
<view style="display: flex; gap: 12px; margin-top: 12px">
<u-button type="primary" size="small" @click="handleQuery" style="flex: 1">搜索</u-button>
<u-button size="small" @click="resetQuery" style="flex: 1">重置</u-button>
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-row">
<view class="search-input-wrap">
<u-search
v-model="queryParams.batchNo"
placeholder="搜索批次号"
:show-action="false"
@search="handleQuery"
@custom="handleQuery"
bg-color="#f6f6f6"
shape="round"
/>
</view>
<view class="filter-toggle" @click="showFilter = !showFilter">
<u-icon
:name="showFilter ? 'arrow-up' : 'list'"
size="16"
color="#606266"
/>
</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-bar" v-if="showFilter">
<view class="filter-item">
<view @click="showBuildingPicker = true">
<u-input
:value="selectedBuildingName"
placeholder="请选择宿舍楼"
placeholder-style="color: #c0c4cc; font-size: 14px"
font-size="14px"
shape="circle"
readonly
border="none"
suffix-icon="arrow-down"
:custom-style="filterInputStyle"
/>
</view>
</view>
<view class="filter-actions">
<u-button type="primary" shape="round" size="small" @click="handleQuery" :loading="pageLoading">
筛选
</u-button>
<u-button type="info" shape="round" size="small" @click="resetQuery" style="margin-left: 10rpx">
重置
</u-button>
</view>
</view>
<u-loading-page :loading="pageLoading && list.length === 0" loading-text="加载中..."></u-loading-page>
<view v-if="!pageLoading || list.length > 0" class="content-container">
<!-- 列表 -->
<view v-if="list.length > 0" class="list-section">
<view v-for="item in list" :key="item.id" class="check-card" @click="handleView(item)">
<!-- 卡片标题:房间名称 -->
<view class="card-header">
<text class="card-title">{{ item.buildingName || '-' }} - {{ item.roomName || '-' }}</text>
<view class="room-title">
<view class="room-icon">
<u-icon name="home" size="18" color="#0051d9" />
</view>
<view class="room-title-text">
<text class="room-name">{{ item.roomName || '未命名房间' }}</text>
<text class="building-name">{{ item.buildingName || '-' }}</text>
</view>
</view>
<u-icon name="arrow-right" size="14" color="#c0c4cc" />
</view>
<!-- 点名统计 -->
<view class="card-stats">
<view class="stat-badge stat-present">
<text>在寝 {{ item.presentCount || 0 }}</text>
<view class="stat-item stat-present">
<text class="stat-num">{{ item.presentCount || 0 }}</text>
<text class="stat-label">在寝</text>
</view>
<view class="stat-badge stat-leave">
<text>请假 {{ item.leaveCount || 0 }}</text>
<view class="stat-item stat-leave">
<text class="stat-num">{{ item.leaveCount || 0 }}</text>
<text class="stat-label">请假</text>
</view>
<view class="stat-badge stat-absent">
<text>不在 {{ item.absentCount || 0 }}</text>
<view class="stat-item stat-absent">
<text class="stat-num">{{ item.absentCount || 0 }}</text>
<text class="stat-label">不在</text>
</view>
<view class="stat-badge stat-empty">
<text>空 {{ item.emptyCount || 0 }}</text>
<view class="stat-item stat-empty">
<text class="stat-num">{{ item.emptyCount || 0 }}</text>
<text class="stat-label">空床</text>
</view>
</view>
<view class="card-info">
<text class="info-text">批次号: {{ item.batchNo }}</text>
<text class="info-text">点名时间: {{ item.checkDate }}</text>
<!-- 卡片内容 -->
<view class="card-body">
<view class="info-row">
<u-icon name="file-text" size="16" color="#909399" />
<text class="info-label">批次</text>
<text class="info-value info-value--ellipsis">{{ item.batchNo || '-' }}</text>
</view>
<view class="info-row">
<u-icon name="clock" size="16" color="#909399" />
<text class="info-label">时间</text>
<text class="info-value">{{ item.checkDate || '-' }}</text>
</view>
</view>
</view>
</view>
<view v-else style="padding: 60rpx 0">
<view v-else-if="!pageLoading" class="empty-container">
<u-empty text="暂无点名记录" mode="data"></u-empty>
</view>
@@ -70,6 +123,21 @@
<u-button size="small" :disabled="queryParams.pageNum >= totalPages" @click="handleNextPage">下一页</u-button>
</view>
</view>
<!-- 宿舍楼选择器 -->
<u-picker
:show="showBuildingPicker"
:columns="buildingColumns"
keyName="text"
@confirm="handleBuildingConfirm"
@cancel="showBuildingPicker = false"
@close="showBuildingPicker = false"
/>
<!-- 悬浮点名按钮 -->
<view class="fab-button" @click="handleAdd">
<u-icon name="plus" size="24" color="#ffffff" />
</view>
</view>
</template>
@@ -82,6 +150,7 @@ export default {
data() {
return {
pageLoading: false,
showFilter: false,
list: [],
total: 0,
queryParams: {
@@ -93,6 +162,13 @@ export default {
buildingOptions: [],
selectedBuildingName: '',
showBuildingPicker: false,
// 筛选输入框样式(与搜索框高度一致)
filterInputStyle: {
backgroundColor: '#f6f6f6',
padding: '0 24rpx',
height: '32px',
boxSizing: 'border-box',
},
};
},
computed: {
@@ -145,15 +221,13 @@ export default {
},
handleBuildingConfirm(e) {
if (e && e.value && e.value.length > 0) {
const building = this.buildingOptions.find((item) => item.id === e.value[0].value);
if (building) {
this.queryParams.buildingId = building.id;
this.selectedBuildingName = building.buildingName;
}
const index = e.indexs && e.indexs[0];
const selected = this.buildingColumns[0] && this.buildingColumns[0][index];
if (selected) {
this.queryParams.buildingId = selected.value;
this.selectedBuildingName = selected.text;
}
this.showBuildingPicker = false;
this.handleQuery();
},
handleAdd() {
@@ -188,92 +262,246 @@ export default {
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f6f7;
background-color: #f8f8f8;
}
.search-bar {
padding: 20rpx;
background: #ffffff;
.search-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.search-input-wrap {
flex: 1;
min-width: 0;
}
.filter-toggle {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #f6f6f6;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&:active {
opacity: 0.8;
}
}
}
.filter-bar {
background: #ffffff;
padding: 0 20rpx 20rpx;
.filter-item {
margin-bottom: 20rpx;
}
.filter-actions {
display: flex;
justify-content: flex-end;
gap: 10rpx;
}
}
.content-container {
padding: 20rpx;
padding-top: 100rpx;
padding-bottom: 160rpx;
}
.action-bar {
margin-bottom: 20rpx;
.fab-button {
position: fixed;
right: 20px;
bottom: 30px;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #007aff, #0051d9);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.3);
transition: all 0.3s ease;
z-index: 999;
&:active {
transform: scale(0.95);
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.4);
}
}
.filter-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
.empty-container {
padding: 80rpx 20rpx;
text-align: center;
}
.list-section {
margin-top: 12rpx;
margin-top: 0;
}
.check-card {
background: #fff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 16rpx;
border: 2rpx solid #e4e7ed;
}
background: #ffffff;
border-radius: 16rpx;
padding: 0;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 81, 217, 0.08);
overflow: hidden;
transition: all 0.2s ease;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
&:active {
transform: scale(0.985);
box-shadow: 0 2rpx 10rpx rgba(0, 81, 217, 0.06);
}
.card-title {
font-size: 28rpx;
font-weight: 600;
color: #303133;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 24rpx 20rpx;
background: linear-gradient(135deg, #f3f7ff 0%, #ffffff 100%);
border-bottom: 1rpx solid #f0f3f8;
.card-stats {
display: flex;
gap: 12rpx;
margin-bottom: 16rpx;
flex-wrap: wrap;
}
.room-title {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
margin-right: 16rpx;
.stat-badge {
padding: 4rpx 16rpx;
border-radius: 8rpx;
font-size: 22rpx;
}
.room-icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 12rpx;
}
.stat-present {
background: #e8f5e9;
color: #19be6b;
}
.room-title-text {
display: flex;
flex-direction: column;
min-width: 0;
.stat-leave {
background: #fff8e1;
color: #e6a23c;
}
.room-name {
font-size: 32rpx;
font-weight: 600;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stat-absent {
background: #ffebee;
color: #f56c6c;
}
.building-name {
font-size: 24rpx;
color: #909399;
margin-top: 4rpx;
}
}
}
}
.stat-empty {
background: #f5f5f5;
color: #909399;
}
.card-stats {
display: flex;
padding: 20rpx 16rpx 8rpx;
gap: 12rpx;
.card-info {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 16rpx 8rpx;
border-radius: 12rpx;
.info-text {
font-size: 24rpx;
color: #909399;
.stat-num {
font-size: 32rpx;
font-weight: 600;
line-height: 1.2;
}
.stat-label {
font-size: 22rpx;
margin-top: 6rpx;
}
&.stat-present {
background: #e8f5e9;
.stat-num,
.stat-label {
color: #19be6b;
}
}
&.stat-leave {
background: #fff8e1;
.stat-num,
.stat-label {
color: #e6a23c;
}
}
&.stat-absent {
background: #ffebee;
.stat-num,
.stat-label {
color: #f56c6c;
}
}
&.stat-empty {
background: #f5f5f5;
.stat-num,
.stat-label {
color: #909399;
}
}
}
}
.card-body {
padding: 12rpx 24rpx 20rpx;
.info-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
&:last-child {
margin-bottom: 0;
}
.info-label {
width: 64rpx;
flex-shrink: 0;
font-size: 24rpx;
color: #909399;
margin-left: 10rpx;
}
.info-value {
flex: 1;
min-width: 0;
font-size: 26rpx;
color: #606266;
&--ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
.pagination-bar {
@@ -288,4 +516,4 @@ export default {
font-size: 24rpx;
color: #606266;
}
</style>
</style>