新增公文流转

This commit is contained in:
hebo 2025-08-17 22:04:29 +08:00
parent f02f5d06e5
commit 1d1d2d6458
25 changed files with 2279 additions and 93 deletions

168
src/api/routine/gw.ts Normal file
View File

@ -0,0 +1,168 @@
import { get, post } from "@/utils/request";
// 公文相关API接口
/**
*
* @param params
*/
export function getGwListApi(params: {
page: number;
pageSize: number;
status?: string;
keyword?: string;
gwType?: string;
urgencyLevel?: string;
startTime?: string;
endTime?: string;
}) {
return get('/api/gw/list', params);
}
/**
*
* @param id ID
*/
export function getGwDetailApi(id: string) {
return get(`/api/gw/detail/${id}`);
}
/**
*
* @param data
*/
export function createGwApi(data: {
title: string;
gwType: string;
urgencyLevel: string;
remark?: string;
files: any[];
approvers: any[];
ccUsers: any[];
}) {
return post('/api/gw/create', data);
}
/**
*
* @param id ID
* @param data
*/
export function updateGwApi(id: string, data: any) {
return post(`/api/gw/update/${id}`, data);
}
/**
*
* @param id ID
*/
export function deleteGwApi(id: string) {
return post(`/api/gw/delete/${id}`, { id });
}
/**
* 稿
* @param data 稿
*/
export function saveDraftApi(data: any) {
return post('/api/gw/draft', data);
}
/**
*
* @param data
*/
export function submitGwApi(data: any) {
return post('/api/gw/submit', data);
}
/**
*
* @param data
*/
export function saveChangesApi(data: {
gwId: string;
approvers: any[];
ccUsers: any[];
operationLogs: any[];
}) {
return post('/api/gw/changes', data);
}
/**
*
* @param keyword
*/
export function searchUsersApi(keyword: string) {
return get('/api/user/search', { keyword });
}
/**
*
* @param gwId ID
*/
export function getApproversApi(gwId: string) {
return get(`/api/gw/approvers/${gwId}`);
}
/**
*
* @param gwId ID
*/
export function getCCUsersApi(gwId: string) {
return get(`/api/gw/cc-users/${gwId}`);
}
/**
*
* @param gwId ID
*/
export function getOperationLogsApi(gwId: string) {
return get(`/api/gw/logs/${gwId}`);
}
/**
*
* @param file
*/
export function uploadFileApi(file: File) {
const formData = new FormData();
formData.append("file", file);
return post('/api/file/upload', formData);
}
/**
*
* @param fileId ID
*/
export function deleteFileApi(fileId: string) {
return post(`/api/file/delete/${fileId}`, { fileId });
}
/**
*
*/
export function getGwStatsApi() {
return get('/api/gw/stats');
}
/**
*
* @param data
*/
export function approveGwApi(data: {
gwId: string;
action: "approve" | "reject";
remark?: string;
}) {
return post('/api/gw/approve', data);
}
/**
*
* @param gwId ID
*/
export function confirmCCApi(gwId: string) {
return post(`/api/gw/cc-confirm/${gwId}`);
}

View File

@ -192,6 +192,27 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/gwlz/gwAdd",
"style": {
"navigationBarTitleText": "公文列表",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/gwlz/index",
"style": {
"navigationBarTitleText": "公文接收",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/gwlz/gwFlow",
"style": {
"navigationBarTitleText": "公文流转",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/JiFenPingJia/JiFenPingJia",
"style": {

View File

@ -180,9 +180,33 @@ const handleLogout = () => {
const sections = reactive<Section[]>([
{
id: "routine",
title: "常规",
title: "教学常规",
permissionKey: "routine", //
items: [
{
id: "r2",
icon: "jfpj",
text: "积分评价",
show: true,
permissionKey: "routine-jfpj", //
path: "/pages/view/routine/JiFenPingJia/JiFenPingJia",
},
{
id: "r3",
icon: "gzltj",
text: "工作量",
show: true,
permissionKey: "routine-gzl", //
path: "/pages/view/routine/GongZuoLiang/index",
},
{
id: "hs4",
icon: "cjfx",
text: "成绩分析",
show: true,
permissionKey: "home-cjfx", //
path: "/pages/view/homeSchool/ChengJiFenXi",
},
{
id: "r1",
icon: "stack-fill",
@ -191,30 +215,38 @@ const sections = reactive<Section[]>([
permissionKey: "routine-jszr", //
path: "/pages/view/routine/JiaoXueZiYuan/index",
},
{
id: "r2",
icon: "file-mark-fill",
text: "积分评价",
show: true,
permissionKey: "routine-jfpj", //
path: "/pages/view/routine/JiFenPingJia/JiFenPingJia",
},
{
id: "r3",
icon: "file-list-3-fil",
text: "工作量",
show: true,
permissionKey: "routine-gzl", //
path: "/pages/view/routine/GongZuoLiang/index",
},
{
id: "r4",
icon: "file-paper-2-fill",
icon: "rzrj",
text: "任教任职",
show: true,
permissionKey: "routine-rzrj", //
path: "/pages/view/routine/RengJiaoRengZhi/index",
},
{
id: "r10",
icon: "qdfb",
text: "签到发布",
show: true,
permissionKey: "routine-qdfb", //
path: "/pages/view/routine/qd/index",
},
{
id: "r7",
icon: "kctb",
text: "课程填报",
show: true,
permissionKey: "routine-kcjs", //
path: "/pages/base/groupTeaching/xkList",
},
{
id: "r6",
icon: "kfxc",
text: "课服巡查",
show: true,
permissionKey: "routine-kfxc", //
path: "/pages/view/routine/kefuxuncha/xcXkList",
},
{
id: "r5",
icon: "hc-fill",
@ -223,64 +255,18 @@ const sections = reactive<Section[]>([
permissionKey: "routine-stxc", //
path: "/pages/view/routine/ShiTangXunCha/index",
},
{
id: "r6",
icon: "pass-pending-fill",
text: "课服巡查",
show: true,
permissionKey: "routine-kfxc", //
path: "/pages/view/routine/kefuxuncha/xcXkList",
},
{
id: "r7",
icon: "file-text-fill-2",
text: "课程填报",
show: true,
permissionKey: "routine-kcjs", //
path: "/pages/base/groupTeaching/xkList",
},
{
id: "r8",
icon: "draftfill",
text: "选课点名",
show: true,
permissionKey: "routine-kcdm", //
path: "/pages/base/groupTeaching/dmXkList",
},
{
id: "r9",
icon: "draftfill",
text: "发布接龙",
show: true,
permissionKey: "routine-bjjl", //
path: "/pages/view/notice/index",
},
{
id: "r10",
icon: "draftfill",
text: "签到发布",
show: true,
permissionKey: "routine-qdfb", //
path: "/pages/view/routine/qd/index",
},
{
id: "r11",
icon: "draftfill",
text: "就餐点名",
show: true,
permissionKey: "routine-jcdm", //
path: "/pages/view/routine/jc/index",
},
],
},
{
id: "home-school",
title: "家校",
title: "家校沟通",
permissionKey: "home", //
items: [
{
id: "hs1",
icon: "file-text-fill",
icon: "jskb",
text: "教师课表",
show: true,
permissionKey: "home-jskb", //
@ -288,7 +274,7 @@ const sections = reactive<Section[]>([
},
{
id: "hs2",
icon: "file-text-fill-2",
icon: "bjkb",
text: "班级课表",
show: true,
permissionKey: "home-bjkb", //
@ -296,30 +282,47 @@ const sections = reactive<Section[]>([
},
{
id: "hs3",
icon: "file-paper-2-fill",
icon: "txl",
text: "家长通讯录",
show: true,
permissionKey: "home-jztxl", //
path: "/pages/view/homeSchool/parentAddressBook/index",
},
{
id: "hs4",
icon: "filechart2fil",
text: "成绩分析",
id: "r8",
icon: "xkdm",
text: "选课点名",
show: true,
permissionKey: "home-cjfx", //
path: "/pages/view/homeSchool/ChengJiFenXi",
permissionKey: "routine-kcdm", //
path: "/pages/base/groupTeaching/dmXkList",
},
{
id: "r11",
icon: "jcdm",
text: "就餐点名",
show: true,
permissionKey: "routine-jcdm", //
path: "/pages/view/routine/jc/index",
},
{
id: "r9",
icon: "jlfb",
text: "发布接龙",
show: true,
permissionKey: "routine-bjjl", //
path: "/pages/view/notice/index",
},
],
},
{
id: "hr",
title: "人事",
title: "行政办公",
permissionKey: "personnel", //
items: [
{
id: "hr1",
icon: "draftfill",
icon: "qjsq",
text: "请假申请",
show: true,
permissionKey: "personnel-qjsq", //
@ -327,15 +330,23 @@ const sections = reactive<Section[]>([
},
{
id: "hr2",
icon: "file-user-fill",
icon: "jsda",
text: "教师档案",
show: true,
permissionKey: "personnel-jsda", //
path: "/pages/view/hr/teacherProfile/index",
},
{
id: "r12",
icon: "gw",
text: "公文流转",
show: true,
permissionKey: "routine-gwlz", //
path: "/pages/view/routine/gwlz/index",
},
{
id: "hr3",
icon: "newspaper-fill",
icon: "gz",
text: "工资条",
show: true,
permissionKey: "personnel-gzt", //

View File

@ -36,11 +36,11 @@ const props = withDefaults(defineProps<{
}>(), {
data: () => ({
id: "",
qjlx: "事假",
qjkstime: "2025-07-07 09:00:00",
qjjstime: "2025-07-08 10:00:00",
qjsc: "25小时",
qjsy: "我有事情",
qjlx: "",
qjkstime: "",
qjjstime: "",
qjsc: "",
qjsy: "",
dkfs: 0,
})
});

View File

@ -0,0 +1,389 @@
<template>
<BasicLayout>
<view class="px-15 pb-15">
<BasicForm @register="register"> </BasicForm>
</view>
<template #bottom>
<view class="flex-row items-center pb-10 pt-5">
<u-button text="暂存" class="mx-15" @click="saveDraft" />
<u-button text="提交" class="mx-15" type="primary" @click="submit" />
</view>
</template>
</BasicLayout>
</template>
<script setup lang="ts">
import { navigateTo } from "@/utils/uniapp";
import { useForm } from "@/components/BasicForm/hooks/useForm";
import { findDicTreeByPidApi } from "@/api/system/dic";
import { useDicStore } from "@/store/modules/dic";
import { useUserStore } from "@/store/modules/user";
import { ref, reactive } from "vue";
const { findByPid } = useDicStore();
const { getJs } = useUserStore();
//
const approvers = ref([]);
const ccUsers = ref([]);
//
const fileList = ref([]);
//
const handleFileUpload = (file) => {
fileList.value.push({
name: file.name,
size: file.size,
url: file.url,
type: file.type,
});
};
//
const handleFileDelete = (index) => {
fileList.value.splice(index, 1);
};
//
const searchApprovers = async (keyword) => {
try {
// API
const result = await searchUsers(keyword);
return result;
} catch (error) {
console.error("搜索审批人失败:", error);
return [];
}
};
//
const addApprover = (user) => {
const order = approvers.value.length + 1;
approvers.value.push({
...user,
order,
status: "pending",
});
};
//
const removeApprover = (user) => {
const index = approvers.value.findIndex(item => item.id === user.id);
if (index > -1) {
approvers.value.splice(index, 1);
//
approvers.value.forEach((item, idx) => {
item.order = idx + 1;
});
}
};
//
const searchCCUsers = async (keyword) => {
try {
// API
const result = await searchUsers(keyword);
return result;
} catch (error) {
console.error("搜索抄送人失败:", error);
return [];
}
};
//
const addCCUser = (user) => {
ccUsers.value.push({
...user,
status: "unread",
});
};
//
const removeCCUser = (user) => {
const index = ccUsers.value.findIndex(item => item.id === user.id);
if (index > -1) {
ccUsers.value.splice(index, 1);
}
};
//
const searchUsers = async (keyword) => {
// API
//
return [
{ id: "1", userName: "张三", deptName: "教务处" },
{ id: "2", userName: "李四", deptName: "学生处" },
];
};
//
const [register, { getValue, setValue, setSchema }] = useForm({
schema: [
{
title: "公文信息",
},
{
field: "title",
label: "公文标题",
component: "BasicInput",
componentProps: {
placeholder: "请输入公文标题",
},
required: true,
},
{
field: "gwType",
label: "公文类型",
component: "BasicPicker",
componentProps: {
api: findByPid,
param: { pid: "GW_TYPE" }, //
rangeKey: "dictionaryValue",
savaKey: "dictionaryCode",
placeholder: "请选择公文类型",
},
required: true,
},
{
field: "urgencyLevel",
label: "紧急程度",
component: "BasicPicker",
componentProps: {
api: findByPid,
param: { pid: "URGENCY_LEVEL" }, //
rangeKey: "dictionaryValue",
savaKey: "dictionaryCode",
placeholder: "请选择紧急程度",
},
required: true,
},
{
field: "remark",
label: "备注",
component: "BasicTextarea",
componentProps: {
placeholder: "请输入备注信息(可选)",
rows: 3,
},
},
{
title: "文件上传",
},
{
field: "files",
label: "附件",
component: "BasicUpload",
componentProps: {
fileList: fileList,
multiple: true,
accept: "*/*",
maxCount: 10,
onUpload: handleFileUpload,
onDelete: handleFileDelete,
},
},
{
title: "审批人设置",
},
{
field: "approverSearch",
label: "搜索审批人",
component: "BasicSearch",
componentProps: {
placeholder: "输入姓名或部门搜索",
onSearch: searchApprovers,
onSelect: addApprover,
},
},
{
field: "approvers",
label: "已选审批人",
component: "BasicList",
componentProps: {
data: approvers,
renderItem: (item) => ({
title: item.userName,
subtitle: `${item.deptName} - 顺序${item.order}`,
rightIcon: "close",
onRightIconClick: () => removeApprover(item),
}),
},
},
{
title: "抄送人设置",
},
{
field: "ccSearch",
label: "搜索抄送人",
component: "BasicSearch",
componentProps: {
placeholder: "输入姓名或部门搜索",
onSearch: searchCCUsers,
onSelect: addCCUser,
},
},
{
field: "ccUsers",
label: "已选抄送人",
component: "BasicList",
componentProps: {
data: ccUsers,
renderItem: (item) => ({
title: item.userName,
subtitle: item.deptName,
rightIcon: "close",
onRightIconClick: () => removeCCUser(item),
}),
},
},
],
});
// 稿
const saveDraft = async () => {
try {
const value = await getValue();
if (!validateForm(value)) return;
const draftData = {
...value,
files: fileList.value,
approvers: approvers.value,
ccUsers: ccUsers.value,
status: "draft",
createdBy: getJs.id,
createdTime: new Date(),
};
// 稿API
await saveDraftApi(draftData);
uni.showToast({
title: "草稿保存成功",
icon: "success",
});
navigateTo("/pages/view/routine/gwList");
} catch (error) {
console.error("保存草稿失败:", error);
uni.showToast({
title: "保存失败",
icon: "error",
});
}
};
//
const submit = async () => {
try {
const value = await getValue();
if (!validateForm(value)) return;
if (approvers.value.length === 0) {
uni.showToast({
title: "请至少选择一名审批人",
icon: "error",
});
return;
}
const submitData = {
...value,
files: fileList.value,
approvers: approvers.value,
ccUsers: ccUsers.value,
status: "pending",
createdBy: getJs.id,
createdTime: new Date(),
};
// API
await submitGwApi(submitData);
uni.showToast({
title: "公文提交成功",
icon: "success",
});
navigateTo("/pages/view/routine/gwList");
} catch (error) {
console.error("提交公文失败:", error);
uni.showToast({
title: "提交失败",
icon: "error",
});
}
};
//
const validateForm = (value) => {
if (!value.title || value.title.trim() === "") {
uni.showToast({
title: "请输入公文标题",
icon: "error",
});
return false;
}
if (!value.gwType) {
uni.showToast({
title: "请选择公文类型",
icon: "error",
});
return false;
}
if (!value.urgencyLevel) {
uni.showToast({
title: "请选择紧急程度",
icon: "error",
});
return false;
}
return true;
};
// API
const saveDraftApi = async (data) => {
// 稿API
console.log("保存草稿:", data);
return Promise.resolve();
};
const submitGwApi = async (data) => {
// API
console.log("提交公文:", data);
return Promise.resolve();
};
</script>
<style lang="scss" scoped>
.approver-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
border-bottom: 1px solid #eee;
}
.cc-user-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
border-bottom: 1px solid #eee;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,857 @@
<template>
<BasicLayout>
<view class="px-15 pb-15">
<!-- 公文基本信息 -->
<view class="gw-info-section">
<view class="section-title">公文信息</view>
<view class="info-item">
<text class="label">标题</text>
<text class="value">{{ gwInfo.title }}</text>
</view>
<view class="info-item">
<text class="label">编号</text>
<text class="value">{{ gwInfo.gwNo }}</text>
</view>
<view class="info-item">
<text class="label">类型</text>
<text class="value">{{ gwInfo.gwType }}</text>
</view>
<view class="info-item">
<text class="label">状态</text>
<text class="value status-tag" :class="getStatusClass(gwInfo.status)">
{{ getStatusText(gwInfo.status) }}
</text>
</view>
<view class="info-item">
<text class="label">创建人</text>
<text class="value">{{ gwInfo.createdBy }}</text>
</view>
<view class="info-item">
<text class="label">创建时间</text>
<text class="value">{{ formatTime(gwInfo.createdTime) }}</text>
</view>
</view>
<!-- 文件信息 -->
<view class="file-section">
<view class="section-title">附件</view>
<view class="file-list">
<view
v-for="(file, index) in gwInfo.files"
:key="index"
class="file-item"
@click="previewFile(file)"
>
<view class="file-icon">📎</view>
<view class="file-info">
<text class="file-name">{{ file.name }}</text>
<text class="file-size">{{ formatFileSize(file.size) }}</text>
</view>
<view class="file-actions">
<u-button
text="预览"
size="mini"
type="primary"
@click.stop="previewFile(file)"
/>
<u-button
text="下载"
size="mini"
@click.stop="downloadFile(file)"
/>
</view>
</view>
</view>
</view>
<!-- 当前处理人 -->
<view class="approver-section">
<view class="section-title">当前审批人</view>
<view class="approver-list">
<view
v-for="approver in approvers"
:key="approver.id"
class="approver-item"
:class="{ 'removed': approver.isRemoved }"
>
<view class="approver-info">
<text class="order">{{ approver.order }}</text>
<text class="name">{{ approver.userName }}</text>
<text class="dept">{{ approver.deptName }}</text>
<text class="status" :class="getApproverStatusClass(approver.status)">
{{ getApproverStatusText(approver.status) }}
</text>
</view>
<view class="approver-actions" v-if="!approver.isRemoved">
<u-button
text="移除"
size="mini"
type="error"
@click="removeApprover(approver)"
/>
</view>
</view>
</view>
<!-- 添加审批人 -->
<view class="add-approver">
<u-button
text="添加审批人"
type="primary"
@click="showAddApproverModal = true"
/>
</view>
</view>
<!-- 抄送人 -->
<view class="cc-section">
<view class="section-title">抄送人</view>
<view class="cc-list">
<view
v-for="ccUser in ccUsers"
:key="ccUser.id"
class="cc-item"
:class="{ 'removed': ccUser.isRemoved }"
>
<view class="cc-info">
<text class="name">{{ ccUser.userName }}</text>
<text class="dept">{{ ccUser.deptName }}</text>
<text class="status" :class="getCCStatusClass(ccUser.status)">
{{ getCCStatusText(ccUser.status) }}
</text>
</view>
<view class="cc-actions" v-if="!ccUser.isRemoved">
<u-button
text="移除"
size="mini"
type="error"
@click="removeCCUser(ccUser)"
/>
</view>
</view>
</view>
<!-- 添加抄送人 -->
<view class="add-cc">
<u-button
text="添加抄送人"
type="primary"
@click="showAddCCModal = true"
/>
</view>
</view>
<!-- 操作记录 -->
<view class="log-section">
<view class="section-title">操作记录</view>
<view class="log-list">
<view
v-for="log in operationLogs"
:key="log.id"
class="log-item"
>
<view class="log-header">
<text class="operator">{{ log.operatorName }}</text>
<text class="time">{{ formatTime(log.operationTime) }}</text>
</view>
<view class="log-content">
<text class="type">{{ log.operationType }}</text>
<text class="content">{{ log.operationContent }}</text>
</view>
<view class="log-detail" v-if="log.beforeChange || log.afterChange">
<u-button
text="详情"
size="mini"
@click="showLogDetail(log)"
/>
</view>
</view>
</view>
</view>
<!-- 保存变更按钮 -->
<view class="save-section">
<u-button
text="保存变更"
type="primary"
size="large"
@click="saveChanges"
/>
</view>
</view>
<!-- 添加审批人弹窗 -->
<u-popup v-model="showAddApproverModal" mode="bottom">
<view class="modal-content">
<view class="modal-header">
<text class="modal-title">添加审批人</text>
<u-button text="关闭" @click="showAddApproverModal = false" />
</view>
<view class="search-section">
<BasicSearch
placeholder="输入姓名或部门搜索"
@search="searchApprovers"
@select="addApprover"
/>
</view>
<view class="position-section">
<text class="position-label">插入位置</text>
<u-picker
:columns="[positionOptions]"
@confirm="onPositionConfirm"
/>
</view>
</view>
</u-popup>
<!-- 添加抄送人弹窗 -->
<u-popup v-model="showAddCCModal" mode="bottom">
<view class="modal-content">
<view class="modal-header">
<text class="modal-title">添加抄送人</text>
<u-button text="关闭" @click="showAddCCModal = false" />
</view>
<view class="search-section">
<BasicSearch
placeholder="输入姓名或部门搜索"
@search="searchCCUsers"
@select="addCCUser"
/>
</view>
</view>
</u-popup>
<!-- 操作记录详情弹窗 -->
<u-popup v-model="showLogDetailModal" mode="center">
<view class="detail-modal">
<view class="detail-header">
<text class="detail-title">操作详情</text>
<u-button text="关闭" @click="showLogDetailModal = false" />
</view>
<view class="detail-content">
<view class="detail-item" v-if="currentLog.beforeChange">
<text class="detail-label">变更前</text>
<text class="detail-value">{{ currentLog.beforeChange }}</text>
</view>
<view class="detail-item" v-if="currentLog.afterChange">
<text class="detail-label">变更后</text>
<text class="detail-value">{{ currentLog.afterChange }}</text>
</view>
<view class="detail-item" v-if="currentLog.remark">
<text class="detail-label">备注</text>
<text class="detail-value">{{ currentLog.remark }}</text>
</view>
</view>
</view>
</u-popup>
</BasicLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import BasicLayout from "@/components/BasicLayout/Layout.vue";
import BasicSearch from "@/components/BasicSearch/Search.vue";
import { getGwDetailApi, searchUsersApi, saveChangesApi } from "@/api/routine/gw";
import dayjs from "dayjs";
//
const gwId = ref("");
//
const showAddApproverModal = ref(false);
const showAddCCModal = ref(false);
const showLogDetailModal = ref(false);
//
const gwInfo = ref<any>({});
const approvers = ref<any[]>([]);
const ccUsers = ref<any[]>([]);
const operationLogs = ref<any[]>([]);
const currentLog = ref<any>({});
const selectedPosition = ref("");
//
const positionOptions = [
"在首位",
"在第二位之后",
"在第三位之后",
"在最后"
];
//
const getGwInfo = async () => {
try {
// API
const result = await getGwDetailApi(gwId);
gwInfo.value = result;
approvers.value = result.approvers || [];
ccUsers.value = result.ccUsers || [];
operationLogs.value = result.operationLogs || [];
} catch (error) {
console.error("获取公文信息失败:", error);
// API使
console.log("使用模拟数据");
gwInfo.value = mockGwDetail;
approvers.value = mockGwDetail.approvers || [];
ccUsers.value = mockGwDetail.ccUsers || [];
operationLogs.value = mockGwDetail.operationLogs || [];
}
};
//
const searchApprovers = async (keyword: string) => {
try {
const result = await searchUsersApi(keyword);
return result;
} catch (error) {
console.error("搜索审批人失败:", error);
return [];
}
};
//
const addApprover = (user: any) => {
const newApprover = {
...user,
order: getNextOrder(),
status: "pending",
isRemoved: false,
};
approvers.value.push(newApprover);
//
addOperationLog({
operationType: "新增审批人",
operationContent: `新增:${user.userName}(顺序${newApprover.order}`,
beforeChange: "",
afterChange: JSON.stringify(newApprover),
});
showAddApproverModal.value = false;
};
//
const removeApprover = (approver: any) => {
approver.isRemoved = true;
//
addOperationLog({
operationType: "移除审批人",
operationContent: `移除:${approver.userName}(顺序${approver.order}`,
beforeChange: JSON.stringify(approver),
afterChange: "",
});
//
reorderApprovers();
};
//
const searchCCUsers = async (keyword: string) => {
try {
const result = await searchUsersApi(keyword);
return result;
} catch (error) {
console.error("搜索抄送人失败:", error);
return [];
}
};
//
const addCCUser = (user: any) => {
const newCCUser = {
...user,
status: "unread",
isRemoved: false,
};
ccUsers.value.push(newCCUser);
//
addOperationLog({
operationType: "新增抄送人",
operationContent: `新增:${user.userName}`,
beforeChange: "",
afterChange: JSON.stringify(newCCUser),
});
showAddCCModal.value = false;
};
//
const removeCCUser = (ccUser: any) => {
ccUser.isRemoved = true;
//
addOperationLog({
operationType: "移除抄送人",
operationContent: `移除:${ccUser.userName}`,
beforeChange: JSON.stringify(ccUser),
afterChange: "",
});
};
//
const onPositionConfirm = (e) => {
selectedPosition.value = e.value[0];
};
//
const getNextOrder = () => {
const activeApprovers = approvers.value.filter(a => !a.isRemoved);
return activeApprovers.length + 1;
};
//
const reorderApprovers = () => {
const activeApprovers = approvers.value.filter(a => !a.isRemoved);
activeApprovers.forEach((approver, index) => {
approver.order = index + 1;
});
};
//
const addOperationLog = (log) => {
const newLog = {
id: Date.now(),
operatorId: "current_user_id", // ID
operatorName: "当前用户", //
operationType: log.operationType,
operationContent: log.operationContent,
beforeChange: log.beforeChange,
afterChange: log.afterChange,
operationTime: new Date(),
remark: log.remark || "",
};
operationLogs.value.unshift(newLog);
};
//
const showLogDetail = (log) => {
currentLog.value = log;
showLogDetailModal.value = true;
};
//
const saveChanges = async () => {
try {
//
const activeApprovers = approvers.value.filter(a => !a.isRemoved);
if (activeApprovers.length === 0) {
uni.showToast({
title: "至少保留一名有效审批人",
icon: "error",
});
return;
}
const changeData = {
gwId: gwId,
approvers: approvers.value,
ccUsers: ccUsers.value,
operationLogs: operationLogs.value,
};
// API
await saveChangesApi(changeData);
uni.showToast({
title: "变更保存成功",
icon: "success",
});
//
await getGwInfo();
} catch (error) {
console.error("保存变更失败:", error);
uni.showToast({
title: "保存失败",
icon: "error",
});
}
};
//
const previewFile = (file) => {
//
console.log("预览文件:", file);
};
//
const downloadFile = (file) => {
//
console.log("下载文件:", file);
};
//
const formatTime = (time) => {
return dayjs(time).format("YYYY-MM-DD HH:mm:ss");
};
//
const formatFileSize = (size) => {
if (size < 1024) return size + "B";
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + "KB";
return (size / (1024 * 1024)).toFixed(2) + "MB";
};
//
const getStatusClass = (status) => {
const statusMap = {
draft: "status-draft",
pending: "status-pending",
approved: "status-approved",
rejected: "status-rejected",
};
return statusMap[status] || "status-default";
};
//
const getStatusText = (status) => {
const statusMap = {
draft: "草稿",
pending: "待审批",
approved: "已通过",
rejected: "已驳回",
};
return statusMap[status] || "未知";
};
//
const getApproverStatusClass = (status) => {
const statusMap = {
pending: "status-pending",
approved: "status-approved",
rejected: "status-rejected",
skipped: "status-skipped",
};
return statusMap[status] || "status-default";
};
//
const getApproverStatusText = (status) => {
const statusMap = {
pending: "待审批",
approved: "已同意",
rejected: "已驳回",
skipped: "已跳过",
};
return statusMap[status] || "未知";
};
//
const getCCStatusClass = (status) => {
const statusMap = {
unread: "status-unread",
read: "status-read",
};
return statusMap[status] || "status-default";
};
//
const getCCStatusText = (status) => {
const statusMap = {
unread: "未读",
read: "已读",
};
return statusMap[status] || "未知";
};
// API使
const mockGwDetail = {
id: "1",
title: "关于2024年教学工作计划的通知",
gwNo: "GW2024001",
gwType: "通知",
status: "pending",
createdBy: "张三",
createdTime: new Date(),
files: [
{ name: "教学工作计划.pdf", size: 1024000, url: "file1.pdf" },
{ name: "附件清单.xlsx", size: 512000, url: "file2.xlsx" },
],
approvers: [
{ id: "1", userName: "李四", deptName: "教务处", order: 1, status: "approved" },
{ id: "2", userName: "王五", deptName: "学生处", order: 2, status: "pending" },
],
ccUsers: [
{ id: "3", userName: "赵六", deptName: "人事处", status: "read" },
],
operationLogs: [
{
id: "1",
operatorName: "张三",
operationType: "创建公文",
operationContent: "创建公文关于2024年教学工作计划的通知",
operationTime: new Date(),
},
],
};
onMounted(() => {
//
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const options = currentPage.options || currentPage.$page?.options || {};
gwId.value = options.id || "";
if (gwId.value) {
getGwInfo();
}
});
</script>
<style lang="scss" scoped>
.gw-info-section,
.file-section,
.approver-section,
.cc-section,
.log-section {
margin-bottom: 20px;
padding: 15px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
border-bottom: 2px solid #007aff;
padding-bottom: 5px;
}
.info-item {
display: flex;
margin-bottom: 10px;
.label {
width: 80px;
color: #666;
font-weight: 500;
}
.value {
flex: 1;
color: #333;
}
}
.status-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
&.status-draft { background: #f0f0f0; color: #666; }
&.status-pending { background: #fff7e6; color: #fa8c16; }
&.status-approved { background: #f6ffed; color: #52c41a; }
&.status-rejected { background: #fff2f0; color: #ff4d4f; }
}
.file-item {
display: flex;
align-items: center;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 10px;
.file-icon {
margin-right: 10px;
font-size: 20px;
}
.file-info {
flex: 1;
.file-name {
display: block;
font-weight: 500;
margin-bottom: 2px;
}
.file-size {
font-size: 12px;
color: #666;
}
}
.file-actions {
display: flex;
gap: 5px;
}
}
.approver-item,
.cc-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 10px;
&.removed {
opacity: 0.5;
background: #f5f5f5;
}
.approver-info,
.cc-info {
flex: 1;
.order {
background: #007aff;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 12px;
margin-right: 8px;
}
.name {
font-weight: 500;
margin-right: 8px;
}
.dept {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.status {
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
&.status-pending { background: #fff7e6; color: #fa8c16; }
&.status-approved { background: #f6ffed; color: #52c41a; }
&.status-rejected { background: #fff2f0; color: #ff4d4f; }
&.status-skipped { background: #f0f0f0; color: #666; }
&.status-unread { background: #fff7e6; color: #fa8c16; }
&.status-read { background: #f6ffed; color: #52c41a; }
}
}
}
.add-approver,
.add-cc {
margin-top: 15px;
text-align: center;
}
.log-item {
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 10px;
.log-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
.operator {
font-weight: 500;
color: #007aff;
}
.time {
font-size: 12px;
color: #666;
}
}
.log-content {
margin-bottom: 8px;
.type {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
margin-right: 8px;
}
.content {
color: #333;
}
}
}
.save-section {
margin-top: 30px;
text-align: center;
}
.modal-content {
padding: 20px;
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.modal-title {
font-size: 16px;
font-weight: bold;
}
}
.search-section {
margin-bottom: 20px;
}
.position-section {
display: flex;
align-items: center;
.position-label {
margin-right: 10px;
}
}
}
.detail-modal {
width: 80vw;
max-width: 400px;
padding: 20px;
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.detail-title {
font-size: 16px;
font-weight: bold;
}
}
.detail-content {
.detail-item {
margin-bottom: 15px;
.detail-label {
font-weight: 500;
color: #666;
margin-right: 10px;
}
.detail-value {
color: #333;
word-break: break-all;
}
}
}
}
</style>

View File

@ -0,0 +1,575 @@
<template>
<BasicLayout>
<view class="px-15 pb-15">
<!-- 搜索和筛选 -->
<view class="search-section">
<BasicSearch
placeholder="搜索公文标题或编号"
@search="handleSearch"
/>
</view>
<!-- 筛选标签 -->
<view class="filter-section">
<view class="filter-tabs">
<view
v-for="tab in filterTabs"
:key="tab.key"
class="filter-tab"
:class="{ active: activeTab === tab.key }"
@click="switchTab(tab.key)"
>
{{ tab.label }}
</view>
</view>
</view>
<!-- 公文列表 -->
<view class="gw-list">
<view
v-for="item in filteredGwList"
:key="item.id"
class="gw-item"
@click="goToDetail(item)"
>
<view class="gw-header">
<view class="gw-title">{{ item.title }}</view>
<view class="gw-status" :class="getStatusClass(item.status)">
{{ getStatusText(item.status) }}
</view>
</view>
<view class="gw-info">
<view class="info-row">
<text class="label">编号</text>
<text class="value">{{ item.gwNo }}</text>
</view>
<view class="info-row">
<text class="label">类型</text>
<text class="value">{{ item.gwType }}</text>
</view>
<view class="info-row">
<text class="label">紧急程度</text>
<text class="value urgency-tag" :class="getUrgencyClass(item.urgencyLevel)">
{{ getUrgencyText(item.urgencyLevel) }}
</text>
</view>
</view>
<view class="gw-footer">
<view class="approver-info">
<text class="label">审批进度</text>
<text class="value">{{ getApproverProgress(item) }}</text>
</view>
<view class="time-info">
<text class="label">创建时间</text>
<text class="value">{{ formatTime(item.createdTime) }}</text>
</view>
</view>
<view class="gw-actions">
<u-button
text="查看详情"
size="mini"
type="primary"
@click.stop="goToDetail(item)"
/>
<u-button
v-if="item.status === GwStatus.DRAFT"
text="编辑"
size="mini"
@click.stop="editGw(item)"
/>
<u-button
v-if="item.status === GwStatus.DRAFT"
text="删除"
size="mini"
type="error"
@click.stop="deleteGw(item)"
/>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="filteredGwList.length === 0" class="empty-state">
<view class="empty-icon">📄</view>
<view class="empty-text">暂无公文数据</view>
</view>
<!-- 加载更多 -->
<view v-if="hasMore && filteredGwList.length > 0" class="load-more">
<u-button
text="加载更多"
@click="loadMore"
:loading="loading"
/>
</view>
</view>
<!-- 底部操作按钮 -->
<template #bottom>
<view class="flex-row items-center pb-10 pt-5">
<u-button
text="新建公文"
class="mx-15"
type="primary"
@click="createNewGw"
/>
</view>
</template>
</BasicLayout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { navigateTo } from "@/utils/uniapp";
import BasicSearch from "@/components/BasicSearch/Search.vue";
import BasicLayout from "@/components/BasicLayout/Layout.vue";
import { getGwListApi, deleteGwApi } from "@/api/routine/gw";
import { GwStatus, UrgencyLevel, ApproverStatus } from "@/types/gw";
import type { GwInfo, GwListItem } from "@/types/gw";
import dayjs from "dayjs";
//
const filterTabs = [
{ key: "all", label: "全部" },
{ key: "draft", label: "草稿" },
{ key: "pending", label: "待审批" },
{ key: "approved", label: "已通过" },
{ key: "rejected", label: "已驳回" },
];
const activeTab = ref("all");
const searchKeyword = ref("");
const gwList = ref<GwListItem[]>([]);
const loading = ref(false);
const hasMore = ref(true);
const page = ref(1);
const pageSize = 20;
//
const filteredGwList = computed(() => {
let list = gwList.value;
//
if (activeTab.value !== "all") {
const statusMap: Record<string, GwStatus> = {
draft: GwStatus.DRAFT,
pending: GwStatus.PENDING,
approved: GwStatus.APPROVED,
rejected: GwStatus.REJECTED,
};
const targetStatus = statusMap[activeTab.value];
if (targetStatus) {
list = list.filter(item => item.status === targetStatus);
}
}
//
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase();
list = list.filter(item =>
item.title.toLowerCase().includes(keyword) ||
item.gwNo.toLowerCase().includes(keyword)
);
}
return list;
});
//
const switchTab = (tabKey: string) => {
activeTab.value = tabKey;
};
//
const handleSearch = (keyword: string) => {
searchKeyword.value = keyword;
};
//
const getGwList = async (isLoadMore = false) => {
try {
loading.value = true;
if (!isLoadMore) {
page.value = 1;
hasMore.value = true;
}
// APIAPI
const statusMap: Record<string, GwStatus> = {
draft: GwStatus.DRAFT,
pending: GwStatus.PENDING,
approved: GwStatus.APPROVED,
rejected: GwStatus.REJECTED,
};
const targetStatus = activeTab.value === "all" ? undefined : statusMap[activeTab.value];
//
let filteredData = mockData;
if (targetStatus) {
filteredData = mockData.filter(item => item.status === targetStatus);
}
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase();
filteredData = filteredData.filter(item =>
item.title.toLowerCase().includes(keyword) ||
item.gwNo.toLowerCase().includes(keyword)
);
}
//
const startIndex = (page.value - 1) * pageSize;
const endIndex = startIndex + pageSize;
const result = {
list: filteredData.slice(startIndex, endIndex),
hasMore: endIndex < filteredData.length,
};
if (isLoadMore) {
gwList.value.push(...result.list);
} else {
gwList.value = result.list;
}
hasMore.value = result.hasMore;
page.value++;
} catch (error) {
console.error("获取公文列表失败:", error);
uni.showToast({
title: "获取列表失败",
icon: "error",
});
} finally {
loading.value = false;
}
};
//
const loadMore = () => {
if (!loading.value && hasMore.value) {
getGwList(true);
}
};
//
const goToDetail = (item: GwListItem) => {
navigateTo(`/pages/view/routine/gwlz/gwFlow?id=${item.id}`);
};
//
const editGw = (item: GwListItem) => {
navigateTo(`/pages/view/routine/gwlz?id=${item.id}&mode=edit`);
};
//
const deleteGw = (item: GwListItem) => {
uni.showModal({
title: "确认删除",
content: `确定要删除公文"${item.title}"吗?`,
success: async (res) => {
if (res.confirm) {
try {
// API
await deleteGwApi(item.id);
//
const index = gwList.value.findIndex(gw => gw.id === item.id);
if (index > -1) {
gwList.value.splice(index, 1);
}
uni.showToast({
title: "删除成功",
icon: "success",
});
} catch (error) {
console.error("删除公文失败:", error);
uni.showToast({
title: "删除失败",
icon: "error",
});
}
}
},
});
};
//
const createNewGw = () => {
navigateTo("/pages/view/routine/gwlz/gwAdd");
};
//
const getStatusClass = (status: GwStatus) => {
const statusMap: Record<GwStatus, string> = {
[GwStatus.DRAFT]: "status-draft",
[GwStatus.PENDING]: "status-pending",
[GwStatus.APPROVED]: "status-approved",
[GwStatus.REJECTED]: "status-rejected",
};
return statusMap[status] || "status-default";
};
//
const getStatusText = (status: GwStatus) => {
const statusMap: Record<GwStatus, string> = {
[GwStatus.DRAFT]: "草稿",
[GwStatus.PENDING]: "待审批",
[GwStatus.APPROVED]: "已通过",
[GwStatus.REJECTED]: "已驳回",
};
return statusMap[status] || "未知";
};
//
const getUrgencyClass = (urgency: UrgencyLevel) => {
const urgencyMap: Record<UrgencyLevel, string> = {
[UrgencyLevel.LOW]: "urgency-low",
[UrgencyLevel.NORMAL]: "urgency-normal",
[UrgencyLevel.HIGH]: "urgency-high",
[UrgencyLevel.URGENT]: "urgency-urgent",
};
return urgencyMap[urgency] || "urgency-normal";
};
//
const getUrgencyText = (urgency: UrgencyLevel) => {
const urgencyMap: Record<UrgencyLevel, string> = {
[UrgencyLevel.LOW]: "普通",
[UrgencyLevel.NORMAL]: "一般",
[UrgencyLevel.HIGH]: "紧急",
[UrgencyLevel.URGENT]: "特急",
};
return urgencyMap[urgency] || "一般";
};
//
const getApproverProgress = (item: GwListItem) => {
if (!item.approvers || item.approvers.length === 0) {
return "无审批人";
}
const total = item.approvers.length;
const approved = item.approvers.filter((a: any) => a.status === ApproverStatus.APPROVED).length;
const rejected = item.approvers.filter((a: any) => a.status === ApproverStatus.REJECTED).length;
if (rejected > 0) {
return `已驳回 (${rejected}/${total})`;
}
return `${approved}/${total} 已审批`;
};
//
const formatTime = (time: string | Date) => {
return dayjs(time).format("MM-DD HH:mm");
};
//
const mockData: GwListItem[] = [
{
id: "1",
title: "关于2024年教学工作计划的通知",
gwNo: "GW2024001",
gwType: "通知",
urgencyLevel: UrgencyLevel.NORMAL,
status: GwStatus.PENDING,
createdBy: "admin",
createdTime: new Date(),
files: [],
approvers: [
{ id: "1", userId: "user1", userName: "审批人1", deptId: "dept1", deptName: "部门1", order: 1, status: ApproverStatus.APPROVED },
{ id: "2", userId: "user2", userName: "审批人2", deptId: "dept2", deptName: "部门2", order: 2, status: ApproverStatus.PENDING },
],
ccUsers: [],
},
{
id: "2",
title: "2024年春季学期课程安排",
gwNo: "GW2024002",
gwType: "安排",
urgencyLevel: UrgencyLevel.HIGH,
status: GwStatus.APPROVED,
createdBy: "admin",
createdTime: new Date(Date.now() - 86400000),
files: [],
approvers: [
{ id: "3", userId: "user1", userName: "审批人1", deptId: "dept1", deptName: "部门1", order: 1, status: ApproverStatus.APPROVED },
{ id: "4", userId: "user2", userName: "审批人2", deptId: "dept2", deptName: "部门2", order: 2, status: ApproverStatus.APPROVED },
],
ccUsers: [],
},
{
id: "3",
title: "学生宿舍管理规定修订稿",
gwNo: "GW2024003",
gwType: "规定",
urgencyLevel: UrgencyLevel.LOW,
status: GwStatus.DRAFT,
createdBy: "admin",
createdTime: new Date(Date.now() - 172800000),
files: [],
approvers: [],
ccUsers: [],
},
];
onMounted(() => {
// 使
gwList.value = mockData;
});
</script>
<style lang="scss" scoped>
.search-section {
margin-bottom: 15px;
}
.filter-section {
margin-bottom: 20px;
.filter-tabs {
display: flex;
background: #f5f5f5;
border-radius: 8px;
padding: 4px;
.filter-tab {
flex: 1;
text-align: center;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
color: #666;
transition: all 0.3s;
&.active {
background: #007aff;
color: white;
}
}
}
}
.gw-list {
.gw-item {
background: white;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.gw-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
.gw-title {
flex: 1;
font-size: 16px;
font-weight: 500;
color: #333;
line-height: 1.4;
margin-right: 10px;
}
.gw-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
&.status-draft { background: #f0f0f0; color: #666; }
&.status-pending { background: #fff7e6; color: #fa8c16; }
&.status-approved { background: #f6ffed; color: #52c41a; }
&.status-rejected { background: #fff2f0; color: #ff4d4f; }
}
}
.gw-info {
margin-bottom: 12px;
.info-row {
display: flex;
margin-bottom: 6px;
.label {
width: 70px;
color: #666;
font-size: 14px;
}
.value {
flex: 1;
color: #333;
font-size: 14px;
}
.urgency-tag {
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
&.urgency-low { background: #f6ffed; color: #52c41a; }
&.urgency-normal { background: #f0f0f0; color: #666; }
&.urgency-high { background: #fff7e6; color: #fa8c16; }
&.urgency-urgent { background: #fff2f0; color: #ff4d4f; }
}
}
}
.gw-footer {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
font-size: 12px;
.approver-info,
.time-info {
.label {
color: #666;
margin-right: 5px;
}
.value {
color: #333;
}
}
}
.gw-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
.empty-icon {
font-size: 48px;
margin-bottom: 15px;
opacity: 0.5;
}
.empty-text {
color: #999;
font-size: 14px;
}
}
.load-more {
text-align: center;
margin-top: 20px;
}
</style>

View File

@ -41,15 +41,15 @@
<text class="stats-title">签到统计</text>
</view>
<view class="stats-content">
<view class="stat-item">
<view class="stat-item" @click="showTeacherList('all')">
<text class="stat-number">{{ totalCount }}</text>
<text class="stat-label">总人数</text>
</view>
<view class="stat-item">
<view class="stat-item" @click="showTeacherList('signed')">
<text class="stat-number signed">{{ signedCount }}</text>
<text class="stat-label">已签到</text>
</view>
<view class="stat-item">
<view class="stat-item" @click="showTeacherList('unsigned')">
<text class="stat-number unsigned">{{ unsignedCount }}</text>
<text class="stat-label">未签到</text>
</view>
@ -93,6 +93,38 @@
</button>
</view>
</template>
<!-- 签到人员弹窗 -->
<uni-popup ref="teacherPopup" type="center" :mask-click="true">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ popupTitle }}</text>
<text class="popup-close" @click="closeTeacherPopup">×</text>
</view>
<scroll-view scroll-y class="popup-body">
<view class="popup-teacher-list">
<view
v-for="teacher in filteredTeacherList"
:key="teacher.id"
class="popup-teacher-item"
>
<view class="popup-teacher-info">
<text class="popup-teacher-name">{{ teacher.jsxm }}</text>
<text class="popup-teacher-position">{{ teacher.dzzw || '' }} {{ teacher.qtzw || '' }}</text>
</view>
<view class="popup-teacher-status">
<text class="popup-status-text" :class="getStatusClass(teacher.qdStatus)">
{{ getStatusText(teacher.qdStatus) }}
</text>
<text v-if="teacher.qdwctime" class="popup-sign-time">
{{ formatTime(teacher.qdwctime) }}
</text>
</view>
</view>
</view>
</scroll-view>
</view>
</uni-popup>
</BasicLayout>
</template>
@ -127,10 +159,24 @@ interface TeacherInfo {
const qdId = ref<string>('');
const qdInfo = ref<QdInfo>({} as QdInfo);
const teacherList = ref<TeacherInfo[]>([]);
const teacherPopup = ref();
const popupTitle = ref('');
const currentFilter = ref('all');
const totalCount = computed(() => teacherList.value.length);
const signedCount = computed(() => teacherList.value.filter(t => t.qdStatus === '1').length);
const unsignedCount = computed(() => totalCount.value - signedCount.value);
const unsignedCount = computed(() => teacherList.value.filter(t => t.qdStatus === '0').length);
const filteredTeacherList = computed(() => {
switch (currentFilter.value) {
case 'signed':
return teacherList.value.filter(t => t.qdStatus === '1');
case 'unsigned':
return teacherList.value.filter(t => t.qdStatus === '0');
default:
return teacherList.value;
}
});
onLoad((options) => {
if (options && options.id) {
@ -159,14 +205,39 @@ const loadQdDetail = async () => {
const loadTeacherList = async () => {
try {
const result = await qdzxFindByQdParamsApi({ qdId: qdId.value });
if (result.resultCode === 1 && result.result) {
teacherList.value = result.result;
// - rows
if (result.rows && Array.isArray(result.rows)) {
teacherList.value = result.rows;
} else {
console.log('接口返回异常 - 没有rows字段或不是数组:', result);
teacherList.value = [];
}
} catch (error) {
console.error('加载教师列表失败:', error);
teacherList.value = [];
}
};
const showTeacherList = (filter: string) => {
currentFilter.value = filter;
switch (filter) {
case 'signed':
popupTitle.value = `已签到人员 (${signedCount.value}人)`;
break;
case 'unsigned':
popupTitle.value = `未签到人员 (${unsignedCount.value}人)`;
break;
default:
popupTitle.value = `全部人员 (${totalCount.value}人)`;
}
teacherPopup.value.open();
};
const closeTeacherPopup = () => {
teacherPopup.value.close();
};
const getStatusClass = (status: string) => {
switch (status) {
case 'A':
@ -330,6 +401,12 @@ const handleBack = () => {
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
transition: transform 0.2s ease;
&:active {
transform: scale(0.95);
}
}
.stat-number {
@ -427,12 +504,100 @@ const handleBack = () => {
.back-btn {
width: 100%;
padding: 12px;
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
}
//
.popup-content {
background: white;
border-radius: 16px;
width: 90vw;
max-width: 400px;
max-height: 80vh;
overflow: hidden;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 15px;
border-bottom: 1px solid #f0f0f0;
}
.popup-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.popup-close {
font-size: 24px;
color: #999;
cursor: pointer;
padding: 5px;
line-height: 1;
}
.popup-body {
max-height: 60vh;
}
.popup-teacher-list {
padding: 15px 20px;
}
.popup-teacher-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
}
.popup-teacher-info {
flex: 1;
}
.popup-teacher-name {
display: block;
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.popup-teacher-position {
font-size: 14px;
color: #666;
}
.popup-teacher-status {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.popup-status-text {
font-size: 12px;
font-weight: 500;
padding: 2px 8px;
border-radius: 6px;
}
.popup-sign-time {
font-size: 11px;
color: #999;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
src/static/base/home/gw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
src/static/base/home/gz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB