公文流转

This commit is contained in:
hebo 2025-09-06 21:46:10 +08:00
parent e97d71f230
commit d866410866
16 changed files with 1361 additions and 303 deletions

View File

@ -41,7 +41,7 @@
</template>
<script lang="ts" setup>
import {ref, onMounted} from "vue";
import {ref, onMounted, onActivated} from "vue";
import { useLayout } from "@/components/BasicListLayout/hooks/useLayout";
import { xxtsListApi } from "@/api/base/server";
import { getTimeAgo } from "@/utils/dateUtils";
@ -80,7 +80,7 @@ const fetchDbLxMap = async () => {
const [register, {reload, setParam}] = useLayout({
api: xxtsListApi,
componentProps: {
auto: false
auto: true // true
},
});
@ -107,6 +107,12 @@ onMounted(() => {
fetchListData(currentTab.value);
});
//
onActivated(() => {
console.log('页面激活,重新加载数据');
fetchListData(currentTab.value);
});
const goToDetail = (data: any) => {
if (data && data.id) {
setDb(data);

View File

@ -29,33 +29,106 @@
<!-- 文件信息 -->
<view class="file-section">
<view class="section-title">附件</view>
<view class="file-list">
<view class="file-list" v-if="hasAttachments">
<!-- 处理单个附件从gwInfo直接获取 -->
<view
v-if="gwInfo.fileUrl"
class="file-item"
@click="previewSingleFile"
>
<view class="file-icon">
<text v-if="isImage(gwInfo.fileFormat || '')">🖼</text>
<text v-else-if="isVideo(gwInfo.fileFormat || '')">🎥</text>
<text v-else-if="canPreview(gwInfo.fileFormat || '')">📄</text>
<text v-else>📎</text>
</view>
<view class="file-info">
<text class="file-name">{{ gwInfo.fileName || '未知文件' }}</text>
<text class="file-type">{{ (gwInfo.fileFormat || 'unknown').toUpperCase() }}</text>
</view>
<view class="file-actions">
<u-button
v-if="canPreview(gwInfo.fileFormat || '') && !isVideo(gwInfo.fileFormat || '')"
text="预览"
size="mini"
type="primary"
@click.stop="previewSingleFile"
/>
<u-button
v-if="isVideo(gwInfo.fileFormat || '')"
text="播放"
size="mini"
type="success"
@click.stop="previewSingleFile"
/>
<u-button
v-if="isImage(gwInfo.fileFormat || '')"
text="查看"
size="mini"
type="warning"
@click.stop="previewSingleFile"
/>
<u-button
text="下载"
size="mini"
type="info"
@click.stop="downloadSingleFile"
/>
</view>
</view>
<!-- 处理多个附件从files数组获取 -->
<view
v-for="(file, index) in gwInfo.files"
:key="index"
class="file-item"
@click="previewFile(file)"
>
<view class="file-icon">📎</view>
<view class="file-icon">
<text v-if="isImage(getFileSuffix(file))">🖼</text>
<text v-else-if="isVideo(getFileSuffix(file))">🎥</text>
<text v-else-if="canPreview(getFileSuffix(file))">📄</text>
<text v-else>📎</text>
</view>
<view class="file-info">
<text class="file-name">{{ file.name }}</text>
<text class="file-name">{{ getFileName(file) }}</text>
<text class="file-size">{{ formatFileSize(file.size) }}</text>
<text class="file-type">{{ getFileSuffix(file).toUpperCase() }}</text>
</view>
<view class="file-actions">
<u-button
v-if="canPreview(getFileSuffix(file)) && !isVideo(getFileSuffix(file))"
text="预览"
size="mini"
type="primary"
@click.stop="previewFile(file)"
/>
<u-button
v-if="isVideo(getFileSuffix(file))"
text="播放"
size="mini"
type="success"
@click.stop="previewFile(file)"
/>
<u-button
v-if="isImage(getFileSuffix(file))"
text="查看"
size="mini"
type="warning"
@click.stop="previewFile(file)"
/>
<u-button
text="下载"
size="mini"
type="info"
@click.stop="downloadFile(file)"
/>
</view>
</view>
</view>
<view v-else class="no-files">
<text>暂无附件</text>
</view>
</view>
<!-- 当前处理人 -->
@ -95,7 +168,7 @@
<view class="section-title">抄送人</view>
<view class="cc-list">
<view
v-for="ccUser in ccUsers"
v-for="ccUser in displayedCcUsers"
:key="ccUser.id"
class="cc-item"
>
@ -108,6 +181,17 @@
</view>
</view>
</view>
<!-- 更多按钮 -->
<view
v-if="ccUsers.length > 2"
class="more-button"
@click="toggleCcExpanded"
>
<text class="more-text">
{{ ccExpanded ? '收起' : `更多(${ccUsers.length - 2})` }}
</text>
<text class="more-icon" :class="{ expanded: ccExpanded }"></text>
</view>
</view>
<!-- 操作记录 -->
@ -115,7 +199,7 @@
<view class="section-title">操作记录</view>
<view class="log-list">
<view
v-for="log in operationLogs"
v-for="log in displayedOperationLogs"
:key="log.id"
class="log-item"
>
@ -136,12 +220,25 @@
</view>
</view>
</view>
<!-- 更多按钮 -->
<view
v-if="operationLogs.length > 2"
class="more-button"
@click="toggleLogExpanded"
>
<text class="more-text">
{{ logExpanded ? '收起' : `更多(${operationLogs.length - 2})` }}
</text>
<text class="more-icon" :class="{ expanded: logExpanded }"></text>
</view>
</view>
</view>
<!-- 底部固定按钮 -->
<view class="bottom-actions">
<view class="bottom-actions" v-if="canCurrentUserOperate">
<!-- 驳回按钮暂时隐藏 -->
<!-- <u-button
text="驳回"
@ -194,13 +291,23 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ref, onMounted, computed } from "vue";
import BasicLayout from "@/components/BasicLayout/Layout.vue";
import { navigateTo } from "@/utils/uniapp";
import { getGwFlowByIdApi, gwApproveApi } from "@/api/routine/gw";
import dayjs from "dayjs";
import { useUserStore } from "@/store/modules/user";
import { imagUrl } from "@/utils";
import {
isVideo,
isImage,
canPreview,
previewFile as previewFileUtil,
previewVideo as previewVideoUtil,
previewImage as previewImageUtil,
downloadFile as downloadFileUtil
} from "@/utils/filePreview";
//
interface GwInfo {
@ -211,7 +318,7 @@ interface GwInfo {
status: string;
createdBy: string;
createdTime: Date;
files: FileInfo[];
files?: FileInfo[]; //
approvers?: Approver[];
ccUsers?: CCUser[];
operationLogs?: OperationLog[];
@ -219,12 +326,19 @@ interface GwInfo {
urgencyLevel: string; //
tjrId: string; //
spRule?: string; //
//
fileUrl?: string; // URL
fileName?: string; //
fileFormat?: string; //
}
interface FileInfo {
name: string;
size: number;
url: string;
resourName?: string; //
resourUrl?: string; // URL
resSuf?: string; //
}
interface Approver {
@ -277,6 +391,62 @@ const ccUsers = ref<CCUser[]>([]);
const operationLogs = ref<OperationLog[]>([]);
const currentLog = ref<OperationLog>({} as OperationLog);
//
const ccExpanded = ref(false);
//
const logExpanded = ref(false);
//
const hasAttachments = computed(() => {
return gwInfo.value.fileUrl || (gwInfo.value.files && gwInfo.value.files.length > 0);
});
//
const displayedCcUsers = computed(() => {
if (ccExpanded.value || ccUsers.value.length <= 2) {
return ccUsers.value;
}
return ccUsers.value.slice(0, 2);
});
//
const displayedOperationLogs = computed(() => {
if (logExpanded.value || operationLogs.value.length <= 2) {
return operationLogs.value;
}
return operationLogs.value.slice(0, 2);
});
// /
const canCurrentUserOperate = computed(() => {
const userStore = useUserStore();
const getUser = userStore.getUser;
const getJs = userStore.getJs;
const currentUserId = getJs?.id;
if (!currentUserId || !approvers.value || approvers.value.length === 0) {
return false;
}
//
const currentUserApprover = approvers.value.find(approver => {
return approver.userId === currentUserId || approver.id === currentUserId;
});
if (!currentUserApprover) {
return false;
}
// approvedrejected
const approveStatus = currentUserApprover.approveStatus;
if (approveStatus === 'approved' || approveStatus === 'rejected') {
return false;
}
return true;
});
//
const getGwInfo = async () => {
@ -358,6 +528,16 @@ const showLogDetail = (log: OperationLog) => {
showLogDetailModal.value = true;
};
//
const toggleCcExpanded = () => {
ccExpanded.value = !ccExpanded.value;
};
//
const toggleLogExpanded = () => {
logExpanded.value = !logExpanded.value;
};
//
const handleReject = () => {
uni.showModal({
@ -494,10 +674,20 @@ const approveGw = async () => {
uni.showToast({
title: "同意成功",
icon: "success",
duration: 1500
});
//
await getGwInfo();
//
setTimeout(() => {
//
uni.navigateBack({
delta: 1,
success: () => {
// 线
uni.$emit('refreshGwList');
}
});
}, 1500);
} else {
throw new Error(response.message || '同意失败');
}
@ -514,6 +704,23 @@ const approveGw = async () => {
}
};
//
const getCurrentUserApproverStatus = () => {
const userStore = useUserStore();
const getJs = userStore.getJs;
const currentUserId = getJs?.id;
if (!currentUserId || !approvers.value || approvers.value.length === 0) {
return '未知';
}
const currentUserApprover = approvers.value.find(approver => {
return approver.userId === currentUserId || approver.id === currentUserId;
});
return currentUserApprover?.approveStatus || '未知';
};
// ID
const getCurrentUserApproverId = (currentUserId: string) => {
console.log('=== getCurrentUserApproverId 函数调试 ===');
@ -560,16 +767,221 @@ const getCurrentUserApproverId = (currentUserId: string) => {
return result;
};
// gwInfo
const previewSingleFile = () => {
console.log("=== 预览单个附件 ===");
console.log("附件信息:", {
fileUrl: gwInfo.value.fileUrl,
fileName: gwInfo.value.fileName,
fileFormat: gwInfo.value.fileFormat
});
if (!gwInfo.value.fileUrl) {
console.error("没有找到附件URL");
return;
}
const fileUrl = imagUrl(gwInfo.value.fileUrl);
const fileName = gwInfo.value.fileName || '未知文件';
const fileFormat = gwInfo.value.fileFormat || '';
console.log("处理后的文件URL:", fileUrl);
console.log("文件名:", fileName);
console.log("文件格式:", fileFormat);
//
if (isVideo(fileFormat)) {
handlePreviewVideoSingle(fileUrl, fileName);
} else if (isImage(fileFormat)) {
handlePreviewImageSingle(fileUrl);
} else if (canPreview(fileFormat)) {
handlePreviewDocumentSingle(fileUrl, fileName, fileFormat);
} else {
//
downloadSingleFile();
}
};
//
const downloadSingleFile = () => {
if (!gwInfo.value.fileUrl) {
console.error("没有找到附件URL");
return;
}
const fileUrl = imagUrl(gwInfo.value.fileUrl);
const fileName = gwInfo.value.fileName || '未知文件';
const fileFormat = gwInfo.value.fileFormat || '';
const fullFileName = fileFormat ? `${fileName}.${fileFormat}` : fileName;
console.log("下载单个附件:", { fileUrl, fullFileName });
downloadFileUtil(fileUrl, fullFileName)
.then(() => {
console.log('文件下载成功');
uni.showToast({
title: '下载成功',
icon: 'success'
});
})
.catch((error: any) => {
console.error('文件下载失败:', error);
uni.showToast({
title: '下载失败',
icon: 'error'
});
});
};
//
const previewFile = (file: FileInfo) => {
//
console.log("预览文件:", file);
console.log("=== 处理文件预览 ===");
console.log("文件信息:", file);
// URL
const fileUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
const fileName = file.resourName ? `${file.resourName}.${file.resSuf}` : file.name;
const fileSuf = file.resSuf || file.name.split('.').pop() || '';
console.log("处理后的文件URL:", fileUrl);
console.log("文件名:", fileName);
console.log("文件后缀:", fileSuf);
//
if (isVideo(fileSuf)) {
handlePreviewVideo(file);
} else if (isImage(fileSuf)) {
handlePreviewImage(file);
} else if (canPreview(fileSuf)) {
handlePreviewDocument(file);
} else {
//
downloadFileAction(file);
}
};
//
const handlePreviewDocument = (file: FileInfo) => {
const fileUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
const fileName = file.resourName ? `${file.resourName}.${file.resSuf}` : file.name;
const fileSuf = file.resSuf || file.name.split('.').pop() || '';
previewFileUtil(fileUrl, fileName, fileSuf)
.then(() => {
console.log('文档预览成功');
})
.catch((error: any) => {
console.error('文档预览失败:', error);
//
downloadFileAction(file);
});
};
//
const handlePreviewDocumentSingle = (fileUrl: string, fileName: string, fileFormat: string) => {
const fullFileName = fileFormat ? `${fileName}.${fileFormat}` : fileName;
previewFileUtil(fileUrl, fullFileName, fileFormat)
.then(() => {
console.log('单个附件文档预览成功');
})
.catch((error: any) => {
console.error('单个附件文档预览失败:', error);
//
downloadSingleFile();
});
};
//
const handlePreviewVideo = (file: FileInfo) => {
console.log('=== 处理视频预览 ===');
console.log('视频文件:', file);
const videoUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
const videoName = file.resourName || file.name;
console.log('处理后的视频URL:', videoUrl);
previewVideoUtil(videoUrl, videoName)
.then(() => {
console.log('视频预览成功');
})
.catch((error: any) => {
console.error('视频预览失败:', error);
});
};
//
const handlePreviewVideoSingle = (videoUrl: string, videoName: string) => {
console.log('=== 处理单个附件视频预览 ===');
console.log('视频URL:', videoUrl);
console.log('视频名称:', videoName);
previewVideoUtil(videoUrl, videoName)
.then(() => {
console.log('单个附件视频预览成功');
})
.catch((error: any) => {
console.error('单个附件视频预览失败:', error);
});
};
//
const handlePreviewImage = (file: FileInfo) => {
const imageUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
previewImageUtil(imageUrl)
.then(() => {
console.log('图片预览成功');
})
.catch((error: any) => {
console.error('图片预览失败:', error);
});
};
//
const handlePreviewImageSingle = (imageUrl: string) => {
console.log('=== 处理单个附件图片预览 ===');
console.log('图片URL:', imageUrl);
previewImageUtil(imageUrl)
.then(() => {
console.log('单个附件图片预览成功');
})
.catch((error: any) => {
console.error('单个附件图片预览失败:', error);
});
};
//
const downloadFile = (file: FileInfo) => {
//
downloadFileAction(file);
};
//
const downloadFileAction = (file: FileInfo) => {
const fileUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
const fileName = file.resourName ? `${file.resourName}.${file.resSuf}` : file.name;
console.log("下载文件:", file);
console.log("下载URL:", fileUrl);
console.log("文件名:", fileName);
downloadFileUtil(fileUrl, fileName)
.then(() => {
console.log('文件下载成功');
uni.showToast({
title: '下载成功',
icon: 'success'
});
})
.catch((error: any) => {
console.error('文件下载失败:', error);
uni.showToast({
title: '下载失败',
icon: 'error'
});
});
};
//
@ -579,11 +991,42 @@ const formatTime = (time: any) => {
//
const formatFileSize = (size: any) => {
if (!size) return "0B";
if (size < 1024) return size + "B";
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + "KB";
return (size / (1024 * 1024)).toFixed(2) + "MB";
};
//
const getFileName = (file: FileInfo) => {
if (file.resourName) {
return file.resourName;
}
if (file.name) {
// name
const lastDotIndex = file.name.lastIndexOf('.');
if (lastDotIndex > 0) {
return file.name.substring(0, lastDotIndex);
}
return file.name;
}
return '未知文件';
};
//
const getFileSuffix = (file: FileInfo) => {
if (file.resSuf) {
return file.resSuf;
}
if (file.name) {
const lastDotIndex = file.name.lastIndexOf('.');
if (lastDotIndex > 0) {
return file.name.substring(lastDotIndex + 1);
}
}
return 'unknown';
};
//
const getStatusClass = (status: any) => {
const statusMap: Record<string, string> = {
@ -830,6 +1273,10 @@ onMounted(() => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.log-section {
margin-bottom: 40px; //
}
.section-title {
font-size: 16px;
font-weight: bold;
@ -885,14 +1332,23 @@ onMounted(() => {
.file-item {
display: flex;
align-items: center;
padding: 10px;
padding: 12px;
border: 1px solid #eee;
border-radius: 4px;
border-radius: 8px;
margin-bottom: 10px;
background: #fafafa;
transition: all 0.3s ease;
&:active {
transform: translateY(1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.file-icon {
margin-right: 10px;
font-size: 20px;
margin-right: 12px;
font-size: 24px;
width: 32px;
text-align: center;
}
.file-info {
@ -901,21 +1357,49 @@ onMounted(() => {
.file-name {
display: block;
font-weight: 500;
margin-bottom: 2px;
margin-bottom: 4px;
color: #333;
font-size: 14px;
}
.file-size {
font-size: 12px;
color: #666;
margin-right: 8px;
}
.file-type {
font-size: 11px;
color: #999;
background: #f0f0f0;
padding: 2px 6px;
border-radius: 4px;
}
}
.file-actions {
display: flex;
gap: 5px;
gap: 4px;
flex-wrap: wrap;
justify-content: flex-end;
.u-button {
min-width: 20px;
height: 26px;
font-size: 12px;
border-radius: 13px;
padding: 0 6px;
}
}
}
.no-files {
text-align: center;
padding: 40px 20px;
color: #999;
font-size: 14px;
}
.approver-item,
.cc-item {
display: flex;
@ -995,6 +1479,41 @@ onMounted(() => {
}
}
.more-button {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
margin-top: 10px;
margin-bottom: 20px; //
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
&:active {
background: #e9ecef;
transform: translateY(1px);
}
.more-text {
font-size: 14px;
color: #007aff;
margin-right: 6px;
}
.more-icon {
font-size: 12px;
color: #007aff;
transition: transform 0.3s ease;
&.expanded {
transform: rotate(180deg);
}
}
}
.log-item {
padding: 10px;
border: 1px solid #eee;

View File

@ -5,8 +5,15 @@
</view>
<template #bottom>
<view class="flex-row items-center pb-10 pt-5">
<u-button text="取消" class="mx-15" @click="handleCancel" />
<u-button text="确认转办" class="mx-15" type="primary" @click="handleTransfer" />
<u-button text="取消" class="mx-15" :disabled="isTransferring" @click="handleCancel" />
<u-button
:text="isTransferring ? '转办中...' : '确认转办'"
class="mx-15"
type="primary"
:disabled="isTransferring"
:loading="isTransferring"
@click="handleTransfer"
/>
</view>
</template>
</BasicLayout>
@ -32,6 +39,8 @@ const xxtsInfo = ref<any>(null);
const gwInfo = ref<any>(null);
const approvers = ref<any[]>([]);
const ccUsers = ref<any[]>([]);
//
const isTransferring = ref(false);
//
const [register, { getValue, setValue }] = useForm({
@ -69,6 +78,26 @@ const [register, { getValue, setValue }] = useForm({
disabled: true,
},
},
{
field: "isCc",
label: "是否抄送",
component: "BasicPicker",
defaultValue: "1",
required: true,
componentProps: {
placeholder: "请选择是否抄送",
range: [
{ label: "是", value: "1" },
{ label: "否", value: "0" }
],
rangeKey: "label",
savaKey: "value",
onChange: async (value: any) => {
//
setValue({ isCc: value });
}
},
},
{
field: "transferTo",
label: "转办人",
@ -107,9 +136,9 @@ const [register, { getValue, setValue }] = useForm({
field: "transferReason",
label: "转办描述",
component: "BasicInput",
required: true,
required: false,
componentProps: {
placeholder: "请输入转办原因和描述",
placeholder: "请输入转办原因和描述(可选)",
type: "textarea",
rows: 5,
},
@ -226,6 +255,11 @@ const handleCancel = () => {
//
const handleTransfer = async () => {
//
if (isTransferring.value) {
return;
}
try {
const value = await getValue();
@ -234,6 +268,9 @@ const handleTransfer = async () => {
return;
}
//
isTransferring.value = true;
//
uni.showLoading({
title: '正在转办...',
@ -260,7 +297,8 @@ const handleTransfer = async () => {
//
transferTo: value.transferTo,
ccTo: value.ccTo || [], //
isCc: value.isCc, //
ccTo: value.isCc === "1" ? (value.ccTo || []) : [], //
spRule: gwInfo.value?.spRule, // gwInfospRule
transferReason: value.transferReason,
transferTime: new Date().toISOString(),
@ -268,6 +306,7 @@ const handleTransfer = async () => {
};
console.log('转办数据:', transferData);
console.log('是否抄送:', value.isCc);
console.log('转办人数据类型:', typeof value.transferTo);
console.log('转办人数据内容:', JSON.stringify(value.transferTo));
console.log('抄送人数据类型:', typeof value.ccTo);
@ -305,11 +344,23 @@ const handleTransfer = async () => {
title: "转办失败",
icon: "error",
});
} finally {
//
isTransferring.value = false;
}
};
//
const validateForm = (value: any) => {
//
if (!value.isCc) {
uni.showToast({
title: "请选择是否抄送",
icon: "error",
});
return false;
}
//
if (!value.transferTo || value.transferTo.length === 0) {
uni.showToast({
@ -319,14 +370,6 @@ const validateForm = (value: any) => {
return false;
}
//
if (!value.transferReason || value.transferReason.trim() === "") {
uni.showToast({
title: "请输入转办描述",
icon: "error",
});
return false;
}
//
if (!xxtsInfo.value?.id) {
@ -353,8 +396,6 @@ const validateForm = (value: any) => {
return false;
}
//
return true;
};
</script>

View File

@ -10,7 +10,7 @@
<!-- 搜索框 -->
<view class="search-item">
<BasicSearch
placeholder="搜索公文标题或编号"
placeholder="搜索公文标题、编号或类型"
@search="handleSearch"
class="search-input"
/>
@ -46,24 +46,29 @@
<view class="info-item">
<text class="info-label">类型</text>
<text class="info-value">{{ data.docType }}</text>
<text class="info-label" style="margin-left: 20px;">提交时间</text>
<text class="info-value">{{ formatDate(data.tjrtime) }}</text>
</view>
<view class="info-item">
<text class="info-label">紧急程度</text>
<text class="info-value urgency-tag" :class="getUrgencyClass(data.urgencyLevel)">
{{ getUrgencyText(data.urgencyLevel) }}
</text>
</view>
<view class="info-item">
<text class="info-label">审批进度</text>
<text class="info-value">{{ getApproverProgress(data) }}</text>
</view>
<view class="info-item">
<text class="info-label">提交人</text>
<text class="info-value">{{ data.tjrxm || '未知' }}</text>
</view>
<view class="info-item">
<text class="info-label">提交时间</text>
<text class="info-value">{{ formatTime(data.tjrtime || data.createdTime) }}</text>
</view>
<!-- 附件列表 -->
<view class="attachments-section" v-if="hasAttachments(data)">
<view class="attachments-list">
<!-- 统一处理所有附件 -->
<view
v-for="(file, index) in parseFileList(data)"
:key="index"
class="attachment-item"
@click="previewAttachmentFile(file)"
>
<view class="attachment-icon">
<text v-if="isImage(getFileSuffix(file))">🖼</text>
<text v-else-if="isVideo(getFileSuffix(file))">🎥</text>
<text v-else-if="canPreview(getFileSuffix(file))">📄</text>
<text v-else>📎</text>
</view>
<text class="attachment-name">{{ getFileName(file) }}</text>
</view>
</view>
</view>
</view>
@ -71,31 +76,19 @@
<view class="card-footer">
<view class="footer-actions">
<u-button
text="详情"
:text="getButtonText(data)"
size="mini"
type="primary"
:class="getButtonClass(data)"
@click="goToDetail(data)"
/>
<u-button
v-if="data.gwStatus === 'B'"
text="编辑"
size="mini"
@click="editGw(data)"
/>
<u-button
v-if="data.gwStatus === 'B'"
text="删除"
size="mini"
type="error"
@click="deleteGw(data)"
/>
</view>
</view>
</view>
</template>
<!-- 新建公文按钮放在bottom插槽中 -->
<template #bottom>
<!-- 新建公文按钮已隐藏 -->
<!-- <template #bottom>
<view class="flex-row items-center pb-10 pt-5">
<u-button
text="新建公文"
@ -104,35 +97,73 @@
@click="createNewGw"
/>
</view>
</template>
</template> -->
</BasicListLayout>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from "vue";
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { navigateTo } from "@/utils/uniapp";
import BasicSearch from "@/components/BasicSearch/Search.vue";
import BasicLayout from "@/components/BasicLayout/Layout.vue";
import BasicListLayout from "@/components/BasicListLayout/ListLayout.vue";
import { useLayout } from "@/components/BasicListLayout/hooks/useLayout";
import { gwFindPageApi, gwLogicDeleteApi } from "@/api/routine/gw";
import { GwStatus, UrgencyLevel, ApproverStatus } from "@/types/gw";
import type { GwInfo, GwListItem } from "@/types/gw";
import { gwFindPageApi } from "@/api/routine/gw";
import dayjs from "dayjs";
import { imagUrl } from "@/utils";
import { useUserStore } from "@/store/modules/user";
import {
isVideo,
isImage,
canPreview,
previewFile as previewFileUtil,
previewVideo as previewVideoUtil,
previewImage as previewImageUtil,
downloadFile as downloadFileUtil
} from "@/utils/filePreview";
//
interface FileInfo {
name: string;
size: number;
url: string;
resourName?: string; //
resourUrl?: string; // URL
resSuf?: string; //
resourSuf?: string; //
}
//
interface GwListItem {
id: string;
title: string;
docType: string;
gwStatus: string;
fileUrl?: string;
fileName?: string;
fileFormat?: string;
files?: FileInfo[];
spZbqd?: string; // ID
tjrtime?: string; //
[key: string]: any;
}
//
const filterTabs = [
{ key: "all", label: "全部" },
{ key: "A", label: "已提交" },
{ key: "B", label: "草稿" },
{ key: "C", label: "审批中" },
{ key: "D", label: "已完结" },
];
const activeTab = ref("all");
const searchKeyword = ref("");
// store
const userStore = useUserStore();
// 使 BasicListLayout
const [register, { reload, setParam }] = useLayout({
api: gwFindPageApi,
@ -141,6 +172,7 @@ const [register, { reload, setParam }] = useLayout({
},
param: {
title: "",
docType: "",
gwStatus: "",
},
});
@ -148,21 +180,28 @@ const [register, { reload, setParam }] = useLayout({
//
const dataList = ref<GwListItem[]>([]);
// ID
const getCurrentTeacherId = () => {
const jsData = userStore.getJs;
return jsData?.id || null;
};
//
const filteredGwList = computed(() => {
let list = dataList.value;
//
if (activeTab.value !== "all") {
list = list.filter(item => item.gwStatus === activeTab.value);
list = list.filter((item: GwListItem) => item.gwStatus === activeTab.value);
}
//
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase();
list = list.filter(item =>
list = list.filter((item: GwListItem) =>
item.title.toLowerCase().includes(keyword) ||
(item.gwNo && item.gwNo.toLowerCase().includes(keyword))
(item.gwNo && item.gwNo.toLowerCase().includes(keyword)) ||
(item.docType && item.docType.toLowerCase().includes(keyword))
);
}
@ -181,61 +220,20 @@ const switchTab = (tabKey: string) => {
//
const handleSearch = (keyword: string) => {
searchKeyword.value = keyword;
//
setParam({ title: keyword });
//
setParam({
title: keyword,
docType: keyword //
});
reload();
};
//
const goToDetail = (item: GwListItem) => {
navigateTo(`/pages/view/routine/gwlz/gwDetail?id=${item.id}`);
navigateTo(`/pages/view/routine/gwlz/gwFlow?id=${item.id}`);
};
//
const editGw = (item: GwListItem) => {
console.log('编辑公文ID:', item.id, '标题:', item.title);
const url = `/pages/view/routine/gwlz/gwAdd?id=${item.id}&mode=edit`;
//
uni.setStorageSync('gwEditMode', 'edit');
uni.setStorageSync('gwEditId', item.id);
console.log('存储编辑参数到本地存储:', { mode: 'edit', id: item.id });
console.log('跳转到编辑页面:', url);
navigateTo(url);
};
//
const deleteGw = (item: GwListItem) => {
uni.showModal({
title: "确认删除",
content: `确定要删除公文"${item.title}"吗?`,
success: async (res) => {
if (res.confirm) {
try {
// API - {ids: item.id}
await gwLogicDeleteApi({ ids: item.id });
//
reload();
uni.showToast({
title: "删除成功",
icon: "success",
});
} catch (error) {
console.error("删除公文失败:", error);
uni.showToast({
title: "删除失败",
icon: "error",
});
}
}
},
});
};
//
//
const createNewGw = () => {
@ -253,8 +251,10 @@ const createNewGw = () => {
//
const getStatusClass = (status: string) => {
const statusMap: Record<string, string> = {
'A': "status-submitted", //
'B': "status-draft", // 稿
'A': "status-draft", // A
'B': "status-submitted", // B
'C': "status-pending", // C
'D': "status-completed", // D
};
return statusMap[status] || "status-default";
};
@ -262,87 +262,245 @@ const getStatusClass = (status: string) => {
//
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
'A': "已提交", //
'B': "草稿", // 稿
'A': "暂存", // A
'B': "提交", // B
'C': "审批中", // C
'D': "已完结", // D
};
return statusMap[status] || "未知";
};
//
const getUrgencyClass = (urgency: string) => {
const urgencyMap: Record<string, string> = {
'low': "urgency-low",
'normal': "urgency-normal",
'high': "urgency-high",
'urgent': "urgency-urgent",
};
return urgencyMap[urgency] || "urgency-normal";
};
//
const getUrgencyText = (urgency: string) => {
const urgencyMap: Record<string, string> = {
'low': "普通",
'normal': "一般",
'high': "紧急",
'urgent': "特急",
};
return urgencyMap[urgency] || "一般";
};
//
const getApproverProgress = (item: GwListItem) => {
let spCount = 0;
let ccCount = 0;
//
const getButtonText = (item: GwListItem) => {
const currentTeacherId = getCurrentTeacherId();
const { gwStatus, spZbqd } = item;
// -
if (item.spId) {
if (Array.isArray(item.spId)) {
spCount = item.spId.length;
} else {
//
const spIdStr = String(item.spId);
if (spIdStr.trim()) {
spCount = spIdStr.split(',').filter((id: string) => id.trim()).length;
}
// IDspZbqdB""
if (currentTeacherId && spZbqd && gwStatus === 'B') {
const approverIds = spZbqd.split(',').map(id => id.trim());
if (approverIds.includes(currentTeacherId)) {
return '审批';
}
}
// -
if (item.ccId) {
if (Array.isArray(item.ccId)) {
ccCount = item.ccId.length;
} else {
//
const ccIdStr = String(item.ccId);
if (ccIdStr.trim()) {
ccCount = ccIdStr.split(',').filter((id: string) => id.trim()).length;
}
}
}
if (spCount === 0 && ccCount === 0) {
return "无审批人和抄送人";
}
let result = "";
if (spCount > 0) {
result += `${spCount}个审批人`;
}
if (ccCount > 0) {
if (result) result += "";
result += `${ccCount}个抄送人`;
}
return result;
// ""
return '详情';
};
//
const getButtonClass = (item: GwListItem) => {
const buttonText = getButtonText(item);
return buttonText === '审批' ? 'action-button-approve' : 'action-button-detail';
};
//
//
//
const formatTime = (time: string | Date | undefined) => {
if (!time) return '暂无';
return dayjs(time).format("MM-DD HH:mm");
};
//
const formatDate = (time: string | Date | undefined) => {
if (!time) return '暂无';
return dayjs(time).format("YYYY-MM-DD");
};
//
const hasAttachments = (data: any) => {
return data.fileUrl || (data.files && data.files.length > 0);
};
//
const parseFileList = (data: any) => {
const fileList: FileInfo[] = [];
// data
if (data.fileUrl && data.fileName) {
const urls = data.fileUrl.split(',').map((url: string) => url.trim());
const names = data.fileName.split(',').map((name: string) => name.trim());
urls.forEach((url: string, index: number) => {
if (url) {
const fileName = names[index] || `文件${index + 1}`;
const fileSuffix = url.split('.').pop() || '';
fileList.push({
name: fileName,
url: url,
resourName: fileName,
resourUrl: url,
resourSuf: fileSuffix,
size: 0 // URL
});
}
});
}
// files
if (data.files && data.files.length > 0) {
fileList.push(...data.files);
}
return fileList;
};
// data
const previewAttachment = (data: any) => {
if (!data.fileUrl) {
console.error("没有找到附件URL");
return;
}
const fileUrl = imagUrl(data.fileUrl);
const fileName = data.fileName || '未知文件';
const fileFormat = data.fileFormat || '';
//
if (isVideo(fileFormat)) {
previewVideoUtil(fileUrl, fileName)
.then(() => console.log('视频预览成功'))
.catch((error: any) => console.error('视频预览失败:', error));
} else if (isImage(fileFormat)) {
previewImageUtil(fileUrl)
.then(() => console.log('图片预览成功'))
.catch((error: any) => console.error('图片预览失败:', error));
} else if (canPreview(fileFormat)) {
const fullFileName = fileFormat ? `${fileName}.${fileFormat}` : fileName;
previewFileUtil(fileUrl, fullFileName, fileFormat)
.then(() => console.log('文档预览成功'))
.catch((error: any) => console.error('文档预览失败:', error));
} else {
//
downloadAttachment(data);
}
};
// files
const previewAttachmentFile = (file: FileInfo) => {
const fileUrl = file.resourUrl ? imagUrl(file.resourUrl) : (file.url ? imagUrl(file.url) : '');
const fileName = file.resourName || file.name || '未知文件';
const fileSuf = getFileSuffix(file);
if (!fileUrl) {
console.error("没有找到文件URL");
return;
}
//
if (isVideo(fileSuf)) {
previewVideoUtil(fileUrl, fileName)
.then(() => console.log('视频预览成功'))
.catch((error: any) => console.error('视频预览失败:', error));
} else if (isImage(fileSuf)) {
previewImageUtil(fileUrl)
.then(() => console.log('图片预览成功'))
.catch((error: any) => console.error('图片预览失败:', error));
} else if (canPreview(fileSuf)) {
previewFileUtil(fileUrl, fileName, fileSuf)
.then(() => console.log('文档预览成功'))
.catch((error: any) => console.error('文档预览失败:', error));
} else {
//
downloadAttachmentFile(file);
}
};
//
const downloadAttachment = (data: any) => {
if (!data.fileUrl) {
console.error("没有找到附件URL");
return;
}
const fileUrl = imagUrl(data.fileUrl);
const fileName = data.fileName || '未知文件';
const fileFormat = data.fileFormat || '';
const fullFileName = fileFormat ? `${fileName}.${fileFormat}` : fileName;
downloadFileUtil(fileUrl, fullFileName)
.then(() => {
console.log('文件下载成功');
uni.showToast({
title: '下载成功',
icon: 'success'
});
})
.catch((error: any) => {
console.error('文件下载失败:', error);
uni.showToast({
title: '下载失败',
icon: 'error'
});
});
};
//
const downloadAttachmentFile = (file: FileInfo) => {
const fileUrl = file.resourUrl ? imagUrl(file.resourUrl) : (file.url ? imagUrl(file.url) : '');
const fileName = file.resourName || file.name || '未知文件';
if (!fileUrl) {
console.error("没有找到文件URL");
return;
}
downloadFileUtil(fileUrl, fileName)
.then(() => {
console.log('文件下载成功');
uni.showToast({
title: '下载成功',
icon: 'success'
});
})
.catch((error: any) => {
console.error('文件下载失败:', error);
uni.showToast({
title: '下载失败',
icon: 'error'
});
});
};
//
const getFileName = (file: FileInfo) => {
if (file.resourName) {
return file.resourName;
}
if (file.name) {
// name
const lastDotIndex = file.name.lastIndexOf('.');
if (lastDotIndex > 0) {
return file.name.substring(0, lastDotIndex);
}
return file.name;
}
return '未知文件';
};
//
const getFileSuffix = (file: FileInfo) => {
if (file.resSuf) {
return file.resSuf;
}
if (file.url) {
const lastDotIndex = file.url.lastIndexOf('.');
if (lastDotIndex > 0) {
return file.url.substring(lastDotIndex + 1);
}
}
if (file.name) {
const lastDotIndex = file.name.lastIndexOf('.');
if (lastDotIndex > 0) {
return file.name.substring(lastDotIndex + 1);
}
}
return 'unknown';
};
//
watch(dataList, (val) => {
//
@ -356,6 +514,17 @@ onShow(() => {
//
onMounted(() => {
reload();
// gwFlow
uni.$on('refreshGwList', () => {
console.log('收到刷新事件,重新加载数据');
reload();
});
});
//
onUnmounted(() => {
uni.$off('refreshGwList');
});
</script>
@ -459,6 +628,7 @@ onMounted(() => {
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
}
@ -477,6 +647,14 @@ onMounted(() => {
color: white;
}
&.status-submitted {
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
color: white;
}
&.status-pending {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
color: white;
}
&.status-completed {
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
color: white;
}
@ -506,10 +684,9 @@ onMounted(() => {
}
.info-label {
width: 80px;
color: #666;
font-size: 14px;
margin-right: 8px;
margin-right: 0;
flex-shrink: 0;
}
@ -519,28 +696,53 @@ onMounted(() => {
flex: 1;
}
.urgency-tag {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
//
&.urgency-low {
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
color: white;
//
.attachments-section {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
//
.attachments-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.attachment-item {
display: flex;
align-items: center;
padding: 6px 10px;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
max-width: 200px;
&:active {
transform: translateY(1px);
background: #e9ecef;
border-color: #007aff;
}
&.urgency-normal {
background: linear-gradient(135deg, #90a4ae 0%, #78909c 100%);
color: white;
.attachment-icon {
margin-right: 6px;
font-size: 16px;
flex-shrink: 0;
}
&.urgency-high {
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
color: white;
}
&.urgency-urgent {
background: linear-gradient(135deg, #ef5350 0%, #e53935 100%);
color: white;
.attachment-name {
font-size: 12px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
}
@ -557,6 +759,32 @@ onMounted(() => {
gap: 8px;
}
//
.action-button-approve,
.action-button-detail {
padding: 4px 8px !important;
border-radius: 12px !important;
font-size: 12px !important;
font-weight: 500 !important;
white-space: nowrap !important;
flex-shrink: 0 !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
height: auto !important;
min-height: 24px !important;
}
.action-button-approve {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%) !important;
color: white !important;
border: none !important;
}
.action-button-detail {
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%) !important;
color: white !important;
border: none !important;
}
//
@media (max-width: 375px) {
.query-component {

View File

@ -35,6 +35,10 @@
<view class="info-label">开课地点</view>
<view class="info-data">{{ xkkc.kcdd }}</view>
</view>
<view class="course-info-item">
<view class="info-label">开课年级</view>
<view class="info-data">{{ xkkc.njname || '暂无' }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课人数</view>
<view class="info-data"

View File

@ -34,6 +34,14 @@
<view class="time-label">上课地点</view>
<view class="time-value">{{ xkkc.kcdd }}</view>
</view>
<view class="time-item">
<view class="time-label">开课年级</view>
<view class="time-value">{{ xkkc.njname || '暂无' }}</view>
</view>
<view class="time-item">
<view class="time-label">上课人数</view>
<view class="time-value">{{ xkkc.hasNum || 0 }} | {{ xkkc.maxNum || 0 }}</view>
</view>
</view>
<!-- 巡查时间状态 -->

View File

@ -71,6 +71,42 @@
</view>
</view>
<!-- 签到时间未开始阶段 -->
<view v-else-if="currentStep === 'timeNotStarted'" class="time-not-started-step">
<view class="time-not-started-content">
<u-icon name="clock" size="80" color="#409EFF" />
<text class="time-not-started-title">签到时间未开始</text>
<text class="time-not-started-subtitle">签到打卡时间还未开始请耐心等待</text>
<text class="time-not-started-tip">请关注签到开始时间</text>
<view class="meeting-info-card">
<text class="card-title">会议信息</text>
<view class="info-item">
<text class="info-label">会议名称</text>
<text class="info-value">{{ meetingInfo?.qdmc || '未设置' }}</text>
</view>
<view class="info-item">
<text class="info-label">签到时间</text>
<text class="info-value">{{ formatTime(meetingInfo?.qdkstime) }} - {{ formatTime(meetingInfo?.qdjstime) }}</text>
</view>
<view class="info-item">
<text class="info-label">打卡开始</text>
<text class="info-value">{{ formatTime(meetingInfo?.dkkstime) }}</text>
</view>
<view class="info-item">
<text class="info-label">会议地点</text>
<text class="info-value">{{ meetingInfo?.qdwz || '未设置' }}</text>
</view>
<view class="info-item">
<text class="info-label">当前时间</text>
<text class="info-value">{{ formatTime(new Date()) }}</text>
</view>
</view>
<button @click="goBack" class="back-btn">返回</button>
</view>
</view>
<!-- 签到时间已结束阶段 -->
<view v-else-if="currentStep === 'timeExpired'" class="time-expired-step">
<view class="time-expired-content">
@ -184,7 +220,7 @@ const qdzxId = ref('');
const qrExpireTime = ref(60); // 60
//
const currentStep = ref<'sign' | 'confirm' | 'success' | 'notInList' | 'timeExpired' | 'qrExpired' | 'alreadySigned'>('confirm');
const currentStep = ref<'sign' | 'confirm' | 'success' | 'notInList' | 'timeNotStarted' | 'timeExpired' | 'qrExpired' | 'alreadySigned'>('confirm');
//
const userInfo = ref<any>(null);
@ -286,10 +322,19 @@ const loadMeetingInfo = async () => {
if (result && result.resultCode === 1) {
meetingInfo.value = result.result;
// 1.
// 1.
const currentTime = new Date();
const startTime = meetingInfo.value?.dkkstime ? new Date(meetingInfo.value.dkkstime) : null;
const endTime = meetingInfo.value?.qdjstime ? new Date(meetingInfo.value.qdjstime) : null;
//
if (startTime && currentTime < startTime) {
// ""
currentStep.value = 'timeNotStarted';
return;
}
//
if (endTime && currentTime > endTime) {
// ""
currentStep.value = 'timeExpired';
@ -418,18 +463,22 @@ const goBack = () => {
.info-item {
display: flex;
margin-bottom: 12px;
margin-bottom: 16px;
align-items: flex-start;
.info-label {
font-size: 14px;
color: #666;
min-width: 80px;
text-align: right !important;
margin-right: 12px;
}
.info-value {
font-size: 14px;
color: #333;
flex: 1;
text-align: left !important;
}
}
}
@ -542,18 +591,22 @@ const goBack = () => {
.info-item {
display: flex;
margin-bottom: 8px;
margin-bottom: 12px;
align-items: flex-start;
.info-label {
font-size: 14px;
color: #666;
min-width: 80px;
text-align: right !important;
margin-right: 12px;
}
.info-value {
font-size: 14px;
color: #333;
flex: 1;
text-align: left !important;
}
}
}
@ -568,6 +621,37 @@ const goBack = () => {
font-size: 16px;
}
/* 签到时间未开始阶段 */
.time-not-started-step {
background: white;
border-radius: 16px;
padding: 40px 24px;
margin-top: 40px;
text-align: center;
}
.time-not-started-content {
.time-not-started-title {
display: block;
font-size: 24px;
font-weight: 600;
color: #333;
margin: 16px 0 8px;
}
.time-not-started-subtitle {
font-size: 14px;
color: #666;
margin-bottom: 16px;
}
.time-not-started-tip {
font-size: 14px;
color: #999;
margin-bottom: 24px;
}
}
/* 签到时间已结束阶段 */
.time-expired-step {
background: white;
@ -647,18 +731,22 @@ const goBack = () => {
.info-item {
display: flex;
margin-bottom: 8px;
margin-bottom: 12px;
align-items: flex-start;
.info-label {
font-size: 14px;
color: #666;
min-width: 80px;
text-align: right !important;
margin-right: 12px;
}
.info-value {
font-size: 14px;
color: #333;
flex: 1;
text-align: left !important;
}
}
}

View File

@ -17,11 +17,15 @@
<text class="info-value">{{ qdInfo.qdwz || '未设置' }}</text>
</view>
<view class="info-item">
<text class="info-label">开始时间</text>
<text class="info-label">签到时间</text>
<text class="info-value">{{ formatTime(qdInfo.dkkstime) }}</text>
</view>
<view class="info-item">
<text class="info-label">会议开始</text>
<text class="info-value">{{ formatTime(qdInfo.qdkstime) }}</text>
</view>
<view class="info-item">
<text class="info-label">结束时间</text>
<text class="info-label">会议结束</text>
<text class="info-value">{{ formatTime(qdInfo.qdjstime) }}</text>
</view>
<view class="info-item">
@ -49,6 +53,10 @@
<text class="stat-number signed">{{ signedCount }}</text>
<text class="stat-label">已签到</text>
</view>
<view class="stat-item" @click="showTeacherList('late')">
<text class="stat-number late">{{ lateCount }}</text>
<text class="stat-label">迟到</text>
</view>
<view class="stat-item" @click="showTeacherList('unsigned')">
<text class="stat-number unsigned">{{ unsignedCount }}</text>
<text class="stat-label">未签到</text>
@ -141,6 +149,7 @@ interface QdInfo {
jsId: string;
jsxm: string;
qdFbtime: string;
dkkstime: string;
qdkstime: string;
qdjstime: string;
qdry: string;
@ -166,11 +175,28 @@ const currentFilter = ref('all');
const totalCount = computed(() => teacherList.value.length);
const signedCount = computed(() => teacherList.value.filter(t => t.qdStatus === '1').length);
const unsignedCount = computed(() => teacherList.value.filter(t => t.qdStatus === '0').length);
const lateCount = computed(() => {
if (!qdInfo.value.qdkstime) return 0;
const meetingStartTime = new Date(qdInfo.value.qdkstime);
return teacherList.value.filter(t => {
if (t.qdStatus !== '1' || !t.qdwctime) return false;
const signInTime = new Date(t.qdwctime);
return signInTime > meetingStartTime;
}).length;
});
const filteredTeacherList = computed(() => {
switch (currentFilter.value) {
case 'signed':
return teacherList.value.filter(t => t.qdStatus === '1');
case 'late':
if (!qdInfo.value.qdkstime) return [];
const meetingStartTime = new Date(qdInfo.value.qdkstime);
return teacherList.value.filter(t => {
if (t.qdStatus !== '1' || !t.qdwctime) return false;
const signInTime = new Date(t.qdwctime);
return signInTime > meetingStartTime;
});
case 'unsigned':
return teacherList.value.filter(t => t.qdStatus === '0');
default:
@ -225,6 +251,9 @@ const showTeacherList = (filter: string) => {
case 'signed':
popupTitle.value = `已签到人员 (${signedCount.value}人)`;
break;
case 'late':
popupTitle.value = `迟到人员 (${lateCount.value}人)`;
break;
case 'unsigned':
popupTitle.value = `未签到人员 (${unsignedCount.value}人)`;
break;
@ -421,6 +450,10 @@ const handleBack = () => {
&.unsigned {
color: #dc3545;
}
&.late {
color: #ff9800;
}
}
.stat-label {

View File

@ -61,11 +61,15 @@
<text class="info-value">{{ data.qdwz || '未设置' }}</text>
</view>
<view class="info-item">
<text class="info-label">开始时间</text>
<text class="info-label">签到时间</text>
<text class="info-value">{{ formatTime(data.dkkstime) }}</text>
</view>
<view class="info-item">
<text class="info-label">会议开始</text>
<text class="info-value">{{ formatTime(data.qdkstime) }}</text>
</view>
<view class="info-item">
<text class="info-label">结束时间</text>
<text class="info-label">会议结束</text>
<text class="info-value">{{ formatTime(data.qdjstime) }}</text>
</view>
<view class="info-item">
@ -129,6 +133,7 @@ interface QdItem {
jsId: string; // ID
jsxm: string; //
qdFbtime: string; //
dkkstime: string; //
qdkstime: string; //
qdjstime: string; //
qdry: string; //
@ -396,6 +401,7 @@ onMounted(() => {
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
}

View File

@ -5,14 +5,18 @@
<!-- 签到名称 -->
<view class="info-card">
<view class="form-item">
<view class="form-label">
<text class="required-asterisk">*</text>
<text class="label-text">签到名称</text>
</view>
<uni-easyinput
type="textarea"
autoHeight
v-model="formData.qdmc"
placeholder="请输入签到名称 (必填)"
:inputBorder="false"
placeholder-style="font-weight:bold; font-size: 18px; color: #999;"
class="title-input"
placeholder="请输入签到名称"
:inputBorder="true"
placeholder-style="font-size: 16px; color: #999;"
class="input-field"
></uni-easyinput>
</view>
</view>
@ -20,13 +24,16 @@
<!-- 签到地点 -->
<view class="info-card">
<view class="form-item">
<view class="form-label">
<text class="required-asterisk">*</text>
<text class="label-text">签到地点</text>
</view>
<uni-easyinput
type="textarea"
autoHeight
type="text"
v-model="formData.qdwz"
placeholder="请输入签到地点 (必填)"
:inputBorder="false"
class="content-input"
placeholder="请输入签到地点"
:inputBorder="true"
class="input-field single-line"
></uni-easyinput>
</view>
</view>
@ -34,7 +41,10 @@
<!-- 签到人员 -->
<view class="info-card">
<view class="card-header picker-header" @click="showTeacherTree">
<text class="section-title">签到人员</text>
<view class="section-title-container">
<text class="required-asterisk">*</text>
<text class="section-title">签到人员</text>
</view>
<view class="target-class">
<text :class="{ placeholder: !formData.targetTeachers.length }">
{{ formData.targetTeachers.length ? `已选择${formData.targetTeachers.length}` : "请选择教师" }}
@ -75,15 +85,36 @@
</view>
</picker>
</view>
<!-- 签到打卡开始时间 -->
<view class="info-card list-item-card">
<uni-datetime-picker type="datetime" v-model="formData.dkkstime">
<view class="list-item-row">
<view class="list-label-container">
<text class="required-asterisk">*</text>
<text class="list-label">签到开始</text>
</view>
<view class="list-value">
<text :class="{ placeholder: !formData.dkkstime }">
{{ formData.dkkstime || '请选择签到打卡开始时间' }}
</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</uni-datetime-picker>
</view>
<!-- 签到开始时间 -->
<view class="info-card list-item-card">
<uni-datetime-picker type="datetime" v-model="formData.qdkstime">
<view class="list-item-row">
<text class="list-label">开始时间</text>
<view class="list-label-container">
<text class="required-asterisk">*</text>
<text class="list-label">会议开始</text>
</view>
<view class="list-value">
<text :class="{ placeholder: !formData.qdkstime }">
{{ formData.qdkstime || '请选择开始时间' }}
{{ formData.qdkstime || '请选择会议开始时间' }}
</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
@ -95,10 +126,13 @@
<view class="info-card list-item-card">
<uni-datetime-picker type="datetime" v-model="formData.qdjstime">
<view class="list-item-row">
<text class="list-label">结束时间</text>
<view class="list-label-container">
<text class="required-asterisk">*</text>
<text class="list-label">会议结束</text>
</view>
<view class="list-value">
<text :class="{ placeholder: !formData.qdjstime }">
{{ formData.qdjstime || '请选择结束时间' }}
{{ formData.qdjstime || '请选择会议结束时间' }}
</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
@ -106,6 +140,7 @@
</uni-datetime-picker>
</view>
<!-- 发布人 -->
<view class="info-card list-item-card">
<view class="list-item-row">
@ -171,6 +206,7 @@ const formData = reactive({
mdqz: '0',
qdkstime: '',
qdjstime: '',
dkkstime: '',
jsxm: '',
qdFbtime: '',
targetTeachers: [] as TeacherInfo[]
@ -273,12 +309,17 @@ const handlePublish = async () => {
}
if (!formData.qdkstime) {
uni.showToast({ title: '请选择开始时间', icon: 'none' });
uni.showToast({ title: '请选择会议开始时间', icon: 'none' });
return;
}
if (!formData.qdjstime) {
uni.showToast({ title: '请选择结束时间', icon: 'none' });
uni.showToast({ title: '请选择会议结束时间', icon: 'none' });
return;
}
if (!formData.dkkstime) {
uni.showToast({ title: '请选择签到打卡开始时间', icon: 'none' });
return;
}
@ -312,6 +353,7 @@ const handlePublish = async () => {
mdqz: formData.mdqz,
qdkstime: formatDateTime(formData.qdkstime),
qdjstime: formatDateTime(formData.qdjstime),
dkkstime: formatDateTime(formData.dkkstime),
jsId: js.id,
jsxm: formData.jsxm,
qdFbtime: formData.qdFbtime,
@ -372,38 +414,72 @@ const handlePublish = async () => {
margin-bottom: 15px;
}
.title-input {
font-size: 18px;
font-weight: bold;
color: #333;
:deep(.uni-easyinput__content) {
background: transparent;
}
:deep(.uni-easyinput__content-input) {
color: #333;
}
:deep(.uni-easyinput__placeholder-class) {
color: #999;
}
.form-label {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.content-input {
.required-asterisk {
color: #ff4757;
font-size: 16px;
font-weight: bold;
margin-right: 4px;
}
.label-text {
font-size: 16px;
font-weight: 600;
color: #333;
}
.section-title-container {
display: flex;
align-items: center;
}
.list-label-container {
display: flex;
align-items: center;
}
.input-field {
font-size: 16px;
color: #333;
:deep(.uni-easyinput__content) {
background: transparent;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 12px;
min-height: 44px;
}
:deep(.uni-easyinput__content-input) {
color: #333;
font-size: 16px;
}
:deep(.uni-easyinput__placeholder-class) {
color: #999;
font-size: 16px;
}
:deep(.uni-easyinput__content):focus-within {
border-color: #007aff;
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.1);
}
&.single-line {
:deep(.uni-easyinput__content) {
min-height: 44px;
height: 44px;
}
:deep(.uni-easyinput__content-input) {
line-height: 44px;
height: 44px;
}
}
}

View File

@ -26,6 +26,10 @@
</view>
<view class="qr-info">
<view class="info-item">
<text class="info-label">签到时间</text>
<text class="info-value">{{ formatTime(meetingInfo?.dkkstime) }}</text>
</view>
<view class="info-item">
<text class="info-label">会议时间</text>
<text class="info-value">{{ formatTime(meetingInfo?.qdkstime) }} - {{ formatTime(meetingInfo?.qdjstime) }}</text>

View File

@ -43,7 +43,7 @@
</view>
<!-- 具体选择 -->
<view class="filter-row" v-if="selectType && datas.length > 0 && selectType !== 1">
<view class="filter-row" v-if="selectType && selectType !== 1">
<view class="filter-label">{{ getSecondSelectLabel() }}</view>
<view class="multi-select-container">
<view class="selected-items-display" @click="showMultiSelectModal">
@ -256,7 +256,8 @@ const onSelectTypeChange = async (e: any) => {
//
await loadKmData();
} else if (selectType.value === 4) {
//
//
zwType.value = 1; //
await loadZwData();
}
};
@ -329,20 +330,20 @@ const loadAllTeachersFromStorage = async () => {
console.log('parsedData是否为对象:', typeof parsedData === 'object');
console.log('parsedData的所有属性:', Object.keys(parsedData || {}));
console.log('parsedData的data属性:', parsedData?.data);
console.log('parsedData的allJs属性:', parsedData?.allJs);
console.log('parsedData的allJsBasicInfoVo属性:', parsedData?.allJsBasicInfoVo);
// allJsparsedData
// allJsBasicInfoVoparsedData
let allJsData;
if (parsedData.data && parsedData.data.allJs) {
allJsData = parsedData.data.allJs;
console.log('从parsedData.data.allJs获取数据');
} else if (parsedData.allJs) {
allJsData = parsedData.allJs;
console.log('从parsedData.allJs获取数据');
if (parsedData.data && parsedData.data.allJsBasicInfoVo) {
allJsData = parsedData.data.allJsBasicInfoVo;
console.log('从parsedData.data.allJsBasicInfoVo获取数据');
} else if (parsedData.allJsBasicInfoVo) {
allJsData = parsedData.allJsBasicInfoVo;
console.log('从parsedData.allJsBasicInfoVo获取数据');
} else {
console.warn('localStorage中没有找到allJs数据');
console.warn('localStorage中没有找到allJsBasicInfoVo数据');
console.warn('可用的属性:', Object.keys(parsedData));
uni.showToast({ title: '未找到教师数据(allJs)', icon: 'none' });
uni.showToast({ title: '未找到教师数据(allJsBasicInfoVo)', icon: 'none' });
return;
}
@ -440,18 +441,18 @@ const loadTeachersByNjIdFromStorage = async () => {
parsedData = storageData;
}
// allJsparsedData
// allJsBasicInfoVoparsedData
let allJsData;
if (parsedData.data && parsedData.data.allJs) {
allJsData = parsedData.data.allJs;
console.log('从parsedData.data.allJs获取数据');
} else if (parsedData.allJs) {
allJsData = parsedData.allJs;
console.log('从parsedData.allJs获取数据');
if (parsedData.data && parsedData.data.allJsBasicInfoVo) {
allJsData = parsedData.data.allJsBasicInfoVo;
console.log('从parsedData.data.allJsBasicInfoVo获取数据');
} else if (parsedData.allJsBasicInfoVo) {
allJsData = parsedData.allJsBasicInfoVo;
console.log('从parsedData.allJsBasicInfoVo获取数据');
} else {
console.warn('localStorage中没有找到allJs数据');
console.warn('localStorage中没有找到allJsBasicInfoVo数据');
console.warn('可用的属性:', Object.keys(parsedData));
uni.showToast({ title: '未找到教师数据(allJs)', icon: 'none' });
uni.showToast({ title: '未找到教师数据(allJsBasicInfoVo)', icon: 'none' });
return;
}
@ -509,6 +510,8 @@ const loadTeachersByZwIdFromStorage = async () => {
console.log('=== 开始从localStorage按职务加载教师数据 ===');
console.log('选中的职务ID:', selectTwoType.value);
console.log('职务类型:', zwType.value === 1 ? '党政职务' : '其他职务');
console.log('selectTwoType.value类型:', typeof selectTwoType.value);
console.log('selectTwoType.value是否为数组:', Array.isArray(selectTwoType.value));
// localStorage
const storageData = uni.getStorageSync('app-common');
@ -535,15 +538,15 @@ const loadTeachersByZwIdFromStorage = async () => {
parsedData = storageData;
}
// parsedData.data.allJs
if (!parsedData.data || !parsedData.data.allJs) {
// parsedData.data.allJsBasicInfoVo
if (!parsedData.data || !parsedData.data.allJsBasicInfoVo) {
console.warn('localStorage中没有找到教师数据');
console.warn('可用的属性:', Object.keys(parsedData || {}));
uni.showToast({ title: '未找到教师数据', icon: 'none' });
return;
}
const allJsData = parsedData.data.allJs;
const allJsData = parsedData.data.allJsBasicInfoVo;
console.log('allJs数据结构:', allJsData);
// allJsresult
@ -556,26 +559,42 @@ const loadTeachersByZwIdFromStorage = async () => {
const teacherArray = allJsData.result;
console.log('教师数组数据:', teacherArray);
console.log('教师数组长度:', teacherArray.length);
//
if (teacherArray.length > 0) {
console.log('第一个教师完整数据:', teacherArray[0]);
console.log('第一个教师的dzzw字段:', teacherArray[0].dzzw);
console.log('第一个教师的qtzw字段:', teacherArray[0].qtzw);
if (teacherArray.length > 1) {
console.log('第二个教师完整数据:', teacherArray[1]);
console.log('第二个教师的dzzw字段:', teacherArray[1].dzzw);
console.log('第二个教师的qtzw字段:', teacherArray[1].qtzw);
}
}
// ID
const selectedZwId = selectTwoType.value[0]; //
const selectedZwIds = selectTwoType.value; //
console.log('选中的职务ID列表:', selectedZwIds);
const filteredTeachers = teacherArray.filter((teacher: any) => {
let hasZwId = false;
if (zwType.value === 1) {
// dzzw
if (teacher.dzzw) {
const dzzwArray = teacher.dzzw.split(',');
hasZwId = dzzwArray.includes(selectedZwId);
const dzzwArray = teacher.dzzw.split(',').map((id: string) => id.trim());
//
hasZwId = selectedZwIds.some(selectedId => dzzwArray.includes(selectedId));
}
console.log(`教师${teacher.jsxm || teacher.name}的党政职务:`, teacher.dzzw, '是否匹配职务ID:', selectedZwId, '结果:', hasZwId);
console.log(`教师${teacher.jsxm || teacher.name}的党政职务:`, teacher.dzzw, '是否匹配职务ID列表:', selectedZwIds, '结果:', hasZwId);
} else if (zwType.value === 2) {
// qtzw
if (teacher.qtzw) {
const qtzwArray = teacher.qtzw.split(',');
hasZwId = qtzwArray.includes(selectedZwId);
const qtzwArray = teacher.qtzw.split(',').map((id: string) => id.trim());
//
hasZwId = selectedZwIds.some(selectedId => qtzwArray.includes(selectedId));
}
console.log(`教师${teacher.jsxm || teacher.name}的其他职务:`, teacher.qtzw, '是否匹配职务ID:', selectedZwId, '结果:', hasZwId);
console.log(`教师${teacher.jsxm || teacher.name}的其他职务:`, teacher.qtzw, '是否匹配职务ID列表:', selectedZwIds, '结果:', hasZwId);
}
return hasZwId;
@ -725,9 +744,9 @@ const loadZwData = async () => {
}
}
// getZwListByLx
if (!zwData) {
console.log('localStorage中没有职务数据开始调用getZwListByLx获取并缓存');
// getZwListByLx
if (!zwData || !zwData['党政职务'] || !zwData['其他职务']) {
console.log('localStorage中没有完整职务数据开始调用getZwListByLx获取并缓存');
try {
// getZwListByLx
@ -773,7 +792,10 @@ const loadZwData = async () => {
if (filteredData.length === 0) {
console.warn('没有找到对应类型的职务数据');
uni.showToast({ title: '未找到对应类型的职务数据', icon: 'none' });
//
// uni.showToast({ title: '', icon: 'none' });
//
datas.value = [];
return;
}
@ -971,15 +993,19 @@ const ensureTeacherDataCached = async () => {
}
//
if (parsedData?.data?.allJs?.result && parsedData.data.allJs.result.length > 0) {
if (parsedData?.data?.allJsBasicInfoVo?.result && parsedData.data.allJsBasicInfoVo.result.length > 0) {
hasTeacherData = true;
console.log('localStorage中已有教师数据数量:', parsedData.data.allJs.result.length);
console.log('localStorage中已有教师数据数量:', parsedData.data.allJsBasicInfoVo.result.length);
}
//
if (parsedData?.data?.zw) {
//
if (parsedData?.data?.zw &&
parsedData.data.zw['党政职务'] &&
parsedData.data.zw['其他职务']) {
hasZwData = true;
console.log('localStorage中已有职务数据');
console.log('localStorage中已有完整的职务数据');
} else if (parsedData?.data?.zw) {
console.log('localStorage中只有部分职务数据需要重新加载');
}
}

View File

@ -51,7 +51,7 @@
</view>
<view class="student-info">
<text class="student-name">{{ xs.xsXm || xs.xsxm }}</text>
<text class="student-class">{{ xs.bjmc }}</text>
<text class="student-class">{{ xs.njmcName || xs.njmc }}{{ xs.bjmc ? ' ' + xs.bjmc : '' }}</text>
</view>
</view>
@ -258,6 +258,7 @@ const loadXsList = async () => {
tx: dmXs.tx || dmXs.xstx || dmXs.avatar,
bjmc: dmXs.bjmc,
njmc: dmXs.njmc,
njmcName: dmXs.njmcName || dmXs.njmc,
jzxm: dmXs.jzxm,
jzdh: dmXs.jzdh,
xsxm: dmXs.xsxm || dmXs.xm,

View File

@ -32,6 +32,10 @@
<view class="info-label">开课地点</view>
<view class="info-data">{{ xkkc.kcdd }}</view>
</view>
<view class="course-info-item">
<view class="info-label">开课年级</view>
<view class="info-data">{{ xkkc.njname || '暂无' }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课人数</view>
<view class="info-data">{{ xkkc.hasNum || 0 }} | {{ xkkc.maxNum || 0 }}</view>
@ -240,7 +244,6 @@ const goDm = (xkkc: any) => {
} else {
msg = "上课时间未到,无法点名";
}
// TODO:
dmFlag = true;
if (dmFlag) {
setData(xkkc);

View File

@ -35,6 +35,10 @@
<view class="info-label">开课地点</view>
<view class="info-data">{{ xkkc.kcdd }}</view>
</view>
<view class="course-info-item">
<view class="info-label">开课年级</view>
<view class="info-data">{{ xkkc.njname || '暂无' }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课人数</view>
<view class="info-data">{{ xkkc.hasNum || 0 }} | {{ xkkc.maxNum || 0 }}</view>

View File

@ -19,6 +19,10 @@
<view class="info-label">上课周期</view>
<view class="info-data">{{ xkkc.skzqmc }}</view>
</view>
<view class="course-info-item">
<view class="info-label">开课年级</view>
<view class="info-data">{{ xkkc.njname || '暂无' }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课时间</view>
<view class="info-data">{{ formatClassTime(xkkc.skkstime, xkkc.skjstime) }}</view>
@ -204,8 +208,13 @@ const schema = reactive<FormsSchema[]>([
{
field: "jhsj",
label: "计划时间",
component: "BasicDateTimes",
componentProps: {},
component: "BasicInput",
componentProps: {
type: "date",
placeholder: "请选择计划日期",
//
style: "position: relative; z-index: 1000;",
},
},
{
field: "jhdd",
@ -222,6 +231,8 @@ const schema = reactive<FormsSchema[]>([
},
componentProps: {
type: "textarea",
maxlength: -1, //
showCount: false, //
},
},
])