This commit is contained in:
ywyonui 2025-08-11 22:43:01 +08:00
commit d0859759c1
17 changed files with 3521 additions and 1336 deletions

View File

@ -94,6 +94,14 @@ export const jsdXkListApi = async (params: any) => {
return await get("/mobile/js/xk/list", params);
};
/**
*
*/
export const getCurrentSemesterTeacherCoursesApi = async (jsId?: string) => {
const params = jsId ? { jsId } : {};
return await get("/api/xkkc/getCurrentSemesterTeacherCourses", params);
};
// 选课列表
export const jsdXkkcSaveApi = async (params: any) => {
return await post("/api/xkkc/save", params);
@ -357,5 +365,12 @@ export const getByUserIdAndInspectItemIdApi = async (params: any) => {
// 清空用户open_id
export const clearUserOpenIdApi = async (params: { userId: string }) => {
return await post(`/api/user/clearOpenId?userId=${params.userId}`);
return await post("/api/user/clearUserOpenId", params);
};
/**
* API
*/
export const jsdXkkcPhotoSaveApi = async (params: any) => {
return await post("/mobile/js/xk/photo/save", params);
};

View File

@ -30,14 +30,14 @@
:src="item.lastRank ? lastIcon : item.showChild ? currentIcon : defaultIcon"></image>
{{ item.name }}
</view>
<!-- 多选时所有节点都显示多选-->
<view class="tki-tree-check" v-if="multiple" @tap.stop="_treeItemSelect(item, index)">
<!-- 只有叶子节点显示可选择的选择-->
<view class="tki-tree-check" @tap.stop="_treeItemSelect(item, index)" :class="{'disabled': !item.lastRank}">
<view class="tki-tree-check-yes" v-if="item.checked" :class="{'radio':!multiple}"
:style="{'border-color':confirmColor}">
<view class="tki-tree-check-yes-b" :style="{'background-color':confirmColor}"></view>
</view>
<view class="tki-tree-check-no" v-else :class="{'radio':!multiple}"
:style="{'border-color':confirmColor}"></view>
:style="{'border-color': !item.lastRank ? '#ccc' : confirmColor}"></view>
</view>
</view>
</block>
@ -277,17 +277,16 @@ export default {
// console.log(this.treeList)
},
_treeItemSelect(item, index) {
//
if (!item.lastRank) {
//
return;
}
if (this.multiple) {
if (!item.lastRank) {
// /
const allChecked = this._isAllChildrenChecked(item);
this._setAllChildrenChecked(item, !allChecked);
item.checked = !allChecked;
} else {
//
this.treeList[index].checked = !this.treeList[index].checked;
this._updateParentChecked(item);
}
//
this.treeList[index].checked = !this.treeList[index].checked;
this._updateParentChecked(item);
this._fixMultiple(index);
} else {
//
@ -496,4 +495,14 @@ export default {
opacity: 0.6;
}
.tki-tree-check.disabled {
opacity: 0.4;
cursor: not-allowed;
}
.tki-tree-check.disabled .tki-tree-check-no {
border-color: #ccc !important;
background-color: #f5f5f5;
}
</style>

View File

@ -505,6 +505,25 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/base/groupTeaching/dmXkkcRecord",
"style": {
"navigationBarTitleText": "点名记录",
"enablePullDownRefresh": false
}
},
{
"path": "pages/base/groupTeaching/photoXkkcDetail",
"style": {
"navigationBarTitleText": "课堂随拍"
}
},
{
"path": "pages/view/routine/kefuxuncha/xcRecord",
"style": {
"navigationBarTitleText": "巡查记录"
}
},
{
"path": "pages/base/xs/qj/sp",
"style": {

View File

@ -3,12 +3,13 @@
<!-- 选课信息头部 - 固定部分 -->
<view class="selection-header">
<view class="header-content">
<view class="title-section" @click="clickShowXkSelector">
<view class="title-section">
<view class="title">
<text v-if="xkData && xkData.xkmc">{{ xkData.xkmc }}</text>
<text v-else>选课信息</text>
<text v-if="xkkcList && xkkcList.length > 0">
{{ getCurrentSemesterName() }} - 我的课程
</text>
<text v-else>暂无课程</text>
</view>
<view class="switch-btn" v-if="xkList.length > 1">切换</view>
</view>
</view>
</view>
@ -19,21 +20,13 @@
<view class="course-list" v-if="xkkcList && xkkcList.length > 0">
<view v-for="(xkkc, index) in xkkcList" :key="xkkc.id || index" class="course-item">
<view class="course-name">{{ xkkc.kcmc }}</view>
<view class="course-info-item">
<view class="info-label">上课周期类型</view>
<view class="info-data">{{ xkkc.skzqlx }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课周期</view>
<view class="info-data">{{ xkkc.skzqmc }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课开始时间</view>
<view class="info-data">{{ xkkc.skkstime }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课结束时间</view>
<view class="info-data">{{ xkkc.skjstime }}</view>
<view class="info-label">上课时间</view>
<view class="info-data">{{ formatClassTime(xkkc.skkstime, xkkc.skjstime) }}</view>
</view>
<view class="course-info-item">
<view class="info-label">开课地点</view>
@ -43,8 +36,11 @@
<view class="info-label">上课人数</view>
<view class="info-data">{{ xkkc.hasNum || 0 }} | {{ xkkc.maxNum || 0 }}</view>
</view>
<view class="separator-line"></view>
<view class="course-btn-group">
<view class="dm-btn" @click.stop="goDm(xkkc)">点名</view>
<view class="record-btn" @click.stop="goRecord(xkkc)">点名记录</view>
<!-- <view class="photo-btn" @click.stop="goPhoto(xkkc)">课堂随拍</view>-->
</view>
</view>
</view>
@ -58,29 +54,6 @@
</view>
</view>
<view>
<!-- 俱乐部选择弹窗 -->
<u-popup :show="showXkFlag" @close="showXkFlag = false" mode="bottom" round="10">
<view class="xk-selector">
<view class="selector-header">
<text class="selector-title">选择俱乐部</text>
<u-icon name="close" size="20" @click="showXkFlag = false"></u-icon>
</view>
<view class="xk-list">
<view v-for="(xk, index) in xkList" :key="index" class="xk-item" :class="{
'xk-item-active': xkData.id === xk.id
}" @click="switchXk(xk)">
<view class="xk-info">
<text class="xk-name">{{ xk.xkmc }}</text>
<text class="xk-type">{{ xk.xkmc }}</text>
</view>
<u-icon v-if="xkData.id === xk.id" name="checkmark" color="#409EFF" size="20"></u-icon>
</view>
</view>
</view>
</u-popup>
</view>
</view>
</template>
@ -92,7 +65,7 @@ import {
} from "vue";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import { jsdXkListApi } from "@/api/base/server";
import { getCurrentSemesterTeacherCoursesApi } from "@/api/base/server";
import { dmBeforeMinuteApi } from "@/api/system/config/index";
import dayjs from "dayjs";
@ -101,15 +74,6 @@ const { getData, setData } = useDataStore();
const wdNameList = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
//
const showXkFlag = ref(false);
const xkList = ref<any>([]);
const xkData = ref();
const courseInfo = ref<any>({});
//
const xkkcList = ref<any[]>([]);
@ -119,75 +83,126 @@ onMounted(async () => {
uni.showLoading({
title: "加载中...",
});
await loadCourseList();
await loadDmBeforeMinute();
uni.hideLoading();
try {
await loadCourseList();
await loadDmBeforeMinute();
} catch (error) {
console.error('页面初始化失败:', error);
uni.showToast({
title: '页面加载失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
});
//
const loadCourseList = async () => {
const res = await jsdXkListApi({
jsId: getJs.id
});
if (res.resultCode == 1) {
if (res.result && res.result.length) {
xkList.value = res.result;
switchXk(res.result[0]);
} else {
xkList.value = [];
xkData.value = {};
xkkcList.value = [];
}
}
try {
const res = await getCurrentSemesterTeacherCoursesApi(getJs.id);
if (res.resultCode == 1) {
if (res.result && res.result.length) {
xkkcList.value = res.result;
//
processCoursePeriods();
} else {
xkkcList.value = [];
}
} else {
xkkcList.value = [];
uni.showToast({
title: res.message || '获取课程列表失败',
icon: 'none'
});
}
} catch (error) {
console.error('加载课程列表失败:', error);
xkkcList.value = [];
uni.showToast({
title: '加载课程列表失败',
icon: 'none'
});
}
};
//
const processCoursePeriods = () => {
for (let i = 0; i < xkkcList.value.length; i++) {
let xkkc = xkkcList.value[i];
//
switch (xkkc.skzqlx) {
case '每天':
xkkc.skzqmc = "每天";
break;
case '每周':
const daysOfWeek = xkkc.skzq.split(',').map(Number);
// wdNameListdaysOfWeek
xkkc.skzqmc = daysOfWeek.map((day: number) => wdNameList[day - 1]).join(',');
break;
case '每月':
const daysOfMonth = xkkc.skzq.split(',').map(Number);
//
xkkc.skzqmc = daysOfMonth.map((day: number) => day + "号").join(',');
break;
}
}
};
//
const getCurrentSemesterName = () => {
if (xkkcList.value && xkkcList.value.length > 0) {
//
return xkkcList.value[0].xqmc || '当前学期';
}
return '当前学期';
};
//
const formatClassTime = (startTime: string, endTime: string) => {
if (!startTime || !endTime) {
return '';
}
try {
//
let start, end;
// HH:mm:ss HH:mm
if (startTime.includes(':') && !startTime.includes('-') && !startTime.includes('/')) {
start = startTime;
end = endTime;
} else {
// dayjs
const startDate = dayjs(startTime);
const endDate = dayjs(endTime);
if (startDate.isValid() && endDate.isValid()) {
start = startDate.format('HH:mm:ss');
end = endDate.format('HH:mm:ss');
} else {
//
return `${startTime}~${endTime}`;
}
}
return `${start}~${end}`;
} catch (error) {
console.error('时间格式化错误:', error);
//
return `${startTime}~${endTime}`;
}
};
//
const loadDmBeforeMinute = async () => {
const res = await dmBeforeMinuteApi();
if (res.resultCode == 1) {
// res.resultintnumberdmBeforeMinute
const res = await dmBeforeMinuteApi();
if (res.resultCode == 1) {
// res.resultintnumberdmBeforeMinute
dmBeforeMinute.value = parseInt(res.result);
}
}
//
function clickShowXkSelector() {
if (xkList.value.length > 1) {
showXkFlag.value = true;
}
}
//
function switchXk(xk: any) {
xkData.value = xk;
xkkcList.value = xk.xkkcs;
showXkFlag.value = false;
for (let i = 0; i < xk.xkkcs.length; i++) {
let xkkc = xk.xkkcs[i];
//
switch (xkkc.skzqlx) {
case '每天':
xkkc.skzqmc = "每天";
break;
case '每周':
const daysOfWeek = xkkc.skzq.split(',').map(Number);
// wdNameListdaysOfWeek
xkkc.skzqmc = daysOfWeek.map((day: number) => wdNameList[day - 1]).join(',');
break;
case '每月':
const daysOfMonth = xkkc.skzq.split(',').map(Number);
//
xkkc.skzqmc = daysOfMonth.map((day: number) => day + "号").join(',');
break;
}
}
//
uni.showToast({
title: `已切换到${xk.xkmc}`,
icon: "none",
});
}
//
const goDm = (xkkc: any) => {
const now = dayjs();
@ -197,50 +212,160 @@ const goDm = (xkkc: any) => {
}
let mDay = now.date();
const strDate = now.format('YYYY-MM-DD') + ' ';
let dmFlag = false;
let msg = "";
//
switch (xkkc.skzqlx) {
case '每天':
dmFlag = true;
break;
case '每周':
const daysOfWeek = xkkc.skzq.split(',').map(Number);
dmFlag = daysOfWeek.includes(wDay);
// wdNameListdaysOfWeek
xkkc.skzqmc = daysOfWeek.map((day: number) => wdNameList[day - 1]).join(',');
break;
case '每月':
const daysOfMonth = xkkc.skzq.split(',').map(Number);
dmFlag = daysOfMonth.includes(mDay);
//
xkkc.skzqmc = daysOfMonth.map((day: number) => day + "号").join(',');
break;
}
//
if (dmFlag) {
// xkkc.skkstimedmBeforeMinute
let dmFlag = false;
let msg = "";
//
switch (xkkc.skzqlx) {
case '每天':
dmFlag = true;
break;
case '每周':
const daysOfWeek = xkkc.skzq.split(',').map(Number);
dmFlag = daysOfWeek.includes(wDay);
// wdNameListdaysOfWeek
xkkc.skzqmc = daysOfWeek.map((day: number) => wdNameList[day - 1]).join(',');
break;
case '每月':
const daysOfMonth = xkkc.skzq.split(',').map(Number);
dmFlag = daysOfMonth.includes(mDay);
//
xkkc.skzqmc = daysOfMonth.map((day: number) => day + "号").join(',');
break;
}
//
if (dmFlag) {
// xkkc.skkstimedmBeforeMinute
const startTime = dayjs(strDate + xkkc.skkstime).subtract(dmBeforeMinute.value, 'minute').format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs(strDate + xkkc.skjstime, 'YYYY-MM-DD HH:mm:ss');
dmFlag = now.isBefore(endTime) && now.isAfter(startTime)
} else {
msg = "没到点名日期";
}
if (dmFlag) {
setData(xkkc);
uni.navigateTo({
url: `/pages/base/groupTeaching/dmXkkcDetail`,
});
} else {
if (msg === "") {
msg = "没到点名时间";
}
uni.showToast({
title: msg,
icon: 'none',
duration: 2000
});
}
const endTime = dayjs(strDate + xkkc.skjstime, 'YYYY-MM-DD HH:mm:ss');
dmFlag = now.isBefore(endTime) && now.isAfter(startTime)
} else {
msg = "上课时间未到,无法点名";
}
if (dmFlag) {
setData(xkkc);
uni.navigateTo({
url: `/pages/base/groupTeaching/dmXkkcDetail`,
});
} else {
if (msg === "") {
msg = "上课时间未到,无法点名";
}
uni.showToast({
title: msg,
icon: 'none',
duration: 2000
});
}
};
//
const goRecord = (xkkc: any) => {
const now = dayjs();
let wDay = now.day();
if (wDay === 0) {
wDay = 7;
}
let mDay = now.date();
const strDate = now.format('YYYY-MM-DD') + ' ';
let recordFlag = false;
let msg = "";
//
switch (xkkc.skzqlx) {
case '每天':
recordFlag = true;
break;
case '每周':
const daysOfWeek = xkkc.skzq.split(',').map(Number);
recordFlag = daysOfWeek.includes(wDay);
// wdNameListdaysOfWeek
xkkc.skzqmc = daysOfWeek.map((day: number) => wdNameList[day - 1]).join(',');
break;
case '每月':
const daysOfMonth = xkkc.skzq.split(',').map(Number);
recordFlag = daysOfMonth.includes(mDay);
//
xkkc.skzqmc = daysOfMonth.map((day: number) => day + "号").join(',');
break;
}
//
if (recordFlag) {
// xkkc.skkstimedmBeforeMinute
const startTime = dayjs(strDate + xkkc.skkstime).subtract(dmBeforeMinute.value, 'minute').format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs(strDate + xkkc.skjstime, 'YYYY-MM-DD HH:mm:ss');
recordFlag = now.isBefore(endTime) && now.isAfter(startTime)
} else {
msg = "上课时间未到,无法查看点名记录";
}
if (recordFlag) {
setData(xkkc);
uni.navigateTo({
url: `/pages/base/groupTeaching/dmXkkcRecord`,
});
} else {
if (msg === "") {
msg = "上课时间未到,无法查看点名记录";
}
uni.showToast({
title: msg,
icon: 'none',
duration: 2000
});
}
};
//
const goPhoto = (xkkc: any) => {
const now = dayjs();
let wDay = now.day();
if (wDay === 0) {
wDay = 7;
}
let mDay = now.date();
const strDate = now.format('YYYY-MM-DD') + ' ';
let photoFlag = false;
let msg = "";
//
switch (xkkc.skzqlx) {
case '每天':
photoFlag = true;
break;
case '每周':
const daysOfWeek = xkkc.skzq.split(',').map(Number);
photoFlag = daysOfWeek.includes(wDay);
// wdNameListdaysOfWeek
xkkc.skzqmc = daysOfWeek.map((day: number) => wdNameList[day - 1]).join(',');
break;
case '每月':
const daysOfMonth = xkkc.skzq.split(',').map(Number);
photoFlag = daysOfMonth.includes(mDay);
//
xkkc.skzqmc = daysOfMonth.map((day: number) => day + "号").join(',');
break;
}
//
if (photoFlag) {
// xkkc.skkstimedmBeforeMinute
const startTime = dayjs(strDate + xkkc.skkstime).subtract(dmBeforeMinute.value, 'minute').format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs(strDate + xkkc.skjstime, 'YYYY-MM-DD HH:mm:ss');
photoFlag = now.isBefore(endTime) && now.isAfter(startTime)
} else {
msg = "上课时间未到,无法随拍";
}
if (photoFlag) {
setData(xkkc);
uni.navigateTo({
url: `/pages/base/groupTeaching/photoXkkcDetail`,
});
} else {
if (msg === "") {
msg = "上课时间未到,无法随拍";
}
uni.showToast({
title: msg,
icon: 'none',
duration: 2000
});
}
};
//
@ -251,7 +376,7 @@ onBeforeUnmount(() => {
<style lang="scss" scoped>
.interest-course {
min-height: 100%;
background-color: #f5f7fa;
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
display: flex;
flex-direction: column;
height: 100%;
@ -287,46 +412,50 @@ onBeforeUnmount(() => {
}
.selection-header {
background: linear-gradient(135deg, #4a90e2, #2879ff);
padding: 20px 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 25px 20px;
color: #fff;
border-radius: 0 0 15px 15px;
box-shadow: 0 4px 12px rgba(40, 121, 255, 0.2);
border-radius: 0 0 20px 20px;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 10;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, rgba(255, 255, 255, 0.1) 0%, transparent 50%, rgba(255, 255, 255, 0.05) 100%);
pointer-events: none;
}
.header-content {
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
z-index: 1;
.title-section {
display: flex;
align-items: center;
justify-content: center;
.title {
flex: 1 0 1px;
font-size: 24px;
font-weight: bold;
}
.subtitle {
font-size: 14px;
opacity: 0.8;
}
.switch-btn {
padding: 5px 15px;
background-color: rgba(255, 255, 255, 0.2);
color: #fff;
border-radius: 15px;
font-size: 15px;
font-size: 20px;
font-weight: 700;
text-align: center;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
letter-spacing: 0.5px;
}
}
}
}
@ -343,65 +472,256 @@ onBeforeUnmount(() => {
.course-item {
position: relative;
width: 100%;
margin-bottom: 15px;
margin-bottom: 20px;
background-color: #fff;
border-radius: 8px;
padding: 15px;
border-radius: 12px;
padding: 20px;
box-sizing: border-box;
border: 1px solid transparent;
transition: all 0.3s ease;
border: 1px solid #f0f0f0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
animation: fadeInUp 0.6s ease-out;
// &:nth-child(odd) {
// margin-right: 10px;
// }
// &:nth-child(even) {
// margin-left: 10px;
// }
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
border-color: #e8e8e8;
}
&.selected {
border: 1px solid #3fbf72;
background-color: rgba(63, 191, 114, 0.05);
box-shadow: 0 2px 8px rgba(63, 191, 114, 0.15);
border: 2px solid #3fbf72;
background-color: rgba(63, 191, 114, 0.02);
box-shadow: 0 4px 20px rgba(63, 191, 114, 0.15);
}
.course-name {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 10px;
font-size: 17px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 15px;
line-height: 1.4;
animation: fadeInLeft 0.5s ease-out 0.1s both;
}
.course-btn-group {
display: flex;
justify-content: flex-end;
gap: 10px; //
.dm-btn {
flex: 1 0 1px;
display: inline-block;
color: #2879ff;
font-size: 14px;
font-weight: 600;
padding: 8px 18px;
border-radius: 8px;
background: linear-gradient(135deg, rgba(40, 121, 255, 0.1), rgba(40, 121, 255, 0.05));
border: 1px solid #2879ff;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(40, 121, 255, 0.15);
animation: fadeInUp 0.5s ease-out 0.3s both;
&:hover {
background: linear-gradient(135deg, rgba(40, 121, 255, 0.15), rgba(40, 121, 255, 0.1));
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(40, 121, 255, 0.25);
}
&:active {
background: linear-gradient(135deg, rgba(40, 121, 255, 0.2), rgba(40, 121, 255, 0.15));
transform: translateY(0);
box-shadow: 0 2px 8px rgba(40, 121, 255, 0.2);
}
}
.record-btn {
display: inline-block;
color: #4CAF50; /* A green color for record */
font-size: 14px;
font-weight: 600;
padding: 8px 18px;
border-radius: 8px;
background: linear-gradient(135deg, rgba(76, 175, 80, 0.1), rgba(76, 175, 80, 0.05));
border: 1px solid #4CAF50;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.15);
animation: fadeInUp 0.5s ease-out 0.4s both;
&:hover {
background: linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(76, 175, 80, 0.1));
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.25);
}
&:active {
background: linear-gradient(135deg, rgba(76, 175, 80, 0.2), rgba(76, 175, 80, 0.15));
transform: translateY(0);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.2);
}
}
.photo-btn {
display: inline-block;
color: #ff6b35;
font-size: 14px;
font-weight: 600;
padding: 8px 18px;
border-radius: 8px;
background: linear-gradient(135deg, rgba(255, 107, 53, 0.1), rgba(255, 107, 53, 0.05));
border: 1px solid #ff6b35;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.15);
animation: fadeInUp 0.5s ease-out 0.4s both;
&:hover {
background: linear-gradient(135deg, rgba(255, 107, 53, 0.15), rgba(255, 107, 53, 0.1));
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(255, 107, 53, 0.25);
}
&:active {
background: linear-gradient(135deg, rgba(255, 107, 53, 0.2), rgba(255, 107, 53, 0.15));
transform: translateY(0);
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.2);
}
}
}
.course-info-item {
display: flex;
margin-bottom: 12px;
font-size: 12px;
margin-bottom: 14px;
font-size: 13px;
align-items: center;
animation: fadeInUp 0.5s ease-out 0.15s both;
.info-label {
color: #949AA4;
color: #666;
flex: 0 0 100px;
font-weight: 500;
}
.info-data {
flex: 1 0 1px;
color: #333;
font-weight: 400;
}
}
.separator-line {
height: 1px;
background: linear-gradient(90deg, transparent, #e8e8e8, transparent);
margin: 18px 0;
opacity: 0.8;
animation: fadeIn 0.5s ease-out 0.25s both;
}
}
}
//
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 学生选择器弹窗样式 */
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 全局图片样式 */
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
//
.empty-course-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
.empty-icon {
margin-bottom: 25px;
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
width: 90px;
height: 90px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 2px solid rgba(255, 255, 255, 0.8);
}
.empty-text {
font-size: 18px;
font-weight: 600;
color: #475569;
margin-bottom: 8px;
letter-spacing: 0.3px;
}
.empty-desc {
font-size: 14px;
color: #64748b;
max-width: 80%;
line-height: 1.6;
}
}
/* 选课已结束样式 */
.enrollment-ended {
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 12px 15px;
font-size: 16px;
font-weight: 500;
color: #fff;
gap: 8px;
}
/* 俱乐部选择弹窗样式 */
.xk-selector {
background-color: #ffffff;
border-top-left-radius: 12px;
@ -430,6 +750,7 @@ onBeforeUnmount(() => {
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #f2f2f2;
transition: all 0.3s ease;
&:last-child {
border-bottom: none;
@ -439,6 +760,10 @@ onBeforeUnmount(() => {
background-color: rgba(64, 158, 255, 0.05);
}
&:hover {
background-color: rgba(64, 158, 255, 0.02);
}
.xk-info {
flex: 1;
margin-left: 12px;
@ -464,62 +789,4 @@ onBeforeUnmount(() => {
}
}
}
/* 全局图片样式 */
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
//
.empty-course-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
.empty-icon {
margin-bottom: 20px;
background-color: #f5f6f7;
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 18px;
font-weight: 500;
color: #303133;
margin-bottom: 8px;
}
.empty-desc {
font-size: 14px;
color: #909399;
max-width: 80%;
line-height: 1.5;
}
}
/* 选课已结束样式 */
.enrollment-ended {
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 12px 15px;
font-size: 16px;
font-weight: 500;
color: #fff;
gap: 8px;
}
</style>

View File

@ -43,7 +43,7 @@
<view class="avatar-container mr-8">
<image
class="student-avatar"
:src="xs.xmtx || '/static/images/default-avatar.png'"
:src="getImageUrl(xs.xstx)"
mode="aspectFill"
></image>
</view>
@ -60,7 +60,7 @@
</view>
</view>
<text class="font-12 cor-666">{{ xs.bjmc }}</text>
<view class="contact-parent mt-8 flex-center">
<view class="contact-parent mt-8 flex-center" @click="contactParent(xs)">
<text class="font-12 cor-primary">联系家长</text>
<u-icon
name="phone"
@ -86,25 +86,41 @@
<template #bottom>
<view class="submit-btn-wrap py-10 px-20 bg-white">
<button class="submit-btn" @click="submit">提交</button>
<button
class="submit-btn"
:class="{ 'submit-btn-disabled': isSubmitting }"
:disabled="isSubmitting"
@click="submit"
>
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
</view>
</template>
<!-- 提交遮罩层 -->
<view v-if="isSubmitting" class="submit-overlay">
<view class="submit-loading">
<u-loading-icon mode="spinner" size="28"></u-loading-icon>
<text class="loading-text">正在提交...</text>
</view>
</view>
</BasicLayout>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {onMounted, ref, computed} from "vue";
import BasicLayout from "@/components/BasicLayout/Layout.vue";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import { jsdXkXsListApi, jsdXkdmListApi } from "@/api/base/server";
import { useDicStore } from "@/store/modules/dic";
import { BASE_IMAGE_URL } from "@/config";
const { findByPid } = useDicStore();
import dayjs from "dayjs";
const { getJs } = useUserStore();
const { getData } = useDataStore();
const { getData, setData } = useDataStore();
const js = computed(() => getJs)
const xkkc = computed(() => getData)
@ -138,6 +154,16 @@ const statusOptions = ref<Array<{ text: string, value: string }>>([]);
const curXs = ref<any>(null);
const defSel = ref<any>([]);
//
const isSubmitting = ref(false);
// URL
const getImageUrl = (imagePath: string) => {
if (!imagePath) return '/static/images/default-avatar.png';
if (imagePath.startsWith('http')) return imagePath;
return BASE_IMAGE_URL + imagePath;
};
//
const getStatusClass = (status: string) => {
switch (status) {
@ -257,21 +283,74 @@ const toRollCallRecord = () => {
//
const contactParent = (student: any) => {
console.log("联系家长", student.name);
//
const completeStudent = {
...student,
//
id: student.xsId || student.id,
xsxm: student.xsxm || student.xm,
xstx: student.xstx || student.avatar,
xb: student.xb || student.gender,
sfzh: student.sfzh,
cstime: student.cstime,
njmc: student.njmcName || student.njmc,
bjmc: student.bjmc,
// njIdbjId
njId: student.njId,
bjId: student.bjId
};
// store使
setData(completeStudent);
//
uni.navigateTo({
url: "/pages/view/homeSchool/parentAddressBook/detail"
});
};
//
const submit = async () => {
const res = await jsdXkdmListApi({
jsId: js.value.id,
xkkcId: xkkc.value.id,
dmtime: now,
xkdmList: xsList.value
});
uni.showToast({
title: "提交成功",
icon: "success",
});
if (isSubmitting.value) {
return;
}
isSubmitting.value = true;
try {
const res = await jsdXkdmListApi({
jsId: js.value.id,
xkkcId: xkkc.value.id,
dmtime: now,
xkdmList: xsList.value
});
if (res && res.resultCode === 1) {
uni.showToast({
title: "提交成功",
icon: "success",
});
//
setTimeout(() => {
//
uni.navigateBack({
delta: 1
});
}, 1500);
} else {
uni.showToast({
title: res?.resultMessage || "提交失败",
icon: "none",
});
}
} catch (error) {
console.error("提交失败:", error);
uni.showToast({
title: "提交失败,请重试",
icon: "none",
});
} finally {
isSubmitting.value = false;
}
};
//
@ -384,6 +463,13 @@ onMounted(async () => {
border-radius: 4px;
border: 1px solid #4080ff;
display: inline-flex;
cursor: pointer;
transition: all 0.2s ease;
&:active {
background-color: rgba(64, 128, 255, 0.1);
transform: scale(0.95);
}
}
.submit-btn {
@ -397,6 +483,65 @@ onMounted(async () => {
justify-content: center;
}
.submit-btn-disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
}
.submit-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
animation: fadeIn 0.3s ease-out;
}
.submit-loading {
background-color: #fff;
padding: 30px 40px;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
animation: slideInUp 0.3s ease-out;
}
.loading-text {
margin-top: 15px;
color: #333;
font-size: 16px;
font-weight: 500;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.cor-primary {
color: #4080ff;
}

View File

@ -0,0 +1,443 @@
<template>
<BasicLayout>
<!-- 课程信息卡片 -->
<view class="course-card mx-15 my-15 bg-white white-bg-color r-md p-15">
<view class="flex-row items-center mb-15">
<view class="course-icon flex-center mr-10">
<u-icon name="calendar" color="#4080ff" size="20"></u-icon>
</view>
<text class="font-16 font-bold">{{ xkkc.kcmc }}</text>
<text class="font-14 cor-999 ml-10">{{ todayInfo.date }} ({{ todayInfo.weekName }})</text>
</view>
</view>
<!-- 点名记录统计 -->
<view class="record-stats mx-15 mb-15">
<view class="stats-grid">
<view class="stat-item total" @click="showStudentList('total')">
<view class="stat-number">{{ totalStudents }}</view>
<view class="stat-label">总人数</view>
</view>
<view class="stat-item present" @click="showStudentList('present')">
<view class="stat-number">{{ presentStudents }}</view>
<view class="stat-label">实到</view>
</view>
<view class="stat-item leave" @click="showStudentList('leave')">
<view class="stat-number">{{ leaveStudents }}</view>
<view class="stat-label">请假</view>
</view>
<view class="stat-item absent" @click="showStudentList('absent')">
<view class="stat-number">{{ absentStudents }}</view>
<view class="stat-label">缺勤</view>
</view>
</view>
</view>
<!-- 学生列表弹窗 -->
<uni-popup ref="studentPopup" type="bottom" :mask-click="false">
<view class="student-popup">
<view class="popup-header">
<text class="popup-title">{{ currentStatTitle }}</text>
<view class="close-btn" @click="closeStudentList">
<u-icon name="close" color="#666" size="20"></u-icon>
</view>
</view>
<view class="student-list">
<view
v-for="(student, index) in currentStudentList"
:key="student.xsId"
class="student-item"
>
<view class="student-avatar">
<image
v-if="student.xstx"
:src="getImageUrl(student.xstx)"
mode="aspectFill"
class="avatar-img"
/>
<view v-else class="avatar-text">{{ student.xsxm?.charAt(0) || '学' }}</view>
</view>
<view class="student-info">
<view class="student-name">{{ student.xsxm }}</view>
<view class="student-class">{{ student.njmcName }} {{ student.bjmc }}</view>
</view>
<view class="student-status">
<text :class="getStatusClass(student.xszt)">{{ getStatusText(student.xszt) }}</text>
</view>
</view>
</view>
</view>
</uni-popup>
<!-- 返回按钮 -->
<view class="bottom-actions mx-15 mb-30">
<button class="back-btn" @click="goBack">返回</button>
</view>
</BasicLayout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import BasicLayout from "@/components/BasicLayout/Layout.vue";
import { jsdXkXsListApi } from "@/api/base/server";
import { BASE_IMAGE_URL } from "@/config";
import dayjs from "dayjs";
const { getJs } = useUserStore();
const { getData } = useDataStore();
const js = computed(() => getJs);
const xkkc = computed(() => getData);
//
const now = dayjs();
let wDay = now.day();
if (wDay === 0) {
wDay = 7;
}
const wdNameList = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
const todayInfo = ref({
date: now.format("YYYY-MM-DD"),
weekName: wdNameList[wDay - 1],
});
//
const studentList = ref<any[]>([]);
const currentStatType = ref<string>('');
const currentStatTitle = ref<string>('');
const currentStudentList = ref<any[]>([]);
//
const totalStudents = computed(() => studentList.value.length);
const presentStudents = computed(() => studentList.value.filter(s => s.xszt === '正常').length);
const leaveStudents = computed(() => studentList.value.filter(s => s.xszt === '请假').length);
const absentStudents = computed(() => studentList.value.filter(s => s.xszt === '缺勤').length);
//
const studentPopup = ref<any>(null);
// URL
const getImageUrl = (path: string) => {
if (!path) return '';
if (path.startsWith('http')) return path;
return BASE_IMAGE_URL + path;
};
//
const getStatusClass = (status: string) => {
switch (status) {
case '正常': return 'status-normal';
case '请假': return 'status-leave';
case '缺勤': return 'status-absent';
default: return 'status-normal';
}
};
//
const getStatusText = (status: string) => {
switch (status) {
case '正常': return '正常';
case '请假': return '请假';
case '缺勤': return '缺勤';
default: return '正常';
}
};
//
const showStudentList = (type: string) => {
currentStatType.value = type;
switch (type) {
case 'total':
currentStatTitle.value = '总人数学生列表';
currentStudentList.value = studentList.value;
break;
case 'present':
currentStatTitle.value = '实到学生列表';
currentStudentList.value = studentList.value.filter(s => s.xszt === '正常');
break;
case 'leave':
currentStatTitle.value = '请假学生列表';
currentStudentList.value = studentList.value.filter(s => s.xszt === '请假');
break;
case 'absent':
currentStatTitle.value = '缺勤学生列表';
currentStudentList.value = studentList.value.filter(s => s.xszt === '缺勤');
break;
}
studentPopup.value.open();
};
//
const closeStudentList = () => {
studentPopup.value.close();
};
//
const goBack = () => {
uni.navigateBack();
};
//
const loadStudentList = async () => {
try {
uni.showLoading({ title: '加载中...' });
const res = await jsdXkXsListApi({
xkkcId: xkkc.value.id,
date: todayInfo.value.date
});
if (res && res.resultCode === 1) {
studentList.value = res.result || [];
} else {
studentList.value = [];
uni.showToast({
title: res?.message || '获取学生列表失败',
icon: 'none'
});
}
} catch (error) {
console.error('加载学生列表失败:', error);
studentList.value = [];
uni.showToast({
title: '加载学生列表失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
};
onMounted(() => {
loadStudentList();
});
</script>
<style scoped lang="scss">
.course-card {
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.course-icon {
width: 30px;
height: 30px;
border-radius: 4px;
background-color: rgba(64, 128, 255, 0.1);
}
.record-stats {
.stats-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
padding-left: 10px;
border-left: 4px solid #4080ff;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
}
.stat-item {
background: white;
border-radius: 12px;
padding: 20px 15px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
cursor: pointer;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
&.total {
border: 2px solid #666;
.stat-number { color: #333; }
}
&.present {
border: 2px solid #2879ff;
.stat-number { color: #2879ff; }
}
&.leave {
border: 2px solid #ff9900;
.stat-number { color: #ff9900; }
}
&.absent {
border: 2px solid #ff4d4f;
.stat-number { color: #ff4d4f; }
}
.stat-number {
font-size: 24px;
font-weight: bold;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: #666;
}
}
}
.student-popup {
background: white;
border-radius: 20px 20px 0 0;
max-height: 70vh;
overflow: hidden;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #eee;
.popup-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.close-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f5f5f5;
cursor: pointer;
}
}
.student-list {
padding: 0 20px 20px;
max-height: 60vh;
overflow-y: auto;
.student-item {
display: flex;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.student-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
margin-right: 15px;
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-text {
width: 100%;
height: 100%;
background: #4080ff;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
}
}
.student-info {
flex: 1;
.student-name {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.student-class {
font-size: 14px;
color: #666;
}
}
.student-status {
.status-normal {
color: #2879ff;
background: rgba(40, 121, 255, 0.1);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-leave {
color: #ff9900;
background: rgba(255, 153, 0, 0.1);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-absent {
color: #ff4d4f;
background: rgba(255, 77, 79, 0.1);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
}
}
}
}
.bottom-actions {
.back-btn {
width: 100%;
height: 44px;
background: #4080ff;
color: white;
border: none;
border-radius: 22px;
font-size: 16px;
font-weight: 500;
}
}
//
.mx-15 { margin-left: 15px; margin-right: 15px; }
.my-15 { margin-top: 15px; margin-bottom: 15px; }
.mb-15 { margin-bottom: 15px; }
.mb-30 { margin-bottom: 30px; }
.mr-10 { margin-right: 10px; }
.ml-10 { margin-left: 10px; }
.bg-white { background-color: white; }
.white-bg-color { background-color: white; }
.r-md { border-radius: 8px; }
.p-15 { padding: 15px; }
.flex-row { display: flex; flex-direction: row; }
.items-center { align-items: center; }
.font-16 { font-size: 16px; }
.font-14 { font-size: 14px; }
.font-bold { font-weight: bold; }
.cor-999 { color: #999; }
</style>

View File

@ -0,0 +1,419 @@
<template>
<BasicLayout>
<!-- 课程信息卡片 -->
<view class="mb-5">
<view class="flex-row items-center white-bg-color p-15 r-md">
<!-- 左侧课程图片 -->
<view class="course-image-section" v-if="xkkc.lxtp">
<image
:src="getImageUrl(xkkc.lxtp)"
class="course-image"
mode="aspectFill"
@error="handleImageError"
/>
</view>
<view class="flex-col ml-10 flex-1 course-info">
<view class="course-name">{{ xkkc.kcmc }}</view>
<view class="course-info-item">
<view class="info-label">上课周期</view>
<view class="info-data">{{ xkkc.skzqmc }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课时间</view>
<view class="info-data">{{ formatClassTime(xkkc.skkstime, xkkc.skjstime) }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课地点</view>
<view class="info-data">{{ xkkc.kcdd }}</view>
</view>
</view>
</view>
</view>
<!-- 拍照和视频功能区域 -->
<view class="p-15">
<!-- 现场拍照 -->
<view class="section-card mb-15">
<view class="section-title">现场拍照</view>
<view class="photo-section">
<view class="photo-preview" v-if="photoList.length > 0">
<view
v-for="(photo, index) in photoList"
:key="index"
class="photo-item"
>
<image
:src="photo.url"
class="photo-image"
mode="aspectFill"
/>
<view class="photo-actions">
<view class="action-btn delete-btn" @click="deletePhoto(index)">
<u-icon name="trash" size="16" color="#fff"></u-icon>
</view>
</view>
</view>
</view>
<view class="photo-upload" @click="takePhoto">
<u-icon name="camera" size="32" color="#999"></u-icon>
<text class="upload-text">点击拍照</text>
</view>
</view>
</view>
<!-- 现场视频 -->
<view class="section-card mb-15">
<view class="section-title">现场视频</view>
<view class="video-section">
<view class="video-preview" v-if="videoList.length > 0">
<view
v-for="(video, index) in videoList"
:key="index"
class="video-item"
>
<video
:src="video.url"
class="video-player"
controls
show-center-play-btn
></video>
<view class="video-actions">
<view class="action-btn delete-btn" @click="deleteVideo(index)">
<u-icon name="trash" size="16" color="#fff"></u-icon>
</view>
</view>
</view>
</view>
<view class="video-upload" @click="recordVideo">
<u-icon name="videocam" size="32" color="#999"></u-icon>
<text class="upload-text">点击录制</text>
</view>
</view>
</view>
</view>
<template #bottom>
<view class="white-bg-color py-5">
<view class="flex-row items-center pb-10 pt-5">
<u-button text="返回" class="ml-15 mr-7" :plain="true" @click="navigateBack" />
<u-button text="提交" class="mr-15 mr-7" type="primary" @click="submit" />
</view>
</view>
</template>
</BasicLayout>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import { navigateBack } from "@/utils/uniapp";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import { jsdXkkcPhotoSaveApi } from "@/api/base/server";
import dayjs from "dayjs";
import { BASE_IMAGE_URL } from "@/config";
const { getJs } = useUserStore();
const { getData } = useDataStore();
const js = computed(() => getJs)
const xkkc = ref<any>({})
//
const photoList = ref<Array<{url: string, path: string}>>([])
const videoList = ref<Array<{url: string, path: string}>>([])
//
const formatClassTime = (startTime: string, endTime: string) => {
if (!startTime || !endTime) {
return '';
}
try {
let start, end;
if (startTime.includes(':') && !startTime.includes('-') && !startTime.includes('/')) {
start = startTime;
end = endTime;
} else {
const startDate = dayjs(startTime);
const endDate = dayjs(endTime);
if (startDate.isValid() && endDate.isValid()) {
start = startDate.format('HH:mm:ss');
end = endDate.format('HH:mm:ss');
} else {
return `${startTime}~${endTime}`;
}
}
return `${start}~${end}`;
} catch (error) {
console.error('时间格式化错误:', error);
return `${startTime}~${endTime}`;
}
};
// URL
const getImageUrl = (imagePath: string) => {
if (!imagePath) {
return '';
}
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
return imagePath;
}
return `${BASE_IMAGE_URL}${imagePath}`;
};
//
const handleImageError = () => {
console.log('课程图片加载失败');
};
//
const takePhoto = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0];
photoList.value.push({
url: tempFilePath,
path: tempFilePath
});
},
fail: (err) => {
console.error('拍照失败:', err);
uni.showToast({
title: '拍照失败',
icon: 'none'
});
}
});
};
//
const recordVideo = () => {
uni.chooseVideo({
sourceType: ['camera'],
maxDuration: 60, // 60
camera: 'back',
success: (res) => {
const tempFilePath = res.tempFilePath;
videoList.value.push({
url: tempFilePath,
path: tempFilePath
});
},
fail: (err) => {
console.error('录制视频失败:', err);
uni.showToast({
title: '录制视频失败',
icon: 'none'
});
}
});
};
//
const deletePhoto = (index: number) => {
photoList.value.splice(index, 1);
};
//
const deleteVideo = (index: number) => {
videoList.value.splice(index, 1);
};
//
const submit = async () => {
if (photoList.value.length === 0 && videoList.value.length === 0) {
uni.showToast({
title: '请至少拍摄一张照片或录制一段视频',
icon: 'none',
duration: 2000
});
return;
}
try {
// API
const submitData = {
xkkcId: xkkc.value.id,
jsId: js.value.id,
photoList: photoList.value,
videoList: videoList.value,
submitTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
};
// API
const res = await jsdXkkcPhotoSaveApi(submitData);
if (res && res.resultCode === 1) {
uni.showToast({
title: '提交成功',
icon: 'success',
duration: 2000
});
//
setTimeout(() => {
uni.navigateBack({
delta: 1
});
}, 1500);
} else {
uni.showToast({
title: res?.resultMessage || '提交失败',
icon: 'none',
duration: 2000
});
}
} catch (error) {
console.error('提交失败:', error);
uni.showToast({
title: '提交失败,请重试',
icon: 'none',
duration: 2000
});
}
};
//
onMounted(() => {
xkkc.value = getData;
});
</script>
<style lang="scss" scoped>
.course-info {
flex: 1;
.course-name {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 10px;
}
.course-info-item {
display: flex;
margin-bottom: 12px;
font-size: 12px;
align-items: center;
.info-label {
color: #949AA4;
flex: 0 0 auto;
margin-right: 4px;
white-space: nowrap;
}
.info-data {
flex: 0 0 auto;
color: #333;
font-weight: 400;
}
}
}
.course-image-section {
flex: 0 0 100px;
margin-right: 15px;
display: flex;
align-items: center;
justify-content: center;
.course-image {
width: 80px;
height: 80px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #f0f0f0;
}
}
.section-card {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
padding-left: 8px;
border-left: 3px solid #007aff;
}
.photo-section, .video-section {
.photo-preview, .video-preview {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 15px;
}
.photo-item, .video-item {
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.photo-image, .video-player {
width: 100%;
height: 100px;
border-radius: 8px;
}
.video-player {
background-color: #000;
}
.photo-actions, .video-actions {
position: absolute;
top: 5px;
right: 5px;
.action-btn {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.delete-btn {
background-color: rgba(255, 0, 0, 0.8);
}
}
}
}
.photo-upload, .video-upload {
width: 100px;
height: 100px;
border: 2px dashed #ddd;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #007aff;
background-color: rgba(0, 122, 255, 0.05);
}
.upload-text {
font-size: 12px;
color: #999;
margin-top: 5px;
}
}
}
</style>

View File

@ -3,12 +3,13 @@
<!-- 选课信息头部 - 固定部分 -->
<view class="selection-header">
<view class="header-content">
<view class="title-section" @click="clickShowXkSelector">
<view class="title-section">
<view class="title">
<text v-if="xkData && xkData.xkmc">{{ xkData.xkmc }}</text>
<text v-else>选课信息</text>
<text v-if="xkkcList && xkkcList.length > 0">
{{ getCurrentSemesterName() }} - 我的课程
</text>
<text v-else>暂无课程</text>
</view>
<view class="switch-btn" v-if="xkList.length > 1">切换</view>
</view>
</view>
</view>
@ -18,22 +19,17 @@
<!-- 课程网格列表 -->
<view class="course-list" v-if="xkkcList && xkkcList.length > 0">
<view v-for="(xkkc, index) in xkkcList" :key="xkkc.id || index" class="course-item">
<view class="course-name">{{ xkkc.kcmc }}</view>
<view class="course-info-item">
<view class="info-label">上课周期类型</view>
<view class="info-data">{{ xkkc.skzqlx }}</view>
<view class="status-badge" :class="getStatusClass(xkkc)">
{{ getStatusText(xkkc) }}
</view>
<view class="course-name">{{ xkkc.kcmc }}</view>
<view class="course-info-item">
<view class="info-label">上课周期</view>
<view class="info-data">{{ xkkc.skzqmc }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课开始时间</view>
<view class="info-data">{{ xkkc.skkstime }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课结束时间</view>
<view class="info-data">{{ xkkc.skjstime }}</view>
<view class="info-label">上课时间</view>
<view class="info-data">{{ formatClassTime(xkkc.skkstime, xkkc.skjstime) }}</view>
</view>
<view class="course-info-item">
<view class="info-label">开课地点</view>
@ -43,8 +39,9 @@
<view class="info-label">上课人数</view>
<view class="info-data">{{ xkkc.hasNum || 0 }} | {{ xkkc.maxNum || 0 }}</view>
</view>
<view class="separator-line"></view>
<view class="course-btn-group">
<view class="detail-btn" @click.stop="goDetail(xkkc)">详情</view>
<view class="detail-btn" @click.stop="goDetail(xkkc)">填报</view>
</view>
</view>
</view>
@ -58,29 +55,6 @@
</view>
</view>
<view>
<!-- 俱乐部选择弹窗 -->
<u-popup :show="showXkFlag" @close="showXkFlag = false" mode="bottom" round="10">
<view class="xk-selector">
<view class="selector-header">
<text class="selector-title">选择俱乐部</text>
<u-icon name="close" size="20" @click="showXkFlag = false"></u-icon>
</view>
<view class="xk-list">
<view v-for="(xk, index) in xkList" :key="index" class="xk-item" :class="{
'xk-item-active': xkData.id === xk.id
}" @click="switchXk(xk)">
<view class="xk-info">
<text class="xk-name">{{ xk.xkmc }}</text>
<text class="xk-type">{{ xk.xkmc }}</text>
</view>
<u-icon v-if="xkData.id === xk.id" name="checkmark" color="#409EFF" size="20"></u-icon>
</view>
</view>
</view>
</u-popup>
</view>
</view>
</template>
@ -92,7 +66,7 @@ import {
} from "vue";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import { jsdXkListApi } from "@/api/base/server";
import { getCurrentSemesterTeacherCoursesApi } from "@/api/base/server";
import dayjs from "dayjs";
const { getJs } = useUserStore();
@ -100,15 +74,6 @@ const { getData, setData } = useDataStore();
const wdNameList = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
//
const showXkFlag = ref(false);
const xkList = ref<any>([]);
const xkData = ref();
const courseInfo = ref<any>({});
//
const xkkcList = ref<any[]>([]);
@ -121,59 +86,118 @@ const loadCourseList = async () => {
uni.showLoading({
title: "加载中...",
});
const res = await jsdXkListApi({
jsId: getJs.id
});
if (res.resultCode == 1) {
if (res.result && res.result.length) {
xkList.value = res.result;
switchXk(res.result[0]);
} else {
xkList.value = [];
xkData.value = {};
xkkcList.value = [];
}
}
uni.hideLoading();
try {
const res = await getCurrentSemesterTeacherCoursesApi(getJs.id);
if (res.resultCode == 1) {
if (res.result && res.result.length) {
xkkcList.value = res.result;
//
processCoursePeriods();
} else {
xkkcList.value = [];
}
} else {
xkkcList.value = [];
uni.showToast({
title: res.resultMessage || '获取课程列表失败',
icon: 'none'
});
}
} catch (error) {
console.error('加载课程列表失败:', error);
xkkcList.value = [];
uni.showToast({
title: '加载课程列表失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
};
//
function clickShowXkSelector() {
if (xkList.value.length > 1) {
showXkFlag.value = true;
//
const processCoursePeriods = () => {
for (let i = 0; i < xkkcList.value.length; i++) {
let xkkc = xkkcList.value[i];
//
switch (xkkc.skzqlx) {
case '每天':
xkkc.skzqmc = "每天";
break;
case '每周':
const daysOfWeek = xkkc.skzq.split(',').map(Number);
// wdNameListdaysOfWeek
xkkc.skzqmc = daysOfWeek.map((day: number) => wdNameList[day - 1]).join(',');
break;
case '每月':
const daysOfMonth = xkkc.skzq.split(',').map(Number);
//
xkkc.skzqmc = daysOfMonth.map((day: number) => day + "号").join(',');
break;
}
}
}
};
//
function switchXk(xk: any) {
xkData.value = xk;
xkkcList.value = xk.xkkcs;
showXkFlag.value = false;
for (let i = 0; i < xk.xkkcs.length; i++) {
let xkkc = xk.xkkcs[i];
//
switch (xkkc.skzqlx) {
case '每天':
xkkc.skzqmc = "每天";
break;
case '每周':
const daysOfWeek = xkkc.skzq.split(',').map(Number);
// wdNameListdaysOfWeek
xkkc.skzqmc = daysOfWeek.map((day: number) => wdNameList[day - 1]).join(',');
break;
case '每月':
const daysOfMonth = xkkc.skzq.split(',').map(Number);
//
xkkc.skzqmc = daysOfMonth.map((day: number) => day + "号").join(',');
break;
}
}
//
uni.showToast({
title: `已切换到${xk.xkmc}`,
icon: "none",
});
}
//
const getCurrentSemesterName = () => {
if (xkkcList.value && xkkcList.value.length > 0) {
//
return xkkcList.value[0].xqmc || '当前学期';
}
return '当前学期';
};
//
const formatClassTime = (startTime: string, endTime: string) => {
if (!startTime || !endTime) {
return '';
}
try {
//
let start, end;
// HH:mm:ss HH:mm
if (startTime.includes(':') && !startTime.includes('-') && !startTime.includes('/')) {
start = startTime;
end = endTime;
} else {
// dayjs
const startDate = dayjs(startTime);
const endDate = dayjs(endTime);
if (startDate.isValid() && endDate.isValid()) {
start = startDate.format('HH:mm:ss');
end = endDate.format('HH:mm:ss');
} else {
//
return `${startTime}~${endTime}`;
}
}
return `${start}~${end}`;
} catch (error) {
console.error('时间格式化错误:', error);
//
return `${startTime}~${endTime}`;
}
};
//
const getStatusText = (xkkc: any) => {
if (!xkkc.kcjsms && !xkkc.jxll) {
return '待填报';
}
return '已填报';
};
//
const getStatusClass = (xkkc: any) => {
if (!xkkc.kcjsms && !xkkc.jxll) {
return 'pending';
}
return 'completed';
};
//
const goDetail = (xkkc: any) => {
@ -191,7 +215,7 @@ onBeforeUnmount(() => {
<style lang="scss" scoped>
.interest-course {
min-height: 100%;
background-color: #f5f7fa;
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
display: flex;
flex-direction: column;
height: 100%;
@ -227,46 +251,50 @@ onBeforeUnmount(() => {
}
.selection-header {
background: linear-gradient(135deg, #4a90e2, #2879ff);
padding: 20px 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 25px 20px;
color: #fff;
border-radius: 0 0 15px 15px;
box-shadow: 0 4px 12px rgba(40, 121, 255, 0.2);
border-radius: 0 0 20px 20px;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 10;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, rgba(255, 255, 255, 0.1) 0%, transparent 50%, rgba(255, 255, 255, 0.05) 100%);
pointer-events: none;
}
.header-content {
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
z-index: 1;
.title-section {
display: flex;
align-items: center;
justify-content: center;
.title {
flex: 1 0 1px;
font-size: 24px;
font-weight: bold;
}
.subtitle {
font-size: 14px;
opacity: 0.8;
}
.switch-btn {
padding: 5px 15px;
background-color: rgba(255, 255, 255, 0.2);
color: #fff;
border-radius: 15px;
font-size: 15px;
font-size: 20px;
font-weight: 700;
text-align: center;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
letter-spacing: 0.5px;
}
}
}
}
@ -283,13 +311,21 @@ onBeforeUnmount(() => {
.course-item {
position: relative;
width: 100%;
margin-bottom: 15px;
margin-bottom: 20px;
background-color: #fff;
border-radius: 8px;
padding: 15px;
border-radius: 12px;
padding: 20px;
box-sizing: border-box;
border: 1px solid transparent;
transition: all 0.3s ease;
border: 1px solid #f0f0f0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
animation: fadeInUp 0.6s ease-out;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
border-color: #e8e8e8;
}
// &:nth-child(odd) {
// margin-right: 10px;
@ -300,111 +336,153 @@ onBeforeUnmount(() => {
// }
&.selected {
border: 1px solid #3fbf72;
background-color: rgba(63, 191, 114, 0.05);
box-shadow: 0 2px 8px rgba(63, 191, 114, 0.15);
border: 2px solid #3fbf72;
background-color: rgba(63, 191, 114, 0.02);
box-shadow: 0 4px 20px rgba(63, 191, 114, 0.15);
}
.status-badge {
position: absolute;
top: 15px;
right: 15px;
background-color: #fff;
border-radius: 20px;
padding: 6px 12px;
font-size: 11px;
font-weight: 600;
border: 1px solid #e0e0e0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
letter-spacing: 0.5px;
animation: fadeInRight 0.5s ease-out 0.2s both;
}
.status-badge.pending {
color: #f56c6c;
border-color: #fde2e2;
background-color: #fef0f0;
}
.status-badge.completed {
color: #67c23a;
border-color: #e1f3d8;
background-color: #f0f9ff;
}
.course-name {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 10px;
font-size: 17px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 15px;
padding-right: 90px;
line-height: 1.4;
animation: fadeInLeft 0.5s ease-out 0.1s both;
}
.course-btn-group {
display: flex;
justify-content: flex-end;
.detail-btn {
flex: 1 0 1px;
display: inline-block;
color: #2879ff;
font-size: 14px;
font-weight: 600;
padding: 8px 18px;
border-radius: 8px;
background: linear-gradient(135deg, rgba(40, 121, 255, 0.1), rgba(40, 121, 255, 0.05));
border: 1px solid #2879ff;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(40, 121, 255, 0.15);
animation: fadeInUp 0.5s ease-out 0.3s both;
&:hover {
background: linear-gradient(135deg, rgba(40, 121, 255, 0.15), rgba(40, 121, 255, 0.1));
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(40, 121, 255, 0.25);
}
&:active {
background: linear-gradient(135deg, rgba(40, 121, 255, 0.2), rgba(40, 121, 255, 0.15));
transform: translateY(0);
box-shadow: 0 2px 8px rgba(40, 121, 255, 0.2);
}
}
}
.course-info-item {
display: flex;
margin-bottom: 12px;
font-size: 12px;
margin-bottom: 14px;
font-size: 13px;
align-items: center;
animation: fadeInUp 0.5s ease-out 0.15s both;
.info-label {
color: #949AA4;
color: #666;
flex: 0 0 100px;
font-weight: 500;
}
.info-data {
flex: 1 0 1px;
color: #333;
font-weight: 400;
}
}
.separator-line {
height: 1px;
background: linear-gradient(90deg, transparent, #e8e8e8, transparent);
margin: 18px 0;
opacity: 0.8;
animation: fadeIn 0.5s ease-out 0.25s both;
}
}
}
/* 学生选择器弹窗样式 */
.xk-selector {
background-color: #ffffff;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
padding-bottom: 20px;
.selector-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f2f2f2;
.selector-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
//
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
.xk-list {
padding: 0 15px;
.xk-item {
display: flex;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #f2f2f2;
&:last-child {
border-bottom: none;
}
&-active {
background-color: rgba(64, 158, 255, 0.05);
}
.xk-info {
flex: 1;
margin-left: 12px;
.xk-name {
font-size: 15px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.xk-type {
font-size: 13px;
color: #303133;
margin-bottom: 4px;
}
.xk-class {
font-size: 13px;
color: #606266;
}
}
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 全局图片样式 */
.w-full {
width: 100%;
@ -420,32 +498,35 @@ onBeforeUnmount(() => {
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
padding: 80px 20px;
text-align: center;
.empty-icon {
margin-bottom: 20px;
background-color: #f5f6f7;
width: 80px;
height: 80px;
margin-bottom: 25px;
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
width: 90px;
height: 90px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 2px solid rgba(255, 255, 255, 0.8);
}
.empty-text {
font-size: 18px;
font-weight: 500;
color: #303133;
font-weight: 600;
color: #475569;
margin-bottom: 8px;
letter-spacing: 0.3px;
}
.empty-desc {
font-size: 14px;
color: #909399;
color: #64748b;
max-width: 80%;
line-height: 1.5;
line-height: 1.6;
}
}

View File

@ -4,70 +4,103 @@
</template> -->
<view class="mb-5">
<view class="flex-row items-center white-bg-color p-15 r-md">
<!-- 左侧课程图片 -->
<view class="course-image-section" v-if="xkkc.lxtp">
<image
:src="getImageUrl(xkkc.lxtp)"
class="course-image"
mode="aspectFill"
@error="handleImageError"
/>
</view>
<view class="flex-col ml-10 flex-1 course-info">
<view class="course-name">{{ xkkc.kcmc }}</view>
<view class="course-info-item">
<view class="info-label">上课周期类型</view>
<view class="info-data">{{ xkkc.skzqlx }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课周期</view>
<view class="info-data">{{ xkkc.skzqmc }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课开始时间</view>
<view class="info-data">{{ xkkc.skkstime }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课结束时间</view>
<view class="info-data">{{ xkkc.skjstime }}</view>
</view>
<view class="course-info-item">
<view class="info-label">开课老师</view>
<view class="info-data">{{ js.jsxm }}</view>
</view>
<view class="course-info-item">
<view class="info-label">开课地点</view>
<view class="info-data">{{ xkkc.kcdd }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课人数</view>
<view class="info-data">{{ xkkc.hasNum || 0 }} | {{ xkkc.maxNum || 0 }}</view>
</view>
<view class="course-info-item">
<view class="info-label">金额</view>
<view class="info-data cor-FF8D02">¥{{ xkkc.kcje }}</view>
<view class="info-label">上课时间</view>
<view class="info-data">{{ formatClassTime(xkkc.skkstime, xkkc.skjstime) }}</view>
</view>
</view>
</view>
</view>
<view class="p-15">
<BasicForm @register="register">
<template #jxjh>
<view class="back-f8f8f8">
<view class="flex-row items-center justify-between py-15 global-bg-color">
<view>
<BasicTitle line title="教学计划" :isBorder="false" />
</view>
<view @click="addEducation">
<BasicIcon type="icon-tianjia" size="25" />
</view>
</view>
<view v-if="education.xl.length > 0">
<template v-for="(item, index) in education.xl" :key="index">
<view class="po-re mb-15">
<BasicForm v-model="item.value" :schema="schema" :formsProps="{ labelWidth: 100 }" />
<view @click="deleteMemberFamily(index as number, item.value)" class="delete-icon mt-5">
<BasicIcon type="clear" size="30" />
</view>
</view>
</template>
</view>
<view v-else class="p-15 flex-row-center color-9 font-13 white-bg-color">教学计划暂无数据</view>
</view>
</template>
</BasicForm>
</view>
<!-- 四个主要部分 -->
<view class="p-15">
<!-- 第一部分教师信息 -->
<view class="section-card mb-15">
<view class="section-title">教师信息 <text class="required">*</text></view>
<view class="teacher-info-header">
<!-- 教师头像 -->
<view class="teacher-avatar-section" v-if="teacherHeadPic">
<image
:src="getImageUrl(teacherHeadPic)"
class="teacher-avatar"
mode="aspectFill"
@error="handleTeacherAvatarError"
/>
</view>
<!-- 右侧信息 -->
<view class="teacher-details">
<view class="detail-row">
<view class="detail-item">
<text class="detail-label">开课老师</text>
<text class="detail-value">{{ js.jsxm }}</text>
</view>
<view class="detail-item">
<text class="detail-label">上课地点</text>
<text class="detail-value">{{ xkkc.kcdd }}</text>
</view>
<view class="detail-item">
<text class="detail-label">上课人数</text>
<text class="detail-value">{{ xkkc.hasNum || 0 }} | {{ xkkc.maxNum || 0 }}</text>
</view>
</view>
</view>
</view>
<!-- 老师信息输入框单独一行 -->
<view class="teacher-input-row">
<textarea
v-model="xkkc.kcjsms"
placeholder="请输入教师信息(必填)"
class="form-textarea"
/>
</view>
</view>
<!-- 第二部分教学理念 -->
<view class="section-card mb-15">
<view class="section-title">教学理念 <text class="required">*</text></view>
<textarea
v-model="xkkc.jxll"
placeholder="请输入教学理念(必填)"
class="form-textarea"
/>
</view>
<!-- 第三部分教学计划 -->
<view class="section-card mb-15">
<view class="section-header">
<view class="section-title">教学计划 <text class="required">*</text></view>
<view @click="addEducation" class="add-icon">
<BasicIcon type="icon-tianjia" size="25" />
</view>
</view>
<view v-if="education.xl.length > 0">
<template v-for="(item, index) in education.xl" :key="index">
<view class="po-re mb-15">
<BasicForm v-model="item.value" :schema="schema" :formsProps="{ labelWidth: 100 }" />
<view @click="deleteMemberFamily(index as number, item.value)" class="delete-icon mt-5">
<BasicIcon type="clear" size="30" />
</view>
</view>
</template>
</view>
<view v-else class="p-15 flex-row-center color-9 font-13 white-bg-color">教学计划暂无数据</view>
</view>
</view>
<template #bottom>
<view class="white-bg-color py-5">
<view class="flex-row items-center pb-10 pt-5">
@ -87,36 +120,76 @@ import { cloneDeep, get } from "lodash";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import { jsdXkkcSaveApi } from "@/api/base/server";
import dayjs from "dayjs";
import { BASE_IMAGE_URL } from "@/config";
const { getJs } = useUserStore();
const { getData } = useDataStore();
const js = computed(() => getJs)
const xkkc = ref<any>({})
//
const teacherHeadPic = ref<string>('')
// localStorage
const getTeacherHeadPic = () => {
try {
console.log('开始获取教师头像...');
const appUserStr = uni.getStorageSync('app-user')
console.log('app-user原始数据:', appUserStr);
// JSON
let appUser;
try {
appUser = typeof appUserStr === 'string' ? JSON.parse(appUserStr) : appUserStr;
} catch (parseError) {
console.error('JSON解析失败:', parseError);
return;
}
console.log('解析后的appUser数据:', appUser);
//
console.log('appUser存在:', !!appUser);
console.log('appUser类型:', typeof appUser);
console.log('appUser的所有键:', Object.keys(appUser));
if (appUser) {
console.log('appUser.userdata存在:', !!appUser.userdata);
console.log('appUser.userdata类型:', typeof appUser.userdata);
if (appUser.userdata) {
console.log('appUser.userdata的所有键:', Object.keys(appUser.userdata));
console.log('appUser.userdata.js存在:', !!appUser.userdata.js);
if (appUser.userdata.js) {
console.log('appUser.userdata.js的所有键:', Object.keys(appUser.userdata.js));
console.log('appUser.userdata.js.headPic存在:', !!appUser.userdata.js.headPic);
console.log('headPic值:', appUser.userdata.js.headPic);
}
}
}
// app-useruserdata
if (appUser && appUser.userdata && appUser.userdata.js) {
console.log('userdata.js数据:', appUser.userdata.js);
if (appUser.userdata.js.headPic) {
console.log('找到headPic:', appUser.userdata.js.headPic);
teacherHeadPic.value = appUser.userdata.js.headPic
console.log('设置teacherHeadPic为:', teacherHeadPic.value);
} else {
console.log('userdata.js中没有headPic字段');
}
} else {
console.log('没有找到userdata或js');
}
} catch (error) {
console.error('获取教师头像失败:', error)
}
}
const [register, { getValue, setValue }] = useForm({
schema: [
{
field: "kcjsms",
label: "老师信息",
component: "BasicInput",
itemProps: {
labelPosition: "top",
},
componentProps: {
type: "textarea",
},
},
{
field: "jxll",
label: "教学理念",
component: "BasicInput",
itemProps: {
labelPosition: "top",
},
componentProps: {
type: "textarea",
},
},
// kcjsmsjxlltextarea
{ colSlot: 'jxjh' },
],
});
@ -174,17 +247,135 @@ function deleteMemberFamily(index: number, item: any) {
education.xl = list
}
//
const formatClassTime = (startTime: string, endTime: string) => {
if (!startTime || !endTime) {
return '';
}
try {
//
let start, end;
// HH:mm:ss HH:mm
if (startTime.includes(':') && !startTime.includes('-') && !startTime.includes('/')) {
start = startTime;
end = endTime;
} else {
// dayjs
const startDate = dayjs(startTime);
const endDate = dayjs(endTime);
if (startDate.isValid() && endDate.isValid()) {
start = startDate.format('HH:mm:ss');
end = endDate.format('HH:mm:ss');
} else {
//
return `${startTime}~${endTime}`;
}
}
return `${start}~${end}`;
} catch (error) {
console.error('时间格式化错误:', error);
//
return `${startTime}~${endTime}`;
}
};
// URL
const getImageUrl = (imagePath: string) => {
console.log('getImageUrl被调用参数:', imagePath);
console.log('BASE_IMAGE_URL:', BASE_IMAGE_URL);
if (!imagePath) {
console.log('图片路径为空,返回空字符串');
return '';
}
// URL
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
console.log('已经是完整URL直接返回:', imagePath);
return imagePath;
}
// BASE_IMAGE_URL
const fullUrl = `${BASE_IMAGE_URL}${imagePath}`;
console.log('拼接后的完整URL:', fullUrl);
return fullUrl;
};
//
const handleImageError = () => {
console.log('课程图片加载失败');
};
//
const handleTeacherAvatarError = () => {
console.log('教师头像加载失败');
};
const submit = async () => {
const data = await getValue()
xkkc.value.kcjsms = data.kcjsms;
xkkc.value.jxll = data.jxll;
//
if (!xkkc.value.kcjsms || xkkc.value.kcjsms.trim() === '') {
uni.showToast({
title: '请输入教师信息',
icon: 'none',
duration: 2000
});
return;
}
if (!xkkc.value.jxll || xkkc.value.jxll.trim() === '') {
uni.showToast({
title: '请输入教学理念',
icon: 'none',
duration: 2000
});
return;
}
if (!education.xl || education.xl.length === 0) {
uni.showToast({
title: '请添加教学计划',
icon: 'none',
duration: 2000
});
return;
}
//
for (let i = 0; i < education.xl.length; i++) {
const item = education.xl[i];
if (!item.value.jhjd || item.value.jhjd.trim() === '' ||
!item.value.jhsj || item.value.jhsj.trim() === '' ||
!item.value.jhdd || item.value.jhdd.trim() === '' ||
!item.value.jhnr || item.value.jhnr.trim() === '') {
uni.showToast({
title: `教学计划第${i + 1}项信息不完整`,
icon: 'none',
duration: 2000
});
return;
}
}
// 使v-modelgetValue()
// xkkc.value.kcjsms xkkc.value.jxll v-model
xkkc.value.jxjh = JSON.stringify(education.xl)
await jsdXkkcSaveApi(xkkc.value).then(res => {
uni.showToast({
title: '提交成功',
icon: 'success',
duration: 2000
})
//
setTimeout(() => {
uni.navigateBack({
delta: 1
});
}, 1500); // 1.5
}).catch(err => {
uni.showToast({
title: '提交失败',
@ -195,14 +386,59 @@ const submit = async () => {
}
//
onMounted(() => {
console.log('页面初始化开始...');
xkkc.value = getData;
setValue(getData);
// xkkcjxjhjson
if (xkkc.value.jxjh && xkkc.value.jxjh.length > 0) {
education.xl = JSON.parse(xkkc.value.jxjh)
//
if (xkkc.value.jxjh) {
try {
// jxjh
let jxjhData = xkkc.value.jxjh;
if (typeof jxjhData === 'string') {
jxjhData = JSON.parse(jxjhData);
}
//
if (Array.isArray(jxjhData)) {
education.xl = jxjhData.map(item => {
// itemvalue
if (item.value) {
return {
value: {
jhjd: item.value.jhjd || '',
jhsj: item.value.jhsj || '',
jhdd: item.value.jhdd || '',
jhnr: item.value.jhnr || ''
}
};
}
return item;
});
console.log('教学计划数据回显成功:', education.xl);
} else {
education.xl = [];
console.log('教学计划数据格式不正确,初始化为空数组');
}
} catch (error) {
console.error('解析教学计划数据失败:', error);
education.xl = [];
}
} else {
education.xl = [];
console.log('没有教学计划数据,初始化为空数组');
}
console.log('调用getTeacherHeadPic前teacherHeadPic值:', teacherHeadPic.value);
getTeacherHeadPic(); //
console.log('调用getTeacherHeadPic后teacherHeadPic值:', teacherHeadPic.value);
//
setTimeout(() => {
console.log('延迟检查teacherHeadPic值:', teacherHeadPic.value);
console.log('teacherHeadPic是否存在:', !!teacherHeadPic.value);
console.log('当前教学计划数据:', education.xl);
}, 1000);
})
</script>
<style lang="scss">
@ -212,7 +448,9 @@ onMounted(() => {
top: 0;
z-index: 1;
}
.course-info {
flex: 1;
.course-name {
font-size: 16px;
@ -224,13 +462,157 @@ onMounted(() => {
display: flex;
margin-bottom: 12px;
font-size: 12px;
align-items: center;
.info-label {
color: #949AA4;
flex: 0 0 100px;
flex: 0 0 auto;
margin-right: 4px;
white-space: nowrap;
}
.info-data {
flex: 1 0 1px;
flex: 0 0 auto;
color: #333;
font-weight: 400;
}
}
}
.course-image-section {
flex: 0 0 100px;
margin-right: 15px;
display: flex;
align-items: center;
justify-content: center;
.course-image {
width: 80px;
height: 80px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #f0f0f0;
}
}
.teacher-name {
vertical-align: middle;
}
.teacher-info-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.teacher-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
}
.detail-row {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.detail-item {
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap;
}
.detail-label {
font-size: 12px;
color: #666;
font-weight: 500;
}
.detail-value {
font-size: 14px;
color: #333;
font-weight: 400;
}
.teacher-input-row {
margin-top: 15px;
}
.add-icon {
cursor: pointer;
color: #007aff;
}
.teacher-avatar-section {
flex: 0 0 60px;
display: flex;
align-items: center;
justify-content: center;
.teacher-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
border: 2px solid #e0e0e0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.teacher-input-section {
flex: 1;
}
.section-card {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
padding-left: 8px;
border-left: 3px solid #007aff;
.required {
color: #f56c6c;
font-weight: 700;
margin-left: 4px;
}
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.form-textarea {
width: 100%; /* 恢复为100%宽度 */
min-height: 80px;
padding: 12px 15px; /* 左右padding保持15px */
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
line-height: 1.5;
resize: vertical;
box-sizing: border-box; /* 确保padding和border包含在宽度内 */
&:focus {
outline: none;
border-color: #007aff;
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.1);
}
&::placeholder {
color: #999;
}
}
</style>

View File

@ -234,7 +234,7 @@ const sections = reactive<Section[]>([
{
id: "r7",
icon: "file-text-fill-2",
text: "课程介绍",
text: "课程填报",
show: true,
permissionKey: "routine-kcjs", //
path: "/pages/base/groupTeaching/xkList",

View File

@ -6,17 +6,17 @@
<view class="header-content">
<image
class="avatar-large"
:src="xsInfo?.avatar"
:src="imagUrl(xsInfo?.avatar)"
mode="aspectFill"
></image>
<text class="student-name">{{ xsInfo?.xm }}</text>
<view class="student-meta">
<uni-icons
:type="xsInfo?.gender === '' ? 'female' : 'male'"
:type="xsInfo?.gender === '2' ? 'female' : 'male'"
size="16"
:color="xsInfo?.gender === '' ? '#ff8d8f' : '#fff'"
:color="xsInfo?.gender === '2' ? '#ff8d8f' : '#fff'"
></uni-icons>
<text class="meta-text">{{ xsInfo?.gender }}</text>
<text class="meta-text">{{ getGenderText(xsInfo?.gender) }}</text>
<view class="separator"></view>
<text class="meta-text">{{ xsInfo?.age }}</text>
</view>
@ -35,7 +35,11 @@
</view>
<view class="info-item">
<text class="info-label">出生日期:</text>
<text class="info-value">{{ xsInfo?.birthDate }}</text>
<text class="info-value">{{ xsInfo?.birthDate || '未知' }}</text>
</view>
<view class="info-item">
<text class="info-label">年龄:</text>
<text class="info-value">{{ xsInfo?.age || '未知' }}</text>
</view>
<!-- <view class="info-item">
<text class="info-label">现居住地:</text>
@ -54,11 +58,16 @@
class="parent-card-item"
>
<view class="parent-avatar-container">
<!-- 如果有头像则显示图片否则显示文字头像 -->
<image
v-if="jz.avatar"
class="parent-avatar"
:src="imagUrl(jz.avatar)"
mode="aspectFill"
></image>
<view v-else class="parent-avatar-text">
{{ jz.jzxm }}
</view>
<view class="call-icon-overlay" @click="callParent(jz.jzsj)">
<uni-icons
type="phone-filled"
@ -67,7 +76,8 @@
></uni-icons>
</view>
</view>
<text class="parent-name">{{ jz.jzxm }}</text>
<!-- 去掉家长姓名显示 -->
<!-- <text class="parent-name">{{ jz.jzxm }}</text> -->
<text class="parent-relation">({{ jz.jzxsgxId }})</text>
</view>
</view>
@ -78,6 +88,7 @@
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import dayjs from "dayjs";
import { imagUrl } from "@/utils";
import { onLoad } from "@dcloudio/uni-app";
@ -87,21 +98,86 @@ const { getData } = useDataStore();
const xsInfo = computed(() => {
const xs = getData || {};
// 使dayjs
const birthDate = dayjs(xs.sfzh.substring(6, 10) + xs.sfzh.substring(10, 12) + xs.sfzh.substring(12, 14));
xs.birthDate = birthDate.format("YYYY-MM-DD");
xs.age = dayjs().diff(birthDate, "year");
return xs;
//
const studentInfo = {
id: xs.xsId || xs.id, //
xm: xs.xsxm || xs.xm, //
avatar: xs.xstx || xs.avatar, //
gender: xs.xb || xs.gender, //
njmc: xs.njmcName || xs.njmc || '', // 使njmcName
bjmc: xs.bjmc || '', //
sfzh: xs.sfzh || '', //
cstime: xs.cstime || '', //
birthDate: '', //
age: 0 //
};
// 使cstime
if (studentInfo.cstime) {
try {
const birthDate = dayjs(studentInfo.cstime);
if (birthDate.isValid()) {
studentInfo.birthDate = birthDate.format("YYYY-MM-DD");
studentInfo.age = dayjs().diff(birthDate, "year");
}
} catch (error) {
console.error("出生日期解析失败:", error);
}
} else if (studentInfo.sfzh && studentInfo.sfzh.length >= 14) {
// cstime
try {
const birthDate = dayjs(studentInfo.sfzh.substring(6, 10) + studentInfo.sfzh.substring(10, 12) + studentInfo.sfzh.substring(12, 14));
studentInfo.birthDate = birthDate.format("YYYY-MM-DD");
studentInfo.age = dayjs().diff(birthDate, "year");
} catch (error) {
console.error("身份证号解析失败:", error);
}
}
//
if (!studentInfo.gender && studentInfo.sfzh && studentInfo.sfzh.length >= 17) {
try {
const genderCode = parseInt(studentInfo.sfzh.substring(16, 17));
studentInfo.gender = genderCode % 2 === 0 ? '2' : '1'; // '2' '1'
} catch (error) {
console.error("性别计算失败:", error);
studentInfo.gender = '未知';
}
}
//
if (!studentInfo.gender) studentInfo.gender = '未知';
if (!studentInfo.njmc) studentInfo.njmc = '未知年级';
if (!studentInfo.bjmc) studentInfo.bjmc = '未知班级';
return studentInfo;
});
const jzList = ref<any[]>([]);
onLoad(async (options) => {
if (xsInfo && xsInfo.value && xsInfo.value.id) {
uni.showLoading({ title: "加载中..." });
const res = await xsJzListByXsIdApi({ xsId: xsInfo.value.id });
jzList.value = res.result || [];
uni.hideLoading();
if (xsInfo.value && xsInfo.value.id) {
try {
uni.showLoading({ title: "加载中..." });
const res = await xsJzListByXsIdApi({ xsId: xsInfo.value.id });
if (res && res.resultCode === 1) {
jzList.value = res.result || [];
} else {
jzList.value = [];
console.warn("获取家长信息失败:", res?.resultMessage);
}
} catch (error) {
console.error("获取家长信息出错:", error);
jzList.value = [];
uni.showToast({
title: "获取家长信息失败",
icon: "none"
});
} finally {
uni.hideLoading();
}
} else {
console.warn("学生信息不完整,无法获取家长信息");
}
});
@ -122,6 +198,25 @@ const callParent = (phoneNumber: string) => {
},
});
};
//
const getGenderText = (gender: string) => {
if (gender === '1') {
return '男';
} else if (gender === '2') {
return '女';
} else {
return '未知';
}
};
//
const getParentAvatar = (avatarUrl: string) => {
if (!avatarUrl) {
return '/static/images/default_avatar.png'; //
}
return avatarUrl;
};
</script>
<style scoped lang="scss">
@ -252,9 +347,16 @@ const callParent = (phoneNumber: string) => {
display: flex;
flex-direction: column;
align-items: center;
// flex: 0 0 auto; // flex
text-align: center;
margin-bottom: 10px; //
margin-bottom: 15px; //
padding: 8px; //
border-radius: 8px; //
transition: all 0.2s ease; //
&:hover {
background-color: rgba(102, 126, 234, 0.05); //
transform: translateY(-2px); //
}
}
.parent-avatar-container {
@ -265,7 +367,29 @@ const callParent = (phoneNumber: string) => {
width: 50px; //
height: 50px;
border-radius: 50%;
background-color: #eee;
background-color: #f0f0f0; //
border: 2px solid #e0e0e0; //
object-fit: cover; //
}
.parent-avatar-text {
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); //
border: 2px solid #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px; //
font-weight: bold;
color: #fff;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
text-align: center; //
line-height: 1.2; //
padding: 2px; //
word-break: break-all; //
}
.call-icon-overlay {
@ -281,9 +405,11 @@ const callParent = (phoneNumber: string) => {
justify-content: center;
border: 1px solid #fff; //
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); //
&:active {
opacity: 0.8;
transform: scale(0.95);
}
}
}
@ -300,7 +426,12 @@ const callParent = (phoneNumber: string) => {
.parent-relation {
font-size: 12px;
color: #999;
color: #666;
background-color: #f5f5f5;
padding: 4px 8px;
border-radius: 12px;
margin-top: 5px;
font-weight: 500;
}
//

View File

@ -1,15 +1,5 @@
<template>
<view class="add-resource-page">
<!-- 页面头部 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<uni-icons type="left" size="20" color="#333"></uni-icons>
<text class="back-text">返回</text>
</view>
<view class="header-title">上传资源</view>
<view class="header-right"></view>
</view>
<!-- 表单内容 -->
<scroll-view scroll-y class="form-scroll-view">
<view class="form-container">
@ -17,19 +7,12 @@
<view class="info-card">
<view class="form-item">
<text class="form-label">资源目录 <text class="required">*</text></text>
<picker
mode="selector"
:range="treeData"
range-key="title"
@change="handleResourceTypeChange"
>
<view class="picker-row">
<text :class="{ placeholder: !formData.resourType }">
{{ getResourceTypeText() || '请选择资源目录' }}
</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</picker>
<view class="picker-row" @click="showResourceTypeTree">
<text :class="{ placeholder: !formData.resourType }">
{{ getResourceTypeText() || '请选择资源目录' }}
</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
@ -136,13 +119,23 @@
<!-- 底部提交按钮 -->
<view class="bottom-actions">
<button class="action-btn cancel-btn" @click="goBack">
取消
</button>
<button class="action-btn confirm-btn" @click="handleSubmitForm" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
</view>
<!-- 资源目录选择树 -->
<BasicTree
ref="treeRef"
:range="treeData"
idKey="key"
rangeKey="title"
title="选择资源目录"
:multiple="false"
:selectParent="false"
@confirm="onTreeConfirm"
@cancel="onTreeCancel"
/>
</view>
</template>
@ -152,6 +145,7 @@ import { typesFindTreeApi } from "@/api/base/server";
import { attachmentUpload } from "@/api/system/upload";
import { imagUrl } from "@/utils";
import { useDicStore } from "@/store/modules/dic";
import BasicTree from "@/components/BasicTree/Tree.vue";
const { findByPid } = useDicStore();
@ -182,6 +176,9 @@ const formData = reactive({
//
const treeData = ref([]);
//
const treeRef = ref();
//
const isSubmitting = ref(false);
@ -191,15 +188,29 @@ const categoryOptions = ref([]);
//
const loadCategoryOptions = async () => {
try {
// 使ID
const result = await findByPid({ pid: 1391443399 });
if (result && Array.isArray(result)) {
categoryOptions.value = result.map(item => ({
//
if (result && result.resultCode === 1 && result.result && Array.isArray(result.result) && result.result.length > 0) {
categoryOptions.value = result.result.map(item => ({
value: item.dictionaryCode,
label: item.dictionaryValue
}));
} else {
// 使
categoryOptions.value = [
{ value: '1', label: '课件' },
{ value: '2', label: '教案' },
{ value: '3', label: '学案' },
{ value: '4', label: '作业' },
{ value: '5', label: '试卷' },
{ value: '6', label: '教材' },
{ value: '7', label: '示范课' },
{ value: '8', label: '音视频合集' },
];
}
} catch (error) {
console.error('加载资源类别失败:', error);
// 使
categoryOptions.value = [
{ value: '1', label: '课件' },
@ -214,16 +225,26 @@ const loadCategoryOptions = async () => {
}
};
//
const goBack = () => {
uni.navigateBack();
//
const showResourceTypeTree = () => {
if (treeRef.value) {
treeRef.value._show();
}
};
//
const handleResourceTypeChange = (e: any) => {
const index = e.detail.value;
const selectedItem = treeData.value[index];
formData.resourType = selectedItem.key;
//
const onTreeConfirm = (selectedItems: any[]) => {
if (selectedItems.length > 0) {
const selectedItem = selectedItems[0]; //
formData.resourType = selectedItem.key;
}
};
//
const onTreeCancel = () => {
//
};
//
@ -415,7 +436,7 @@ const handleSubmitForm = async () => {
id: formData.id
};
console.log('提交参数:', params);
const result = await resourcesSaveApi(params);
@ -457,16 +478,45 @@ const resetFormData = () => {
const loadTreeData = async () => {
try {
const res = await typesFindTreeApi();
treeData.value = res.result || [];
console.log('树形数据加载完成:', treeData.value);
// BasicTree
if (res && Array.isArray(res)) {
treeData.value = res.map(item => ({
key: item.key || item.id,
title: item.title || item.name,
children: item.children ? item.children.map(child => ({
key: child.key || child.id,
title: child.title || child.name,
children: child.children || []
})) : []
}));
} else {
treeData.value = res.result || [];
}
} catch (error) {
console.error('加载树形数据失败:', error);
// 使
treeData.value = [];
}
};
//
const getResourceTypeText = () => {
const selectedItem = treeData.value.find(item => item.key === formData.resourType);
//
const findSelectedItem = (items: any[], targetKey: string): any => {
for (const item of items) {
if (item.key === targetKey) {
return item;
}
if (item.children && item.children.length > 0) {
const found = findSelectedItem(item.children, targetKey);
if (found) return found;
}
}
return null;
};
const selectedItem = findSelectedItem(treeData.value, formData.resourType);
return selectedItem ? selectedItem.title : '';
};
@ -490,39 +540,7 @@ onMounted(async () => {
flex-direction: column;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
position: sticky;
top: 0;
z-index: 10;
}
.header-left {
display: flex;
align-items: center;
gap: 10rpx;
cursor: pointer;
}
.back-text {
font-size: 28rpx;
color: #333;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.header-right {
width: 80rpx;
}
.form-scroll-view {
flex: 1;
@ -671,19 +689,8 @@ onMounted(async () => {
border-top: 1rpx solid #f0f0f0;
}
.cancel-btn {
flex: 1;
padding: 20rpx;
border-radius: 8rpx;
font-size: 28rpx;
font-weight: 500;
border: none;
background: #f5f5f5;
color: #666;
}
.confirm-btn {
flex: 1;
width: 100%;
padding: 20rpx;
border-radius: 8rpx;
font-size: 28rpx;

View File

@ -0,0 +1,289 @@
<template>
<BasicLayout>
<!-- 巡查记录列表 -->
<view class="inspection-list">
<BasicListLayout
@register="registerInspection"
style="position: absolute"
>
<template v-slot="{ data, index }">
<view class="inspection-record bg-white r-md p-15 mb-15">
<view class="record-header">
<view class="record-time">
<u-icon name="clock" color="#666" size="14"></u-icon>
<text class="time-text">{{ formatTime(data.xctime) }}</text>
</view>
<view class="record-status">
<text class="status-text">已巡查</text>
</view>
</view>
<view class="record-content">
<view class="content-item">
<text class="item-label">巡查教师</text>
<text class="item-value">{{ data.jsxm }}</text>
</view>
<view class="content-item flex-col">
<text class="item-label" style="flex: 0 0 25px"
>巡查项目</text
>
<view class="item-value" style="width: 100%">
<template
v-if="data.xkXcXmList && data.xkXcXmList.length > 0"
>
<view
v-for="(xm, idx) in data.xkXcXmList"
:key="xm.xcXmId"
style="margin-bottom: 4px"
>
<view>
<text>{{ idx + 1 }}{{ xm.xcMc }}</text>
<view
style="
display: flex;
justify-content: space-between;
margin: 4px 0;
"
>
<text>分值{{ xm.xmFz }}</text>
<text
>巡查结果{{
xm.xcJg === "A" ? "有" : "无"
}}</text
>
</view>
</view>
</view>
</template>
<template v-else> 无巡查项目 </template>
</view>
</view>
<view
class="content-item"
v-if="data.zp && data.zp.length > 0"
>
<text class="item-label">巡查图片</text>
<view
class="item-value"
style="display: flex; flex-wrap: wrap; gap: 8px"
>
<image
v-for="(img, imgIdx) in getImageArray(data.zp)"
:key="imgIdx"
:src="imagUrl(img)"
mode="aspectFill"
style="
width: 60px;
height: 60px;
border-radius: 4px;
border: 1px solid #eee;
cursor: pointer;
"
@click="handlePreviewImage(img, getImageArray(data.zp))"
/>
</view>
</view>
<view
class="content-item"
v-if="data.sp && data.sp.length > 0"
>
<text class="item-label">巡查视频</text>
<view
class="item-value"
style="display: flex; flex-wrap: wrap; gap: 8px"
>
<view
v-for="(video, vIdx) in getVideoArray(data.sp)"
:key="vIdx"
style="
width: 80px;
height: 60px;
position: relative;
border-radius: 4px;
overflow: hidden;
border: 1px solid #eee;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: #000;
"
@click="handlePreviewVideo(getVideoArray(data.sp), vIdx)"
>
<video
:src="video"
style="width: 100%; height: 100%; object-fit: cover"
:controls="false"
:show-center-play-btn="false"
:show-play-btn="false"
:show-fullscreen-btn="false"
:show-progress="false"
:show-mute-btn="false"
:enable-progress-gesture="false"
:enable-play-gesture="false"
:loop="false"
:muted="true"
:poster="''"
></video>
<view
style="
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
"
>
<u-icon
name="play-right-fill"
color="#fff"
size="28"
></u-icon>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
</BasicListLayout>
</view>
</BasicLayout>
</template>
<script setup lang="ts">
import { xkXcFindPageApi } from "@/api/base/xkXcApi";
import BasicLayout from "@/components/BasicLayout/Layout.vue";
import { useLayout } from "@/components/BasicListLayout/hooks/useLayout";
import { useDataStore } from "@/store/modules/data";
import { useUserStore } from "@/store/modules/user";
import { computed, onMounted, ref } from "vue";
import dayjs from "dayjs";
import { imagUrl } from "@/utils";
const { getJs } = useUserStore();
const { getData } = useDataStore();
const js = computed(() => getJs);
const xkkc = computed(() => getData);
//
let inspectionParams = ref({
rows: 10,
xkkcId: xkkc.value.id,
jsId: js.value.id,
});
//
const [registerInspection, { reload }] = useLayout({
api: xkXcFindPageApi,
componentProps: {},
param: inspectionParams.value,
});
//
const handlePreviewImage = (img: string, images: string[]) => {
// uni-appAPI
uni.previewImage({
current: img,
urls: images,
});
};
//
const handlePreviewVideo = (videos: string[], index: number) => {
// uni-appAPI
// uni.previewMedia H5//APP
uni.previewMedia({
current: index,
sources: videos.map((url) => ({
url,
type: "video",
})),
});
};
//
const formatTime = (timestamp: string) => {
return dayjs(timestamp).format("YYYY-MM-DD HH:mm");
};
//
const getImageArray = (str: string) => {
if (!str) return [];
return str.split(",").map((item) => item.trim());
};
//
const getVideoArray = (str: string) => {
if (!str) return [];
return str.split(",").map((item) => item.trim());
};
//
onMounted(() => {
reload();
});
</script>
<style scoped lang="scss">
.inspection-list {
position: relative;
height: calc(100vh - 50px);
.inspection-record {
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.record-time {
display: flex;
align-items: center;
font-size: 14px;
color: #666;
.time-text {
margin-left: 5px;
}
}
.record-status {
padding: 3px 8px;
border-radius: 4px;
border: 1px solid #4080ff;
display: inline-flex;
font-size: 12px;
color: #4080ff;
}
}
.record-content {
.content-item {
display: flex;
margin-bottom: 5px;
font-size: 14px;
color: #333;
.item-label {
font-weight: bold;
flex: 0 0 80px;
}
}
}
}
::v-deep .zp-loading-fixed {
position: absolute;
}
::v-deep .d-load-main {
position: absolute;
}
}
</style>

View File

@ -23,21 +23,13 @@
class="course-item"
>
<view class="course-name">{{ xkkc.kcmc }}</view>
<view class="course-info-item">
<view class="info-label">上课周期类型</view>
<view class="info-data">{{ xkkc.skzqlx }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课周期</view>
<view class="info-data">{{ xkkc.skzqmc }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课开始时间</view>
<view class="info-data">{{ xkkc.skkstime }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课结束时间</view>
<view class="info-data">{{ xkkc.skjstime }}</view>
<view class="info-label">上课时间</view>
<view class="info-data">{{ formatClassTime(xkkc.skkstime, xkkc.skjstime) }}</view>
</view>
<view class="course-info-item">
<view class="info-label">开课地点</view>
@ -49,8 +41,10 @@
>{{ xkkc.hasNum || 0 }} | {{ xkkc.maxNum || 0 }}</view
>
</view>
<view class="separator-line"></view>
<view class="course-btn-group">
<view class="xc-btn" @click.stop="goXc(xkkc)">巡查</view>
<view class="record-btn" @click.stop="goRecord(xkkc)">巡查记录</view>
</view>
</view>
</view>
@ -110,6 +104,7 @@ import { jsdXkListApi } from "@/api/base/server";
import { useDataStore } from "@/store/modules/data";
import { useUserStore } from "@/store/modules/user";
import { onBeforeUnmount, onMounted, ref } from "vue";
import dayjs from "dayjs";
const { getJs } = useUserStore();
const { getData, setData } = useDataStore();
@ -198,6 +193,50 @@ const goXc = (xkkc: any) => {
});
};
//
const goRecord = (xkkc: any) => {
setData(xkkc);
uni.navigateTo({
url: `/pages/view/routine/kefuxuncha/xcRecord`,
});
};
//
const formatClassTime = (startTime: string, endTime: string) => {
if (!startTime || !endTime) {
return '';
}
try {
//
let start, end;
// HH:mm:ss HH:mm
if (startTime.includes(':') && !startTime.includes('-') && !startTime.includes('/')) {
start = startTime;
end = endTime;
} else {
// dayjs
const startDate = dayjs(startTime);
const endDate = dayjs(endTime);
if (startDate.isValid() && endDate.isValid()) {
start = startDate.format('HH:mm:ss');
end = endDate.format('HH:mm:ss');
} else {
//
return `${startTime}~${endTime}`;
}
}
return `${start}~${end}`;
} catch (error) {
console.error('时间格式化错误:', error);
//
return `${startTime}~${endTime}`;
}
};
//
onBeforeUnmount(() => {});
</script>
@ -205,7 +244,7 @@ onBeforeUnmount(() => {});
<style lang="scss" scoped>
.interest-course {
min-height: 100%;
background-color: #f5f7fa;
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
display: flex;
flex-direction: column;
height: 100%;
@ -213,43 +252,63 @@ onBeforeUnmount(() => {});
}
.selection-header {
background: linear-gradient(135deg, #4a90e2, #2879ff);
padding: 20px 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 25px 20px;
color: #fff;
border-radius: 0 0 15px 15px;
box-shadow: 0 4px 12px rgba(40, 121, 255, 0.2);
border-radius: 0 0 20px 20px;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 10;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, rgba(255, 255, 255, 0.1) 0%, transparent 50%, rgba(255, 255, 255, 0.05) 100%);
pointer-events: none;
}
.header-content {
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
z-index: 1;
.title-section {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
.title {
flex: 1 0 1px;
font-size: 24px;
font-weight: bold;
}
.subtitle {
font-size: 14px;
opacity: 0.8;
font-size: 20px;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
letter-spacing: 0.5px;
}
.switch-btn {
padding: 5px 15px;
padding: 8px 16px;
background-color: rgba(255, 255, 255, 0.2);
color: #fff;
border-radius: 15px;
font-size: 15px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
&:hover {
background-color: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
}
}
}
@ -259,13 +318,7 @@ onBeforeUnmount(() => {});
.scrollable-content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.inspection-tabs {
padding: 15px 15px 0 15px;
background-color: #fff;
margin-bottom: 10px;
-webkit-overflow-scrolling: touch; // iOS
}
.course-list {
@ -274,56 +327,216 @@ onBeforeUnmount(() => {});
.course-item {
position: relative;
width: 100%;
margin-bottom: 15px;
margin-bottom: 20px;
background-color: #fff;
border-radius: 8px;
padding: 15px;
border-radius: 12px;
padding: 20px;
box-sizing: border-box;
border: 1px solid transparent;
transition: all 0.3s ease;
border: 1px solid #f0f0f0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
animation: fadeInUp 0.6s ease-out;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
border-color: #e8e8e8;
}
&.selected {
border: 1px solid #3fbf72;
background-color: rgba(63, 191, 114, 0.05);
box-shadow: 0 2px 8px rgba(63, 191, 114, 0.15);
border: 2px solid #3fbf72;
background-color: rgba(63, 191, 114, 0.02);
box-shadow: 0 4px 20px rgba(63, 191, 114, 0.15);
}
.course-name {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 10px;
font-size: 17px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 15px;
line-height: 1.4;
animation: fadeInLeft 0.5s ease-out 0.1s both;
}
.course-btn-group {
display: flex;
justify-content: flex-end;
gap: 10px; // Added gap for spacing between buttons
.xc-btn {
flex: 1 0 1px;
display: inline-block;
color: #2879ff;
color: #ff6b35;
font-size: 14px;
font-weight: 600;
padding: 8px 18px;
border-radius: 8px;
background: linear-gradient(135deg, rgba(255, 107, 53, 0.1), rgba(255, 107, 53, 0.05));
border: 1px solid #ff6b35;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.15);
animation: fadeInUp 0.5s ease-out 0.3s both;
&:hover {
background: linear-gradient(135deg, rgba(255, 107, 53, 0.15), rgba(255, 107, 53, 0.1));
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(255, 107, 53, 0.25);
}
&:active {
background: linear-gradient(135deg, rgba(255, 107, 53, 0.2), rgba(255, 107, 53, 0.15));
transform: translateY(0);
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.2);
}
}
.record-btn {
display: inline-block;
color: #409EFF;
font-size: 14px;
font-weight: 600;
padding: 8px 18px;
border-radius: 8px;
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1), rgba(64, 158, 255, 0.05));
border: 1px solid #409EFF;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
animation: fadeInUp 0.5s ease-out 0.3s both;
&:hover {
background: linear-gradient(135deg, rgba(64, 158, 255, 0.15), rgba(64, 158, 255, 0.1));
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(64, 158, 255, 0.25);
}
&:active {
background: linear-gradient(135deg, rgba(64, 158, 255, 0.2), rgba(64, 158, 255, 0.15));
transform: translateY(0);
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
}
}
}
.course-info-item {
display: flex;
margin-bottom: 12px;
font-size: 12px;
margin-bottom: 14px;
font-size: 13px;
align-items: center;
animation: fadeInUp 0.5s ease-out 0.15s both;
.info-label {
color: #949aa4;
color: #666;
flex: 0 0 100px;
font-weight: 500;
}
.info-data {
flex: 1 0 1px;
color: #333;
font-weight: 400;
}
}
.separator-line {
height: 1px;
background: linear-gradient(90deg, transparent, #e8e8e8, transparent);
margin: 18px 0;
opacity: 0.8;
animation: fadeIn 0.5s ease-out 0.25s both;
}
}
}
/* 学生选择器弹窗样式 */
//
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 全局图片样式 */
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
//
.empty-course-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
.empty-icon {
margin-bottom: 25px;
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
width: 90px;
height: 90px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 2px solid rgba(255, 255, 255, 0.8);
}
.empty-text {
font-size: 18px;
font-weight: 600;
color: #475569;
margin-bottom: 8px;
letter-spacing: 0.3px;
}
.empty-desc {
font-size: 14px;
color: #64748b;
max-width: 80%;
line-height: 1.6;
}
}
/* 选课选择弹窗样式 */
.xk-selector {
background-color: #ffffff;
border-top-left-radius: 12px;
@ -352,6 +565,7 @@ onBeforeUnmount(() => {});
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #f2f2f2;
transition: all 0.3s ease;
&:last-child {
border-bottom: none;
@ -361,6 +575,10 @@ onBeforeUnmount(() => {});
background-color: rgba(64, 158, 255, 0.05);
}
&:hover {
background-color: rgba(64, 158, 255, 0.02);
}
.xk-info {
flex: 1;
margin-left: 12px;
@ -386,62 +604,4 @@ onBeforeUnmount(() => {});
}
}
}
/* 全局图片样式 */
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
//
.empty-course-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
.empty-icon {
margin-bottom: 20px;
background-color: #f5f6f7;
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 18px;
font-weight: 500;
color: #303133;
margin-bottom: 8px;
}
.empty-desc {
font-size: 14px;
color: #909399;
max-width: 80%;
line-height: 1.5;
}
}
/* 选课已结束样式 */
.enrollment-ended {
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 12px 15px;
font-size: 16px;
font-weight: 500;
color: #fff;
gap: 8px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -230,7 +230,7 @@ export const previewFile = (fileUrl: string, fileName: string, fileType: string)
const encodedUrl = btoa(finalFileUrl);
console.log('使用kkFileView预览Base64编码后的URL:', encodedUrl);
const previewUrl = `http://yufangzc.com:8891/onlinePreview?url=${encodeURIComponent(encodedUrl)}`;
const previewUrl = `${KK_FILE_VIEW_URL}/onlinePreview?url=${encodeURIComponent(encodedUrl)}`;
console.log('最终预览URL (普通文件):', previewUrl);
const needLandscape = ['ppt', 'pptx'].includes(type);

View File

@ -7,20 +7,34 @@ import {URL_REG} from "@/utils/RegExp";
*
* */
export function imagUrl(path: string): string {
if (!URL_REG.test(path)) {
if (BASE_IMAGE_URL === '/') {
return path
}
return BASE_IMAGE_URL + path;
// 兜底:空值直接返回空字符串,避免拼接出 ...undefined
if (!path) {
return ''
}
return path;
// 已是完整 URL 则原样返回
if (URL_REG.test(path)) {
return path
}
// BASE_IMAGE_URL 为根路径时,直接返回规范化后的相对路径
if (BASE_IMAGE_URL === '/') {
return path.startsWith('/') ? path : `/${path}`
}
// 规范化:确保只存在一个斜杠分隔
const base = BASE_IMAGE_URL.endsWith('/') ? BASE_IMAGE_URL.slice(0, -1) : BASE_IMAGE_URL
const tail = path.startsWith('/') ? path : `/${path}`
return `${base}${tail}`
}
export function replaceImageUrl(imageUrl: string) {
if (BASE_IMAGE_URL !== '/') {
return imageUrl.replace(BASE_IMAGE_URL, '')
}
return BASE_IMAGE_URL
if (!imageUrl) return ''
if (BASE_IMAGE_URL === '/') return BASE_IMAGE_URL
// 仅当以 BASE_IMAGE_URL 开头时才裁剪
return imageUrl.startsWith(BASE_IMAGE_URL)
? imageUrl.slice(BASE_IMAGE_URL.length)
: imageUrl
}
// 点击选项处理逻辑