新增公文流转
168
src/api/routine/gw.ts
Normal 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}`);
|
||||
}
|
||||
@ -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": {
|
||||
|
||||
@ -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", // 工资条权限编码
|
||||
|
||||
@ -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,
|
||||
})
|
||||
});
|
||||
|
||||
389
src/pages/view/routine/gwlz/gwAdd.vue
Normal 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>
|
||||
|
||||
857
src/pages/view/routine/gwlz/gwFlow.vue
Normal 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>
|
||||
575
src/pages/view/routine/gwlz/index.vue
Normal 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;
|
||||
}
|
||||
|
||||
// 模拟API调用,实际开发时替换为真实API
|
||||
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>
|
||||
|
||||
@ -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>
|
||||
BIN
src/static/base/home/bjkb.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/static/base/home/cjfx.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
src/static/base/home/gw.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/static/base/home/gz.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
src/static/base/home/gzltj.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/static/base/home/jcdm.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src/static/base/home/jfpj.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/static/base/home/jlfb.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
src/static/base/home/jsda.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
src/static/base/home/jskb.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/static/base/home/kctb.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
src/static/base/home/kfxc.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
src/static/base/home/qdfb.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/static/base/home/qjsq.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src/static/base/home/rzrj.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src/static/base/home/txl.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
src/static/base/home/xkdm.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |