<!--
|
业务审批申请列表(转正/调岗/交接/请假/加班)
|
-->
|
<template>
|
<view class="oa-approval-page">
|
<PageHeader :title="pageTitle"
|
@back="goBack" />
|
|
<view class="oa-toolbar">
|
<view class="oa-filter-chip"
|
:class="{ active: hasActiveFilter }"
|
@click="showFilter = true">
|
<up-icon name="list"
|
size="18"
|
:color="hasActiveFilter ? '#2979ff' : '#666'" />
|
<text class="chip-label">筛选</text>
|
<text v-if="filterSummary"
|
class="chip-value">{{ filterSummary }}</text>
|
<text v-else
|
class="chip-placeholder">全部条件</text>
|
</view>
|
<view class="oa-icon-btn"
|
@click="handleSearch">
|
<up-icon name="search"
|
size="20"
|
color="#666" />
|
</view>
|
</view>
|
|
<ApprovalModuleSearchPopup v-model:show="showFilter"
|
:module-key="moduleKey"
|
v-model="searchForm"
|
@search="handleSearch"
|
@reset="handleReset" />
|
|
<scroll-view class="oa-list-scroll"
|
scroll-y
|
:show-scrollbar="false"
|
:style="{ height: listScrollHeight + 'px' }"
|
@scrolltolower="loadMore">
|
<view v-if="displayList.length"
|
class="oa-card-list">
|
<view v-for="item in displayList"
|
:key="item.id"
|
class="oa-card"
|
@click="openDetail(item)">
|
<view class="oa-card-head">
|
<view class="oa-card-title-wrap">
|
<text class="oa-card-title">{{ cardTitle(item) }}</text>
|
<text v-if="item.instanceNo"
|
class="oa-card-sub">{{ item.instanceNo }}</text>
|
</view>
|
<text :class="['oa-status', businessStatusClass(item.status)]">
|
{{ businessStatusText(item.status) }}
|
</text>
|
</view>
|
|
<view class="oa-card-body">
|
<view class="oa-info-grid">
|
<view v-for="(row, idx) in visibleDisplayRows(item)"
|
:key="'f-' + idx"
|
class="oa-info-row">
|
<text class="oa-info-label">{{ row.label }}</text>
|
<text class="oa-info-value">{{ row.value || "-" }}</text>
|
</view>
|
<view class="oa-info-row">
|
<text class="oa-info-label">申请人</text>
|
<text class="oa-info-value">{{ item.applicantName || "-" }}</text>
|
</view>
|
<view class="oa-info-row">
|
<text class="oa-info-label">申请时间</text>
|
<text class="oa-info-value">{{ item.createTime || "-" }}</text>
|
</view>
|
</view>
|
</view>
|
|
<view v-if="canEditBusinessInstanceRow(item)"
|
class="oa-card-foot"
|
@click.stop>
|
<text class="oa-foot-btn btn-edit"
|
@click="goEdit(item)">修改</text>
|
<text class="oa-foot-btn btn-delete"
|
@click="confirmDelete(item)">删除</text>
|
</view>
|
</view>
|
<up-loadmore :status="pageStatus" />
|
</view>
|
|
<view v-else-if="!tableLoading"
|
class="oa-empty">
|
<up-empty mode="list"
|
:text="`暂无${pageTitle}数据`" />
|
</view>
|
<view v-if="tableLoading && !list.length"
|
class="oa-loading">
|
<up-loading-icon mode="circle" />
|
</view>
|
</scroll-view>
|
|
<view class="fab-button"
|
@click="handleAdd">
|
<up-icon name="plus"
|
size="28"
|
color="#ffffff" />
|
</view>
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, onMounted, reactive, ref } from "vue";
|
import { onShow } from "@dcloudio/uni-app";
|
import PageHeader from "@/components/PageHeader.vue";
|
import ApprovalModuleSearchPopup from "./ApprovalModuleSearchPopup.vue";
|
import {
|
deleteApprovalInstance,
|
listApprovalInstancePage,
|
} from "@/api/oa/approvalInstance.js";
|
import { OA_NAV } from "@/config/oaPaths.js";
|
import { fetchApprovalTemplateTypes } from "../_utils/approvalTemplateType.js";
|
import {
|
getApprovalModuleConfig,
|
getModuleListBusinessType,
|
} from "../_utils/approvalModuleRegistry.js";
|
import {
|
buildModuleListDto,
|
createModuleSearchForm,
|
filterRowsByModuleSearch,
|
filterRowsByModuleBusinessType,
|
formatDateRangeLabel,
|
getModuleSearchMeta,
|
hasActiveModuleSearch,
|
} from "../_utils/approvalModuleListSearch.js";
|
import {
|
buildInstanceListParams,
|
businessStatusClass,
|
businessStatusText,
|
canEditBusinessInstanceRow,
|
EDIT_STORAGE_KEY,
|
mapInstanceListRow,
|
stashInstanceRow,
|
unwrapInstancePage,
|
} from "../_utils/approveListUtils.js";
|
|
const props = defineProps({
|
moduleKey: { type: String, required: true },
|
});
|
|
const moduleConfig = computed(() => getApprovalModuleConfig(props.moduleKey));
|
const pageTitle = computed(() => moduleConfig.value?.label || "申请");
|
|
const showFilter = ref(false);
|
const searchForm = reactive(createModuleSearchForm(props.moduleKey));
|
const list = ref([]);
|
const tableLoading = ref(false);
|
const pageStatus = ref("loadmore");
|
const businessType = ref("");
|
const typeOptions = ref([]);
|
|
const page = reactive({ current: 1, size: 10, total: 0 });
|
const listScrollHeight = ref(400);
|
|
function calcListScrollHeight() {
|
const sys = uni.getSystemInfoSync();
|
const statusBar = sys.statusBarHeight || 0;
|
const navBar = 44;
|
const toolbar = 56;
|
const fabGap = 16;
|
listScrollHeight.value = Math.max(
|
200,
|
sys.windowHeight - statusBar - navBar - toolbar - fabGap
|
);
|
}
|
|
const displayList = computed(() => {
|
const byType = filterRowsByModuleBusinessType(
|
props.moduleKey,
|
list.value,
|
typeOptions.value
|
);
|
return filterRowsByModuleSearch(props.moduleKey, byType, searchForm);
|
});
|
|
const hasActiveFilter = computed(() => Boolean(filterSummary.value));
|
|
const filterSummary = computed(() => {
|
const parts = [];
|
const meta = getModuleSearchMeta(props.moduleKey);
|
for (const field of meta.fields || []) {
|
const val = searchForm[field.key];
|
if (field.type === "input" && (val || "").trim()) {
|
parts.push(`${field.label}:${String(val).trim()}`);
|
} else if (field.type === "daterange" && Array.isArray(val) && val[0]) {
|
parts.push(`${field.label}:${formatDateRangeLabel(val)}`);
|
} else if (field.type === "select" && val) {
|
const opt = (field.options || []).find(o => o.value === val);
|
parts.push(`${field.label}:${opt?.label || val}`);
|
} else if (field.type === "user" && val) {
|
parts.push(`${field.label}:已选`);
|
}
|
}
|
return parts.join(";");
|
});
|
|
function cardTitle(item) {
|
return item.summary || item.title || pageTitle.value;
|
}
|
|
function visibleDisplayRows(item) {
|
const rows = item.displayRows || [];
|
return rows.slice(0, 2);
|
}
|
|
const buildListRequestParams = () => {
|
const extraDto = buildModuleListDto(props.moduleKey, searchForm);
|
return buildInstanceListParams({
|
page,
|
businessType: businessType.value,
|
extraDto,
|
searchForm,
|
});
|
};
|
|
const fetchList = async (reset = false) => {
|
if (reset) {
|
page.current = 1;
|
pageStatus.value = "loadmore";
|
list.value = [];
|
}
|
if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
|
if (!businessType.value && businessType.value !== 0) return;
|
|
pageStatus.value = "loading";
|
tableLoading.value = true;
|
|
try {
|
const res = await listApprovalInstancePage(buildListRequestParams());
|
const { records, total } = unwrapInstancePage(res);
|
const listFields = moduleConfig.value?.listFields || [];
|
let mapped = records.map(row => mapInstanceListRow(row, listFields));
|
if (hasActiveModuleSearch(props.moduleKey, searchForm)) {
|
mapped = filterRowsByModuleSearch(props.moduleKey, mapped, searchForm);
|
}
|
|
if (page.current === 1) {
|
list.value = mapped;
|
} else {
|
list.value = [...list.value, ...mapped];
|
}
|
const dropped = records.length - mapped.length;
|
page.total = hasActiveModuleSearch(props.moduleKey, searchForm)
|
? list.value.length
|
: dropped > 0
|
? Math.max(0, Number(total) - dropped)
|
: Number(total);
|
|
if (list.value.length >= total || records.length < page.size) {
|
pageStatus.value = "nomore";
|
} else {
|
pageStatus.value = "loadmore";
|
page.current += 1;
|
}
|
} catch {
|
if (page.current === 1) list.value = [];
|
pageStatus.value = "loadmore";
|
uni.showToast({ title: `${pageTitle.value}加载失败`, icon: "none" });
|
} finally {
|
tableLoading.value = false;
|
}
|
};
|
|
const initBusinessType = async () => {
|
const fixed = getModuleListBusinessType(props.moduleKey);
|
businessType.value = fixed != null && fixed !== "" ? fixed : "";
|
try {
|
typeOptions.value = await fetchApprovalTemplateTypes();
|
} catch {
|
typeOptions.value = [];
|
}
|
};
|
|
const handleSearch = () => fetchList(true);
|
const handleReset = () => {
|
Object.assign(searchForm, createModuleSearchForm(props.moduleKey));
|
fetchList(true);
|
};
|
const loadMore = () => {
|
if (pageStatus.value === "loadmore") fetchList(false);
|
};
|
const goBack = () => uni.navigateBack();
|
|
const openDetail = item => {
|
if (!item?.id) return;
|
stashInstanceRow(item);
|
uni.navigateTo({
|
url: `${OA_NAV.approveListDetail}?id=${item.id}&from=business`,
|
});
|
};
|
|
const goEdit = item => {
|
if (!canEditBusinessInstanceRow(item)) {
|
uni.showToast({ title: "进行中或已完成的审批不可修改", icon: "none" });
|
return;
|
}
|
if (!item?.id) return;
|
uni.setStorageSync(EDIT_STORAGE_KEY, item);
|
stashInstanceRow(item);
|
uni.navigateTo({
|
url: `${OA_NAV.approveListApply}?id=${item.id}&moduleKey=${props.moduleKey}`,
|
});
|
};
|
|
const confirmDelete = item => {
|
if (!item?.id) return;
|
const title = item.title || item.templateName || item.instanceNo || "该审批";
|
uni.showModal({
|
title: "删除确认",
|
content: `确定要删除「${title}」吗?删除后不可恢复。`,
|
confirmText: "确定删除",
|
confirmColor: "#f56c6c",
|
success: async res => {
|
if (!res.confirm) return;
|
try {
|
await deleteApprovalInstance([item.id]);
|
uni.showToast({ title: "删除成功", icon: "success" });
|
fetchList(true);
|
} catch {
|
uni.showToast({ title: "删除失败", icon: "none" });
|
}
|
},
|
});
|
};
|
|
const handleAdd = () => {
|
uni.navigateTo({
|
url: `${OA_NAV.approveListTemplateSelect}?moduleKey=${props.moduleKey}`,
|
});
|
};
|
|
onMounted(() => {
|
calcListScrollHeight();
|
});
|
|
onShow(async () => {
|
calcListScrollHeight();
|
await initBusinessType();
|
fetchList(true);
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "@/styles/sales-common.scss";
|
@import "../_styles/oa-approval-list.scss";
|
</style>
|