一师一策调整

This commit is contained in:
hebo 2025-10-07 08:58:02 +08:00
parent de30a7c235
commit 8306bb151f
22 changed files with 7993 additions and 511 deletions

57
src/api/base/kccyApi.ts Normal file
View File

@ -0,0 +1,57 @@
import { get, post } from "@/utils/request";
/**
*
*/
export function kccyFindPageApi(params: any) {
return get('/api/kccy/findPage', params);
}
/**
* ID查询课程成员详情
*/
export function kccyFindByIdApi(params: any) {
return get('/api/kccy/findById', params);
}
/**
* /
*/
export function kccySaveApi(params: any) {
return post('/api/kccy/save', params);
}
/**
*
*/
export function kccyLogicDeleteApi(params: any) {
return post('/api/kccy/logicDelete', params);
}
/**
* ID查询成员列表
*/
export function kccyFindByKcjbIdApi(params: any) {
return get('/api/kccy/findByKcjbId', params);
}
/**
*
*/
export function kccyExportFieldChooseApi(params: any) {
return get('/api/kccy/exportFieldChoose', params);
}
/**
*
*/
export function kccyExportFileApi(params: any) {
return post('/api/kccy/export', params);
}
/**
*
*/
export function kccyImportDataApi(params: any) {
return post('/api/kccy/importData', params);
}

29
src/api/base/kcjbApi.ts Normal file
View File

@ -0,0 +1,29 @@
import { get, post } from "@/utils/request";
/**
*
*/
export function kcjbFindPageApi(params: any) {
return get('/api/kcjb/findPage', params);
}
/**
*
*/
export function kcjbFindByIdApi(params: any) {
return get('/api/kcjb/findById', params);
}
/**
*
*/
export function kcjbRegisterApi(params: any) {
return post('/api/kcjb/register', params);
}
/**
*
*/
export function kcjbBannerApi() {
return get('/api/kcjb/banner');
}

79
src/api/base/rwApi.ts Normal file
View File

@ -0,0 +1,79 @@
import { get, post } from "@/utils/request";
/**
*
*/
export function rwFindPageApi(params: any) {
return get('/api/rw/findPage', params);
}
export function rwFindPageSummaryApi(params: any) {
return get('/api/rw/findPageSummary', params);
}
export function rwCompletionSummaryApi() {
return get('/api/rw/completionSummary');
}
/**
* /
*/
export function rwSaveApi(params: any) {
return post('/api/rw/save', params);
}
/**
*
*/
export function rwLogicDeleteApi(params: any) {
return post('/api/rw/logicDelete', params);
}
/**
* id查询
*/
export function rwFindByIdApi(params: any) {
return get('/api/rw/findById', params);
}
/**
*
*/
export function rwFindAllApi() {
return get('/api/rw/findAll');
}
/**
*
*/
export function rwExportFieldChooseApi(params: any) {
return get('/api/rw/exportFieldChoose', params);
}
/**
*
*/
export function rwExportFileApi(params: any) {
return post('/api/rw/export', params);
}
/**
*
*/
export function rwImportDataApi(params: any) {
return post('/api/rw/importData', params);
}
/**
*
*/
export function rwDelApi(params: any) {
return post('/api/rw/delete', params);
}
/**
*
*/
export function rwPushJsApi(params: any) {
return post('/api/rw/pushJs', params);
}

113
src/api/base/rwzxApi.ts Normal file
View File

@ -0,0 +1,113 @@
import { get, post } from "@/utils/request";
/**
*
*/
export function rwzxFindPageApi(params: any) {
return get('/api/rwzx/findPage', params);
}
/**
* ID查询执行情况
*/
export function rwzxFindByRwIdApi(params: any) {
return get('/api/rwzx/findByRwId', params);
}
/**
* ID查询已执行信息
*/
export function executedInfoByRwIdApi(params: any) {
return get('/api/rwzx/executedInfoByRwId', params);
}
/**
* ID查询未执行教师信息
*/
export function noxecuteJsbyRwIdApi(params: any) {
return get('/api/rwzx/noxecuteJsbyRwId', params);
}
/**
* /
*/
export function rwzxSaveApi(params: any) {
return post('/api/rwzx/save', params);
}
/**
*
*/
export function rwzxLogicDeleteApi(params: any) {
return post('/api/rwzx/logicDelete', params);
}
/**
* id查询任务执行
*/
export function rwzxFindByIdApi(params: any) {
return get('/api/rwzx/findById', params);
}
/**
*
*/
export function rwzxFindAllApi() {
return get('/api/rwzx/findAll');
}
/**
*
*/
export function rwzxCompletionSummaryApi(params: any) {
return get('/api/rwzx/completionSummary', params);
}
/**
*
*/
export function rwzxExportFieldChooseApi(params: any) {
return get('/api/rwzx/exportFieldChoose', params);
}
/**
*
*/
export function rwzxExportFileApi(params: any) {
return post('/api/rwzx/export', params);
}
/**
*
*/
export function rwzxImportDataApi(params: any) {
return post('/api/rwzx/importData', params);
}
/**
*
*/
export function rwzxDelApi(params: any) {
return post('/api/rwzx/delete', params);
}
/**
*
*/
export function rwzxNoxecuteJsbyRwIdApi(params: any) {
return get('/api/rwzx/noxecuteJsbyRwId', params);
}
/**
*
*/
export function rwzxExecutedInfoByRwIdApi(params: any) {
return get('/api/rwzx/executedInfoByRwId', params);
}
/**
*
*/
export function rwzxBatchCreateApi(params: any) {
return post('/api/rwzx/batchCreate', params);
}

View File

@ -13,7 +13,7 @@
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
<u-popup :show="showPopup" @close="showPopup = false">
<u-popup :show="showPopup" @close="showPopup = false" mode="bottom" width="100%">
<view class="js-picker-popup">
<view class="js-picker-header">
<view class="js-cancel-btn" @click="handleCancel">取消</view>

View File

@ -87,11 +87,56 @@
</view>
</view>
</view>
<!-- 文件上传区域 -->
<view class="upload-section" v-if="enableFile">
<view class="section-title">
<text class="title-text">文件</text>
<text class="count-text">({{ fileList.length }}/{{ maxFileCount }})</text>
</view>
<view class="file-list">
<view
class="file-item"
v-for="(file, index) in fileList"
:key="index"
>
<view class="file-preview" @click="previewFile(index)">
<view class="file-icon">
<uni-icons :type="getFileIcon(file.extension || '')" size="32" color="#666"></uni-icons>
</view>
<view class="file-info">
<text class="file-name">{{ file.name }}</text>
<text class="file-size" v-if="file.size">{{ formatFileSize(file.size) }}</text>
<text class="file-type">{{ file.extension?.toUpperCase() || 'FILE' }}</text>
</view>
</view>
<view class="file-actions">
<view class="delete-btn" @click="removeFile(index)">
<uni-icons type="close" size="16" color="#fff"></uni-icons>
</view>
</view>
</view>
<view
class="add-btn"
v-if="fileList.length < maxFileCount"
@click="chooseFile"
>
<uni-icons type="paperclip" size="24" color="#999"></uni-icons>
<text class="add-text">添加文件</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { imagUrl } from '@/utils'
import {
previewFile as previewFileUtil
} from '@/utils/filePreview'
//
interface ImageItem {
@ -111,6 +156,16 @@ interface VideoItem {
thumbnail?: string
}
interface FileItem {
tempPath?: string
url?: string
name?: string
type?: string
size?: number
extension?: string
mimeType?: string
}
// Props
interface Props {
//
@ -123,6 +178,12 @@ interface Props {
maxVideoCount?: number
videoList?: VideoItem[]
//
enableFile?: boolean
maxFileCount?: number
fileList?: FileItem[]
allowedFileTypes?: string[]
//
compressConfig?: {
image: {
@ -153,6 +214,11 @@ const props = withDefaults(defineProps<Props>(), {
maxVideoCount: 3,
videoList: () => [],
enableFile: false,
maxFileCount: 5,
fileList: () => [],
allowedFileTypes: () => ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'mp3', 'wav', 'zip', 'rar'],
compressConfig: () => ({
image: {
quality: 60,
@ -176,16 +242,20 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<{
'update:imageList': [images: ImageItem[]]
'update:videoList': [videos: VideoItem[]]
'update:fileList': [files: FileItem[]]
'image-upload-success': [image: ImageItem, index: number]
'image-upload-error': [error: any, index: number]
'video-upload-success': [video: VideoItem, index: number]
'video-upload-error': [error: any, index: number]
'upload-progress': [type: 'image' | 'video', current: number, total: number]
'file-upload-success': [file: FileItem, index: number]
'file-upload-error': [error: any, index: number]
'upload-progress': [type: 'image' | 'video' | 'file', current: number, total: number]
}>()
//
const imageList = ref<ImageItem[]>([...props.imageList])
const videoList = ref<VideoItem[]>([...props.videoList])
const fileList = ref<FileItem[]>([...props.fileList])
// props
watch(() => props.imageList, (newList) => {
@ -196,6 +266,10 @@ watch(() => props.videoList, (newList) => {
videoList.value = [...newList]
}, { deep: true })
watch(() => props.fileList, (newList) => {
fileList.value = [...newList]
}, { deep: true })
//
const COMPRESS_CONFIG = computed(() => props.compressConfig)
@ -483,6 +557,180 @@ const chooseVideo = () => {
})
}
//
const chooseFile = () => {
uni.chooseFile({
count: props.maxFileCount - fileList.value.length,
type: 'all',
success: async (res) => {
const tempFiles = res.tempFiles
if (Array.isArray(tempFiles) && tempFiles.length > 0) {
try {
for (const file of tempFiles) {
const fileInfo = file as any
const fileName = fileInfo.name || ''
const fileExtension = fileName.split('.').pop()?.toLowerCase() || ''
//
if (props.allowedFileTypes && props.allowedFileTypes.length > 0) {
if (!props.allowedFileTypes.includes(fileExtension)) {
showToast({
title: `不支持的文件类型: ${fileExtension}`,
icon: 'none'
})
continue
}
}
//
let fileType = 'document'
if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(fileExtension)) {
fileType = 'image'
} else if (['mp4', 'mov', 'avi', 'wmv', 'flv'].includes(fileExtension)) {
fileType = 'video'
} else if (['mp3', 'wav', 'aac', 'ogg'].includes(fileExtension)) {
fileType = 'audio'
}
//
const fileItem: FileItem = {
tempPath: fileInfo.path,
name: fileName,
type: fileType,
size: fileInfo.size,
extension: fileExtension,
mimeType: fileInfo.type || ''
}
//
fileList.value.push(fileItem)
//
if (props.autoUpload && props.uploadApi) {
await uploadFile(fileItem, fileList.value.length - 1)
}
}
//
emit('update:fileList', fileList.value)
} catch (error) {
console.error('文件处理失败:', error)
showToast({ title: '文件处理失败', icon: 'none' })
}
}
},
fail: (error) => {
console.error('选择文件失败:', error)
if (error.errMsg && !error.errMsg.includes('cancel')) {
showToast({ title: '选择文件失败', icon: 'none' })
}
}
})
}
//
const uploadFile = async (fileItem: FileItem, index: number) => {
if (!props.uploadApi || !fileItem.tempPath) return
try {
const result = await props.uploadApi(fileItem.tempPath)
if (result && result.resultCode === 1 && result.result && result.result.length > 0) {
const uploadedFile = result.result[0]
//
fileItem.url = uploadedFile.filePath
fileItem.tempPath = undefined //
//
emit('file-upload-success', fileItem, index)
showToast({ title: '文件上传成功', icon: 'success' })
} else {
throw new Error('上传失败')
}
} catch (error) {
console.error('文件上传失败:', error)
emit('file-upload-error', error, index)
showToast({ title: '文件上传失败', icon: 'none' })
//
fileList.value.splice(index, 1)
}
}
//
const previewFile = (index: number) => {
const file = fileList.value[index]
if (!file) return
if (file.type === 'image' && file.url) {
//
uni.previewImage({
urls: [file.url],
current: file.url
})
} else if (file.url) {
// 使
const fileUrl = imagUrl(file.url)
const fileName = file.name || '未知文件'
const fileExtension = file.extension || file.url.split('.').pop() || ''
// 使 kkview
const fullFileName = fileExtension ? `${fileName}.${fileExtension}` : fileName
previewFileUtil(fileUrl, fullFileName, fileExtension)
.catch((error: any) => {
uni.showToast({
title: '预览失败',
icon: 'error'
})
})
} else {
//
showToast({
title: `预览 ${file.name} 功能待实现`,
icon: 'none'
})
}
}
//
const removeFile = (index: number) => {
fileList.value.splice(index, 1)
emit('update:fileList', fileList.value)
}
//
const getFileIcon = (extension: string): string => {
const iconMap: { [key: string]: string } = {
//
'pdf': 'book',
'doc': 'book',
'docx': 'book',
'txt': 'book',
//
'xls': 'table',
'xlsx': 'table',
// 稿
'ppt': 'slideshow',
'pptx': 'slideshow',
//
'mp3': 'mic',
'wav': 'mic',
'aac': 'mic',
'ogg': 'mic',
//
'zip': 'folder',
'rar': 'folder',
'7z': 'folder',
//
'default': 'paperclip'
}
return iconMap[extension.toLowerCase()] || iconMap['default']
}
//
const uploadImages = async (images: ImageItem[]) => {
if (!props.uploadApi) return
@ -683,10 +931,13 @@ const showToast = (options: { title: string; icon?: string; duration?: number })
defineExpose({
chooseImage,
chooseVideo,
chooseFile,
uploadImages,
uploadVideos,
uploadFile,
removeImage,
removeVideo
removeVideo,
removeFile
})
</script>
@ -842,4 +1093,84 @@ defineExpose({
color: #999;
margin-top: 8rpx;
}
/* 文件相关样式 */
.file-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.file-item {
display: flex;
align-items: center;
background: #f8f9fa;
border-radius: 8rpx;
padding: 16rpx;
border: 1rpx solid #e9ecef;
position: relative;
}
.file-preview {
display: flex;
align-items: center;
flex: 1;
cursor: pointer;
}
.file-icon {
width: 60rpx;
height: 60rpx;
background: white;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
border: 1rpx solid #dee2e6;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-size: 28rpx;
color: #333;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4rpx;
}
.file-size {
font-size: 24rpx;
color: #999;
display: block;
margin-bottom: 2rpx;
}
.file-type {
font-size: 20rpx;
color: #666;
display: block;
}
.file-actions {
position: absolute;
top: 8rpx;
right: 8rpx;
}
.file-actions .delete-btn {
width: 32rpx;
height: 32rpx;
background: rgba(255, 59, 48, 0.8);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -17,6 +17,17 @@ export interface VideoItem {
thumbnail?: string // 缩略图路径
}
// 文件项接口
export interface FileItem {
tempPath?: string // 临时路径(用于预览)
url?: string // 服务器路径(上传成功后)
name?: string // 文件名
type?: string // 文件类型image/video/audio/document等
size?: number // 文件大小(字节)
extension?: string // 文件扩展名
mimeType?: string // MIME类型
}
// 压缩配置接口
export interface CompressConfig {
image: {
@ -45,6 +56,12 @@ export interface ImageVideoUploadProps {
maxVideoCount?: number
videoList?: VideoItem[]
// 文件相关
enableFile?: boolean
maxFileCount?: number
fileList?: FileItem[]
allowedFileTypes?: string[] // 允许的文件类型,如 ['pdf', 'doc', 'docx', 'mp3', 'wav']
// 压缩配置
compressConfig?: CompressConfig
@ -57,11 +74,14 @@ export interface ImageVideoUploadProps {
export interface ImageVideoUploadEmits {
'update:imageList': [images: ImageItem[]]
'update:videoList': [videos: VideoItem[]]
'update:fileList': [files: FileItem[]]
'image-upload-success': [image: ImageItem, index: number]
'image-upload-error': [error: any, index: number]
'video-upload-success': [video: VideoItem, index: number]
'video-upload-error': [error: any, index: number]
'upload-progress': [type: 'image' | 'video', current: number, total: number]
'file-upload-success': [file: FileItem, index: number]
'file-upload-error': [error: any, index: number]
'upload-progress': [type: 'image' | 'video' | 'file', current: number, total: number]
}
// 默认压缩配置

View File

@ -136,6 +136,48 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/yishiyice/addkcrw",
"style": {
"navigationBarTitleText": "新增任务",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/yishiyice/editkcrw",
"style": {
"navigationBarTitleText": "修改任务",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/yishiyice/detail",
"style": {
"navigationBarTitleText": "任务列表",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/yishiyice/push",
"style": {
"navigationBarTitleText": "任务推送",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/yishiyice/kcrwzx",
"style": {
"navigationBarTitleText": "任务执行",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/yishiyice/kcrwzxtj",
"style": {
"navigationBarTitleText": "提交任务",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/yishiyice/success",
"style": {
@ -143,6 +185,20 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/rw/index",
"style": {
"navigationBarTitleText": "任务执行",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/rw/detail",
"style": {
"navigationBarTitleText": "任务详情",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/JiaoXueZiYuan/index",
"style": {

View File

@ -95,7 +95,7 @@ onLoad(async (options) => {
rwflx.value = result.rwlxes;
rw.value = result;
for (let i = 0; i < rwflx.value.length; i++) {
if (rwflx.value[i].rwfl == "sctp" || rwflx.value[i].rwfl == "scsp" || rwflx.value[i].rwfl == "scwd") {
if (rwflx.value[i].rwfl == "sczy") {
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,

View File

@ -263,7 +263,23 @@ const sections = reactive<Section[]>([
permissionKey: "routine-yrcg", //
path: "/pages/view/quantitativeAssessment/index/index",
},
{
id: "r14",
icon: "ysyc",
text: "一师一策",
show: true,
permissionKey: "routine-ysyc", //
path: "/pages/view/routine/yishiyice/index",
},
{
id: "r15",
icon: "rwzx",
text: "任务执行",
show: true,
permissionKey: "routine-ysyc", //
path: "/pages/view/rw/index",
},
//path: "/pages/view/routine/yishiyice/index"rw/index,
],
},
{

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,312 +1,500 @@
<template>
<BasicLayout :show-nav-bar="true" :nav-bar-props="{ title: '一师一策' }">
<view class="yishiyice-page">
<!-- Search Bar -->
<view class="search-bar-container">
<!-- 查询组件 -->
<view class="query-component">
<view class="search-card">
<view class="search-item">
<uni-search-bar
placeholder="请输入搜索内容..."
v-model="searchForm.kcmc"
placeholder="请输入课程名称..."
bgColor="#f4f5f7"
radius="100"
cancelButton="none"
@confirm="handleSearch"
@input="handleSearchInput"
></uni-search-bar>
</view>
<view class="search-actions">
<u-button
text="查询"
type="primary"
size="small"
@click="handleSearch"
class="search-btn"
/>
<u-button
text="重置"
type="info"
size="small"
@click="handleReset"
class="reset-btn"
/>
</view>
</view>
</view>
<!-- Carousel/Banner -->
<view class="banner-section">
<swiper
v-if="bannerList.length > 0"
class="banner-swiper"
circular
indicator-dots
autoplay
:interval="3000"
:duration="500"
indicator-color="rgba(255, 255, 255, 0.5)"
indicator-active-color="#ffffff"
<!-- 列表组件 -->
<view class="list-component">
<scroll-view
scroll-y
class="list-scroll-view"
@scrolltolower="loadMore"
lower-threshold="100"
>
<swiper-item v-for="(item, index) in bannerList" :key="index" class="swiper-item">
<view class="banner-content" :style="{ background: item.bgColor }">
<view class="text-content">
<text class="banner-title">{{ item.title }}</text>
<text class="banner-subtitle">{{ item.subtitle }}</text>
</view>
<image :src="item.imageUrl" mode="aspectFit" class="banner-image"></image>
</view>
</swiper-item>
</swiper>
<!-- Placeholder if no banner items -->
<view v-else class="banner-placeholder">
<text>暂无内容</text>
<view v-if="isLoading && courseList.length === 0" class="loading-indicator">加载中...</view>
<template v-else-if="courseList.length > 0">
<view v-for="course in courseList" :key="course.id" class="course-card" @click="viewCourseDetail(course.id)">
<view class="card-header">
<text class="course-title">{{ course.kcmc }}</text>
<view class="arrow-icon">
<text class="arrow-text">></text>
</view>
</view>
<!-- Course List -->
<view class="course-list">
<view v-for="course in courseList" :key="course.id" class="course-card">
<image :src="course.imageUrl || '/static/default-cover.png'" mode="aspectFill" class="course-image"></image>
<view class="card-body">
<view class="course-info">
<text class="course-title">{{ course.title }}</text>
<text class="course-desc">{{ course.description }}</text>
<text class="course-date">{{ course.date }}</text>
<view class="info-item">
<text class="info-label">课程描述</text>
<text class="info-value">{{ course.kcms || '暂无描述' }}</text>
</view>
<button class="register-btn" size="mini" @click="registerCourse(course.id)">立即报名</button>
<view class="info-item" v-if="course.jsxm">
<text class="info-label">主讲教师</text>
<text class="info-value">{{ course.jsxm }}</text>
</view>
<view class="info-item" v-if="course.bzrxm">
<text class="info-label">班主任</text>
<text class="info-value">{{ course.bzrxm }}</text>
</view>
<view class="info-item" v-if="course.ljlxxm">
<text class="info-label">联系人</text>
<text class="info-value">{{ course.ljlxxm }}</text>
</view>
</view>
</view>
</view>
</template>
<view v-else class="empty-state">暂无课程数据</view>
<!-- 加载更多 -->
<view v-if="isLoading && courseList.length > 0" class="loading-more">
<text>加载中...</text>
</view>
<!-- 没有更多数据 -->
<view v-if="!hasMore && courseList.length > 0" class="no-more">
<text>没有更多数据了</text>
</view>
</scroll-view>
</view>
<!-- 新增按钮 - 固定在底部 -->
<view class="add-button-fixed">
<u-button
text="新增课程"
type="primary"
@click="goToAddCourse"
class="add-btn"
/>
</view>
</view>
</BasicLayout>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { ref, reactive, onMounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { kcjbFindPageApi, kcjbBannerApi, kcjbRegisterApi } from "@/api/base/kcjbApi";
// Interface for Banner items
interface BannerItem {
id: string;
title: string;
subtitle: string;
imageUrl: string;
bgColor: string;
}
// Interface for Course items
interface CourseItem {
id: string;
title: string;
description: string;
date: string;
imageUrl: string;
kcmc: string; //
kcms: string; //
cjsj: string; //
kctp: string; //
kczt: string; // ABC
kclx?: string; //
jsxm?: string; //
bzrxm?: string; //
ljlxxm?: string; //
}
// Mock Banner Data
const bannerList = ref<BannerItem[]>([
{
id: 'b1',
title: '一师一策',
subtitle: '一师一优课、一课一名师',
imageUrl: '/static/mock/banner-yishiyice.png', // Replace with actual image path
bgColor: 'linear-gradient(to right, #4a7cf6, #6f9eff)' // Example gradient
},
// Add more banner items if needed
]);
//
const searchForm = reactive({
kcmc: '' //
});
// Mock Course List Data
const courseList = ref<CourseItem[]>([
{
id: 'c1',
title: '教学力课程',
description: '每月第2周例会集中培训+外出培训',
date: '2025-02-11 12:33:12',
imageUrl: '/static/mock/course-jiaoxue.png' // Replace with actual image paths
},
{
id: 'c2',
title: '摄影艺术',
description: '每月第3周例会时间',
date: '2025-02-11 12:33:12',
imageUrl: '/static/mock/course-sheying.png'
},
{
id: 'c3',
title: '育人力课程',
description: '每月第2周例会集中培训+外出培训',
date: '2025-02-11 12:33:12',
imageUrl: '/static/mock/course-yuren.png'
},
{
id: 'c4',
title: '课程力课程',
description: '每月第3周例会时间',
date: '2025-02-11 12:33:12',
imageUrl: '/static/mock/course-kecheng.png'
},
{
id: 'c5',
title: '研究力课程',
description: '每月第3周例会时间',
date: '2025-02-11 12:33:12',
imageUrl: '/static/mock/course-yanjiu.png'
},
]);
//
const courseList = ref<CourseItem[]>([]);
const isLoading = ref(false);
const hasMore = ref(true);
const currentPage = ref(1);
const pageSize = ref(10);
// --- Methods ---
const handleSearch = (value: string) => {
console.log('搜索内容:', value);
// TODO: Implement search logic
uni.showToast({ title: `搜索: ${value}`, icon: 'none' });
//
const handleSearchInput = (value: string) => {
searchForm.kcmc = value;
};
const registerCourse = (courseId: string) => {
//
const handleSearch = () => {
console.log('搜索课程名称:', searchForm.kcmc);
getCourseList(false);
};
//
const handleReset = () => {
searchForm.kcmc = '';
getCourseList(false);
};
//
const getCourseList = async (isLoadMore = false) => {
if (isLoading.value) return;
isLoading.value = true;
try {
const params = {
page: isLoadMore ? currentPage.value + 1 : 1,
rows: pageSize.value,
kcmc: searchForm.kcmc
};
console.log('API请求参数:', params);
const response = await kcjbFindPageApi(params);
const newData = response.rows || [];
if (isLoadMore) {
courseList.value.push(...newData);
currentPage.value++;
} else {
courseList.value = newData;
currentPage.value = 1;
}
hasMore.value = newData.length === pageSize.value;
} catch (error) {
console.error('获取课程列表失败:', error);
courseList.value = [];
} finally {
isLoading.value = false;
}
};
//
const getStatusClass = (status: string) => {
switch (status) {
case 'A':
return 'status-normal';
case 'B':
return 'status-paused';
case 'C':
return 'status-ended';
default:
return 'status-default';
}
};
//
const getStatusText = (status: string) => {
switch (status) {
case 'A':
return '正常';
case 'B':
return '暂停';
case 'C':
return '已结束';
default:
return '未知';
}
};
//
const formatTime = (time: string) => {
if (!time) return '未设置';
return new Date(time).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
//
const registerCourse = async (courseId: string) => {
try {
console.log('报名课程:', courseId);
// TODO: Implement actual registration API call here
// For now, just navigate to the success page
uni.navigateTo({ url: '/pages/view/routine/yishiyice/success' });
const params = {
courseId: courseId
};
// TODO: Add functions to fetch banner and course data from API
await kcjbRegisterApi(params);
uni.showToast({ title: '报名成功', icon: 'success' });
setTimeout(() => {
uni.navigateTo({ url: '/pages/view/routine/yishiyice/success' });
}, 1500);
} catch (error) {
console.error('报名失败:', error);
uni.showToast({ title: '报名失败,请重试', icon: 'error' });
}
};
//
const viewCourseDetail = (courseId: string) => {
console.log('查看课程详情:', courseId);
uni.navigateTo({
url: `/pages/view/routine/yishiyice/detail?id=${courseId}`
});
};
//
const goToAddCourse = () => {
uni.navigateTo({
url: '/pages/view/routine/yishiyice/add'
});
};
//
const loadMore = () => {
if (!isLoading.value && hasMore.value) {
getCourseList(true);
}
};
//
onShow(() => {
getCourseList(false);
});
//
onMounted(() => {
getCourseList(false);
});
</script>
<style scoped lang="scss">
<style lang="scss" scoped>
.yishiyice-page {
background-color: #f4f5f7;
min-height: 100%; // Ensure it fills the layout height
padding-bottom: 15px; // Space at the bottom
}
.search-bar-container {
padding: 10px 15px;
background-color: #ffffff;
// Remove or adjust border based on design
// border-bottom: 1px solid #f0f0f0;
}
.banner-section {
padding: 15px 15px 0 15px;
}
.banner-swiper {
height: 120px; // Adjust height as needed
border-radius: 8px;
overflow: hidden;
}
.swiper-item {
box-sizing: border-box;
}
.banner-content {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-sizing: border-box;
position: relative;
.text-content {
display: flex;
flex-direction: column;
color: #ffffff;
z-index: 1;
height: 100vh;
background-color: #f5f7fa;
}
.banner-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 5px;
//
.query-component {
padding: 15px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
}
.banner-subtitle {
font-size: 13px;
opacity: 0.9;
.search-card {
.search-item {
margin-bottom: 12px;
}
.search-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
.search-btn, .reset-btn {
min-width: 60px;
}
.banner-image {
width: 100px; // Adjust size as needed
height: 80px;
position: absolute; // Or adjust flex layout
right: 15px;
bottom: 0px;
z-index: 0;
opacity: 0.9;
}
}
.banner-placeholder {
height: 120px;
background-color: #e9e9eb;
border-radius: 8px;
//
.list-component {
flex: 1;
overflow: hidden;
.list-scroll-view {
height: 100%;
padding: 15px;
padding-bottom: 80px; //
box-sizing: border-box;
background-color: #f5f7fa;
}
}
//
.loading-indicator {
text-align: center;
padding: 20px;
color: #409eff;
font-size: 14px;
}
//
.course-card {
background-color: #ffffff;
border-radius: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #f0f0f0;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:active {
transform: translateY(1px);
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
}
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
border-radius: 2px;
}
.card-header {
display: flex;
justify-content: center;
justify-content: space-between;
align-items: center;
padding: 16px 16px 12px 16px;
.course-title {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
flex: 1;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
margin-right: 12px;
}
.arrow-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
flex-shrink: 0;
.arrow-text {
font-size: 18px;
color: #c0c4cc;
font-weight: bold;
}
}
}
.card-body {
padding: 0 16px 16px 16px;
.course-info {
display: flex;
flex-direction: column;
gap: 8px;
.info-item {
display: flex;
align-items: center;
.info-label {
font-size: 14px;
color: #666;
min-width: 80px;
}
.info-value {
font-size: 14px;
color: #333;
flex: 1;
}
}
}
}
}
//
.empty-state {
text-align: center;
padding: 60px 20px;
color: #c0c4cc;
font-size: 14px;
}
//
.loading-more {
text-align: center;
padding: 20px;
color: #409eff;
font-size: 14px;
}
//
.no-more {
text-align: center;
padding: 20px;
color: #909399;
font-size: 14px;
}
.course-list {
//
.add-button-fixed {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 15px;
}
background-color: #fff;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 10;
border-top: 1px solid #eee;
.course-card {
display: flex;
align-items: center;
background-color: #ffffff;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
.course-image {
width: 80px;
height: 80px;
border-radius: 6px;
margin-right: 12px;
flex-shrink: 0;
background-color: #eee;
}
.course-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0; // Prevent text overflow issues
margin-right: 10px;
.course-title {
.add-btn {
width: 100%;
background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
border-radius: 12px;
padding: 12px 24px;
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.3);
font-weight: 600;
font-size: 16px;
font-weight: bold;
color: #303133;
margin-bottom: 4px;
// Ellipsis for long titles
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.course-desc {
font-size: 13px;
color: #909399;
margin-bottom: 4px;
// Ellipsis for long descriptions (optional, maybe 1 line)
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
// display: -webkit-box;
// -webkit-line-clamp: 1;
// -webkit-box-orient: vertical;
}
.course-date {
font-size: 12px;
color: #c0c4cc;
}
}
.register-btn {
background-color: #409eff;
color: #ffffff;
border: none;
border-radius: 15px; // Make it rounder
padding: 4px 12px;
font-size: 13px;
line-height: 1.4;
height: auto; // Let padding define height
flex-shrink: 0;
margin-left: auto; // Push to the right
&:active {
transform: translateY(1px);
}
&::after {
border: none;
}
&:active {
background-color: #3a8ee6;
}
}
//
@media screen and (max-width: 375px) {
.add-button-fixed {
bottom: 20px;
right: 20px;
.add-btn {
width: 50px;
height: 50px;
}
}
}
// Optional: If you need to adjust internal padding/height of search bar
// ::v-deep .uni-searchbar__box {
// height: 34px !important;
// line-height: 34px !important;
// }
//
:deep(.uni-searchbar__box) {
border-radius: 20px !important;
}
:deep(.uni-searchbar__text-placeholder) {
color: #999999 !important;
}
</style>

View File

@ -0,0 +1,886 @@
<template>
<view class="task-execution-page">
<!-- 页面标题 -->
<view class="page-header">
<text class="page-title">任务执行情况</text>
</view>
<!-- 任务信息 -->
<view class="task-info-card">
<view class="task-title">{{ taskInfo.rwmc }}</view>
<view class="task-meta">
<text class="meta-item">截止时间{{ formatDate(taskInfo.rwjstime) }}</text>
<text class="meta-item">负责人{{ taskInfo.rwfzrxm }}</text>
</view>
</view>
<!-- 完成情况统计 -->
<view class="completion-summary">
<view class="summary-header">
<text class="summary-title">完成情况{{ completionStats.completed }} | {{ completionStats.total }}</text>
</view>
<!-- Tab切换 -->
<view class="tab-container">
<view
class="tab-item"
:class="{ active: activeTab === 'submitted' }"
@click="switchTab('submitted')"
>
<text class="tab-text">已提交</text>
<text class="tab-count">({{ completionStats.completed }})</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'pending' }"
@click="switchTab('pending')"
>
<text class="tab-text">未提交</text>
<text class="tab-count">({{ completionStats.pending }})</text>
</view>
</view>
</view>
<!-- 执行人员列表 -->
<view class="execution-list">
<scroll-view scroll-y class="execution-scroll">
<view v-if="loading" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="filteredExecutionList.length === 0" class="empty-state">
<text class="empty-text">{{ activeTab === 'submitted' ? '暂无已提交记录' : '暂无未提交记录' }}</text>
</view>
<view v-else>
<view
v-for="execution in filteredExecutionList"
:key="execution.id"
class="execution-item"
@click="viewExecutionDetail(execution)"
>
<!-- 执行人信息 -->
<view class="executor-info">
<view class="executor-avatar">
<text class="avatar-text">{{ execution.rwzxfzrxm?.charAt(0) || '?' }}</text>
</view>
<view class="executor-details">
<text class="executor-name">{{ execution.rwzxfzrxm }}</text>
<text class="executor-id">{{ execution.rwzxfzr }}</text>
</view>
</view>
<!-- 执行状态 -->
<view class="execution-status">
<view :class="['status-badge', execution.rwzxzt === 'A' ? 'completed' : 'pending']">
<text class="status-text">{{ execution.rwzxzt === 'A' ? '已完成' : '未完成' }}</text>
</view>
<!-- 完成时间 -->
<view v-if="execution.rwzxzt === 'A' && execution.rwzxtime" class="completion-time">
<text class="time-text">{{ formatDateTime(execution.rwzxtime) }}</text>
</view>
</view>
<!-- 箭头图标 -->
<view class="arrow-icon">
<text class="arrow-text"></text>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 评论区域参考后台界面 -->
<view class="comment-section">
<view class="comment-header">
<text class="comment-title">评论交流</text>
<view class="comment-stats">
<text class="stats-item">👍 {{ commentStats.likes }}</text>
<text class="stats-item">💬 {{ commentStats.comments }}</text>
</view>
</view>
<!-- 评论列表 -->
<scroll-view scroll-y class="comment-list" :style="{ maxHeight: '200px' }">
<view v-if="commentList.length === 0" class="empty-comments">
<text class="empty-text">暂无评论</text>
</view>
<view v-else>
<view
v-for="comment in commentList"
:key="comment.id"
class="comment-item"
>
<view class="comment-user">
<view class="comment-avatar">
<text class="avatar-text">{{ comment.userName?.charAt(0) || '?' }}</text>
</view>
<text class="comment-username">{{ comment.userName }}</text>
</view>
<view class="comment-content">
<text class="comment-text">{{ comment.content }}</text>
</view>
<view class="comment-time">
<text class="time-text">{{ formatDateTime(comment.createTime) }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 评论输入 -->
<view class="comment-input-area">
<input
v-model="newComment"
placeholder="发表评论..."
class="comment-input"
@confirm="submitComment"
/>
<button
class="comment-submit-btn"
@click="submitComment"
:disabled="!newComment.trim()"
>
发送
</button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onLoad } from "vue";
import { onLoad as onPageLoad } from "@dcloudio/uni-app";
import { executedInfoByRwIdApi } from "@/api/base/rwzxApi";
//
interface TaskInfo {
id: string;
rwmc: string;
rwms: string;
rwjstime: string;
rwfzrxm: string;
rwfzr: string;
rwStatus: string;
rwlyId: string;
}
interface ExecutionItem {
id: string;
rwId: string;
rwzxfzr: string;
rwzxfzrxm: string;
iszxwc: string; // A: , B:
rwzxzt: string; // A: , B:
rwzxtime: string;
rwzxnr: string;
createTime: string;
jsId?: string; // ID
}
interface CommentItem {
id: string;
rwId: string;
userName: string;
content: string;
createTime: string;
}
//
const taskInfo = ref<TaskInfo>({
id: '',
rwmc: '',
rwms: '',
rwjstime: '',
rwfzrxm: '',
rwfzr: '',
rwStatus: '',
rwlyId: ''
});
const courseId = ref('');
const activeTab = ref('submitted'); // submitted: , pending:
const loading = ref(false);
const executionList = ref<ExecutionItem[]>([]);
const commentList = ref<CommentItem[]>([]);
const newComment = ref('');
const completedCount = ref(0);
const pendingCount = ref(0);
//
const completionStats = computed(() => {
const total = completedCount.value + pendingCount.value;
return {
total,
completed: completedCount.value,
pending: pendingCount.value
};
});
const commentStats = ref({
likes: 1587,
comments: 3
});
// TabiszxwcA=B=
const filteredExecutionList = computed(() => {
return executionList.value.filter(item => {
if (activeTab.value === 'submitted') {
return item.iszxwc === 'A'; //
} else {
return item.iszxwc === 'B'; //
}
});
});
//
onPageLoad((options: any) => {
console.log('任务执行页面接收到的参数:', options);
if (options.taskInfo) {
try {
taskInfo.value = JSON.parse(decodeURIComponent(options.taskInfo));
courseId.value = taskInfo.value.rwlyId || options.courseId || '';
console.log('任务信息:', taskInfo.value);
console.log('课程ID:', courseId.value);
loadExecutionList();
loadCommentList();
//
setTimeout(() => {
loadStatistics();
}, 100);
} catch (error) {
console.error('解析任务信息失败:', error);
uni.showToast({
title: '参数解析失败',
icon: 'error'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
} else {
console.error('缺少任务信息参数');
uni.showToast({
title: '缺少任务信息',
icon: 'error'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
// iszxwc
const loadStatistics = async () => {
try {
console.log('开始加载统计数据任务ID:', taskInfo.value.id);
// executedInfoByRwId
const response = await executedInfoByRwIdApi({ rwId: taskInfo.value.id });
console.log('执行情况API响应:', response);
// API
let allData = [];
if (response && response.resultCode === 1) {
allData = response.result || response.rows || response.data || [];
} else if (Array.isArray(response)) {
allData = response;
}
// iszxwcA=B=
const submittedData = allData.filter(item => item.iszxwc === 'A');
const pendingData = allData.filter(item => item.iszxwc === 'B');
completedCount.value = submittedData.length;
pendingCount.value = pendingData.length;
console.log('统计结果 - 已提交:', completedCount.value, '未提交:', pendingCount.value);
} catch (error) {
console.error('加载统计数据失败:', error);
// 使
completedCount.value = 0;
pendingCount.value = 0;
}
};
// executedInfoByRwId
const loadExecutionList = async () => {
loading.value = true;
try {
console.log('开始加载任务执行情况任务ID:', taskInfo.value.id);
// executedInfoByRwId
const response = await executedInfoByRwIdApi({ rwId: taskInfo.value.id });
console.log('执行情况API响应:', response);
// API
let executionData = [];
if (response) {
if (response.hasOwnProperty('resultCode')) {
if (response.resultCode === 1 || response.resultCode === 0) {
executionData = response.result || response.rows || response.data || [];
} else {
throw new Error(response?.message || response?.msg || '获取执行情况失败');
}
} else if (response.rows || response.data) {
executionData = response.rows || response.data || [];
} else if (Array.isArray(response)) {
executionData = response;
}
}
// iszxwc
executionList.value = executionData.map(item => ({
...item,
// iszxwcrwzxztA=()B=()
rwzxzt: item.iszxwc === 'A' ? 'A' : 'B'
}));
console.log('解析后的执行情况列表:', executionList.value);
} catch (error) {
console.error('加载任务执行情况失败:', error);
uni.showToast({
title: '加载执行情况失败',
icon: 'error'
});
executionList.value = [];
} finally {
loading.value = false;
}
};
//
const loadCommentList = async () => {
try {
// TODO: API
//
commentList.value = [
{
id: '1',
rwId: taskInfo.value.id,
userName: '测试',
content: '发表评论...',
createTime: new Date().toISOString()
}
];
} catch (error) {
console.error('加载评论列表失败:', error);
commentList.value = [];
}
};
// Tab
const switchTab = (tab: string) => {
activeTab.value = tab;
// TabcomputedfilteredExecutionList
};
//
const viewExecutionDetail = (execution: ExecutionItem) => {
// ID
const params = {
id: taskInfo.value.id,
jsId: execution.rwzxfzr,
executionId: execution.id
};
const paramStr = encodeURIComponent(JSON.stringify(params));
uni.navigateTo({
url: `/pages/view/routine/yishiyice/kcrwzxtj?params=${paramStr}`,
success: () => {
console.log('跳转到任务提交页面成功', {
taskId: taskInfo.value.id,
jsId: execution.rwzxfzr,
executionId: execution.id,
executionData: execution
});
},
fail: (error) => {
console.error('跳转到任务提交页面失败:', error);
uni.showToast({
title: '页面跳转失败',
icon: 'error'
});
}
});
};
//
const submitComment = async () => {
if (!newComment.value.trim()) {
return;
}
try {
// TODO: API
const commentData = {
rwId: taskInfo.value.id,
content: newComment.value.trim()
};
console.log('提交评论:', commentData);
//
commentList.value.push({
id: Date.now().toString(),
rwId: taskInfo.value.id,
userName: '当前用户', // TODO:
content: newComment.value.trim(),
createTime: new Date().toISOString()
});
commentStats.value.comments += 1;
newComment.value = '';
uni.showToast({
title: '评论成功',
icon: 'success'
});
} catch (error) {
console.error('提交评论失败:', error);
uni.showToast({
title: '评论失败',
icon: 'error'
});
}
};
//
const formatDate = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
};
const formatDateTime = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
};
</script>
<style lang="scss" scoped>
.task-execution-page {
min-height: 100vh;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
}
.page-header {
background-color: #fff;
padding: 15px;
border-bottom: 1px solid #f0f0f0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
.page-title {
font-size: 18px;
font-weight: 600;
color: #333;
text-align: center;
}
}
.task-info-card {
margin: 16px;
padding: 16px;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #f0f0f0;
.task-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.task-meta {
display: flex;
flex-direction: column;
gap: 4px;
.meta-item {
font-size: 12px;
color: #666;
}
}
}
.completion-summary {
margin: 0 16px 16px;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #f0f0f0;
overflow: hidden;
.summary-header {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
background-color: #f8f9fa;
.summary-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
.tab-container {
display: flex;
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
background-color: #fff;
border-right: 1px solid #f0f0f0;
transition: all 0.3s ease;
&:last-child {
border-right: none;
}
&.active {
background-color: #1890ff;
.tab-text, .tab-count {
color: #fff;
}
}
.tab-text {
font-size: 14px;
font-weight: 500;
color: #666;
margin-right: 4px;
}
.tab-count {
font-size: 12px;
color: #999;
}
}
}
}
.execution-list {
flex: 1;
margin: 0 16px;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #f0f0f0;
overflow: hidden;
}
.execution-scroll {
max-height: 300px;
padding: 16px;
}
.execution-item {
display: flex;
align-items: center;
padding: 12px;
margin-bottom: 8px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e8e8e8;
transition: all 0.2s ease;
&:active {
background-color: #e9ecef;
transform: scale(0.98);
}
&:last-child {
margin-bottom: 0;
}
}
.executor-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
.executor-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
.avatar-text {
color: #fff;
font-size: 14px;
font-weight: 500;
}
}
.executor-details {
flex: 1;
.executor-name {
display: block;
font-size: 14px;
color: #333;
font-weight: 500;
margin-bottom: 2px;
}
.executor-id {
font-size: 12px;
color: #666;
}
}
}
.execution-status {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
.status-badge {
padding: 4px 8px;
border-radius: 12px;
&.completed {
background-color: #f6ffed;
border: 1px solid #b7eb8f;
.status-text {
color: #52c41a;
font-size: 12px;
font-weight: 500;
}
}
&.pending {
background-color: #fff2e8;
border: 1px solid #ffbb96;
.status-text {
color: #fa8c16;
font-size: 12px;
font-weight: 500;
}
}
}
.completion-time {
.time-text {
font-size: 10px;
color: #999;
}
}
}
.arrow-icon {
margin-left: 8px;
.arrow-text {
font-size: 16px;
color: #ccc;
}
}
.comment-section {
margin: 16px;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #f0f0f0;
overflow: hidden;
.comment-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
.comment-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.comment-stats {
display: flex;
gap: 16px;
.stats-item {
font-size: 12px;
color: #666;
}
}
}
.comment-list {
padding: 12px 16px;
}
.comment-item {
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
margin-bottom: 0;
border-bottom: none;
}
.comment-user {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
.comment-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
.avatar-text {
color: #fff;
font-size: 10px;
font-weight: 500;
}
}
.comment-username {
font-size: 12px;
color: #333;
font-weight: 500;
}
}
.comment-content {
margin-bottom: 4px;
.comment-text {
font-size: 13px;
color: #666;
line-height: 1.4;
}
}
.comment-time {
.time-text {
font-size: 10px;
color: #999;
}
}
}
.empty-comments {
padding: 20px;
text-align: center;
.empty-text {
color: #ccc;
font-size: 12px;
}
}
.comment-input-area {
display: flex;
padding: 12px 16px;
border-top: 1px solid #f0f0f0;
background-color: #f8f9fa;
gap: 12px;
.comment-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 14px;
background-color: #fff;
&:focus {
border-color: #1890ff;
outline: none;
}
}
.comment-submit-btn {
padding: 8px 16px;
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
&:active:not(:disabled) {
background-color: #1976d2;
transform: scale(0.98);
}
}
}
}
.loading-container,
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #666;
.loading-text,
.empty-text {
font-size: 14px;
}
}
//
@media screen and (max-width: 375px) {
.execution-item {
padding: 10px;
.executor-avatar {
width: 32px;
height: 32px;
.avatar-text {
font-size: 12px;
}
}
}
.comment-stats {
flex-direction: column;
gap: 4px !important;
}
}
</style>

View File

@ -0,0 +1,431 @@
<!-- src/pages/base/message/detail.vue -->
<template>
<view class="message-detail-page">
<view v-if="isLoading" class="loading-indicator">加载中...</view>
<view v-else class="detail-content">
<view class="detail-header">
<view class="title-tag-row">
<text class="detail-title">{{ rw.rwmc }}</text>
</view>
<view class="detail-meta">
<text>{{ rw.rwkstime }}</text>
<!-- <text>{{ messageDetail.timeAgo }}</text>-->
</view>
</view>
<view class="detail-body">
<BasicForm :schema="schema" v-model="formData">
<!-- 注册自定义组件 -->
<template #ImageVideoUpload="{ field, label, required, componentProps, onChange }">
<ImageVideoUpload
v-model:image-list="formData[`${field}_images`]"
v-model:video-list="formData[`${field}_videos`]"
v-model:file-list="formData[`${field}_files`]"
@image-upload-success="(file, index) => onChange?.(file, field)"
@video-upload-success="(file, index) => onChange?.(file, field)"
@file-upload-success="(file, index) => onChange?.(file, field)"
v-bind="componentProps"
/>
</template>
</BasicForm>
</view>
<view class="detail-footer">
<button type="primary" class="action-button" @click="saveRwZx">提交</button>
</view>
</view>
<!-- <view v-else class="empty-state">消息详情未找到</view>-->
</view>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import {onLoad} from '@dcloudio/uni-app';
import {rwFindInfoByRwId, rwflFindRwlxsByRwId, rwzxExecutedInfoByRwIdAndJsApi, rwzxSaveApi} from "@/api/base/server";
import {useForm} from "@/components/BasicForm/hooks/useForm";
import {navigateBack, showToast} from "@/utils/uniapp";
import {useUserStore} from "@/store/modules/user";
import { ImageVideoUpload, type FileItem, COMPRESS_PRESETS } from "@/components/ImageVideoUpload";
import { attachmentUpload } from "@/api/system/upload";
interface MessageDetail {
id: string; // Assuming an ID is passed or can be derived
title: string;
desc: string;
date: string;
timeAgo: string;
tagText: string;
tagType: string;
// Add other fields as necessary
}
const formData: any = ref({})
const messageId = ref<string>('');
const jsId = ref<string>(''); // ID
const executionId = ref<string>(''); // ID
const messageDetail = ref<MessageDetail | null>({
id: 'todo1',
title: '教务通知 (待办)',
desc: '学校召开期初教学准备会议暨首次教学工作例会. 会议强调了新学期的教学重点和要求,请各位老师认真准备。',
date: '2025-02-17',
timeAgo: '8 mins 前',
tagText: '通知',
tagType: 'notice',
likes: 6,
comments: 12
});
const isLoading = ref(false);
const rwflx: any = ref([])
const rw = ref({})
const schema = reactive<FormsSchema[]>([])
const {getUser} = useUserStore()
async function saveRwZx() {
const result = [];
for (let i = 0; i < rwflx.value.length; i++) {
const fieldId = rwflx.value[i].id;
let fieldValue = formData.value[fieldId];
//
if (rwflx.value[i].rwfl == "sctp" || rwflx.value[i].rwfl == "scsp" || rwflx.value[i].rwfl == "scwd") {
// URL
const fileUrls = [];
if (rwflx.value[i].rwfl == "sctp") {
//
const images = formData.value[`${fieldId}_images`] || [];
fileUrls.push(...images.map((img: any) => img.url).filter(Boolean));
} else if (rwflx.value[i].rwfl == "scsp") {
//
const videos = formData.value[`${fieldId}_videos`] || [];
fileUrls.push(...videos.map((video: any) => video.url).filter(Boolean));
} else if (rwflx.value[i].rwfl == "scwd") {
//
const files = formData.value[`${fieldId}_files`] || [];
fileUrls.push(...files.map((file: any) => file.url).filter(Boolean));
}
fieldValue = fileUrls.join(',');
}
console.log(44, fieldId, fieldValue)
if (rwflx.value[i].rwbs && (!fieldValue || fieldValue == "")) {
showToast("请填写必填项!")
return;
}
result.push({
rwlxId: fieldId,
rwzxqdtx: fieldValue,
})
}
await rwzxSaveApi({
id: executionId.value, // ID
rwId: rw.value.id, // ID
rwzxfzr: jsId.value || getUser.id, // ID
mobile: getUser.mobile, //
rwzxqdDtos: result //
})
showToast("操作成功!");
uni.navigateBack({delta: 1})
}
const rwzxqds = ref([])
onLoad(async (options) => {
console.log('页面加载参数:', options);
let taskId = '';
//
if (options && options.params) {
try {
const params = JSON.parse(decodeURIComponent(options.params));
taskId = params.id;
jsId.value = params.jsId || '';
executionId.value = params.executionId || '';
console.log('解析参数成功:', { taskId, jsId: jsId.value, executionId: executionId.value });
} catch (error) {
console.error('解析参数失败:', error);
taskId = options.id || '';
}
} else if (options && options.id) {
taskId = options.id;
}
if (taskId) {
const {result} = await rwFindInfoByRwId({
rwId: taskId
});
rwflx.value = result.rwlxes;
rw.value = result;
for (let i = 0; i < rwflx.value.length; i++) {
//
if (rwflx.value[i].rwfl == "sctp" || rwflx.value[i].rwfl == "scsp" || rwflx.value[i].rwfl == "scwd") {
//
const fieldId = rwflx.value[i].id;
if (rwflx.value[i].rwfl == "sctp") {
formData.value[`${fieldId}_images`] = [];
} else if (rwflx.value[i].rwfl == "scsp") {
formData.value[`${fieldId}_videos`] = [];
} else if (rwflx.value[i].rwfl == "scwd") {
formData.value[`${fieldId}_files`] = [];
}
//
let componentConfig = {};
if (rwflx.value[i].rwfl == "sctp") {
//
componentConfig = {
component: "ImageVideoUpload",
componentProps: {
enableImage: true,
enableVideo: false,
enableFile: false,
maxImageCount: 5,
uploadApi: attachmentUpload,
compressConfig: COMPRESS_PRESETS.medium
}
};
} else if (rwflx.value[i].rwfl == "scsp") {
//
componentConfig = {
component: "ImageVideoUpload",
componentProps: {
enableImage: false,
enableVideo: true,
enableFile: false,
maxVideoCount: 3,
uploadApi: attachmentUpload,
compressConfig: COMPRESS_PRESETS.medium
}
};
} else if (rwflx.value[i].rwfl == "scwd") {
//
componentConfig = {
component: "ImageVideoUpload",
componentProps: {
enableImage: false,
enableVideo: false,
enableFile: true,
maxFileCount: 5,
allowedFileTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'mp3', 'wav', 'zip', 'rar'],
uploadApi: attachmentUpload,
compressConfig: COMPRESS_PRESETS.medium
}
};
}
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
required: rwflx.value[i].rwbs,
itemProps: {
labelPosition: "top",
},
...componentConfig
})
} else if (rwflx.value[i].rwfl == "text") {
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicInput",
required: rwflx.value[i].rwbs,
itemProps: {
labelPosition: "top",
},
componentProps: {
type: "textarea",
placeholder: "请输入内容"
},
})
} else if (rwflx.value[i].rwfl == "dxsx" || rwflx.value[i].rwfl == "dxxz") {
let options = rwflx.value[i].remark.split("");
let range = [];
for (let i = 0; i < options.length; i++) {
range.push({
name: options[i]
});
}
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicPicker",
required: rwflx.value[i].rwbs,
itemProps: {
labelPosition: "top",
},
componentProps: {
range: range,
rangeKey: "name",
savaKey: "name",
},
})
}
}
const res = await rwzxExecutedInfoByRwIdAndJsApi({
rwId: taskId,
jsId: jsId.value || getUser.id // 使jsId使ID
});
if (res && res.result && res.result.length) {
rwzxqds.value = res.result;
const showData = {};
for (let i = 0; i < rwzxqds.value.length; i++) {
showData[rwzxqds.value[i].rwlxId] = rwzxqds.value[i].rwzxqdtx;
}
formData.value = showData;
}
} else {
console.error('Message ID/Data is missing!');
uni.showToast({title: '加载失败,缺少信息', icon: 'none'});
}
});
</script>
<style scoped lang="scss">
.message-detail-page {
background-color: #f4f5f7;
min-height: 100vh;
padding: 15px;
box-sizing: border-box;
}
.loading-indicator,
.empty-state {
text-align: center;
color: #999;
padding: 40px 15px;
font-size: 14px;
}
.detail-content {
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.detail-header {
border-bottom: 1px solid #f0f0f0;
padding-bottom: 15px;
margin-bottom: 20px;
.title-tag-row {
display: flex;
justify-content: space-between;
align-items: flex-start; // Align items to the top
margin-bottom: 10px;
}
.detail-title {
font-size: 18px;
font-weight: bold;
color: #333;
flex: 1; // Allow title to take available space
margin-right: 10px; // Space between title and tag
line-height: 1.4;
text-align: center; //
}
.tag { // Reuse tag styles from index page if possible, or define here
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
color: #ffffff;
white-space: nowrap;
flex-shrink: 0; // Prevent tag from shrinking
&.notice {
background-color: #447ade;
}
&.task {
background-color: #19be6b;
}
&.approval {
background-color: #ff9f0a;
}
&.submit {
background-color: #8e8e93;
}
}
.detail-meta {
font-size: 12px;
color: #999;
text {
margin-right: 15px;
}
}
}
.detail-body {
margin-bottom: 40px;
.detail-desc {
font-size: 15px;
color: #555;
line-height: 1.7;
word-break: break-word;
}
}
.detail-footer {
// text-align: center; // Removed center alignment
// Add margin if needed, e.g., margin-top: 20px;
}
// Style for the action button
.action-button {
width: 100%; // Make button full width
height: 44px; // Standard button height
line-height: 44px; // Match height for vertical centering
font-size: 16px; // Slightly larger font
font-weight: 500; // Medium weight
border-radius: 8px; // Consistent border radius
margin-top: 20px; // Add space above the button
// Ensure primary color is applied correctly (uni-app default should work)
// background-color: #447ade;
// color: #ffffff;
}
// textFiled
:deep(.uni-input),
:deep(.uni-textarea) {
width: 100% !important;
min-height: 35px !important;
font-size: 13px !important;
border: 1px #CCCCCC solid !important;
border-radius: 3px !important;
padding: 8px 12px !important;
box-sizing: border-box !important;
}
// BasicForm
:deep(.basic-form) {
.uni-input,
.uni-textarea {
width: 100% !important;
min-height: 35px !important;
font-size: 13px !important;
border: 1px #CCCCCC solid !important;
border-radius: 3px !important;
padding: 8px 12px !important;
box-sizing: border-box !important;
}
}
// input textarea
:deep(input),
:deep(textarea) {
width: 100% !important;
min-height: 35px !important;
font-size: 13px !important;
border: 1px #CCCCCC solid !important;
border-radius: 3px !important;
padding: 8px 12px !important;
box-sizing: border-box !important;
}
</style>

View File

@ -0,0 +1,875 @@
<template>
<view class="push-page">
<!-- 任务信息 -->
<view class="task-info-card">
<view class="task-title">{{ taskInfo.rwmc }}</view>
<view class="task-meta">
<text class="meta-item">截止时间{{ formatDate(taskInfo.rwjstime) }}</text>
<text class="meta-item">负责人{{ taskInfo.rwfzrxm }}</text>
</view>
</view>
<!-- 推送对象选择 -->
<view class="push-section">
<view class="section-header">
<text class="section-title">选择推送对象</text>
<view class="section-actions">
<text class="action-btn" @click="selectAllMembers">全选</text>
<text class="action-btn" @click="unselectAllMembers">取消全选</text>
</view>
</view>
<!-- 成员分组列表 -->
<scroll-view scroll-y class="members-scroll">
<view
v-for="(groupMembers, groupName) in groupedMembers"
:key="groupName"
class="member-group-section"
>
<!-- 分组头部 -->
<view class="group-header">
<view class="group-checkbox-container" @click="handleGroupCheckChange(String(groupName))">
<view
:class="['checkbox-box', {
checked: isGroupChecked(String(groupName)),
indeterminate: isGroupIndeterminate(String(groupName))
}]"
>
<text v-if="isGroupChecked(String(groupName))" class="check-icon"></text>
<text v-else-if="isGroupIndeterminate(String(groupName))" class="check-icon">-</text>
</view>
<view class="group-info">
<text class="group-title">{{ groupName }}</text>
<text class="group-count">({{ groupMembers.length }})</text>
</view>
</view>
</view>
<!-- 分组成员列表 -->
<view class="group-members-list">
<view class="members-grid">
<view
v-for="member in groupMembers"
:key="member.id"
class="member-check-item"
@click="handleMemberCheckChange(member.id)"
>
<view :class="['checkbox-box', { checked: selectedMembers[member.id] }]">
<text v-if="selectedMembers[member.id]" class="check-icon"></text>
</view>
<view class="member-info">
<view class="member-avatar">
<text class="avatar-text">{{ member.jsxm?.charAt(0) || '?' }}</text>
</view>
<view class="member-details">
<text class="member-name">{{ member.jsxm }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="Object.keys(groupedMembers).length === 0" class="empty-state">
<text class="empty-text">暂无课程成员</text>
<text class="empty-hint">请先添加课程成员</text>
</view>
</scroll-view>
<!-- 推送统计 -->
<view class="push-summary">
<text class="summary-text">已选择 {{ selectedCount }} 名成员</text>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="bottom-actions">
<view class="action-buttons">
<button class="cancel-btn" @click="goBack" :disabled="isSubmitting">
取消
</button>
<button
class="confirm-btn"
@click="handleConfirmPush"
:disabled="isSubmitting || selectedCount === 0"
>
{{ isSubmitting ? '推送中...' : '确认推送' }}
</button>
</view>
</view>
<!-- 推送遮罩层 -->
<view v-if="isSubmitting" class="push-overlay">
<view class="push-loading">
<view class="loading-spinner"></view>
<text class="loading-text">正在推送任务...</text>
<text class="loading-hint">请稍候不要重复点击</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { kccyFindByKcjbIdApi } from "@/api/base/kccyApi";
import { rwPushJsApi } from "@/api/base/rwApi";
//
interface TaskInfo {
id: string;
rwmc: string;
rwms: string;
rwjstime: string;
rwfzrxm: string;
rwfzr: string;
rwStatus: string;
rwlyId: string;
}
interface MemberItem {
id: string;
jsId: string;
jsxm: string;
kcjbId: string;
fzmc: string;
}
// <EFBFBD>?
const taskInfo = ref<TaskInfo>({
id: '',
rwmc: '',
rwms: '',
rwjstime: '',
rwfzrxm: '',
rwfzr: '',
rwStatus: '',
rwlyId: ''
});
const courseId = ref('');
const memberList = ref<MemberItem[]>([]);
const selectedMembers = reactive<{ [key: string]: boolean }>({});
const isSubmitting = ref(false);
const lastClickTime = ref(0);
const DEBOUNCE_DELAY = 2000; // 2
// <EFBFBD>?
const groupedMembers = computed(() => {
const groups: { [key: string]: MemberItem[] } = {};
memberList.value.forEach(member => {
const groupName = member.fzmc || '未分组';
if (!groups[groupName]) {
groups[groupName] = [];
}
groups[groupName].push(member);
});
return groups;
});
const selectedCount = computed(() => {
return Object.values(selectedMembers).filter(Boolean).length;
});
//
const isGroupChecked = (groupName: string) => {
const groupMembers = groupedMembers.value[groupName] || [];
return groupMembers.length > 0 && groupMembers.every(member => selectedMembers[member.id]);
};
//
const isGroupIndeterminate = (groupName: string) => {
const groupMembers = groupedMembers.value[groupName] || [];
const checkedCount = groupMembers.filter(member => selectedMembers[member.id]).length;
return checkedCount > 0 && checkedCount < groupMembers.length;
};
//
onLoad((options: any) => {
console.log('推送页面接收到的参数:', options);
//
const storedTaskData = uni.getStorageSync('pushTaskData');
if (storedTaskData) {
try {
taskInfo.value = storedTaskData;
courseId.value = taskInfo.value.rwlyId || options.courseId || '';
console.log('任务信息:', taskInfo.value);
console.log('课程ID:', courseId.value);
//
uni.removeStorageSync('pushTaskData');
loadCourseMembers();
} catch (error) {
console.error('解析任务信息失败:', error);
uni.showToast({
title: '参数解析失败',
icon: 'error'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
} else {
console.error('缺少任务信息参数');
uni.showToast({
title: '缺少任务信息',
icon: 'error'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
//
const loadCourseMembers = async () => {
try {
console.log('开始加载课程成员课程ID:', courseId.value);
const response = await kccyFindByKcjbIdApi({ kcjbId: courseId.value });
console.log('课程成员API响应:', response);
// API
let memberData = [];
if (response) {
if (response.hasOwnProperty('resultCode')) {
if (response.resultCode === 1 || response.resultCode === 0) {
memberData = response.result || response.rows || response.data || [];
} else {
throw new Error(response?.message || response?.msg || '获取课程成员失败');
}
} else if (response.rows || response.data) {
memberData = response.rows || response.data || [];
} else if (Array.isArray(response)) {
memberData = response;
}
}
memberList.value = memberData;
console.log('解析后的成员列表:', memberList.value);
//
memberList.value.forEach(member => {
selectedMembers[member.id] = true;
});
} catch (error) {
console.error('加载课程成员失败:', error);
uni.showToast({
title: '加载成员失败',
icon: 'error'
});
memberList.value = [];
}
};
//
const handleGroupCheckChange = (groupName: string) => {
const groupMembers = groupedMembers.value[groupName] || [];
const isChecked = isGroupChecked(groupName);
groupMembers.forEach(member => {
selectedMembers[member.id] = !isChecked;
});
};
//
const handleMemberCheckChange = (memberId: string) => {
selectedMembers[memberId] = !selectedMembers[memberId];
};
//
const selectAllMembers = () => {
memberList.value.forEach(member => {
selectedMembers[member.id] = true;
});
};
//
const unselectAllMembers = () => {
memberList.value.forEach(member => {
selectedMembers[member.id] = false;
});
};
//
const handleConfirmPush = () => {
const currentTime = Date.now();
// 2
if (currentTime - lastClickTime.value < DEBOUNCE_DELAY) {
uni.showToast({
title: '请勿重复点击,请稍候',
icon: 'none',
duration: 1500
});
return;
}
//
if (isSubmitting.value) {
uni.showToast({
title: '正在推送中,请稍候',
icon: 'none',
duration: 1500
});
return;
}
lastClickTime.value = currentTime;
confirmPush();
};
//
const confirmPush = async () => {
if (selectedCount.value === 0) {
uni.showToast({
title: '请选择推送对象',
icon: 'error'
});
return;
}
//
const selectedMemberList = memberList.value.filter(member => selectedMembers[member.id]);
uni.showModal({
title: '确认推送',
content: `确定要推送任务"${taskInfo.value.rwmc}"给${selectedCount.value}名成员吗?`,
success: async (res) => {
if (res.confirm) {
await executePush(selectedMemberList);
}
}
});
};
//
const executePush = async (selectedMemberList: MemberItem[]) => {
isSubmitting.value = true;
try {
// API
const pushData = {
rwId: taskInfo.value.id,
jsIds: selectedMemberList.map(member => member.jsId).join(','),
rwlyId: courseId.value
};
const response = await rwPushJsApi(pushData);
console.log('推送API响应:', response);
if (response && response.resultCode === 1) {
uni.showToast({
title: `推送成功!已推送给${selectedCount.value}名成员`,
icon: 'success',
duration: 2000
});
setTimeout(() => {
uni.navigateBack();
}, 2000);
} else {
throw new Error(response?.message || '推送失败');
}
} catch (error) {
console.error('推送失败', error);
uni.showToast({
title: '推送失败,请重试',
icon: 'error'
});
} finally {
isSubmitting.value = false;
}
};
//
const formatDate = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
};
// <EFBFBD>?
const goBack = () => {
uni.navigateBack();
};
</script>
<style lang="scss" scoped>
.push-page {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #e8f4fd 100%);
display: flex;
flex-direction: column;
}
.task-info-card {
margin: 16px;
padding: 16px;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border-radius: 12px;
box-shadow:
0 2px 12px rgba(0, 0, 0, 0.06),
0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #409eff 0%, #67c23a 50%, #e6a23c 100%);
}
.task-title {
font-size: 16px;
font-weight: 700;
color: #1f2937;
margin-bottom: 8px;
line-height: 1.4;
letter-spacing: 0.2px;
}
.task-meta {
display: flex;
flex-direction: column;
gap: 8px;
.meta-item {
font-size: 14px;
color: #6b7280;
font-weight: 500;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.03);
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
}
}
.push-section {
flex: 1;
margin: 0 16px 80px 16px;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border-radius: 12px;
box-shadow:
0 2px 12px rgba(0, 0, 0, 0.06),
0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #409eff 0%, #67c23a 50%, #e6a23c 100%);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
margin-top: 3px;
.section-title {
font-size: 16px;
font-weight: 700;
color: #1f2937;
letter-spacing: 0.2px;
}
.section-actions {
display: flex;
gap: 12px;
.action-btn {
color: #409eff;
font-size: 14px;
font-weight: 600;
padding: 8px 16px;
border-radius: 8px;
background: rgba(64, 158, 255, 0.1);
border: 1px solid rgba(64, 158, 255, 0.2);
transition: all 0.3s ease;
&:active {
background: rgba(64, 158, 255, 0.2);
transform: translateY(1px);
}
}
}
}
}
.members-scroll {
flex: 1;
max-height: 60vh;
padding: 16px;
}
.member-group-section {
margin-bottom: 16px;
border: 1px solid #e5e7eb;
border-radius: 10px;
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
transition: all 0.3s ease;
position: relative;
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
}
.group-header {
padding: 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
.group-checkbox-container {
display: flex;
align-items: center;
gap: 12px;
.group-info {
display: flex;
align-items: center;
gap: 8px;
.group-title {
font-weight: 700;
color: #409eff;
font-size: 16px;
letter-spacing: 0.3px;
}
.group-count {
color: #6b7280;
font-size: 14px;
font-weight: 500;
}
}
}
}
.group-members-list {
padding: 8px 12px;
}
.members-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
}
.member-check-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border-radius: 8px;
border: 1px solid #e5e7eb;
transition: all 0.3s ease;
position: relative;
&:active {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-color: #409eff;
transform: translateY(1px);
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
}
.member-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
.member-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 4px rgba(64, 158, 255, 0.3);
.avatar-text {
color: #fff;
font-size: 14px;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
}
.member-details {
flex: 1;
.member-name {
display: block;
font-size: 14px;
color: #1f2937;
font-weight: 600;
letter-spacing: 0.1px;
}
}
}
}
.checkbox-box {
width: 18px;
height: 18px;
border: 2px solid #d1d5db;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
transition: all 0.3s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&.checked {
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
border-color: #409eff;
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.3);
.check-icon {
color: #fff;
font-size: 12px;
font-weight: bold;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
}
&.indeterminate {
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
border-color: #409eff;
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.3);
.check-icon {
color: #fff;
font-size: 16px;
font-weight: bold;
line-height: 1;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #666;
.empty-text {
font-size: 16px;
margin-bottom: 8px;
}
.empty-hint {
font-size: 12px;
color: #999;
}
}
.push-summary {
padding: 12px 16px;
background: linear-gradient(135deg, #f6ffed 0%, #f0f9ff 100%);
border-top: 1px solid rgba(0, 0, 0, 0.05);
text-align: center;
.summary-text {
color: #52c41a;
font-weight: 600;
font-size: 14px;
letter-spacing: 0.1px;
}
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
border-top: 1px solid #e5e5e5;
padding: 12px 16px;
z-index: 999;
.action-buttons {
display: flex;
gap: 12px;
.cancel-btn, .confirm-btn {
flex: 1;
height: 40px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: translateY(1px);
}
}
.cancel-btn {
background-color: #909399;
color: #fff;
&:hover {
background-color: #82848a;
}
}
.confirm-btn {
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
color: #fff;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
&:hover:not(:disabled) {
background: linear-gradient(135deg, #3a8ee6 0%, #337ecc 100%);
}
}
}
}
// <EFBFBD>?
@media screen and (max-width: 375px) {
.section-header {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.section-actions {
justify-content: center;
}
.member-check-item {
padding: 6px;
.member-avatar {
width: 28px;
height: 28px;
.avatar-text {
font-size: 12px;
}
}
}
}
/* 推送遮罩层 */
.push-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
backdrop-filter: blur(4px);
.push-loading {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 32px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.1),
0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
min-width: 200px;
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(64, 158, 255, 0.2);
border-top: 3px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
font-size: 16px;
font-weight: 600;
color: #1f2937;
text-align: center;
letter-spacing: 0.2px;
}
.loading-hint {
font-size: 12px;
color: #6b7280;
text-align: center;
opacity: 0.8;
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 遮罩层响应式调整 */
@media screen and (max-width: 375px) {
.push-overlay .push-loading {
margin: 20px;
padding: 24px;
.loading-spinner {
width: 32px;
height: 32px;
}
.loading-text {
font-size: 14px;
}
.loading-hint {
font-size: 11px;
}
}
}
</style>

View File

@ -95,7 +95,7 @@ onLoad(async (options) => {
rwflx.value = result.rwlxes;
rw.value = result;
for (let i = 0; i < rwflx.value.length; i++) {
if (rwflx.value[i].rwfl == "sctp" || rwflx.value[i].rwfl == "scsp" || rwflx.value[i].rwfl == "scwd") {
if (rwflx.value[i].rwfl == "sczy") {
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,

View File

@ -1,273 +1,322 @@
<!-- src/pages/base/message/detail.vue -->
<template>
<view class="message-detail-page">
<view v-if="isLoading" class="loading-indicator">加载中...</view>
<view v-else class="detail-content">
<view class="detail-header">
<view class="title-tag-row">
<text class="detail-title">{{ rw.rwmc }}</text>
<view class="tag" :class="rw.tagType">{{ rw.tagText }}</view>
</view>
<view class="detail-meta">
<text>{{ rw.rwkstime }}</text>
<!-- <text>{{ messageDetail.timeAgo }}</text>-->
</view>
</view>
<view class="detail-body">
<BasicForm :schema="schema" v-model="formData">
<div class="container">
<!-- 搜索框 -->
<div class="search-form">
<input
v-model="searchKeyword"
type="text"
placeholder="搜索任务..."
class="search-input"
@input="onSearch"
/>
</div>
</BasicForm>
</view>
<view class="detail-footer" v-if="rwzxqds.length==0">
<button type="primary" class="action-button" @click="saveRwZx">处理</button>
</view>
</view>
<!-- <view v-else class="empty-state">消息详情未找到</view>-->
</view>
<!-- 任务列表 -->
<div class="task-list">
<div
v-for="task in filteredTasks"
:key="task.id"
class="item"
@click="handleTaskClick(task)"
>
<div class="item-title">{{ task.rwmc || '任务名称' }}</div>
<div class="detail">
任务描述<span class="detail-content">{{ task.rwms || '暂无描述' }}</span>
</div>
<div class="detail">
执行状态<span class="detail-content" :class="getStatusClass(task.zxzt)">{{ getStatusText(task.zxzt) }}</span>
</div>
<div class="detail">
发布时间<span class="detail-content">{{ formatDate(task.fbsj) }}</span>
</div>
<div class="detail">
截止时间<span class="detail-content">{{ formatDate(task.jzsj) }}</span>
</div>
<div class="detail" v-if="task.zxjg">
执行结果<span class="detail-content">{{ task.zxjg }}</span>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredTasks.length === 0 && !loading" class="empty-state">
<div class="empty-text">暂无任务数据</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<div class="loading-text">加载中...</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import {onLoad} from '@dcloudio/uni-app';
import {rwFindInfoByRwId, rwflFindRwlxsByRwId, rwzxExecutedInfoByRwIdAndJsApi, rwzxSaveApi} from "@/api/base/server";
import {useForm} from "@/components/BasicForm/hooks/useForm";
import {navigateBack, showToast} from "@/utils/uniapp";
import {useUserStore} from "@/store/modules/user";
<script>
import { rwzxFindAllApi } from '@/api/base/rwzxApi'
interface MessageDetail {
id: string; // Assuming an ID is passed or can be derived
title: string;
desc: string;
date: string;
timeAgo: string;
tagText: string;
tagType: string;
// Add other fields as necessary
export default {
name: 'TaskList',
data() {
return {
tasks: [], //
filteredTasks: [], //
searchKeyword: '', //
loading: false, //
}
},
mounted() {
this.loadTasks()
},
methods: {
//
async loadTasks() {
try {
this.loading = true
const response = await rwzxFindAllApi()
console.log('API响应数据:', response)
const formData: any = ref({})
const messageId = ref<string>('');
const messageDetail = ref<MessageDetail | null>({
id: 'todo1',
title: '教务通知 (待办)',
desc: '学校召开期初教学准备会议暨首次教学工作例会. 会议强调了新学期的教学重点和要求,请各位老师认真准备。',
date: '2025-02-17',
timeAgo: '8 mins 前',
tagText: '通知',
tagType: 'notice',
likes: 6,
comments: 12
});
const isLoading = ref(false);
const rwflx: any = ref([])
const rw = ref({})
const schema = reactive<FormsSchema[]>([])
const {getUser} = useUserStore()
async function saveRwZx() {
const result = [];
for (let i = 0; i < rwflx.value.length; i++) {
console.log(44, rwflx.value[i].id, formData.value[rwflx.value[i].id])
if (rwflx.value[i].rwbs && (!formData.value[rwflx.value[i].id] || formData.value[rwflx.value[i].id] == "")) {
showToast("请填写必填项!")
return;
}
result.push({
rwlxId: rwflx.value[i].id,
rwzxqdtx: formData.value[rwflx.value[i].id],
})
}
await rwzxSaveApi({
mobile: getUser.mobile,
rwId: rw.value.id,
rwzxqdDtos: result
})
showToast("操作成功!");
uni.navigateBack({delta: 1})
}
const rwzxqds = ref([])
onLoad(async (options) => {
if (options && options.id) {
const {result} = await rwFindInfoByRwId({
rwId: options.id
});
rwflx.value = result.rwlxes;
rw.value = result;
for (let i = 0; i < rwflx.value.length; i++) {
if (rwflx.value[i].rwfl == "sctp" || rwflx.value[i].rwfl == "scsp" || rwflx.value[i].rwfl == "scwd") {
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicUpload",
required: rwflx.value[i].rwbs,
componentProps: {}
})
} else if (rwflx.value[i].rwfl == "text") {
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicInput",
required: rwflx.value[i].rwbs,
itemProps: {
labelPosition: "top",
},
componentProps: {
type: "textarea",
},
})
} else if (rwflx.value[i].rwfl == "dxsx" || rwflx.value[i].rwfl == "dxxz") {
let options = rwflx.value[i].remark.split("");
let range = [];
for (let i = 0; i < options.length; i++) {
range.push({
name: options[i]
});
}
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicPicker",
required: rwflx.value[i].rwbs,
itemProps: {
labelPosition: "top",
},
componentProps: {
range: range,
rangeKey: "name",
savaKey: "name",
},
})
}
}
const res = await rwzxExecutedInfoByRwIdAndJsApi({
rwId: options.id,
mobile: getUser.mobile
});
if (res && res.result && res.result.length) {
rwzxqds.value = res.result;
const showData = {};
for (let i = 0; i < rwzxqds.value.length; i++) {
showData[rwzxqds.value[i].rwlxId] = rwzxqds.value[i].rwzxqdtx;
}
formData.value = showData;
}
// APIresultCode: 1 rows
if (response && (response.resultCode === 1 || response.resultCode === 0)) {
this.tasks = response.result || response.rows || response.data || []
this.filteredTasks = [...this.tasks]
console.log('任务列表加载成功:', this.tasks)
} else {
console.error('Message ID/Data is missing!');
uni.showToast({title: '加载失败,缺少信息', icon: 'none'});
const errorMsg = response?.message || response?.msg || '获取任务列表失败'
console.error('获取任务列表失败:', errorMsg)
alert(errorMsg)
}
} catch (error) {
console.error('加载任务列表失败:', error)
alert('网络错误,请稍后重试')
} finally {
this.loading = false
}
},
//
onSearch() {
if (!this.searchKeyword.trim()) {
this.filteredTasks = [...this.tasks]
return
}
const keyword = this.searchKeyword.toLowerCase()
this.filteredTasks = this.tasks.filter(task => {
return (
(task.rwmc && task.rwmc.toLowerCase().includes(keyword)) ||
(task.rwms && task.rwms.toLowerCase().includes(keyword)) ||
(task.zxjg && task.zxjg.toLowerCase().includes(keyword))
)
})
},
//
handleTaskClick(task) {
//
console.log('点击任务:', task)
//
// this.$router.push(`/task/detail/${task.id}`)
},
//
getStatusText(status) {
const statusMap = {
'0': '未开始',
'1': '进行中',
'2': '已完成',
'3': '已逾期',
'': '未知状态'
}
return statusMap[status] || '未知状态'
},
//
getStatusClass(status) {
const classMap = {
'0': 'status-pending',
'1': 'status-progress',
'2': 'status-completed',
'3': 'status-overdue'
}
return classMap[status] || 'status-unknown'
},
//
formatDate(dateStr) {
if (!dateStr) return '暂无'
try {
const date = new Date(dateStr)
if (isNaN(date.getTime())) return dateStr
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
} catch (error) {
return dateStr
}
}
}
}
});
</script>
<style scoped lang="scss">
.message-detail-page {
background-color: #f4f5f7;
<style scoped>
/* 基础样式 */
.container {
padding: 20px 10px;
min-height: 100vh;
padding: 15px;
background-color: #F6F9FC;
}
/* 搜索框样式 */
.search-form {
margin-bottom: 15px;
padding: 0 5px;
}
.search-input {
width: 100%;
padding: 10px 15px;
font-size: 14px;
border: 1px solid #CCCCCC;
border-radius: 20px;
background-color: white;
outline: none;
box-sizing: border-box;
}
.loading-indicator,
.empty-state {
text-align: center;
color: #999;
padding: 40px 15px;
font-size: 14px;
.search-input:focus {
border-color: #409EFF;
box-shadow: 0 0 5px rgba(64, 158, 255, 0.3);
}
/* 任务列表样式 */
.task-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.item {
margin: 5px 0;
background-color: white;
display: flex;
flex-direction: column;
justify-content: left;
padding: 15px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s ease;
}
.item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.item:active {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.item-title {
font-size: 16px;
font-weight: bold;
padding: 3px 0 8px 0;
border-bottom: 1px solid #EEEEEE;
line-height: 1.4;
color: #333;
}
.detail {
font-size: 13px;
line-height: 2em;
color: #666666;
display: flex;
align-items: center;
}
.detail-content {
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-left: 8px;
flex: 1;
}
.detail-header {
border-bottom: 1px solid #f0f0f0;
padding-bottom: 15px;
margin-bottom: 20px;
/* 状态样式 */
.status-pending {
color: #909399;
}
.title-tag-row {
.status-progress {
color: #409EFF;
font-weight: bold;
}
.status-completed {
color: #67C23A;
font-weight: bold;
}
.status-overdue {
color: #F56C6C;
font-weight: bold;
}
.status-unknown {
color: #909399;
}
/* 空状态样式 */
.empty-state {
display: flex;
justify-content: space-between;
align-items: flex-start; // Align items to the top
margin-bottom: 10px;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.detail-title {
font-size: 18px;
font-weight: bold;
color: #333;
flex: 1; // Allow title to take available space
margin-right: 10px; // Space between title and tag
line-height: 1.4;
.empty-text {
font-size: 16px;
color: #909399;
margin-top: 20px;
}
.tag { // Reuse tag styles from index page if possible, or define here
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
color: #ffffff;
white-space: nowrap;
flex-shrink: 0; // Prevent tag from shrinking
&.notice {
background-color: #447ade;
/* 加载状态样式 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
}
&.task {
background-color: #19be6b;
.loading-text {
font-size: 14px;
color: #409EFF;
margin-top: 10px;
}
&.approval {
background-color: #ff9f0a;
/* 响应式适配 */
@media (max-width: 480px) {
.container {
padding: 15px 8px;
}
&.submit {
background-color: #8e8e93;
}
.item {
padding: 12px;
}
.detail-meta {
font-size: 12px;
color: #999;
text {
margin-right: 15px;
}
}
}
.detail-body {
margin-bottom: 40px;
.detail-desc {
.item-title {
font-size: 15px;
color: #555;
line-height: 1.7;
word-break: break-word;
}
}
.detail-footer {
// text-align: center; // Removed center alignment
// Add margin if needed, e.g., margin-top: 20px;
.detail {
font-size: 12px;
}
// Style for the action button
.action-button {
width: 100%; // Make button full width
height: 44px; // Standard button height
line-height: 44px; // Match height for vertical centering
font-size: 16px; // Slightly larger font
font-weight: 500; // Medium weight
border-radius: 8px; // Consistent border radius
margin-top: 20px; // Add space above the button
// Ensure primary color is applied correctly (uni-app default should work)
// background-color: #447ade;
// color: #ffffff;
}
</style>

View File

@ -0,0 +1,273 @@
<!-- src/pages/base/message/detail.vue -->
<template>
<view class="message-detail-page">
<view v-if="isLoading" class="loading-indicator">加载中...</view>
<view v-else class="detail-content">
<view class="detail-header">
<view class="title-tag-row">
<text class="detail-title">{{ rw.rwmc }}</text>
<view class="tag" :class="rw.tagType">{{ rw.tagText }}</view>
</view>
<view class="detail-meta">
<text>{{ rw.rwkstime }}</text>
<!-- <text>{{ messageDetail.timeAgo }}</text>-->
</view>
</view>
<view class="detail-body">
<BasicForm :schema="schema" v-model="formData">
</BasicForm>
</view>
<view class="detail-footer" v-if="rwzxqds.length==0">
<button type="primary" class="action-button" @click="saveRwZx">处理</button>
</view>
</view>
<!-- <view v-else class="empty-state">消息详情未找到</view>-->
</view>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import {onLoad} from '@dcloudio/uni-app';
import {rwFindInfoByRwId, rwflFindRwlxsByRwId, rwzxExecutedInfoByRwIdAndJsApi, rwzxSaveApi} from "@/api/base/server";
import {useForm} from "@/components/BasicForm/hooks/useForm";
import {navigateBack, showToast} from "@/utils/uniapp";
import {useUserStore} from "@/store/modules/user";
interface MessageDetail {
id: string; // Assuming an ID is passed or can be derived
title: string;
desc: string;
date: string;
timeAgo: string;
tagText: string;
tagType: string;
// Add other fields as necessary
}
const formData: any = ref({})
const messageId = ref<string>('');
const messageDetail = ref<MessageDetail | null>({
id: 'todo1',
title: '教务通知 (待办)',
desc: '学校召开期初教学准备会议暨首次教学工作例会. 会议强调了新学期的教学重点和要求,请各位老师认真准备。',
date: '2025-02-17',
timeAgo: '8 mins 前',
tagText: '通知',
tagType: 'notice',
likes: 6,
comments: 12
});
const isLoading = ref(false);
const rwflx: any = ref([])
const rw = ref({})
const schema = reactive<FormsSchema[]>([])
const {getUser} = useUserStore()
async function saveRwZx() {
const result = [];
for (let i = 0; i < rwflx.value.length; i++) {
console.log(44, rwflx.value[i].id, formData.value[rwflx.value[i].id])
if (rwflx.value[i].rwbs && (!formData.value[rwflx.value[i].id] || formData.value[rwflx.value[i].id] == "")) {
showToast("请填写必填项!")
return;
}
result.push({
rwlxId: rwflx.value[i].id,
rwzxqdtx: formData.value[rwflx.value[i].id],
})
}
await rwzxSaveApi({
mobile: getUser.mobile,
rwId: rw.value.id,
rwzxqdDtos: result
})
showToast("操作成功!");
uni.navigateBack({delta: 1})
}
const rwzxqds = ref([])
onLoad(async (options) => {
if (options && options.id) {
const {result} = await rwFindInfoByRwId({
rwId: options.id
});
rwflx.value = result.rwlxes;
rw.value = result;
for (let i = 0; i < rwflx.value.length; i++) {
if (rwflx.value[i].rwfl == "sczy") {
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicUpload",
required: rwflx.value[i].rwbs,
componentProps: {}
})
} else if (rwflx.value[i].rwfl == "text") {
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicInput",
required: rwflx.value[i].rwbs,
itemProps: {
labelPosition: "top",
},
componentProps: {
type: "textarea",
},
})
} else if (rwflx.value[i].rwfl == "dxsx" || rwflx.value[i].rwfl == "dxxz") {
let options = rwflx.value[i].remark.split("");
let range = [];
for (let i = 0; i < options.length; i++) {
range.push({
name: options[i]
});
}
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicPicker",
required: rwflx.value[i].rwbs,
itemProps: {
labelPosition: "top",
},
componentProps: {
range: range,
rangeKey: "name",
savaKey: "name",
},
})
}
}
const res = await rwzxExecutedInfoByRwIdAndJsApi({
rwId: options.id,
mobile: getUser.mobile
});
if (res && res.result && res.result.length) {
rwzxqds.value = res.result;
const showData = {};
for (let i = 0; i < rwzxqds.value.length; i++) {
showData[rwzxqds.value[i].rwlxId] = rwzxqds.value[i].rwzxqdtx;
}
formData.value = showData;
}
} else {
console.error('Message ID/Data is missing!');
uni.showToast({title: '加载失败,缺少信息', icon: 'none'});
}
});
</script>
<style scoped lang="scss">
.message-detail-page {
background-color: #f4f5f7;
min-height: 100vh;
padding: 15px;
box-sizing: border-box;
}
.loading-indicator,
.empty-state {
text-align: center;
color: #999;
padding: 40px 15px;
font-size: 14px;
}
.detail-content {
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.detail-header {
border-bottom: 1px solid #f0f0f0;
padding-bottom: 15px;
margin-bottom: 20px;
.title-tag-row {
display: flex;
justify-content: space-between;
align-items: flex-start; // Align items to the top
margin-bottom: 10px;
}
.detail-title {
font-size: 18px;
font-weight: bold;
color: #333;
flex: 1; // Allow title to take available space
margin-right: 10px; // Space between title and tag
line-height: 1.4;
}
.tag { // Reuse tag styles from index page if possible, or define here
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
color: #ffffff;
white-space: nowrap;
flex-shrink: 0; // Prevent tag from shrinking
&.notice {
background-color: #447ade;
}
&.task {
background-color: #19be6b;
}
&.approval {
background-color: #ff9f0a;
}
&.submit {
background-color: #8e8e93;
}
}
.detail-meta {
font-size: 12px;
color: #999;
text {
margin-right: 15px;
}
}
}
.detail-body {
margin-bottom: 40px;
.detail-desc {
font-size: 15px;
color: #555;
line-height: 1.7;
word-break: break-word;
}
}
.detail-footer {
// text-align: center; // Removed center alignment
// Add margin if needed, e.g., margin-top: 20px;
}
// Style for the action button
.action-button {
width: 100%; // Make button full width
height: 44px; // Standard button height
line-height: 44px; // Match height for vertical centering
font-size: 16px; // Slightly larger font
font-weight: 500; // Medium weight
border-radius: 8px; // Consistent border radius
margin-top: 20px; // Add space above the button
// Ensure primary color is applied correctly (uni-app default should work)
// background-color: #447ade;
// color: #ffffff;
}
</style>

View File

@ -0,0 +1,273 @@
<!-- src/pages/base/message/detail.vue -->
<template>
<view class="message-detail-page">
<view v-if="isLoading" class="loading-indicator">加载中...</view>
<view v-else class="detail-content">
<view class="detail-header">
<view class="title-tag-row">
<text class="detail-title">{{ rw.rwmc }}</text>
<view class="tag" :class="rw.tagType">{{ rw.tagText }}</view>
</view>
<view class="detail-meta">
<text>{{ rw.rwkstime }}</text>
<!-- <text>{{ messageDetail.timeAgo }}</text>-->
</view>
</view>
<view class="detail-body">
<BasicForm :schema="schema" v-model="formData">
</BasicForm>
</view>
<view class="detail-footer" v-if="rwzxqds.length==0">
<button type="primary" class="action-button" @click="saveRwZx">处理</button>
</view>
</view>
<!-- <view v-else class="empty-state">消息详情未找到</view>-->
</view>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import {onLoad} from '@dcloudio/uni-app';
import {rwFindInfoByRwId, rwflFindRwlxsByRwId, rwzxExecutedInfoByRwIdAndJsApi, rwzxSaveApi} from "@/api/base/server";
import {useForm} from "@/components/BasicForm/hooks/useForm";
import {navigateBack, showToast} from "@/utils/uniapp";
import {useUserStore} from "@/store/modules/user";
interface MessageDetail {
id: string; // Assuming an ID is passed or can be derived
title: string;
desc: string;
date: string;
timeAgo: string;
tagText: string;
tagType: string;
// Add other fields as necessary
}
const formData: any = ref({})
const messageId = ref<string>('');
const messageDetail = ref<MessageDetail | null>({
id: 'todo1',
title: '教务通知 (待办)',
desc: '学校召开期初教学准备会议暨首次教学工作例会. 会议强调了新学期的教学重点和要求,请各位老师认真准备。',
date: '2025-02-17',
timeAgo: '8 mins 前',
tagText: '通知',
tagType: 'notice',
likes: 6,
comments: 12
});
const isLoading = ref(false);
const rwflx: any = ref([])
const rw = ref({})
const schema = reactive<FormsSchema[]>([])
const {getUser} = useUserStore()
async function saveRwZx() {
const result = [];
for (let i = 0; i < rwflx.value.length; i++) {
console.log(44, rwflx.value[i].id, formData.value[rwflx.value[i].id])
if (rwflx.value[i].rwbs && (!formData.value[rwflx.value[i].id] || formData.value[rwflx.value[i].id] == "")) {
showToast("请填写必填项!")
return;
}
result.push({
rwlxId: rwflx.value[i].id,
rwzxqdtx: formData.value[rwflx.value[i].id],
})
}
await rwzxSaveApi({
mobile: getUser.mobile,
rwId: rw.value.id,
rwzxqdDtos: result
})
showToast("操作成功!");
uni.navigateBack({delta: 1})
}
const rwzxqds = ref([])
onLoad(async (options) => {
if (options && options.id) {
const {result} = await rwFindInfoByRwId({
rwId: options.id
});
rwflx.value = result.rwlxes;
rw.value = result;
for (let i = 0; i < rwflx.value.length; i++) {
if (rwflx.value[i].rwfl == "sczy") {
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicUpload",
required: rwflx.value[i].rwbs,
componentProps: {}
})
} else if (rwflx.value[i].rwfl == "text") {
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicInput",
required: rwflx.value[i].rwbs,
itemProps: {
labelPosition: "top",
},
componentProps: {
type: "textarea",
},
})
} else if (rwflx.value[i].rwfl == "dxsx" || rwflx.value[i].rwfl == "dxxz") {
let options = rwflx.value[i].remark.split("");
let range = [];
for (let i = 0; i < options.length; i++) {
range.push({
name: options[i]
});
}
schema.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicPicker",
required: rwflx.value[i].rwbs,
itemProps: {
labelPosition: "top",
},
componentProps: {
range: range,
rangeKey: "name",
savaKey: "name",
},
})
}
}
const res = await rwzxExecutedInfoByRwIdAndJsApi({
rwId: options.id,
mobile: getUser.mobile
});
if (res && res.result && res.result.length) {
rwzxqds.value = res.result;
const showData = {};
for (let i = 0; i < rwzxqds.value.length; i++) {
showData[rwzxqds.value[i].rwlxId] = rwzxqds.value[i].rwzxqdtx;
}
formData.value = showData;
}
} else {
console.error('Message ID/Data is missing!');
uni.showToast({title: '加载失败,缺少信息', icon: 'none'});
}
});
</script>
<style scoped lang="scss">
.message-detail-page {
background-color: #f4f5f7;
min-height: 100vh;
padding: 15px;
box-sizing: border-box;
}
.loading-indicator,
.empty-state {
text-align: center;
color: #999;
padding: 40px 15px;
font-size: 14px;
}
.detail-content {
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.detail-header {
border-bottom: 1px solid #f0f0f0;
padding-bottom: 15px;
margin-bottom: 20px;
.title-tag-row {
display: flex;
justify-content: space-between;
align-items: flex-start; // Align items to the top
margin-bottom: 10px;
}
.detail-title {
font-size: 18px;
font-weight: bold;
color: #333;
flex: 1; // Allow title to take available space
margin-right: 10px; // Space between title and tag
line-height: 1.4;
}
.tag { // Reuse tag styles from index page if possible, or define here
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
color: #ffffff;
white-space: nowrap;
flex-shrink: 0; // Prevent tag from shrinking
&.notice {
background-color: #447ade;
}
&.task {
background-color: #19be6b;
}
&.approval {
background-color: #ff9f0a;
}
&.submit {
background-color: #8e8e93;
}
}
.detail-meta {
font-size: 12px;
color: #999;
text {
margin-right: 15px;
}
}
}
.detail-body {
margin-bottom: 40px;
.detail-desc {
font-size: 15px;
color: #555;
line-height: 1.7;
word-break: break-word;
}
}
.detail-footer {
// text-align: center; // Removed center alignment
// Add margin if needed, e.g., margin-top: 20px;
}
// Style for the action button
.action-button {
width: 100%; // Make button full width
height: 44px; // Standard button height
line-height: 44px; // Match height for vertical centering
font-size: 16px; // Slightly larger font
font-weight: 500; // Medium weight
border-radius: 8px; // Consistent border radius
margin-top: 20px; // Add space above the button
// Ensure primary color is applied correctly (uni-app default should work)
// background-color: #447ade;
// color: #ffffff;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB