公文流转

This commit is contained in:
hebo 2025-08-24 21:46:29 +08:00
parent 9e2d89e911
commit 7f3c9705cc
12 changed files with 2078 additions and 724 deletions

View File

@ -3,166 +3,115 @@ import { get, post } from "@/utils/request";
// 公文相关API接口 // 公文相关API接口
/** /**
* *
* @param params
*/ */
export function getGwListApi(params: { export function gwFindPageApi(params: any) {
page: number; return get('/api/gw/findPage', params);
pageSize: number;
status?: string;
keyword?: string;
gwType?: string;
urgencyLevel?: string;
startTime?: string;
endTime?: string;
}) {
return get('/api/gw/list', params);
} }
/** /**
* * ID查询公文详情
* @param id ID
*/ */
export function getGwDetailApi(id: string) { export function gwFindByIdApi(params: any) {
return get(`/api/gw/detail/${id}`); return get('/api/gw/findById', params);
} }
/** /**
* * /
* @param data
*/ */
export function createGwApi(data: { export function gwSaveApi(params: any) {
title: string; return post('/api/gw/save', params);
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) { export function gwLogicDeleteApi(params: any) {
return post(`/api/gw/update/${id}`, data); // 将参数拼接到URL中确保后端能正确接收
} const queryString = Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&');
/** // 尝试POST请求如果不行可以改为GET请求
* return post(`/api/gw/logicDelete?${queryString}`, params);
* @param id ID
*/ // 如果POST请求有问题可以尝试GET请求
export function deleteGwApi(id: string) { // return get(`/api/gw/logicDelete?${queryString}`);
return post(`/api/gw/delete/${id}`, { id });
} }
/** /**
* 稿 * 稿
* @param data 稿
*/ */
export function saveDraftApi(data: any) { export function gwDraftApi(params: any) {
return post('/api/gw/draft', data); return post('/api/gw/draft', params);
} }
/** /**
* *
* @param data
*/ */
export function submitGwApi(data: any) { export function gwSubmitApi(params: any) {
return post('/api/gw/submit', data); return post('/api/gw/submit', params);
} }
/** /**
* *
* @param data
*/ */
export function saveChangesApi(data: { export function gwApproveApi(params: any) {
gwId: string; return post('/api/gw/approve', params);
approvers: any[];
ccUsers: any[];
operationLogs: any[];
}) {
return post('/api/gw/changes', data);
} }
/** /**
* *
* @param keyword
*/ */
export function searchUsersApi(keyword: string) { export function gwCcConfirmApi(params: any) {
return get('/api/user/search', { keyword }); return post('/api/gw/cc-confirm', params);
} }
/** /**
* *
* @param gwId ID
*/ */
export function getApproversApi(gwId: string) { export function gwApproversApi(params: any) {
return get(`/api/gw/approvers/${gwId}`); return get('/api/gw/approvers', params);
} }
/** /**
* *
* @param gwId ID
*/ */
export function getCCUsersApi(gwId: string) { export function gwCcUsersApi(params: any) {
return get(`/api/gw/cc-users/${gwId}`); return get('/api/gw/cc-users', params);
} }
/** /**
* *
* @param gwId ID
*/ */
export function getOperationLogsApi(gwId: string) { export function gwLogsApi(params: any) {
return get(`/api/gw/logs/${gwId}`); return get('/api/gw/logs', params);
}
/**
*
* @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() { export function gwStatsApi(params: any) {
return get('/api/gw/stats'); return get('/api/gw/stats', params);
} }
/** /**
* *
* @param data
*/ */
export function approveGwApi(data: { export function fileUploadApi(params: any) {
gwId: string; return post('/api/file/upload', params);
action: "approve" | "reject";
remark?: string;
}) {
return post('/api/gw/approve', data);
} }
/** /**
* *
* @param gwId ID
*/ */
export function confirmCCApi(gwId: string) { export function fileDeleteApi(params: any) {
return post(`/api/gw/cc-confirm/${gwId}`); return post('/api/file/delete', params);
}
/**
*
*/
export function userSearchApi(params: any) {
return get('/api/user/search', params);
} }

View File

@ -0,0 +1,112 @@
import { get, post } from "@/utils/request";
// 流程管理设置相关API接口
/**
*
* @param params
*/
export function lcglSetFindPageApi(params: {
page: number;
pageSize: number;
ruleId?: string;
ruleName?: string;
status?: string;
startTime?: string;
endTime?: string;
}) {
return get('/api/lcglSet/findPage', params);
}
/**
* /
* @param params
*/
export function lcglSetSaveApi(params: {
id?: string;
ruleId: string;
ruleName: string;
spId?: string;
ccId?: string;
status?: string;
remark?: string;
}) {
return post('/api/lcglSet/save', params);
}
/**
*
* @param params
*/
export function lcglSetLogicDeleteApi(params: { id: string }) {
return post('/api/lcglSet/logicDelete', params);
}
/**
* ID查询流程设置详情
* @param params
*/
export function lcglSetFindByIdApi(params: { id: string }) {
return get('/api/lcglSet/findById', params);
}
/**
*
*/
export function lcglSetFindAllApi() {
return get('/api/lcglSet/findAll');
}
/**
* ID查询流程设置
* @param params
*/
export function lcglSetFindByRuleIdApi(params: { ruleId: string }) {
return get('/api/lcglSet/findByRuleId', params);
}
/**
*
* @param params
*/
export function lcglSetUpdateStatusApi(params: {
id: string;
status: string;
}) {
return post('/api/lcglSet/updateStatus', params);
}
/**
*
* @param params
*/
export function lcglSetCopyApi(params: {
id: string;
newRuleId: string;
newRuleName: string;
}) {
return post('/api/lcglSet/copy', params);
}
/**
*
* @param params
*/
export function lcglSetExportApi(params: {
ids?: string[];
ruleId?: string;
status?: string;
}) {
return post('/api/lcglSet/export', params);
}
/**
*
* @param params
*/
export function lcglSetImportApi(params: {
file: File;
overwrite?: boolean;
}) {
return post('/api/lcglSet/import', params);
}

View File

@ -0,0 +1,91 @@
<template>
<view class="py-7">
<BasicJsPicker
ref="pickerRef"
v-model="pickerValue"
v-bind="attrs.componentProps"
@change="handleChange"
/>
</view>
</template>
<script setup lang="ts">
/**
* FormBasicJsPicker 教师选择器表单组件
* 用于在 BasicForm 中使用 BasicJsPicker 组件
*/
import BasicJsPicker from "@/components/BasicJsPicker/Picker.vue";
const pickerRef = ref<any>(null);
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
const attrs: any = useAttrs();
//
const pickerValue = computed({
get() {
return props.modelValue;
},
set(value) {
emit("update:modelValue", value);
},
});
// displayValue open BasicJsPicker
//
const handleChange = (value: any) => {
// multiple
if (attrs.componentProps.multiple) {
//
pickerValue.value = value;
} else {
// null
pickerValue.value = value;
}
};
//
defineExpose({
open: () => pickerRef.value?.open(),
close: () => pickerRef.value?.close()
});
</script>
<style lang="scss" scoped>
.py-7 {
padding: 7rpx 0;
}
.wh-full {
width: 100%;
}
.flex-row {
display: flex;
flex-direction: row;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.font-13 {
font-size: 13rpx;
}
.text-ellipsis-1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.color-9 {
color: #999;
}
</style>

View File

@ -8,6 +8,7 @@
<FormBasicCode v-bind="attrs" v-if="isShow('BasicCode')" v-model="newValue"/> <FormBasicCode v-bind="attrs" v-if="isShow('BasicCode')" v-model="newValue"/>
<FormBasicSwitch v-bind="attrs" v-if="isShow('BasicSwitch')" v-model="newValue"/> <FormBasicSwitch v-bind="attrs" v-if="isShow('BasicSwitch')" v-model="newValue"/>
<FormBasicPicker v-bind="attrs" v-if="isShow('BasicPicker')" v-model="newValue"/> <FormBasicPicker v-bind="attrs" v-if="isShow('BasicPicker')" v-model="newValue"/>
<FormBasicJsPicker v-bind="attrs" v-if="isShow('BasicJsPicker')" v-model="newValue"/>
<FormBasicNumberBox v-bind="attrs" v-if="isShow('BasicNumberBox')" v-model="newValue"/> <FormBasicNumberBox v-bind="attrs" v-if="isShow('BasicNumberBox')" v-model="newValue"/>
<FormBasicSelectBox v-bind="attrs" v-if="isShow('BasicSelectBox')" v-model="newValue"/> <FormBasicSelectBox v-bind="attrs" v-if="isShow('BasicSelectBox')" v-model="newValue"/>
<FormBasicLocation v-bind="attrs" v-if="isShow('BasicLocation')" v-model="newValue"/> <FormBasicLocation v-bind="attrs" v-if="isShow('BasicLocation')" v-model="newValue"/>

View File

@ -17,6 +17,7 @@ type Component =
| 'BasicDateTime' | 'BasicDateTime'
| 'BasicInput' | 'BasicInput'
| 'BasicPicker' | 'BasicPicker'
| 'BasicJsPicker'
| 'BasicRate' | 'BasicRate'
| 'BasicSwitch' | 'BasicSwitch'
| 'BasicUpload' | 'BasicUpload'

View File

@ -0,0 +1,387 @@
<template>
<view class="basic-js-picker">
<view class="js-selected" @click="showPicker">
<view class="js-selected-name">
<text class="data" v-if="selectedList && selectedList.length">{{ getShowSelectedName() }}</text>
<text class="data" style="color: #999;" v-else>{{ placeholder }}</text>
</view>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
<u-popup :show="showPopup" @close="showPopup=false">
<view class="js-picker-popup">
<view class="js-picker-header">
<view class="js-cancel-btn" @click="handleCancel">取消</view>
<view class="js-picker-title">{{ title }}</view>
<view class="js-ok-btn" @click="handleOk">确定</view>
</view>
<view class="js-picker-search" v-if="showSearch">
<BasicSearch @change="handleSearch" :showAction="false" height="36" :placeholder="searchPlaceholder"/>
</view>
<view class="js-list">
<scroll-view scroll-y style="max-height: 60vh" :scroll-top="targetScrollTop">
<view class="js-item" v-for="(item, index) in jsList" :key="index"
:class="{ selected: item.selected }" @click="handleSelect(item)">
<view class="js-name">{{ item.label }}</view>
<BasicIcon type="checkmarkempty" v-if="item.selected" color="#333" />
</view>
</scroll-view>
</view>
</view>
</u-popup>
</view>
</template>
<script lang="ts" setup>
import { useCommonStore } from "@/store/modules/common";
const { getAllJs } = useCommonStore();
//
const props = withDefaults(defineProps<{
modelValue?: any,
defaultValue?: any,
parentData?: any,
multiple?: boolean,
// id
excludeIds?: any,
//
title?: string,
placeholder?: string,
searchPlaceholder?: string,
showSearch?: boolean
}>(), {
modelValue: null,
defaultValue: null,
parentData: null,
multiple: false,
excludeIds: [],
title: '请选择教师',
placeholder: '请选择老师',
searchPlaceholder: '输入教师名称查询',
showSearch: true
});
// emit
const emit = defineEmits(['change', 'update:modelValue'])
const targetScrollTop = ref(0); // scroll-top
const showPopup = ref(false);
const jsListAll = ref<any>([]);
const jsList = ref<any>([]);
let searchKey = "";
const selectedList = ref<any>([]);
// v-model
const currentValue = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
}
});
//
const internalValue = ref(props.modelValue || props.defaultValue);
const getShowSelectedName = () => {
const names = selectedList.value.map((item: any) => item.label).join(",");
return names;
};
const handleSearch = (value: any) => {
searchKey = value;
rebuildJsList();
return
};
const handleSelect = (item: any) => {
if (props.multiple) {
//
const index = selectedList.value.findIndex((selected: any) => selected.value === item.value);
if (index > -1) {
//
selectedList.value.splice(index, 1);
item.selected = false;
} else {
//
selectedList.value.push(item);
item.selected = true;
}
} else {
//
jsList.value.forEach((listItem: any) => listItem.selected = false);
selectedList.value = [item];
item.selected = true;
}
}
const handleCancel = () => {
showPopup.value = false;
}
const handleOk = () => {
showPopup.value = false;
let result;
if (props.multiple) {
result = selectedList.value;
} else {
result = selectedList.value[0] || null;
}
// v-model
currentValue.value = result;
// change
emit("change", result, props.parentData);
};
const showPicker = () => {
showPopup.value = true;
};
const rebuildJsList = () => {
jsList.value = [];
jsListAll.value.forEach((item: any) => {
if (props.excludeIds && props.excludeIds.length && props.excludeIds.includes(item.id)) {
return;
}
// jsxm
if (!searchKey || item.jsxm.includes(searchKey)) {
jsList.value.push({
label: item.jsxm,
value: item.id,
headPic: item.headPic,
selected: false
});
}
});
//
restoreSelection();
};
const restoreSelection = () => {
if (!selectedList.value.length) {
return;
}
// selectedList
jsList.value.forEach((item: any) => {
const isSelected = selectedList.value.some((selected: any) => selected.value === item.value);
item.selected = isSelected;
});
};
//
defineExpose({
showPicker,
open: showPicker,
close: () => showPopup.value = false
});
onMounted(async () => {
const res = await getAllJs()
jsListAll.value = res.result || [];
rebuildJsList();
// tickjsList
await nextTick();
//
const initialValue = internalValue.value || props.defaultValue;
if (initialValue) {
setInitialValue(initialValue);
}
});
//
const setInitialValue = (value: any) => {
if (!jsList.value.length) {
return;
}
if (props.multiple) {
if (!Array.isArray(value) || !value.length) {
return;
}
selectedList.value = [];
//
jsList.value.forEach((item: any) => {
item.selected = false;
});
//
let processedValue = value;
// {0: {value: '1001', label: ''}, 1: {value: '1002', label: ''}}
if (value[0] && typeof value[0] === 'object' && (value[0].value || value[0].id)) {
processedValue = value.map((item: any) => item.value || item.id);
}
//
selectedList.value = [];
//
processedValue.forEach((id: any) => {
const foundItem = jsList.value.find((item: any) => item.value === id || item.id === id);
if (foundItem) {
foundItem.selected = true;
selectedList.value.push(foundItem);
}
});
//
jsList.value.forEach((item: any) => {
if (!processedValue.includes(item.value) && !processedValue.includes(item.id)) {
item.selected = false;
}
});
} else {
//
//
jsList.value.forEach((item: any) => {
item.selected = false;
});
//
let processedValue = value;
if (value && typeof value === 'object' && (value.value || value.id)) {
processedValue = value.value || value.id;
}
// valuevalueid
const targetItem = jsList.value.find((item: any) => item.value === processedValue || item.id === processedValue);
if (targetItem) {
targetItem.selected = true;
selectedList.value = [targetItem];
}
}
// selectedListjsListAll
if (selectedList.value.length === 0 && value && Array.isArray(value)) {
selectedList.value = [];
//
let processedValue = value;
if (value[0] && typeof value[0] === 'object' && (value[0].value || value[0].id)) {
processedValue = value.map((item: any) => item.value || item.id);
}
processedValue.forEach((id: any) => {
const foundItem = jsListAll.value.find((item: any) => item.id === id);
if (foundItem) {
const itemForList = {
label: foundItem.jsxm,
value: foundItem.id,
headPic: foundItem.headPic,
selected: true
};
selectedList.value.push(itemForList);
// jsList
const jsListItem = jsList.value.find((item: any) => item.value === foundItem.id);
if (jsListItem) {
jsListItem.selected = true;
}
}
});
}
};
// modelValue
watch(() => props.modelValue, (newValue) => {
if (newValue !== internalValue.value) {
internalValue.value = newValue;
// jsListjsList
if (jsList.value.length > 0) {
setInitialValue(newValue);
}
}
}, { immediate: true });
// defaultValue
watch(() => props.defaultValue, (newValue) => {
if (newValue && jsList.value.length > 0) {
setInitialValue(newValue);
}
}, { immediate: false });
// jsList jsList
watch(() => jsList.value, (newJsList) => {
if (newJsList.length > 0) {
const initialValue = internalValue.value || props.defaultValue;
if (initialValue) {
setInitialValue(initialValue);
}
}
}, { immediate: false });
</script>
<style lang="scss" scoped>
.basic-js-picker {
flex: 1;
.js-selected {
display: flex;
align-items: center;
justify-content: center;
.js-selected-name {
flex: 1;
}
}
.js-picker-popup {
min-height: 50vh;
max-height: 90vh;
padding: 10px;
display: flex;
flex-direction: column;
.js-picker-header {
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
.js-cancel-btn {
flex: 0 0 50;
padding: 5px 10px;
}
.js-ok-btn {
flex: 0 0 50;
padding: 5px 10px;
color: #3c9cff;
}
.js-picker-title {
flex: 1 0 1px;
text-align: center;
font-size: 20px;
}
}
.js-picker-search {
margin: 10px 0;
}
.js-list {
flex: 1 0 1px;
.js-item {
display: flex;
padding: 10px 15px;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
&.selected {
background-color: beige;
}
.js-name {
flex: 1;
}
}
}
}
}
</style>

View File

@ -35,7 +35,14 @@ function deletePic(event: any) {
} }
} }
const props = defineProps(['modelValue']) // beforeUpload prop
const props = defineProps({
modelValue: String,
beforeUpload: {
type: Function,
default: null
}
})
watchEffect(() => { watchEffect(() => {
if (props.modelValue) { if (props.modelValue) {
@ -67,6 +74,12 @@ async function afterRead(event: any) {
}) })
for (let i = 0; i < lists.length; i++) { for (let i = 0; i < lists.length; i++) {
if (props.beforeUpload && !props.beforeUpload(lists[i])) {
// beforeUpload false
data.fileList.splice(fileListLen + i, 1)
continue
}
showLoading({title: '上传中'}) showLoading({title: '上传中'})
const res:any = await attachmentUpload(lists[i].url) const res:any = await attachmentUpload(lists[i].url)
const result = res.result const result = res.result

View File

@ -179,14 +179,7 @@
} }
}, },
{ {
"path": "pages/view/routine/GongWenLiuZhuan/index", "path": "pages/view/routine/gwlz/gwDetail",
"style": {
"navigationBarTitleText": "公文流转",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/GongWenLiuZhuan/detail",
"style": { "style": {
"navigationBarTitleText": "公文详情", "navigationBarTitleText": "公文详情",
"enablePullDownRefresh": false "enablePullDownRefresh": false
@ -195,21 +188,21 @@
{ {
"path": "pages/view/routine/gwlz/gwAdd", "path": "pages/view/routine/gwlz/gwAdd",
"style": { "style": {
"navigationBarTitleText": "公文列表", "navigationBarTitleText": "公文调整",
"enablePullDownRefresh": false "enablePullDownRefresh": false
} }
}, },
{ {
"path": "pages/view/routine/gwlz/index", "path": "pages/view/routine/gwlz/index",
"style": { "style": {
"navigationBarTitleText": "公文接收", "navigationBarTitleText": "公文列表",
"enablePullDownRefresh": false "enablePullDownRefresh": false
} }
}, },
{ {
"path": "pages/view/routine/gwlz/gwFlow", "path": "pages/view/routine/gwlz/gwFlow",
"style": { "style": {
"navigationBarTitleText": "公文流转", "navigationBarTitleText": "公文审批",
"enablePullDownRefresh": false "enablePullDownRefresh": false
} }
}, },

View File

@ -344,6 +344,7 @@ const sections = reactive<Section[]>([
permissionKey: "routine-gwlz", // permissionKey: "routine-gwlz", //
path: "/pages/view/routine/gwlz/index", path: "/pages/view/routine/gwlz/index",
}, },
{ {
id: "hr3", id: "hr3",
icon: "gz", icon: "gz",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,210 @@
<template>
<BasicLayout>
<view class="px-15 pb-15">
<BasicForm @register="register" :readonly="true"> </BasicForm>
</view>
</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 { gwFindByIdApi } from "@/api/routine/gw";
import { useDicStore } from "@/store/modules/dic";
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
const { findByPid } = useDicStore();
// ID
const gwId = ref<string>('');
//
const [register, { setValue, setSchema }] = useForm({
schema: [
{
title: "公文信息",
},
{
field: "title",
label: "公文标题",
component: "BasicInput",
componentProps: {
placeholder: "公文标题",
readonly: true,
},
},
{
field: "docType",
label: "公文类型",
component: "BasicPicker",
componentProps: {
api: findByPid,
param: { pid: "2146632731" }, //
rangeKey: "dictionaryValue",
savaKey: "dictionaryCode",
placeholder: "公文类型",
readonly: true,
},
},
{
field: "urgencyLevel",
label: "紧急程度",
component: "BasicPicker",
componentProps: {
api: findByPid,
param: { pid: "2146632735" }, //
rangeKey: "dictionaryValue",
savaKey: "dictionaryCode",
placeholder: "紧急程度",
readonly: true,
},
},
{
field: "fileUrl",
label: "附件",
component: "BasicUpload",
componentProps: {
multiple: false,
accept: "*/*",
maxCount: 1,
disabled: true,
limit: 1,
readonly: true,
},
},
{
field: "remark",
label: "备注",
component: "BasicInput",
componentProps: {
placeholder: "备注信息",
type: "textarea",
rows: 3,
readonly: true,
},
},
{
title: "审批设置",
},
{
field: "spId",
label: "审批人",
component: "BasicJsPicker",
componentProps: {
multiple: true,
placeholder: "审批人",
title: "审批人",
searchPlaceholder: "输入教师姓名搜索",
readonly: true,
},
},
{
field: "ccId",
label: "抄送人",
component: "BasicJsPicker",
componentProps: {
multiple: true,
placeholder: "抄送人",
title: "抄送人",
searchPlaceholder: "输入教师姓名搜索",
readonly: true,
},
},
{
field: "tjrName",
label: "提交人",
component: "BasicInput",
componentProps: {
placeholder: "提交人",
readonly: true,
},
},
{
field: "tjrtime",
label: "提交时间",
component: "BasicInput",
componentProps: {
placeholder: "提交时间",
readonly: true,
},
},
{
field: "gwStatus",
label: "公文状态",
component: "BasicInput",
componentProps: {
placeholder: "公文状态",
readonly: true,
},
},
],
});
// - 使onLoad
onLoad((options: any) => {
console.log('页面加载,路由参数:', options);
if (options && options.id) {
gwId.value = options.id;
getGwDetail();
} else {
console.error('未获取到公文ID参数');
uni.showToast({
title: "参数错误",
icon: "error",
});
//
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
//
const getGwDetail = async () => {
try {
const response = await gwFindByIdApi({ id: gwId.value });
if (response && response.result) {
const gwData = response.result;
//
await setValue({
title: gwData.title || '',
docType: gwData.docType || '',
urgencyLevel: gwData.urgencyLevel || '',
fileUrl: gwData.fileUrl || '',
fileName: gwData.fileName || '',
fileFormat: gwData.fileFormat || '',
remark: gwData.remark || '',
spId: gwData.spId ? gwData.spId.split(',').filter((id: string) => id.trim()) : [],
ccId: gwData.ccId ? gwData.ccId.split(',').filter((id: string) => id.trim()) : [],
tjrName: gwData.tjrxm || gwData.tjrName || '',
tjrtime: gwData.tjrtime || '',
gwStatus: getStatusText(gwData.gwStatus || ''),
});
}
} catch (error) {
console.error("获取公文详情失败:", error);
uni.showToast({
title: "获取详情失败",
icon: "error",
});
}
};
//
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
'A': "暂存",
'B': "提交",
};
return statusMap[status] || "未知";
};
</script>
<style lang="scss" scoped>
//
</style>

View File

@ -1,16 +1,22 @@
<template> <template>
<BasicLayout> <view class="gw-list-page">
<view class="px-15 pb-15"> <!-- 列表组件 -->
<!-- 搜索和筛选 --> <view class="list-component">
<view class="search-section"> <BasicListLayout @register="register" v-model="dataList">
<!-- 搜索和筛选组件放在top插槽中 -->
<template #top>
<view class="query-component">
<view class="search-card">
<!-- 搜索框 -->
<view class="search-item">
<BasicSearch <BasicSearch
placeholder="搜索公文标题或编号" placeholder="搜索公文标题或编号"
@search="handleSearch" @search="handleSearch"
class="search-input"
/> />
</view> </view>
<!-- 筛选标签 --> <!-- 筛选标签 -->
<view class="filter-section">
<view class="filter-tabs"> <view class="filter-tabs">
<view <view
v-for="tab in filterTabs" v-for="tab in filterTabs"
@ -23,91 +29,72 @@
</view> </view>
</view> </view>
</view> </view>
</view>
</template>
<!-- 公文列表 --> <template v-slot="{ data }">
<view class="gw-list"> <view class="gw-card">
<view <view class="card-header">
v-for="item in filteredGwList" <text class="gw-title">{{ data.title }}</text>
:key="item.id" <text class="gw-status" :class="getStatusClass(data.gwStatus)">
class="gw-item" {{ getStatusText(data.gwStatus) }}
@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> </text>
</view> </view>
</view>
<view class="gw-footer"> <view class="card-body">
<view class="approver-info"> <view class="gw-info">
<text class="label">审批进度</text> <view class="info-item">
<text class="value">{{ getApproverProgress(item) }}</text> <text class="info-label">类型</text>
<text class="info-value">{{ data.docType }}</text>
</view>
<view class="info-item">
<text class="info-label">紧急程度</text>
<text class="info-value urgency-tag" :class="getUrgencyClass(data.urgencyLevel)">
{{ getUrgencyText(data.urgencyLevel) }}
</text>
</view>
<view class="info-item">
<text class="info-label">审批进度</text>
<text class="info-value">{{ getApproverProgress(data) }}</text>
</view>
<view class="info-item">
<text class="info-label">提交人</text>
<text class="info-value">{{ data.tjrxm || '未知' }}</text>
</view>
<view class="info-item">
<text class="info-label">提交时间</text>
<text class="info-value">{{ formatTime(data.tjrtime || data.createdTime) }}</text>
</view> </view>
<view class="time-info">
<text class="label">创建时间</text>
<text class="value">{{ formatTime(item.createdTime) }}</text>
</view> </view>
</view> </view>
<view class="gw-actions"> <view class="card-footer">
<view class="footer-actions">
<u-button <u-button
text="查看详情" text="详情"
size="mini" size="mini"
type="primary" type="primary"
@click.stop="goToDetail(item)" @click="goToDetail(data)"
/> />
<u-button <u-button
v-if="item.status === GwStatus.DRAFT" v-if="data.gwStatus === 'B'"
text="编辑" text="编辑"
size="mini" size="mini"
@click.stop="editGw(item)" @click="editGw(data)"
/> />
<u-button <u-button
v-if="item.status === GwStatus.DRAFT" v-if="data.gwStatus === 'B'"
text="删除" text="删除"
size="mini" size="mini"
type="error" type="error"
@click.stop="deleteGw(item)" @click="deleteGw(data)"
/> />
</view> </view>
</view> </view>
</view> </view>
</template>
<!-- 空状态 --> <!-- 新建公文按钮放在bottom插槽中 -->
<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> <template #bottom>
<view class="flex-row items-center pb-10 pt-5"> <view class="flex-row items-center pb-10 pt-5">
<u-button <u-button
@ -118,15 +105,20 @@
/> />
</view> </view>
</template> </template>
</BasicLayout> </BasicListLayout>
</view>
</view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from "vue"; import { ref, computed, watch, onMounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { navigateTo } from "@/utils/uniapp"; import { navigateTo } from "@/utils/uniapp";
import BasicSearch from "@/components/BasicSearch/Search.vue"; import BasicSearch from "@/components/BasicSearch/Search.vue";
import BasicLayout from "@/components/BasicLayout/Layout.vue"; import BasicLayout from "@/components/BasicLayout/Layout.vue";
import { getGwListApi, deleteGwApi } from "@/api/routine/gw"; import BasicListLayout from "@/components/BasicListLayout/ListLayout.vue";
import { useLayout } from "@/components/BasicListLayout/hooks/useLayout";
import { gwFindPageApi, gwLogicDeleteApi } from "@/api/routine/gw";
import { GwStatus, UrgencyLevel, ApproverStatus } from "@/types/gw"; import { GwStatus, UrgencyLevel, ApproverStatus } from "@/types/gw";
import type { GwInfo, GwListItem } from "@/types/gw"; import type { GwInfo, GwListItem } from "@/types/gw";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -134,36 +126,35 @@ import dayjs from "dayjs";
// //
const filterTabs = [ const filterTabs = [
{ key: "all", label: "全部" }, { key: "all", label: "全部" },
{ key: "draft", label: "草稿" }, { key: "A", label: "已提交" },
{ key: "pending", label: "待审批" }, { key: "B", label: "草稿" },
{ key: "approved", label: "已通过" },
{ key: "rejected", label: "已驳回" },
]; ];
const activeTab = ref("all"); const activeTab = ref("all");
const searchKeyword = ref(""); const searchKeyword = ref("");
const gwList = ref<GwListItem[]>([]);
const loading = ref(false); // 使 BasicListLayout
const hasMore = ref(true); const [register, { reload, setParam }] = useLayout({
const page = ref(1); api: gwFindPageApi,
const pageSize = 20; componentProps: {
defaultPageSize: 20,
},
param: {
title: "",
gwStatus: "",
},
});
//
const dataList = ref<GwListItem[]>([]);
// //
const filteredGwList = computed(() => { const filteredGwList = computed(() => {
let list = gwList.value; let list = dataList.value;
// //
if (activeTab.value !== "all") { if (activeTab.value !== "all") {
const statusMap: Record<string, GwStatus> = { list = list.filter(item => item.gwStatus === activeTab.value);
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);
}
} }
// //
@ -171,7 +162,7 @@ const filteredGwList = computed(() => {
const keyword = searchKeyword.value.toLowerCase(); const keyword = searchKeyword.value.toLowerCase();
list = list.filter(item => list = list.filter(item =>
item.title.toLowerCase().includes(keyword) || item.title.toLowerCase().includes(keyword) ||
item.gwNo.toLowerCase().includes(keyword) (item.gwNo && item.gwNo.toLowerCase().includes(keyword))
); );
} }
@ -181,88 +172,34 @@ const filteredGwList = computed(() => {
// //
const switchTab = (tabKey: string) => { const switchTab = (tabKey: string) => {
activeTab.value = tabKey; activeTab.value = tabKey;
//
const gwStatus = tabKey === "all" ? "" : tabKey;
setParam({ gwStatus });
reload();
}; };
// //
const handleSearch = (keyword: string) => { const handleSearch = (keyword: string) => {
searchKeyword.value = keyword; searchKeyword.value = keyword;
}; //
setParam({ title: keyword });
// reload();
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) => { const goToDetail = (item: GwListItem) => {
navigateTo(`/pages/view/routine/gwlz/gwFlow?id=${item.id}`); navigateTo(`/pages/view/routine/gwlz/gwDetail?id=${item.id}`);
}; };
// //
const editGw = (item: GwListItem) => { const editGw = (item: GwListItem) => {
navigateTo(`/pages/view/routine/gwlz?id=${item.id}&mode=edit`); const url = `/pages/view/routine/gwlz/gwAdd?id=${item.id}&mode=edit`;
//
uni.setStorageSync('gwEditMode', 'edit');
uni.setStorageSync('gwEditId', item.id);
navigateTo(url);
}; };
// //
@ -273,14 +210,11 @@ const deleteGw = (item: GwListItem) => {
success: async (res) => { success: async (res) => {
if (res.confirm) { if (res.confirm) {
try { try {
// API // API - {ids: item.id}
await deleteGwApi(item.id); await gwLogicDeleteApi({ ids: item.id });
// //
const index = gwList.value.findIndex(gw => gw.id === item.id); reload();
if (index > -1) {
gwList.value.splice(index, 1);
}
uni.showToast({ uni.showToast({
title: "删除成功", title: "删除成功",
@ -305,139 +239,161 @@ const createNewGw = () => {
}; };
// //
const getStatusClass = (status: GwStatus) => { const getStatusClass = (status: string) => {
const statusMap: Record<GwStatus, string> = { const statusMap: Record<string, string> = {
[GwStatus.DRAFT]: "status-draft", 'A': "status-submitted", //
[GwStatus.PENDING]: "status-pending", 'B': "status-draft", // 稿
[GwStatus.APPROVED]: "status-approved",
[GwStatus.REJECTED]: "status-rejected",
}; };
return statusMap[status] || "status-default"; return statusMap[status] || "status-default";
}; };
// //
const getStatusText = (status: GwStatus) => { const getStatusText = (status: string) => {
const statusMap: Record<GwStatus, string> = { const statusMap: Record<string, string> = {
[GwStatus.DRAFT]: "草稿", 'A': "已提交", //
[GwStatus.PENDING]: "待审批", 'B': "草稿", // 稿
[GwStatus.APPROVED]: "已通过",
[GwStatus.REJECTED]: "已驳回",
}; };
return statusMap[status] || "未知"; return statusMap[status] || "未知";
}; };
// //
const getUrgencyClass = (urgency: UrgencyLevel) => { const getUrgencyClass = (urgency: string) => {
const urgencyMap: Record<UrgencyLevel, string> = { const urgencyMap: Record<string, string> = {
[UrgencyLevel.LOW]: "urgency-low", 'low': "urgency-low",
[UrgencyLevel.NORMAL]: "urgency-normal", 'normal': "urgency-normal",
[UrgencyLevel.HIGH]: "urgency-high", 'high': "urgency-high",
[UrgencyLevel.URGENT]: "urgency-urgent", 'urgent': "urgency-urgent",
}; };
return urgencyMap[urgency] || "urgency-normal"; return urgencyMap[urgency] || "urgency-normal";
}; };
// //
const getUrgencyText = (urgency: UrgencyLevel) => { const getUrgencyText = (urgency: string) => {
const urgencyMap: Record<UrgencyLevel, string> = { const urgencyMap: Record<string, string> = {
[UrgencyLevel.LOW]: "普通", 'low': "普通",
[UrgencyLevel.NORMAL]: "一般", 'normal': "一般",
[UrgencyLevel.HIGH]: "紧急", 'high': "紧急",
[UrgencyLevel.URGENT]: "特急", 'urgent': "特急",
}; };
return urgencyMap[urgency] || "一般"; return urgencyMap[urgency] || "一般";
}; };
// //
const getApproverProgress = (item: GwListItem) => { const getApproverProgress = (item: GwListItem) => {
if (!item.approvers || item.approvers.length === 0) { let spCount = 0;
return "无审批人"; let ccCount = 0;
// -
if (item.spId) {
if (Array.isArray(item.spId)) {
spCount = item.spId.length;
} else {
//
const spIdStr = String(item.spId);
if (spIdStr.trim()) {
spCount = spIdStr.split(',').filter((id: string) => id.trim()).length;
}
}
} }
const total = item.approvers.length; // -
const approved = item.approvers.filter((a: any) => a.status === ApproverStatus.APPROVED).length; if (item.ccId) {
const rejected = item.approvers.filter((a: any) => a.status === ApproverStatus.REJECTED).length; if (Array.isArray(item.ccId)) {
ccCount = item.ccId.length;
if (rejected > 0) { } else {
return `已驳回 (${rejected}/${total})`; //
const ccIdStr = String(item.ccId);
if (ccIdStr.trim()) {
ccCount = ccIdStr.split(',').filter((id: string) => id.trim()).length;
}
}
} }
return `${approved}/${total} 已审批`; if (spCount === 0 && ccCount === 0) {
return "无审批人和抄送人";
}
let result = "";
if (spCount > 0) {
result += `${spCount}个审批人`;
}
if (ccCount > 0) {
if (result) result += "";
result += `${ccCount}个抄送人`;
}
return result;
}; };
// //
const formatTime = (time: string | Date) => { const formatTime = (time: string | Date | undefined) => {
if (!time) return '暂无';
return dayjs(time).format("MM-DD HH:mm"); return dayjs(time).format("MM-DD HH:mm");
}; };
// //
const mockData: GwListItem[] = [ watch(dataList, (val) => {
{ //
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: [],
},
];
//
onShow(() => {
reload();
});
//
onMounted(() => { onMounted(() => {
// 使 reload();
gwList.value = mockData;
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.search-section { .gw-list-page {
margin-bottom: 15px; display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f7fa;
} }
.filter-section { .list-component {
margin-bottom: 20px; flex: 1;
overflow: hidden;
}
.query-component {
padding: 15px;
background-color: white;
border-bottom: 1px solid #eee;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.search-card {
background-color: #ffffff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #f0f0f0;
}
.search-item {
margin-bottom: 12px;
}
.search-input {
width: 100%;
height: 40px;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 0 15px;
font-size: 14px;
color: #333;
}
.filter-tabs { .filter-tabs {
display: flex; display: flex;
background: #f5f5f5; gap: 8px;
border-radius: 8px; }
padding: 4px;
.filter-tab { .filter-tab {
flex: 1; flex: 1;
@ -447,129 +403,191 @@ onMounted(() => {
font-size: 14px; font-size: 14px;
color: #666; color: #666;
transition: all 0.3s; transition: all 0.3s;
background-color: #f0f0f0;
border: 1px solid #e9ecef;
&.active { &.active {
background: #007aff; background-color: #007aff;
color: white; color: white;
} border-color: #007aff;
}
} }
} }
.gw-list { .gw-card {
.gw-item { background-color: white;
background: white;
border-radius: 8px; border-radius: 8px;
padding: 15px; padding: 15px;
margin-bottom: 15px; margin-bottom: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
.gw-header { &:active {
transform: translateY(1px);
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
}
}
.card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
margin-bottom: 12px; margin-bottom: 12px;
gap: 12px;
}
.gw-title { .gw-title {
flex: 1;
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 600;
color: #333; color: #2c3e50;
flex: 1;
line-height: 1.4; line-height: 1.4;
margin-right: 10px; overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
} }
.gw-status { .gw-status {
padding: 4px 8px; padding: 4px 8px;
border-radius: 4px; border-radius: 12px;
font-size: 12px; font-size: 12px;
font-weight: 500;
white-space: nowrap; white-space: nowrap;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&.status-draft { background: #f0f0f0; color: #666; } &.status-draft {
&.status-pending { background: #fff7e6; color: #fa8c16; } background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
&.status-approved { background: #f6ffed; color: #52c41a; } color: white;
&.status-rejected { background: #fff2f0; color: #ff4d4f; }
} }
&.status-submitted {
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
color: white;
}
&.status-approved {
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
color: white;
}
&.status-rejected {
background: linear-gradient(135deg, #ef5350 0%, #e53935 100%);
color: white;
}
}
.card-body {
margin-bottom: 15px;
} }
.gw-info { .gw-info {
margin-bottom: 12px;
.info-row {
display: flex; display: flex;
margin-bottom: 6px; flex-direction: column;
gap: 8px;
.label {
width: 70px;
color: #666;
font-size: 14px;
} }
.value { .info-item {
flex: 1; display: flex;
align-items: center;
}
.info-label {
width: 80px;
color: #666;
font-size: 14px;
margin-right: 8px;
flex-shrink: 0;
}
.info-value {
color: #333; color: #333;
font-size: 14px; font-size: 14px;
flex: 1;
} }
.urgency-tag { .urgency-tag {
padding: 2px 6px; padding: 4px 8px;
border-radius: 4px; border-radius: 12px;
font-size: 12px; font-size: 12px;
font-weight: 500;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&.urgency-low { background: #f6ffed; color: #52c41a; } &.urgency-low {
&.urgency-normal { background: #f0f0f0; color: #666; } background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
&.urgency-high { background: #fff7e6; color: #fa8c16; } color: white;
&.urgency-urgent { background: #fff2f0; color: #ff4d4f; }
} }
&.urgency-normal {
background: linear-gradient(135deg, #90a4ae 0%, #78909c 100%);
color: white;
}
&.urgency-high {
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
color: white;
}
&.urgency-urgent {
background: linear-gradient(135deg, #ef5350 0%, #e53935 100%);
color: white;
} }
} }
.gw-footer { .card-footer {
display: flex; display: flex;
justify-content: space-between; justify-content: flex-end;
margin-bottom: 15px; align-items: center;
font-size: 12px; padding-top: 12px;
border-top: 1px solid #f0f0f0;
.approver-info,
.time-info {
.label {
color: #666;
margin-right: 5px;
} }
.value { .footer-actions {
color: #333;
}
}
}
.gw-actions {
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: flex-end;
} }
//
@media (max-width: 375px) {
.query-component {
padding: 12px;
}
.search-card {
padding: 12px;
}
.gw-card {
padding: 12px;
margin-bottom: 8px;
}
.card-header .gw-title {
font-size: 15px;
}
.filter-tabs {
gap: 6px;
}
.filter-tab {
padding: 6px 10px;
font-size: 13px;
} }
} }
.empty-state { //
text-align: center; .gw-card {
padding: 60px 20px; animation: fadeInUp 0.3s ease-out;
.empty-icon {
font-size: 48px;
margin-bottom: 15px;
opacity: 0.5;
} }
.empty-text { @keyframes fadeInUp {
color: #999; from {
font-size: 14px; opacity: 0;
transform: translateY(20px);
} }
to {
opacity: 1;
transform: translateY(0);
} }
.load-more {
text-align: center;
margin-top: 20px;
} }
</style> </style>