| | |
| | | <!-- |
| | | 模块中文名:审批列表 |
| | | 目录标识:ApproveManage/approve-list(approve-list → 中文:审批列表) |
| | | 复用页面:@/views/procurementManagement/procurementLedger/index.vue(采购台账;文件名 index.vue → 入口页) |
| | | --> |
| | | <!--OA模块:审批列表--> |
| | | <template> |
| | | <ProcurementLedger /> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div class="search_fields"> |
| | | <span class="search_title">审批类型:</span> |
| | | <el-select |
| | | v-model="searchForm.approvalType" |
| | | placeholder="请选择审批类型" |
| | | clearable |
| | | filterable |
| | | style="width: 200px" |
| | | > |
| | | <el-option |
| | | v-for="opt in APPROVAL_TYPE_OPTIONS" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | </el-select> |
| | | <span class="search_title" style="margin-left: 12px">申请人名称:</span> |
| | | <el-input |
| | | v-model="searchForm.applicantKeyword" |
| | | style="width: 200px" |
| | | placeholder="请输入申请人名称" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | <span class="search_title" style="margin-left: 12px">创建时间:</span> |
| | | <el-date-picker |
| | | v-model="searchForm.createTimeRange" |
| | | type="daterange" |
| | | range-separator="-" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 260px" |
| | | clearable |
| | | /> |
| | | <el-button type="primary" :icon="Search" style="margin-left: 10px" @click="handleQuery">搜索</el-button> |
| | | <el-button :icon="RefreshRight" @click="resetSearch">重置</el-button> |
| | | </div> |
| | | <div class="search_actions"> |
| | | <el-button type="primary" :icon="Plus" @click="openSubmitDialog">提交审批</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="false" |
| | | :tableLoading="tableLoading" |
| | | :total="page.total" |
| | | @pagination="pagination" |
| | | > |
| | | <template #approveType="{ row }"> |
| | | <span class="approve-type-cell" :style="approvalTypeStyle(row.approvalType)"> |
| | | {{ approvalTypeLabel(row.approvalType) }} |
| | | </span> |
| | | </template> |
| | | <template #approvalMethod="{ row }"> |
| | | <span class="approval-method-text">{{ approvalModeLabel(row.approvalMode) }}</span> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | |
| | | <!-- 提交审批(按模板) --> |
| | | <el-dialog |
| | | v-model="submitDialog.visible" |
| | | :title="submitDialog.step === 1 ? '选择审批模板' : `提交${activeTemplate?.label || '审批'}`" |
| | | width="720px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="approve-submit-dialog" |
| | | @closed="submitDialog.step = 1" |
| | | > |
| | | <template v-if="submitDialog.step === 1"> |
| | | <p class="template-hint">请选择要提交的审批类型,系统将按对应模板引导填报(字段后期与后端同步)。</p> |
| | | <div class="template-grid"> |
| | | <div |
| | | v-for="(tpl, key) in SUBMIT_TEMPLATES" |
| | | :key="key" |
| | | class="template-card" |
| | | @click="onTemplatePick(key)" |
| | | > |
| | | <span class="template-card-type" :style="approvalTypeStyle(tpl.approvalType)"> |
| | | {{ tpl.label }} |
| | | </span> |
| | | <span class="template-card-desc">{{ tpl.summaryPlaceholder || "点击填写并提交" }}</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <template v-else> |
| | | <el-form ref="submitFormRef" :model="submitForm" :rules="submitFormRules" label-width="120px"> |
| | | <el-form-item label="审批类型"> |
| | | <span class="approve-type-cell" :style="approvalTypeStyle(activeTemplate.approvalType)"> |
| | | {{ activeTemplate.label }} |
| | | </span> |
| | | <el-button type="primary" link class="ml12" @click="backToTemplatePick">更换模板</el-button> |
| | | </el-form-item> |
| | | <el-form-item label="审批方式" prop="approvalMode"> |
| | | <el-radio-group v-model="submitForm.approvalMode"> |
| | | <el-radio value="parallel">与签</el-radio> |
| | | <el-radio value="or_sign">或签</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <template v-for="field in activeTemplate.fields" :key="field.key"> |
| | | <el-form-item :label="field.label" :prop="`formPayload.${field.key}`"> |
| | | <el-input |
| | | v-if="field.type === 'text'" |
| | | v-model="submitForm.formPayload[field.key]" |
| | | :placeholder="`请输入${field.label}`" |
| | | maxlength="200" |
| | | /> |
| | | <el-input |
| | | v-else-if="field.type === 'textarea'" |
| | | v-model="submitForm.formPayload[field.key]" |
| | | type="textarea" |
| | | :rows="field.rows || 3" |
| | | :placeholder="`请填写${field.label}`" |
| | | maxlength="2000" |
| | | show-word-limit |
| | | /> |
| | | <el-input-number |
| | | v-else-if="field.type === 'number'" |
| | | v-model="submitForm.formPayload[field.key]" |
| | | :min="field.min ?? 0" |
| | | :precision="field.precision ?? 0" |
| | | controls-position="right" |
| | | style="width: 100%" |
| | | /> |
| | | <el-date-picker |
| | | v-else-if="field.type === 'date'" |
| | | v-model="submitForm.formPayload[field.key]" |
| | | type="date" |
| | | :placeholder="`请选择${field.label}`" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | /> |
| | | <el-date-picker |
| | | v-else-if="field.type === 'datetimerange'" |
| | | v-model="submitForm.formPayload[field.key]" |
| | | type="datetimerange" |
| | | range-separator="至" |
| | | start-placeholder="开始时间" |
| | | end-placeholder="结束时间" |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | style="width: 100%" |
| | | /> |
| | | <el-select |
| | | v-else-if="field.type === 'select'" |
| | | v-model="submitForm.formPayload[field.key]" |
| | | :placeholder="`请选择${field.label}`" |
| | | style="width: 100%" |
| | | clearable |
| | | > |
| | | <el-option v-for="o in field.options" :key="o.value" :label="o.label" :value="o.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </template> |
| | | <el-form-item label="审批流程"> |
| | | <ApprovalFlowEditor v-model="submitForm.approvalFlowNodes" :user-options="flowUserOptions" /> |
| | | <p class="flow-tip">至少保留一个审批节点;提交后进入「审核中」状态。</p> |
| | | </el-form-item> |
| | | </el-form> |
| | | </template> |
| | | |
| | | <template #footer> |
| | | <el-button v-if="submitDialog.step === 2" type="primary" @click="onSubmitNew">提 交</el-button> |
| | | <el-button @click="submitDialog.visible = false">{{ submitDialog.step === 1 ? "取 消" : "关 闭" }}</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 详情 --> |
| | | <el-dialog |
| | | v-model="detailDialog.visible" |
| | | title="审批详情" |
| | | width="920px" |
| | | append-to-body |
| | | destroy-on-close |
| | | > |
| | | <ApproveDetailPanel :row="detailRow" /> |
| | | <el-divider content-position="left">审批流程</el-divider> |
| | | <ApprovalFlowProgress |
| | | :nodes="detailRow.approvalFlowNodes" |
| | | :current-index="detailRow.currentNodeIndex ?? 0" |
| | | /> |
| | | <el-divider content-position="left">审批记录</el-divider> |
| | | <el-timeline v-if="detailRow.approvalRecords?.length"> |
| | | <el-timeline-item |
| | | v-for="(rec, i) in detailRow.approvalRecords" |
| | | :key="i" |
| | | :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'" |
| | | :timestamp="rec.time" |
| | | > |
| | | {{ rec.operatorName }} — {{ approvalActionLabel(rec.result) }}:{{ rec.opinion || "无意见" }} |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | <el-empty v-else description="暂无审批记录" :image-size="60" /> |
| | | <template #footer> |
| | | <el-button |
| | | v-if="detailRow.approvalStatus === 'pending'" |
| | | type="primary" |
| | | @click="openApproveFromDetail" |
| | | > |
| | | 去审批 |
| | | </el-button> |
| | | <el-button @click="detailDialog.visible = false">关 闭</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 审批操作 --> |
| | | <el-dialog |
| | | v-model="approveDialog.visible" |
| | | title="审批处理" |
| | | width="960px" |
| | | append-to-body |
| | | destroy-on-close |
| | | @closed="approveOpinion = ''" |
| | | > |
| | | <ApproveDetailPanel :row="approveDialog.row" /> |
| | | <el-divider content-position="left">流程进度</el-divider> |
| | | <ApprovalFlowProgress |
| | | :nodes="approveDialog.row?.approvalFlowNodes" |
| | | :current-index="approveDialog.row?.currentNodeIndex ?? 0" |
| | | /> |
| | | <el-form label-width="100px" class="mt16"> |
| | | <el-form-item label="审批意见" required> |
| | | <el-input |
| | | v-model="approveOpinion" |
| | | type="textarea" |
| | | :rows="3" |
| | | maxlength="500" |
| | | show-word-limit |
| | | placeholder="通过可留空;驳回请填写具体原因" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button type="success" @click="onApprove('approved')">通 过</el-button> |
| | | <el-button type="danger" @click="onApprove('rejected')">驳 回</el-button> |
| | | <el-button @click="approveDialog.visible = false">取 消</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue' |
| | | import { Plus, RefreshRight } from "@element-plus/icons-vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { onMounted, ref } from "vue"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import ApprovalFlowEditor from "@/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue"; |
| | | import ApprovalFlowProgress from "@/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/ApprovalFlowProgress.vue"; |
| | | import { approvalTypeStyle } from "./approveListConstants.js"; |
| | | import ApproveDetailPanel from "./components/ApproveDetailPanel.vue"; |
| | | import { useApproveList } from "./useApproveList.js"; |
| | | |
| | | const al = useApproveList(); |
| | | const { |
| | | Search, |
| | | APPROVAL_TYPE_OPTIONS, |
| | | SUBMIT_TEMPLATES, |
| | | approvalTypeLabel, |
| | | approvalModeLabel, |
| | | approvalActionLabel, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | tableColumn, |
| | | detailDialog, |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | | submitDialog, |
| | | submitForm, |
| | | submitFormRef, |
| | | activeTemplate, |
| | | submitFormRules, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | openSubmitDialog, |
| | | onTemplatePick, |
| | | backToTemplatePick, |
| | | submitNewApproval, |
| | | submitApprove, |
| | | openDetail, |
| | | openApprove, |
| | | } = al; |
| | | |
| | | const flowUserOptions = ref([]); |
| | | |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | | if (payload?.data && Array.isArray(payload.data)) return payload.data; |
| | | if (payload?.rows && Array.isArray(payload.rows)) return payload.rows; |
| | | return []; |
| | | } |
| | | |
| | | function isActiveUser(u) { |
| | | if (u.delFlag === "2" || u.delFlag === 2) return false; |
| | | if (u.status == null) return true; |
| | | return String(u.status) === "0"; |
| | | } |
| | | |
| | | async function loadUsers() { |
| | | try { |
| | | const res = await userListNoPageByTenantId(); |
| | | flowUserOptions.value = unwrapArray(res).filter(isActiveUser); |
| | | } catch { |
| | | flowUserOptions.value = []; |
| | | } |
| | | } |
| | | |
| | | async function onSubmitNew() { |
| | | const ok = await submitNewApproval(); |
| | | if (ok) ElMessage.success("审批已提交"); |
| | | } |
| | | |
| | | function onApprove(result) { |
| | | const ret = submitApprove(result); |
| | | if (ret?.needOpinion) { |
| | | ElMessage.warning("驳回时请填写审批意见"); |
| | | return; |
| | | } |
| | | if (ret?.ok) { |
| | | ElMessage.success(result === "approved" ? "已通过" : "已驳回"); |
| | | } |
| | | } |
| | | |
| | | function openApproveFromDetail() { |
| | | const row = detailRow.value; |
| | | detailDialog.visible = false; |
| | | openApprove(row); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadUsers(); |
| | | handleQuery(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .mb20 { |
| | | margin-bottom: 20px; |
| | | } |
| | | .ml12 { |
| | | margin-left: 12px; |
| | | } |
| | | .mt16 { |
| | | margin-top: 16px; |
| | | } |
| | | .search_form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | .search_fields { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | .search_actions { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | .approve-type-cell { |
| | | display: inline-block; |
| | | padding: 2px 10px; |
| | | border-radius: 4px; |
| | | font-size: 13px; |
| | | line-height: 1.5; |
| | | } |
| | | .approval-method-text { |
| | | color: var(--el-color-danger); |
| | | font-weight: 500; |
| | | } |
| | | .template-hint { |
| | | font-size: 13px; |
| | | color: var(--el-text-color-secondary); |
| | | margin: 0 0 16px; |
| | | } |
| | | .template-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
| | | gap: 12px; |
| | | } |
| | | .template-card { |
| | | padding: 14px 16px; |
| | | border: 1px solid var(--el-border-color-lighter); |
| | | border-radius: var(--radius-md, 8px); |
| | | cursor: pointer; |
| | | transition: border-color 0.2s, box-shadow 0.2s; |
| | | background: var(--el-fill-color-blank); |
| | | } |
| | | .template-card:hover { |
| | | border-color: var(--el-color-primary); |
| | | box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.06)); |
| | | } |
| | | .template-card-type { |
| | | display: inline-block; |
| | | padding: 2px 8px; |
| | | border-radius: 4px; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | margin-bottom: 8px; |
| | | } |
| | | .template-card-desc { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | line-height: 1.5; |
| | | } |
| | | .flow-tip { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | margin-top: 8px; |
| | | } |
| | | .approve-submit-dialog :deep(.el-dialog__body) { |
| | | padding-top: 12px; |
| | | } |
| | | </style> |