| | |
| | | {{ 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 || '审批'}`" |
| | | :title="submitDialogTitle" |
| | | width="720px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="approve-submit-dialog" |
| | | @closed="submitDialog.step = 1" |
| | | @closed="resetSubmitDialogState" |
| | | > |
| | | <template v-if="submitDialog.step === 1"> |
| | | <p class="template-hint">请选择要提交的审批类型,系统将按对应模板引导填报(字段后期与后端同步)。</p> |
| | | <div class="template-grid"> |
| | | <template v-if="submitDialog.step === 1 && !isSubmitEdit"> |
| | | <p class="template-hint">请选择已启用的审批模板,系统将按模板配置引导填报。</p> |
| | | <div v-loading="submitTemplatesLoading" class="template-grid"> |
| | | <div |
| | | v-for="(tpl, key) in SUBMIT_TEMPLATES" |
| | | :key="key" |
| | | v-for="card in submitTemplateCards" |
| | | :key="card.key" |
| | | class="template-card" |
| | | @click="onTemplatePick(key)" |
| | | @click="onTemplatePick(card)" |
| | | > |
| | | <span class="template-card-type" :style="approvalTypeStyle(tpl.approvalType)"> |
| | | {{ tpl.label }} |
| | | <span class="template-card-type" :style="approvalTypeStyle(card.approvalType)"> |
| | | {{ card.label }} |
| | | </span> |
| | | <span class="template-card-desc">{{ tpl.summaryPlaceholder || "点击填写并提交" }}</span> |
| | | <span class="template-card-desc">{{ card.summaryPlaceholder }}</span> |
| | | </div> |
| | | <el-empty |
| | | v-if="!submitTemplatesLoading && !submitTemplateCards.length" |
| | | description="暂无可用审批模板" |
| | | :image-size="80" |
| | | class="template-empty" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <template v-else> |
| | | <div v-loading="submitTemplatesLoading && !isSubmitEdit"> |
| | | <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-button |
| | | v-if="!isSubmitEdit" |
| | | 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> |
| | | <FormPayloadFields |
| | | :fields="submitFormFields" |
| | | :form-payload="submitForm.formPayload" |
| | | /> |
| | | <el-form-item label="审批流程" required> |
| | | <TemplateFlowEditor v-model="submitForm.flowNodes" :user-options="flowUserOptions" /> |
| | | <p class="flow-tip"> |
| | | 按顺序流转:可为每个节点添加多名审批人;会签需全部通过,或签任一人通过即可进入下一节点。 |
| | | </p> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </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> |
| | | <el-button |
| | | v-if="submitDialog.step === 2 || isSubmitEdit" |
| | | type="primary" |
| | | :loading="submitSaving" |
| | | @click="onSubmitInstance" |
| | | > |
| | | {{ isSubmitEdit ? "保 存" : "提 交" }} |
| | | </el-button> |
| | | <el-button @click="submitDialog.visible = false"> |
| | | {{ submitDialog.step === 1 && !isSubmitEdit ? "取 消" : "关 闭" }} |
| | | </el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | |
| | | width="920px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="approve-detail-dialog" |
| | | > |
| | | <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" /> |
| | | <div class="approve-detail-body"> |
| | | <ApproveDetailPanel :row="detailRow" /> |
| | | <div class="detail-block"> |
| | | <div class="detail-block-title"> |
| | | 审批流程({{ detailRow.tasks?.length || detailRow.flowNodes?.length || 0 }} 项) |
| | | </div> |
| | | <InstanceFlowDisplay :tasks="detailRow.tasks" :nodes="detailRow.flowNodes" /> |
| | | </div> |
| | | <div class="detail-block"> |
| | | <div class="detail-block-title">审批记录</div> |
| | | <el-timeline v-if="detailRow.approvalRecords?.length" class="approve-record-timeline"> |
| | | <el-timeline-item |
| | | v-for="(rec, i) in detailRow.approvalRecords" |
| | | :key="rec.id ?? i" |
| | | :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'" |
| | | :timestamp="formatRecordTime(rec.time)" |
| | | placement="top" |
| | | > |
| | | <div class="record-item"> |
| | | <span class="record-operator">{{ rec.operatorName || "—" }}</span> |
| | | <el-tag |
| | | size="small" |
| | | :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'info'" |
| | | effect="plain" |
| | | > |
| | | {{ approvalActionLabel(rec.result) }} |
| | | </el-tag> |
| | | <p class="record-opinion">{{ rec.opinion || "无意见" }}</p> |
| | | </div> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | <el-empty v-else description="暂无审批记录" :image-size="48" /> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <el-button |
| | | v-if="detailRow.approvalStatus === 'pending'" |
| | | @click="openEditFromDetail" |
| | | > |
| | | 修 改 |
| | | </el-button> |
| | | <el-button |
| | | v-if="detailRow.approvalStatus === 'pending' && detailRow.isApprove" |
| | | type="primary" |
| | | @click="openApproveFromDetail" |
| | | > |
| | |
| | | @closed="approveOpinion = ''" |
| | | > |
| | | <ApproveDetailPanel :row="approveDialog.row" /> |
| | | <el-divider content-position="left">流程进度</el-divider> |
| | | <ApprovalFlowProgress |
| | | :nodes="approveDialog.row?.approvalFlowNodes" |
| | | :current-index="approveDialog.row?.currentNodeIndex ?? 0" |
| | | /> |
| | | <div class="detail-block mt16"> |
| | | <div class="detail-block-title"> |
| | | 审批流程({{ approveDialog.row?.tasks?.length || approveDialog.row?.flowNodes?.length || 0 }} 项) |
| | | </div> |
| | | <InstanceFlowDisplay :tasks="approveDialog.row?.tasks" :nodes="approveDialog.row?.flowNodes" /> |
| | | </div> |
| | | <el-form label-width="100px" class="mt16"> |
| | | <el-form-item label="审批意见" required> |
| | | <el-input |
| | |
| | | </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> |
| | | <el-button |
| | | type="success" |
| | | :loading="approveSubmitting" |
| | | @click="onApprove('approved')" |
| | | > |
| | | 通 过 |
| | | </el-button> |
| | | <el-button |
| | | type="danger" |
| | | :loading="approveSubmitting" |
| | | @click="onApprove('rejected')" |
| | | > |
| | | 驳 回 |
| | | </el-button> |
| | | <el-button :disabled="approveSubmitting" @click="approveDialog.visible = false"> |
| | | 取 消 |
| | | </el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | |
| | | 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 TemplateFlowEditor from "../approve-template/components/TemplateFlowEditor.vue"; |
| | | import FormPayloadFields from "./components/FormPayloadFields.vue"; |
| | | import { formatDisplayTime } from "../approve-template/approveTemplateConstants.js"; |
| | | import { approvalTypeStyle } from "./approveListConstants.js"; |
| | | import ApproveDetailPanel from "./components/ApproveDetailPanel.vue"; |
| | | import InstanceFlowDisplay from "./components/InstanceFlowDisplay.vue"; |
| | | import { useApproveList } from "./useApproveList.js"; |
| | | |
| | | const al = useApproveList(); |
| | | const { |
| | | Search, |
| | | APPROVAL_TYPE_OPTIONS, |
| | | SUBMIT_TEMPLATES, |
| | | submitTemplateCards, |
| | | submitTemplatesLoading, |
| | | approvalTypeLabel, |
| | | approvalModeLabel, |
| | | approvalActionLabel, |
| | | searchForm, |
| | | tableLoading, |
| | |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | | approveSubmitting, |
| | | submitDialog, |
| | | isSubmitEdit, |
| | | submitDialogTitle, |
| | | submitForm, |
| | | submitFormRef, |
| | | submitSaving, |
| | | activeTemplate, |
| | | submitFormFields, |
| | | submitFormRules, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | resetSubmitDialogState, |
| | | openSubmitDialog, |
| | | openEditDialog, |
| | | onTemplatePick, |
| | | backToTemplatePick, |
| | | submitNewApproval, |
| | | submitInstanceForm, |
| | | submitApprove, |
| | | openDetail, |
| | | openApprove, |
| | |
| | | } |
| | | } |
| | | |
| | | async function onSubmitNew() { |
| | | const ok = await submitNewApproval(); |
| | | if (ok) ElMessage.success("审批已提交"); |
| | | async function onSubmitInstance() { |
| | | const ok = await submitInstanceForm(); |
| | | if (ok) ElMessage.success(isSubmitEdit.value ? "修改成功" : "审批已提交"); |
| | | } |
| | | |
| | | function onApprove(result) { |
| | | const ret = submitApprove(result); |
| | | async function onApprove(result) { |
| | | const ret = await submitApprove(result); |
| | | if (ret?.needOpinion) { |
| | | ElMessage.warning("驳回时请填写审批意见"); |
| | | return; |
| | |
| | | } |
| | | } |
| | | |
| | | function formatRecordTime(time) { |
| | | return formatDisplayTime(time) || "—"; |
| | | } |
| | | |
| | | function openApproveFromDetail() { |
| | | const row = detailRow.value; |
| | | detailDialog.visible = false; |
| | | openApprove(row); |
| | | } |
| | | |
| | | function openEditFromDetail() { |
| | | const row = detailRow.value; |
| | | detailDialog.visible = false; |
| | | openEditDialog(row); |
| | | } |
| | | |
| | | onMounted(() => { |
| | |
| | | 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); |
| | |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
| | | gap: 12px; |
| | | min-height: 120px; |
| | | } |
| | | .template-empty { |
| | | grid-column: 1 / -1; |
| | | } |
| | | .template-card { |
| | | padding: 14px 16px; |
| | |
| | | .approve-submit-dialog :deep(.el-dialog__body) { |
| | | padding-top: 12px; |
| | | } |
| | | .approve-detail-dialog :deep(.el-dialog__body) { |
| | | padding-top: 16px; |
| | | max-height: 70vh; |
| | | overflow-y: auto; |
| | | } |
| | | .approve-detail-body .detail-block { |
| | | margin-top: 20px; |
| | | } |
| | | .approve-detail-body .detail-block-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary); |
| | | margin: 0 0 12px; |
| | | padding-left: 10px; |
| | | border-left: 3px solid var(--el-color-primary); |
| | | line-height: 1.4; |
| | | } |
| | | .approve-record-timeline { |
| | | padding-left: 4px; |
| | | } |
| | | .record-item { |
| | | padding: 4px 0 2px; |
| | | } |
| | | .record-operator { |
| | | font-weight: 600; |
| | | margin-right: 8px; |
| | | color: var(--el-text-color-primary); |
| | | } |
| | | .record-opinion { |
| | | margin: 8px 0 0; |
| | | font-size: 13px; |
| | | color: var(--el-text-color-regular); |
| | | line-height: 1.5; |
| | | } |
| | | .detail-block-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary); |
| | | margin: 0 0 12px; |
| | | padding-left: 10px; |
| | | border-left: 3px solid var(--el-color-primary); |
| | | line-height: 1.4; |
| | | } |
| | | </style> |