Compare commits
2
Commits
4306cbd74b
...
699a117678
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
699a117678 | ||
|
|
a758966fae |
No files matched your search
@@ -22,6 +22,25 @@ const staticMainChildren = [
|
|||||||
component: () => import("@/views/system/approval/index.vue"),
|
component: () => import("@/views/system/approval/index.vue"),
|
||||||
meta: { requiresAuth: true, title: "审批流", modulePath: "/systemManager" }
|
meta: { requiresAuth: true, title: "审批流", modulePath: "/systemManager" }
|
||||||
},
|
},
|
||||||
|
// OA 审批中心:待我审批 / 审批列表 / 审批设置(静态注册保证 URL 直达)
|
||||||
|
{
|
||||||
|
path: "/apps/oa/approvalcenter/mine",
|
||||||
|
name: "OaApprovalCenterMine",
|
||||||
|
component: () => import("@/views/apps/oa/approvalcenter/mine.vue"),
|
||||||
|
meta: { requiresAuth: true, title: "待我审批", modulePath: "/apps/oa" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/apps/oa/approvalcenter/list",
|
||||||
|
name: "OaApprovalCenterList",
|
||||||
|
component: () => import("@/views/apps/oa/approvalcenter/list.vue"),
|
||||||
|
meta: { requiresAuth: true, title: "审批列表", modulePath: "/apps/oa" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/apps/oa/approvalcenter/settings",
|
||||||
|
name: "OaApprovalCenterSettings",
|
||||||
|
component: () => import("@/views/apps/oa/approvalcenter/settings.vue"),
|
||||||
|
meta: { requiresAuth: true, title: "审批设置", modulePath: "/apps/oa" }
|
||||||
|
},
|
||||||
// CMS 文章/产品/解决方案是系统内置功能,不依赖数据库菜单配置。
|
// CMS 文章/产品/解决方案是系统内置功能,不依赖数据库菜单配置。
|
||||||
// 统一结构:index 为列表页,type 为分类页。
|
// 统一结构:index 为列表页,type 为分类页。
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* 环境变量(见 .env.development / .env.production):
|
* 环境变量(见 .env.development / .env.production):
|
||||||
* VITE_AUTH_BASE=https://api.yunzer.cn/auth 认证中心地址,缺省回退到 ${VITE_API_BASE_URL}/auth
|
* VITE_AUTH_BASE=https://api.yunzer.cn/auth 认证中心地址,缺省回退到 ${VITE_API_BASE_URL}/auth
|
||||||
* VITE_AUTH_CLIENT_ID=yz-backend
|
* VITE_AUTH_CLIENT_ID=yz-backend
|
||||||
* VITE_AUTH_REDIRECT_URI=https://back.yunzer.cn/#/auth/callback
|
* VITE_AUTH_REDIRECT_URI=https://back.yunzer.cn/auth-callback.html
|
||||||
* VITE_AUTH_POST_LOGOUT_URI=https://back.yunzer.cn/#/
|
* VITE_AUTH_POST_LOGOUT_URI=https://back.yunzer.cn/#/
|
||||||
*
|
*
|
||||||
* 说明:token 仍存在 localStorage(沿用现有 utils/request.js 与 stores/auth.js),
|
* 说明:token 仍存在 localStorage(沿用现有 utils/request.js 与 stores/auth.js),
|
||||||
@@ -21,8 +21,9 @@ const AUTH_BASE = (import.meta.env.VITE_AUTH_BASE || (API_BASE ? `${API_BASE}/au
|
|||||||
""
|
""
|
||||||
);
|
);
|
||||||
const CLIENT_ID = import.meta.env.VITE_AUTH_CLIENT_ID || "yz-backend";
|
const CLIENT_ID = import.meta.env.VITE_AUTH_CLIENT_ID || "yz-backend";
|
||||||
|
// 缺省回落到静态中转页(不带 #);该地址必须已在认证中心白名单登记
|
||||||
const REDIRECT_URI =
|
const REDIRECT_URI =
|
||||||
import.meta.env.VITE_AUTH_REDIRECT_URI || `${window.location.origin}/#/auth/callback`;
|
import.meta.env.VITE_AUTH_REDIRECT_URI || `${window.location.origin}/auth-callback.html`;
|
||||||
const POST_LOGOUT_URI =
|
const POST_LOGOUT_URI =
|
||||||
import.meta.env.VITE_AUTH_POST_LOGOUT_URI || `${window.location.origin}/#/`;
|
import.meta.env.VITE_AUTH_POST_LOGOUT_URI || `${window.location.origin}/#/`;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
title="审批进度"
|
||||||
|
width="560px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<div v-loading="loading" class="progress-wrap">
|
||||||
|
<template v-if="detail">
|
||||||
|
<div class="progress-head">
|
||||||
|
<div class="progress-title">
|
||||||
|
<span class="title-text">{{ detail.biz_title }}</span>
|
||||||
|
<el-tag :type="instanceStatusType(detail.status)" size="small">
|
||||||
|
{{ instanceStatusLabel(detail.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="progress-meta">
|
||||||
|
<span v-if="detail.flow_name">流程:{{ detail.flow_name }}</span>
|
||||||
|
<span>发起人:{{ detail.submit_user_name || "-" }}</span>
|
||||||
|
<span>发起时间:{{ detail.submit_time || "-" }}</span>
|
||||||
|
<span v-if="detail.finish_time">完成时间:{{ detail.finish_time }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-timeline class="progress-timeline">
|
||||||
|
<el-timeline-item
|
||||||
|
v-for="node in detail.timeline"
|
||||||
|
:key="node.task_id"
|
||||||
|
:type="timelineDotType(node.status)"
|
||||||
|
:timestamp="node.finish_time || ''"
|
||||||
|
>
|
||||||
|
<div class="timeline-node">
|
||||||
|
<div class="node-head">
|
||||||
|
<span class="node-name">{{ node.node_name }}</span>
|
||||||
|
<el-tag :type="taskStatusType(node.status)" size="small" effect="plain">
|
||||||
|
{{ taskStatusLabel(node.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="node-approver">处理人:{{ node.approver || "-" }}</div>
|
||||||
|
<div v-if="node.comment" class="node-comment">意见:{{ node.comment }}</div>
|
||||||
|
</div>
|
||||||
|
</el-timeline-item>
|
||||||
|
</el-timeline>
|
||||||
|
<el-empty v-if="!detail.timeline?.length" description="暂无审批记录" :image-size="80" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { getCenterInstanceDetail } from "@/api/approval";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
instanceId: { type: [Number, String], default: 0 },
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit("update:modelValue", val),
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const detail = ref(null);
|
||||||
|
|
||||||
|
// 实例状态:1审批中 2通过 3驳回 4撤回 5取消
|
||||||
|
const instanceStatusLabel = (status) =>
|
||||||
|
({ 1: "审批中", 2: "已通过", 3: "已驳回", 4: "已撤回", 5: "已取消" }[status] || "未知");
|
||||||
|
const instanceStatusType = (status) =>
|
||||||
|
({ 1: "warning", 2: "success", 3: "danger", 4: "info", 5: "info" }[status] || "info");
|
||||||
|
// 任务状态:1待审批 2已通过 3已驳回 6已通知(抄送)
|
||||||
|
const taskStatusLabel = (status) =>
|
||||||
|
({ 1: "待审批", 2: "已通过", 3: "已驳回", 6: "已通知" }[status] || "未知");
|
||||||
|
const taskStatusType = (status) =>
|
||||||
|
({ 1: "warning", 2: "success", 3: "danger", 6: "info" }[status] || "info");
|
||||||
|
const timelineDotType = (status) => taskStatusType(status);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.modelValue, props.instanceId],
|
||||||
|
([open, id]) => {
|
||||||
|
if (open && id) loadDetail(id);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadDetail = async (id) => {
|
||||||
|
loading.value = true;
|
||||||
|
detail.value = null;
|
||||||
|
try {
|
||||||
|
const res = await getCenterInstanceDetail(id);
|
||||||
|
detail.value = res?.data?.data ?? res?.data ?? null;
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || "加载审批进度失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.progress-wrap {
|
||||||
|
min-height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-head {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-timeline {
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-node {
|
||||||
|
.node-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-approver,
|
||||||
|
.node-comment {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -308,6 +308,97 @@ func (c *AppOaController) ExpenseRecords() {
|
|||||||
c.oaOk(rows)
|
c.oaOk(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// expenseActPayload 审批操作请求体。
|
||||||
|
type expenseActPayload struct {
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// expenseReadActPayload 读取审批意见(可空)。
|
||||||
|
func (c *AppOaController) expenseReadActPayload() string {
|
||||||
|
var payload expenseActPayload
|
||||||
|
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
if err == nil && len(raw) > 0 {
|
||||||
|
_ = json.Unmarshal(raw, &payload)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(payload.Comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpenseApprove POST /api/oa/expense/approve/:id 通过当前审批节点(body: {comment})。
|
||||||
|
// 审批人视角:无需是单据创建人,只要是当前节点待办审批人即可操作。
|
||||||
|
func (c *AppOaController) ExpenseApprove() {
|
||||||
|
claims, err := c.oaClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaErr(401, 401, "未登录或无权限")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.oaScheduleIDParam()
|
||||||
|
if id == 0 {
|
||||||
|
c.oaErr(400, 400, "无效ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code, msg, hint := reimburseActCore(claims, id, true, c.expenseReadActPayload())
|
||||||
|
if msg != "" {
|
||||||
|
c.oaErr(code, code, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaOk(map[string]interface{}{"message": hint})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpenseReject POST /api/oa/expense/reject/:id 驳回报销单(body: {comment})。
|
||||||
|
func (c *AppOaController) ExpenseReject() {
|
||||||
|
claims, err := c.oaClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaErr(401, 401, "未登录或无权限")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.oaScheduleIDParam()
|
||||||
|
if id == 0 {
|
||||||
|
c.oaErr(400, 400, "无效ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code, msg, hint := reimburseActCore(claims, id, false, c.expenseReadActPayload())
|
||||||
|
if msg != "" {
|
||||||
|
c.oaErr(code, code, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaOk(map[string]interface{}{"message": hint})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpenseMyApproveTask GET /api/oa/expense/my-approve-task/:id
|
||||||
|
// 查当前用户在该单据上的待审批任务:返回 {task_id,node_name},无待办返回 data=null。
|
||||||
|
// 小程序详情页据此决定是否展示「通过/驳回」按钮。
|
||||||
|
func (c *AppOaController) ExpenseMyApproveTask() {
|
||||||
|
claims, err := c.oaClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.oaErr(401, 401, "未登录或无权限")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.oaScheduleIDParam()
|
||||||
|
if id == 0 {
|
||||||
|
c.oaErr(400, 400, "无效ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := reimburseMyTaskCore(claims, id)
|
||||||
|
if err != nil {
|
||||||
|
c.oaErr(500, 500, "查询审批任务失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if task == nil {
|
||||||
|
c.oaOk(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.oaOk(map[string]interface{}{
|
||||||
|
"task_id": task.ID,
|
||||||
|
"node_name": task.NodeName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ExpenseTypes GET /api/oa/expense/types 费用类型下拉(全局通用 + 本租户专有,仅启用)。
|
// ExpenseTypes GET /api/oa/expense/types 费用类型下拉(全局通用 + 本租户专有,仅启用)。
|
||||||
func (c *AppOaController) ExpenseTypes() {
|
func (c *AppOaController) ExpenseTypes() {
|
||||||
claims, err := c.oaClaims()
|
claims, err := c.oaClaims()
|
||||||
|
|||||||
@@ -380,6 +380,73 @@ func (c *BackendApprovalController) FlowDelete() {
|
|||||||
_ = c.ServeJSON()
|
_ = c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FlowPerm POST /backend/approval/flow/perm
|
||||||
|
// 审批中心「审批权限」设置:只更新流程权限相关字段(适用部门 / 是否默认 / 启用状态),
|
||||||
|
// 不动节点编排。设为默认流时自动取消同分类(biz_type)下其它流程的默认标记。
|
||||||
|
func (c *BackendApprovalController) FlowPerm() {
|
||||||
|
claims, err := requireBackend(&c.Controller)
|
||||||
|
if err != nil {
|
||||||
|
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tid := uint64(claims.TenantId)
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
ID uint64 `json:"id"`
|
||||||
|
ApplyDeptIDs []uint64 `json:"apply_dept_ids"`
|
||||||
|
IsDefault int8 `json:"is_default"`
|
||||||
|
Status int8 `json:"status"`
|
||||||
|
}
|
||||||
|
if err := c.readTenantBody(&payload); err != nil {
|
||||||
|
jsonErr(&c.Controller, 400, 400, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if payload.ID == 0 {
|
||||||
|
jsonErr(&c.Controller, 400, 400, "无效ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var f models.TenantApprovalFlow
|
||||||
|
if err := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
|
||||||
|
Filter("id", payload.ID).Filter("tid", tid).Filter("is_deleted", 0).One(&f); err != nil {
|
||||||
|
jsonErr(&c.Controller, 404, 404, "流程不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
params := orm.Params{
|
||||||
|
"apply_dept_ids": joinUintCSV(payload.ApplyDeptIDs),
|
||||||
|
"is_default": payload.IsDefault,
|
||||||
|
"status": payload.Status,
|
||||||
|
"update_time": now,
|
||||||
|
}
|
||||||
|
if _, err := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
|
||||||
|
Filter("id", payload.ID).Filter("tid", tid).Update(params); err != nil {
|
||||||
|
jsonErr(&c.Controller, 500, 500, "保存失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设为默认流:取消同分类其它流程的默认标记,保证每个分类只有一条默认流
|
||||||
|
if payload.IsDefault == 1 {
|
||||||
|
models.Orm.QueryTable(new(models.TenantApprovalFlow)).
|
||||||
|
Filter("tid", tid).Filter("biz_type", f.BizType).
|
||||||
|
Filter("id__ne", payload.ID).Filter("is_deleted", 0).
|
||||||
|
Update(orm.Params{"is_default": 0, "update_time": now})
|
||||||
|
}
|
||||||
|
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinUintCSV 把 ID 列表拼成逗号分隔串(与 parseIDSlice 互逆)。
|
||||||
|
func joinUintCSV(ids []uint64) string {
|
||||||
|
parts := make([]string, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
if id > 0 {
|
||||||
|
parts = append(parts, strconv.FormatUint(id, 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
// Options GET /backend/approval/options
|
// Options GET /backend/approval/options
|
||||||
// 编排界面数据源:当前租户的功能模块 + 审批人候选(用户 / 角色 / 部门 / 职位)。
|
// 编排界面数据源:当前租户的功能模块 + 审批人候选(用户 / 角色 / 部门 / 职位)。
|
||||||
func (c *BackendApprovalController) Options() {
|
func (c *BackendApprovalController) Options() {
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/models"
|
||||||
|
"server/pkg/jwtutil"
|
||||||
|
approvalEngine "server/services/approval"
|
||||||
|
|
||||||
|
beego "github.com/beego/beego/v2/server/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApprovalCenterController 审批中心(OA 审批工作台):
|
||||||
|
// - 待我审批 / 我已审批 / 抄送我的:按当前用户查询运行期任务(yz_approval_task);
|
||||||
|
// - 我发起的:按提交人查询审批实例(yz_approval_instance);
|
||||||
|
// - 通用审批操作:复用运行期引擎 services/approval 的 Approve(或签/会签/多级推进均在引擎内),
|
||||||
|
// 业务侧回调(如报销单状态同步、审批记录时间线)按 biz_type 分发。
|
||||||
|
//
|
||||||
|
// 目前走引擎的 OA 业务为报销(biz_type=reimburse,回退 oa),后续新业务接入后无需改本控制器。
|
||||||
|
type ApprovalCenterController struct {
|
||||||
|
beego.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// centerTaskType 任务列表视角:pending-待我审批 done-我已审批 cc-抄送我的
|
||||||
|
const centerTaskTypeAll = "pending|done|cc"
|
||||||
|
|
||||||
|
func (c *ApprovalCenterController) centerJSON(httpStatus, bizCode int, msg string, data interface{}) {
|
||||||
|
c.Ctx.Output.SetStatus(httpStatus)
|
||||||
|
resp := map[string]interface{}{"code": bizCode, "msg": msg}
|
||||||
|
if data != nil {
|
||||||
|
resp["data"] = data
|
||||||
|
}
|
||||||
|
c.Data["json"] = resp
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// centerClaims 解析 backend 令牌。
|
||||||
|
func (c *ApprovalCenterController) centerClaims() (*jwtutil.Claims, error) {
|
||||||
|
return requireBackend(&c.Controller)
|
||||||
|
}
|
||||||
|
|
||||||
|
// centerTaskView 任务行(含实例概要信息),列表与操作返回共用。
|
||||||
|
type centerTaskView struct {
|
||||||
|
TaskID uint64 `json:"task_id"`
|
||||||
|
NodeName string `json:"node_name"`
|
||||||
|
NodeSort int `json:"node_sort"`
|
||||||
|
MultiMode int8 `json:"multi_mode"`
|
||||||
|
Status int8 `json:"status"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
FinishTime string `json:"finish_time"`
|
||||||
|
Deadline string `json:"deadline"`
|
||||||
|
InstanceID uint64 `json:"instance_id"`
|
||||||
|
InstanceStatus int8 `json:"instance_status"`
|
||||||
|
BizType string `json:"biz_type"`
|
||||||
|
BizID uint64 `json:"biz_id"`
|
||||||
|
BizTitle string `json:"biz_title"`
|
||||||
|
FlowName string `json:"flow_name"`
|
||||||
|
SubmitUserID uint64 `json:"submit_user_id"`
|
||||||
|
SubmitUserName string `json:"submit_user_name"`
|
||||||
|
SubmitTime string `json:"submit_time"`
|
||||||
|
CurrentNode string `json:"current_node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskList GET /backend/approval/center/task/list?type=pending|done|cc&page=&pageSize=
|
||||||
|
// 我的任务列表。pending=待我审批(status=待办);done=我已审批(通过/驳回);
|
||||||
|
// cc=抄送我的(通知节点生成的已通知任务)。
|
||||||
|
func (c *ApprovalCenterController) TaskList() {
|
||||||
|
claims, err := c.centerClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.centerJSON(401, 401, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
taskType := c.GetString("type")
|
||||||
|
switch taskType {
|
||||||
|
case "pending", "done", "cc":
|
||||||
|
default:
|
||||||
|
c.centerJSON(400, 400, "type 参数须为 "+centerTaskTypeAll, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page, _ := c.GetInt("page", 1)
|
||||||
|
pageSize, _ := c.GetInt("pageSize", 10)
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
qs := models.Orm.QueryTable(new(models.ApprovalTask)).
|
||||||
|
Filter("scope", models.ApprovalScopeTenant).
|
||||||
|
Filter("tid", claims.TenantId).
|
||||||
|
Filter("approver_id", claims.UserID).
|
||||||
|
Filter("is_deleted", 0)
|
||||||
|
switch taskType {
|
||||||
|
case "pending":
|
||||||
|
qs = qs.Filter("status", models.ApprovalTaskPending)
|
||||||
|
case "done":
|
||||||
|
qs = qs.Filter("status__in", []int8{models.ApprovalTaskApproved, models.ApprovalTaskRejected})
|
||||||
|
case "cc":
|
||||||
|
qs = qs.Filter("status", models.ApprovalTaskNotified)
|
||||||
|
}
|
||||||
|
|
||||||
|
total, _ := qs.Count()
|
||||||
|
var tasks []models.ApprovalTask
|
||||||
|
if _, err := qs.OrderBy("-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&tasks); err != nil {
|
||||||
|
c.centerJSON(500, 500, "查询任务失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
list := make([]centerTaskView, 0, len(tasks))
|
||||||
|
if len(tasks) > 0 {
|
||||||
|
instMap := c.loadInstanceMap(tasks)
|
||||||
|
for _, t := range tasks {
|
||||||
|
view := centerTaskView{
|
||||||
|
TaskID: t.ID,
|
||||||
|
NodeName: t.NodeName,
|
||||||
|
NodeSort: t.Sort,
|
||||||
|
MultiMode: t.MultiMode,
|
||||||
|
Status: t.Status,
|
||||||
|
Comment: t.Comment,
|
||||||
|
}
|
||||||
|
if t.FinishTime != nil {
|
||||||
|
view.FinishTime = t.FinishTime.Format("2006-01-02 15:04")
|
||||||
|
}
|
||||||
|
if t.Deadline != nil {
|
||||||
|
view.Deadline = t.Deadline.Format("2006-01-02 15:04")
|
||||||
|
}
|
||||||
|
if inst, ok := instMap[t.InstanceID]; ok {
|
||||||
|
view.InstanceID = inst.ID
|
||||||
|
view.InstanceStatus = inst.Status
|
||||||
|
view.BizType = inst.BizType
|
||||||
|
view.BizID = inst.BizID
|
||||||
|
view.BizTitle = inst.BizTitle
|
||||||
|
view.SubmitUserID = inst.SubmitUserID
|
||||||
|
view.SubmitUserName = inst.SubmitUserName
|
||||||
|
if inst.SubmitTime != nil {
|
||||||
|
view.SubmitTime = inst.SubmitTime.Format("2006-01-02 15:04")
|
||||||
|
}
|
||||||
|
view.CurrentNode = inst.CurrentNode
|
||||||
|
}
|
||||||
|
// 兜底:任务行自带的业务信息(实例被删时仍可展示)
|
||||||
|
if view.BizTitle == "" {
|
||||||
|
view.BizType = t.BizType
|
||||||
|
view.BizID = t.BizID
|
||||||
|
}
|
||||||
|
list = append(list, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.centerJSON(200, 200, "ok", map[string]interface{}{"list": list, "total": total})
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadInstanceMap 批量加载任务关联的实例。
|
||||||
|
func (c *ApprovalCenterController) loadInstanceMap(tasks []models.ApprovalTask) map[uint64]models.ApprovalInstance {
|
||||||
|
ids := make([]uint64, 0, len(tasks))
|
||||||
|
seen := make(map[uint64]bool)
|
||||||
|
for _, t := range tasks {
|
||||||
|
if t.InstanceID > 0 && !seen[t.InstanceID] {
|
||||||
|
seen[t.InstanceID] = true
|
||||||
|
ids = append(ids, t.InstanceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instMap := make(map[uint64]models.ApprovalInstance, len(ids))
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return instMap
|
||||||
|
}
|
||||||
|
var insts []models.ApprovalInstance
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.ApprovalInstance)).Filter("id__in", ids).All(&insts)
|
||||||
|
for _, inst := range insts {
|
||||||
|
instMap[inst.ID] = inst
|
||||||
|
}
|
||||||
|
return instMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskAct POST /backend/approval/center/task/act
|
||||||
|
// 通用审批操作(body: {task_id, pass, comment})。业务回调按 biz_type 分发:
|
||||||
|
// 报销类(reimburse/oa)同步单据状态并写审批记录;其它业务仅推进引擎。
|
||||||
|
func (c *ApprovalCenterController) TaskAct() {
|
||||||
|
claims, err := c.centerClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.centerJSON(401, 401, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
TaskID uint64 `json:"task_id"`
|
||||||
|
Pass bool `json:"pass"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}
|
||||||
|
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
if err != nil || json.Unmarshal(raw, &payload) != nil || payload.TaskID == 0 {
|
||||||
|
c.centerJSON(400, 400, "参数错误", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var task models.ApprovalTask
|
||||||
|
if err := models.Orm.QueryTable(new(models.ApprovalTask)).
|
||||||
|
Filter("id", payload.TaskID).
|
||||||
|
Filter("tid", claims.TenantId).
|
||||||
|
Filter("approver_id", claims.UserID).
|
||||||
|
Filter("is_deleted", 0).One(&task); err != nil {
|
||||||
|
c.centerJSON(404, 404, "审批任务不存在", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := approvalEngine.Approve(task.ID, approvalEngine.ApproveInput{
|
||||||
|
ApproverID: uint64(claims.UserID),
|
||||||
|
Pass: payload.Pass,
|
||||||
|
Comment: payload.Comment,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.centerJSON(400, 400, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// OA 报销业务回调:同步单据状态 + 时间线记录
|
||||||
|
if isReimburseBizType(task.BizType) {
|
||||||
|
applyReimburseApprovalResult(uint64(claims.TenantId), task.BizID,
|
||||||
|
uint64(claims.UserID), claims.Username, task.Sort,
|
||||||
|
payload.Pass, payload.Comment, result.Finished, result.InstanceStat)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := result.Message
|
||||||
|
if msg == "" {
|
||||||
|
msg = "操作成功"
|
||||||
|
}
|
||||||
|
c.centerJSON(200, 200, msg, map[string]interface{}{
|
||||||
|
"finished": result.Finished,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstanceList GET /backend/approval/center/instance/list?status=&page=&pageSize=
|
||||||
|
// 我发起的审批实例列表(status 可选:1审批中 2通过 3驳回 4撤回)。
|
||||||
|
func (c *ApprovalCenterController) InstanceList() {
|
||||||
|
claims, err := c.centerClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.centerJSON(401, 401, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page, _ := c.GetInt("page", 1)
|
||||||
|
pageSize, _ := c.GetInt("pageSize", 10)
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
qs := models.Orm.QueryTable(new(models.ApprovalInstance)).
|
||||||
|
Filter("scope", models.ApprovalScopeTenant).
|
||||||
|
Filter("tid", claims.TenantId).
|
||||||
|
Filter("submit_user_id", claims.UserID).
|
||||||
|
Filter("is_deleted", 0)
|
||||||
|
if status, err := c.GetInt8("status"); err == nil && status > 0 {
|
||||||
|
qs = qs.Filter("status", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
total, _ := qs.Count()
|
||||||
|
var insts []models.ApprovalInstance
|
||||||
|
if _, err := qs.OrderBy("-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&insts); err != nil {
|
||||||
|
c.centerJSON(500, 500, "查询失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
flowNames := c.loadFlowNames(insts)
|
||||||
|
list := make([]map[string]interface{}, 0, len(insts))
|
||||||
|
for _, inst := range insts {
|
||||||
|
row := map[string]interface{}{
|
||||||
|
"id": inst.ID,
|
||||||
|
"biz_type": inst.BizType,
|
||||||
|
"biz_id": inst.BizID,
|
||||||
|
"biz_title": inst.BizTitle,
|
||||||
|
"status": inst.Status,
|
||||||
|
"current_node": inst.CurrentNode,
|
||||||
|
"submit_time": formatCenterTime(inst.SubmitTime),
|
||||||
|
"finish_time": formatCenterTime(inst.FinishTime),
|
||||||
|
"submit_dept_name": "",
|
||||||
|
}
|
||||||
|
if name, ok := flowNames[inst.FlowID]; ok {
|
||||||
|
row["flow_name"] = name
|
||||||
|
}
|
||||||
|
list = append(list, row)
|
||||||
|
}
|
||||||
|
c.centerJSON(200, 200, "ok", map[string]interface{}{"list": list, "total": total})
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadFlowNames 批量加载实例关联的流程名称。
|
||||||
|
func (c *ApprovalCenterController) loadFlowNames(insts []models.ApprovalInstance) map[uint64]string {
|
||||||
|
ids := make([]uint64, 0, len(insts))
|
||||||
|
seen := make(map[uint64]bool)
|
||||||
|
for _, inst := range insts {
|
||||||
|
if inst.FlowID > 0 && !seen[inst.FlowID] {
|
||||||
|
seen[inst.FlowID] = true
|
||||||
|
ids = append(ids, inst.FlowID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names := make(map[uint64]string, len(ids))
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
var flows []models.TenantApprovalFlow
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.TenantApprovalFlow)).Filter("id__in", ids).All(&flows)
|
||||||
|
for _, f := range flows {
|
||||||
|
names[f.ID] = f.FlowName
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstanceDetail GET /backend/approval/center/instance/detail/:id
|
||||||
|
// 实例详情:实例信息 + 节点任务时间线(含通知/抄送任务),按 sort 升序。
|
||||||
|
func (c *ApprovalCenterController) InstanceDetail() {
|
||||||
|
claims, err := c.centerClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.centerJSON(401, 401, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||||
|
if id == 0 {
|
||||||
|
c.centerJSON(400, 400, "无效ID", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var inst models.ApprovalInstance
|
||||||
|
if err := models.Orm.QueryTable(new(models.ApprovalInstance)).
|
||||||
|
Filter("id", id).
|
||||||
|
Filter("tid", claims.TenantId).
|
||||||
|
Filter("is_deleted", 0).One(&inst); err != nil {
|
||||||
|
c.centerJSON(404, 404, "审批实例不存在", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks []models.ApprovalTask
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.ApprovalTask)).
|
||||||
|
Filter("instance_id", inst.ID).
|
||||||
|
Filter("is_deleted", 0).
|
||||||
|
OrderBy("sort", "id").All(&tasks)
|
||||||
|
|
||||||
|
timeline := make([]map[string]interface{}, 0, len(tasks))
|
||||||
|
for _, t := range tasks {
|
||||||
|
timeline = append(timeline, map[string]interface{}{
|
||||||
|
"task_id": t.ID,
|
||||||
|
"node_key": t.NodeKey,
|
||||||
|
"node_name": t.NodeName,
|
||||||
|
"node_sort": t.Sort,
|
||||||
|
"approver_id": t.ApproverID,
|
||||||
|
"approver": t.ApproverName,
|
||||||
|
"status": t.Status,
|
||||||
|
"comment": t.Comment,
|
||||||
|
"finish_time": formatCenterTime(t.FinishTime),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
flowName := ""
|
||||||
|
if name, ok := c.loadFlowNames([]models.ApprovalInstance{inst})[inst.FlowID]; ok {
|
||||||
|
flowName = name
|
||||||
|
}
|
||||||
|
|
||||||
|
c.centerJSON(200, 200, "ok", map[string]interface{}{
|
||||||
|
"id": inst.ID,
|
||||||
|
"biz_type": inst.BizType,
|
||||||
|
"biz_id": inst.BizID,
|
||||||
|
"biz_title": inst.BizTitle,
|
||||||
|
"status": inst.Status,
|
||||||
|
"flow_name": flowName,
|
||||||
|
"submit_user_name": inst.SubmitUserName,
|
||||||
|
"submit_time": formatCenterTime(inst.SubmitTime),
|
||||||
|
"finish_time": formatCenterTime(inst.FinishTime),
|
||||||
|
"current_node": inst.CurrentNode,
|
||||||
|
"timeline": timeline,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatCenterTime 时间指针格式化(空返回空串)。
|
||||||
|
func formatCenterTime(t *time.Time) string {
|
||||||
|
if t == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Format("2006-01-02 15:04")
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path"
|
"path"
|
||||||
@@ -356,8 +357,39 @@ func reimburseListData(claims *jwtutil.Claims, page, pageSize int, keyword strin
|
|||||||
if pageSize < 1 || pageSize > 100 {
|
if pageSize < 1 || pageSize > 100 {
|
||||||
pageSize = 10
|
pageSize = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
qs := models.Orm.QueryTable(new(models.BackendReimbursement)).
|
qs := models.Orm.QueryTable(new(models.BackendReimbursement)).
|
||||||
Filter("tid", claims.TenantId).Filter("user_id", claims.UserID).Filter("is_deleted", 0)
|
Filter("tid", claims.TenantId).Filter("user_id", claims.UserID).Filter("is_deleted", 0)
|
||||||
|
|
||||||
|
// 待我审批(status=-1 约定值):审批人视角,切换为「我名下运行中实例的待办单据」
|
||||||
|
// 查询(yz_approval_task.approver_id=当前用户),不再按创建人 user_id 过滤
|
||||||
|
mineOnly := hasStatus && status == -1
|
||||||
|
if mineOnly {
|
||||||
|
hasStatus = false
|
||||||
|
taskQs := models.Orm.QueryTable(new(models.ApprovalTask)).
|
||||||
|
Filter("scope", models.ApprovalScopeTenant).
|
||||||
|
Filter("tid", claims.TenantId).
|
||||||
|
Filter("approver_id", claims.UserID).
|
||||||
|
Filter("status", models.ApprovalTaskPending).
|
||||||
|
Filter("is_deleted", 0).
|
||||||
|
Filter("biz_type__in", reimburseFallbackBizTypes)
|
||||||
|
var tasks []models.ApprovalTask
|
||||||
|
if _, err := taskQs.All(&tasks, "biz_id"); err != nil {
|
||||||
|
return nil, 500, "查询审批待办失败"
|
||||||
|
}
|
||||||
|
ids := make([]uint64, 0, len(tasks))
|
||||||
|
for _, t := range tasks {
|
||||||
|
if t.BizID > 0 {
|
||||||
|
ids = append(ids, t.BizID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return map[string]interface{}{"list": []map[string]interface{}{}, "total": 0}, 200, ""
|
||||||
|
}
|
||||||
|
qs = models.Orm.QueryTable(new(models.BackendReimbursement)).
|
||||||
|
Filter("tid", claims.TenantId).Filter("id__in", ids).Filter("is_deleted", 0)
|
||||||
|
}
|
||||||
|
|
||||||
if keyword != "" {
|
if keyword != "" {
|
||||||
qs = qs.Filter("description__icontains", keyword)
|
qs = qs.Filter("description__icontains", keyword)
|
||||||
}
|
}
|
||||||
@@ -879,6 +911,94 @@ func (c *BackendReimburseController) Submit() {
|
|||||||
c.reply(200, "success", nil)
|
c.reply(200, "success", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reimburseActPayload 审批操作请求体。
|
||||||
|
type reimburseActPayload struct {
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// readReimburseActPayload 读取审批意见(可空)。
|
||||||
|
func (c *BackendReimburseController) readReimburseActPayload() string {
|
||||||
|
var payload reimburseActPayload
|
||||||
|
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
if err == nil && len(raw) > 0 {
|
||||||
|
_ = json.Unmarshal(raw, &payload)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(payload.Comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approve POST /backend/reimbursements/approve/:id 通过当前审批节点(body: {comment})。
|
||||||
|
func (c *BackendReimburseController) Approve() {
|
||||||
|
claims, ok := c.claims()
|
||||||
|
if !ok {
|
||||||
|
c.reply(401, "未登录或无权限", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.getID()
|
||||||
|
if !ok {
|
||||||
|
c.reply(400, "无效ID", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code, msg, hint := reimburseActCore(claims, id, true, c.readReimburseActPayload())
|
||||||
|
if msg != "" {
|
||||||
|
c.reply(code, msg, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.reply(200, hint, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject POST /backend/reimbursements/reject/:id 驳回报销单(body: {comment})。
|
||||||
|
func (c *BackendReimburseController) Reject() {
|
||||||
|
claims, ok := c.claims()
|
||||||
|
if !ok {
|
||||||
|
c.reply(401, "未登录或无权限", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.getID()
|
||||||
|
if !ok {
|
||||||
|
c.reply(400, "无效ID", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code, msg, hint := reimburseActCore(claims, id, false, c.readReimburseActPayload())
|
||||||
|
if msg != "" {
|
||||||
|
c.reply(code, msg, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.reply(200, hint, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MyApproveTask GET /backend/reimbursements/my-approve-task/:id
|
||||||
|
// 查当前用户在该单据上的待审批任务:返回 {task_id, node_name},无待办返回 data=null。
|
||||||
|
func (c *BackendReimburseController) MyApproveTask() {
|
||||||
|
claims, ok := c.claims()
|
||||||
|
if !ok {
|
||||||
|
c.reply(401, "未登录或无权限", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.getID()
|
||||||
|
if !ok {
|
||||||
|
c.reply(400, "无效ID", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task, err := reimburseMyTaskCore(claims, id)
|
||||||
|
if err != nil {
|
||||||
|
c.reply(500, "查询审批任务失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if task == nil {
|
||||||
|
c.reply(200, "success", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.reply(200, "success", map[string]interface{}{
|
||||||
|
"task_id": task.ID,
|
||||||
|
"node_name": task.NodeName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// reimburseFallbackBizTypes 报销匹配审批流的单据类型回退顺序:
|
||||||
|
// 先按报销专有流(biz_type=reimburse),未配置时回退到 OA 分类默认流
|
||||||
|
// (FlowSave 未显式指定单据类型时 biz_type=module_code,即 oa)。
|
||||||
|
var reimburseFallbackBizTypes = []string{models.ApprovalBizReimburse, "oa"}
|
||||||
|
|
||||||
// reimburseSubmitCore 提交审批共享逻辑:仅草稿可提交,需租户已配置启用审批流。
|
// reimburseSubmitCore 提交审批共享逻辑:仅草稿可提交,需租户已配置启用审批流。
|
||||||
//
|
//
|
||||||
// 提交进入统一审批引擎(biz_type=reimburse、biz_id=报销单ID),引擎负责节点流转;
|
// 提交进入统一审批引擎(biz_type=reimburse、biz_id=报销单ID),引擎负责节点流转;
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ func Register() {
|
|||||||
beego.Router("/api/oa/expense/item/update/:id/:item_id", &controllers.AppOaController{}, "post:ExpenseUpdateItem")
|
beego.Router("/api/oa/expense/item/update/:id/:item_id", &controllers.AppOaController{}, "post:ExpenseUpdateItem")
|
||||||
beego.Router("/api/oa/expense/item/delete/:id/:item_id", &controllers.AppOaController{}, "delete:ExpenseDeleteItem")
|
beego.Router("/api/oa/expense/item/delete/:id/:item_id", &controllers.AppOaController{}, "delete:ExpenseDeleteItem")
|
||||||
beego.Router("/api/oa/expense/submit/:id", &controllers.AppOaController{}, "post:ExpenseSubmit")
|
beego.Router("/api/oa/expense/submit/:id", &controllers.AppOaController{}, "post:ExpenseSubmit")
|
||||||
|
beego.Router("/api/oa/expense/approve/:id", &controllers.AppOaController{}, "post:ExpenseApprove")
|
||||||
|
beego.Router("/api/oa/expense/reject/:id", &controllers.AppOaController{}, "post:ExpenseReject")
|
||||||
|
beego.Router("/api/oa/expense/my-approve-task/:id", &controllers.AppOaController{}, "get:ExpenseMyApproveTask")
|
||||||
beego.Router("/api/oa/expense/status/:id", &controllers.AppOaController{}, "post:ExpenseStatus")
|
beego.Router("/api/oa/expense/status/:id", &controllers.AppOaController{}, "post:ExpenseStatus")
|
||||||
beego.Router("/api/oa/expense/records/:id", &controllers.AppOaController{}, "get:ExpenseRecords")
|
beego.Router("/api/oa/expense/records/:id", &controllers.AppOaController{}, "get:ExpenseRecords")
|
||||||
|
|
||||||
|
|||||||
@@ -303,6 +303,9 @@ func RegisterAuthRoutes() {
|
|||||||
beego.Router("/backend/reimbursements/types/:type_id", &controllers.BackendReimburseController{}, "post:UpdateType;delete:DeleteType")
|
beego.Router("/backend/reimbursements/types/:type_id", &controllers.BackendReimburseController{}, "post:UpdateType;delete:DeleteType")
|
||||||
beego.Router("/backend/reimbursements/types", &controllers.BackendReimburseController{}, "get:Types;post:CreateType")
|
beego.Router("/backend/reimbursements/types", &controllers.BackendReimburseController{}, "get:Types;post:CreateType")
|
||||||
beego.Router("/backend/reimbursements/flows", &controllers.BackendReimburseController{}, "get:Flows")
|
beego.Router("/backend/reimbursements/flows", &controllers.BackendReimburseController{}, "get:Flows")
|
||||||
|
beego.Router("/backend/reimbursements/approve/:id", &controllers.BackendReimburseController{}, "post:Approve")
|
||||||
|
beego.Router("/backend/reimbursements/reject/:id", &controllers.BackendReimburseController{}, "post:Reject")
|
||||||
|
beego.Router("/backend/reimbursements/my-approve-task/:id", &controllers.BackendReimburseController{}, "get:MyApproveTask")
|
||||||
beego.Router("/backend/reimbursements/:id/items", &controllers.BackendReimburseController{}, "post:AddItem")
|
beego.Router("/backend/reimbursements/:id/items", &controllers.BackendReimburseController{}, "post:AddItem")
|
||||||
beego.Router("/backend/reimbursements/:id/items/:item_id", &controllers.BackendReimburseController{}, "post:UpdateItem;delete:DeleteItem")
|
beego.Router("/backend/reimbursements/:id/items/:item_id", &controllers.BackendReimburseController{}, "post:UpdateItem;delete:DeleteItem")
|
||||||
beego.Router("/backend/reimbursements/:id", &controllers.BackendReimburseController{}, "get:Detail;post:Update;delete:Delete")
|
beego.Router("/backend/reimbursements/:id", &controllers.BackendReimburseController{}, "get:Detail;post:Update;delete:Delete")
|
||||||
|
|||||||
@@ -254,6 +254,21 @@ export function resolveImageUrl(url) {
|
|||||||
return `${getBaseURL().replace(/\/$/, '')}${value.startsWith('/') ? '' : '/'}${value}`
|
return `${getBaseURL().replace(/\/$/, '')}${value.startsWith('/') ? '' : '/'}${value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 查我的待审批任务(审批人视角):返回 {task_id,node_name},无待办返回 null */
|
||||||
|
export function fetchMyApproveTask(id) {
|
||||||
|
return request({ url: `/api/oa/expense/my-approve-task/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通过当前审批节点(body: {comment}),返回引擎提示(会签等待/流转/完成) */
|
||||||
|
export function approveExpense(id, comment = '') {
|
||||||
|
return request({ url: `/api/oa/expense/approve/${id}`, method: 'POST', data: { comment } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 驳回报销单(body: {comment}) */
|
||||||
|
export function rejectExpense(id, comment = '') {
|
||||||
|
return request({ url: `/api/oa/expense/reject/${id}`, method: 'POST', data: { comment } })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发票引用是否为 PDF 文件:PDF 不能用 image 组件渲染(会被 ORB 拦截不显示),
|
* 发票引用是否为 PDF 文件:PDF 不能用 image 组件渲染(会被 ORB 拦截不显示),
|
||||||
* 需要走 openDocument 打开。
|
* 需要走 openDocument 打开。
|
||||||
|
|||||||
@@ -81,6 +81,11 @@
|
|||||||
},
|
},
|
||||||
"vueVersion" : "3",
|
"vueVersion" : "3",
|
||||||
"h5" : {
|
"h5" : {
|
||||||
"title" : "云泽系统"
|
"title" : "云泽系统",
|
||||||
|
"router" : {
|
||||||
|
"mode" : "hash",
|
||||||
|
"base" : "/h5/"
|
||||||
|
},
|
||||||
|
"publicPath" : "/h5/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,6 +143,9 @@ import {
|
|||||||
submitExpense,
|
submitExpense,
|
||||||
confirmExpensePaid,
|
confirmExpensePaid,
|
||||||
fetchExpenseRecords,
|
fetchExpenseRecords,
|
||||||
|
fetchMyApproveTask,
|
||||||
|
approveExpense,
|
||||||
|
rejectExpense,
|
||||||
getExpenseStatusLabel,
|
getExpenseStatusLabel,
|
||||||
getExpenseStatusType,
|
getExpenseStatusType,
|
||||||
getExpenseActionLabel,
|
getExpenseActionLabel,
|
||||||
@@ -155,6 +158,7 @@ const expenseId = ref('')
|
|||||||
const detail = ref(null)
|
const detail = ref(null)
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
const records = ref([])
|
const records = ref([])
|
||||||
|
const myTask = ref(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const isDraft = computed(() => Number(detail.value?.status) === EXPENSE_STATUS.DRAFT)
|
const isDraft = computed(() => Number(detail.value?.status) === EXPENSE_STATUS.DRAFT)
|
||||||
@@ -290,6 +294,14 @@ async function loadDetail() {
|
|||||||
detail.value = data
|
detail.value = data
|
||||||
items.value = data.items || []
|
items.value = data.items || []
|
||||||
records.value = await fetchExpenseRecords(expenseId.value)
|
records.value = await fetchExpenseRecords(expenseId.value)
|
||||||
|
// 审批人视角:审批中的单据查询自己是否有待办任务(非审批人静默返回 null)
|
||||||
|
try {
|
||||||
|
myTask.value = Number(data.status) === EXPENSE_STATUS.APPROVING
|
||||||
|
? await fetchMyApproveTask(expenseId.value)
|
||||||
|
: null
|
||||||
|
} catch (e) {
|
||||||
|
myTask.value = null
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
@@ -387,6 +399,43 @@ function onDelete() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 通过当前审批节点:成功后按引擎提示刷新(会签等待/流转/完成) */
|
||||||
|
function onApprove() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '通过审批',
|
||||||
|
content: `确认通过「${myTask.value?.node_name || '审批'}」节点吗?`,
|
||||||
|
success: async (res) => {
|
||||||
|
if (!res.confirm) return
|
||||||
|
try {
|
||||||
|
const result = await approveExpense(expenseId.value, '')
|
||||||
|
uni.showToast({ title: result?.message || '已通过', icon: 'none' })
|
||||||
|
loadDetail()
|
||||||
|
} catch (e) {
|
||||||
|
// request 封装已弹出服务端错误提示
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 驳回报销单:填写审批意见(可空) */
|
||||||
|
function onReject() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '驳回',
|
||||||
|
editable: true,
|
||||||
|
placeholderText: '请输入驳回原因(选填)',
|
||||||
|
success: async (res) => {
|
||||||
|
if (!res.confirm) return
|
||||||
|
try {
|
||||||
|
await rejectExpense(expenseId.value, (res.content || '').trim())
|
||||||
|
uni.showToast({ title: '已驳回', icon: 'none' })
|
||||||
|
loadDetail()
|
||||||
|
} catch (e) {
|
||||||
|
// request 封装已弹出服务端错误提示
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -599,6 +648,25 @@ function onDelete() {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-approve {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
.state-box {
|
.state-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ import {
|
|||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
const tabs = [{ value: '', label: '全部' }, ...EXPENSE_STATUS_OPTIONS]
|
// -1 为「待我审批」约定值(后端按当前用户运行中审批待办过滤)
|
||||||
|
const tabs = [{ value: -1, label: '待我审批' }, { value: '', label: '全部' }, ...EXPENSE_STATUS_OPTIONS]
|
||||||
|
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url'
|
|||||||
const rootDir = path.dirname(fileURLToPath(import.meta.url))
|
const rootDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: '/h5/',
|
||||||
plugins: [uni()],
|
plugins: [uni()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Reference in new issue
Block a user