From a758966faee8d4c77a591eade0b43b9041c4c975 Mon Sep 17 00:00:00 2001 From: lzq <357099073@qq.com> Date: Sat, 17 Oct 2026 11:52:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/api/approval.js | 27 ++ backend/src/router/index.js | 19 + backend/src/utils/authClient.js | 5 +- .../components/ProgressTimeline.vue | 159 ++++++++ .../src/views/apps/oa/approvalcenter/list.vue | 312 ++++++++++++++ .../src/views/apps/oa/approvalcenter/mine.vue | 230 +++++++++++ .../views/apps/oa/approvalcenter/settings.vue | 297 ++++++++++++++ go/controllers/app_oa_expense.go | 91 +++++ go/controllers/backend_approval.go | 67 +++ go/controllers/backend_approval_center.go | 382 ++++++++++++++++++ go/controllers/backend_reimburse.go | 310 +++++++++++++- go/routers/api/api.go | 3 + go/routers/backend/backend.go | 9 + uniapp/api/expense.js | 15 + uniapp/manifest.json | 7 +- uniapp/pages/tools/expense/detail.vue | 84 +++- uniapp/pages/tools/expense/index.vue | 3 +- uniapp/vite.config.js | 1 + 18 files changed, 2002 insertions(+), 19 deletions(-) create mode 100644 backend/src/views/apps/oa/approvalcenter/components/ProgressTimeline.vue create mode 100644 backend/src/views/apps/oa/approvalcenter/list.vue create mode 100644 backend/src/views/apps/oa/approvalcenter/mine.vue create mode 100644 backend/src/views/apps/oa/approvalcenter/settings.vue create mode 100644 go/controllers/backend_approval_center.go diff --git a/backend/src/api/approval.js b/backend/src/api/approval.js index 27825e9..5fbc146 100644 --- a/backend/src/api/approval.js +++ b/backend/src/api/approval.js @@ -57,3 +57,30 @@ export function getApprovalFlowPick(params) { export function getAvailableFlows(params) { return request({ url: '/backend/approval/flow/available', method: 'get', params }); } + +// ==================== 审批中心 ==================== + +// 我的任务列表:type=pending 待我审批 / done 我已审批 / cc 抄送我的 +export function getCenterTaskList(params) { + return request({ url: '/backend/approval/center/task/list', method: 'get', params }); +} + +// 通用审批操作:{ task_id, pass, comment } +export function actCenterTask(data) { + return request({ url: '/backend/approval/center/task/act', method: 'post', data }); +} + +// 我发起的审批实例列表(status 可选:1审批中 2通过 3驳回 4撤回) +export function getCenterInstanceList(params) { + return request({ url: '/backend/approval/center/instance/list', method: 'get', params }); +} + +// 审批实例详情(含节点任务时间线) +export function getCenterInstanceDetail(id) { + return request({ url: `/backend/approval/center/instance/detail/${id}`, method: 'get' }); +} + +// 审批权限设置:只更新流程的适用部门/是否默认/启用状态(body: {id, apply_dept_ids, is_default, status}) +export function saveApprovalFlowPerm(data) { + return request({ url: '/backend/approval/flow/perm', method: 'post', data }); +} diff --git a/backend/src/router/index.js b/backend/src/router/index.js index 2ebcd67..d6054b0 100644 --- a/backend/src/router/index.js +++ b/backend/src/router/index.js @@ -22,6 +22,25 @@ const staticMainChildren = [ component: () => import("@/views/system/approval/index.vue"), 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 文章/产品/解决方案是系统内置功能,不依赖数据库菜单配置。 // 统一结构:index 为列表页,type 为分类页。 { diff --git a/backend/src/utils/authClient.js b/backend/src/utils/authClient.js index 626cee1..77f4ff3 100644 --- a/backend/src/utils/authClient.js +++ b/backend/src/utils/authClient.js @@ -7,7 +7,7 @@ * 环境变量(见 .env.development / .env.production): * VITE_AUTH_BASE=https://api.yunzer.cn/auth 认证中心地址,缺省回退到 ${VITE_API_BASE_URL}/auth * 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/#/ * * 说明: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 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 = import.meta.env.VITE_AUTH_POST_LOGOUT_URI || `${window.location.origin}/#/`; diff --git a/backend/src/views/apps/oa/approvalcenter/components/ProgressTimeline.vue b/backend/src/views/apps/oa/approvalcenter/components/ProgressTimeline.vue new file mode 100644 index 0000000..d7d7926 --- /dev/null +++ b/backend/src/views/apps/oa/approvalcenter/components/ProgressTimeline.vue @@ -0,0 +1,159 @@ + + + + + + + {{ detail.biz_title }} + + {{ instanceStatusLabel(detail.status) }} + + + + 流程:{{ detail.flow_name }} + 发起人:{{ detail.submit_user_name || "-" }} + 发起时间:{{ detail.submit_time || "-" }} + 完成时间:{{ detail.finish_time }} + + + + + + + + {{ node.node_name }} + + {{ taskStatusLabel(node.status) }} + + + 处理人:{{ node.approver || "-" }} + 意见:{{ node.comment }} + + + + + + + + + + + + diff --git a/backend/src/views/apps/oa/approvalcenter/list.vue b/backend/src/views/apps/oa/approvalcenter/list.vue new file mode 100644 index 0000000..4c8ba3b --- /dev/null +++ b/backend/src/views/apps/oa/approvalcenter/list.vue @@ -0,0 +1,312 @@ + + + + + 审批列表 + 查看我参与的全部审批记录 + + + + + + + + + + + + + {{ bizTypeLabel(row.biz_type) }} + + + + {{ row.submit_user_name || "-" }} + + + {{ row.node_name || "-" }} + + + + + {{ taskStatusLabel(row.status) }} + + + + + {{ row.comment || "-" }} + + + {{ row.finish_time || "-" }} + + + + 进度 + + + + + + + + + + + + + + + + + + + + + + {{ row.flow_name || "-" }} + + + + {{ bizTypeLabel(row.biz_type) }} + + + + + + {{ instanceStatusLabel(row.status) }} + + + + + {{ row.submit_time || "-" }} + + + {{ row.finish_time || "-" }} + + + + 进度 + + + + + + + + + + + + + + + {{ bizTypeLabel(row.biz_type) }} + + + + {{ row.submit_user_name || "-" }} + + + {{ row.node_name || "-" }} + + + {{ row.finish_time || "-" }} + + + + 进度 + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/views/apps/oa/approvalcenter/mine.vue b/backend/src/views/apps/oa/approvalcenter/mine.vue new file mode 100644 index 0000000..6176fb2 --- /dev/null +++ b/backend/src/views/apps/oa/approvalcenter/mine.vue @@ -0,0 +1,230 @@ + + + + + 待我审批 + 处理与我相关的 OA 审批事项 + + + + 待办 {{ pagination.total }} 条 + + + + + + + + + + + {{ bizTypeLabel(row.biz_type) }} + + + + {{ row.submit_user_name || "-" }} + + + {{ row.submit_time || "-" }} + + + {{ row.node_name || "-" }} + + + + {{ + row.deadline || "-" + }} + + + + + 通过 + 驳回 + 进度 + + + + + + + + + + + + + + + + + diff --git a/backend/src/views/apps/oa/approvalcenter/settings.vue b/backend/src/views/apps/oa/approvalcenter/settings.vue new file mode 100644 index 0000000..0bb6ecd --- /dev/null +++ b/backend/src/views/apps/oa/approvalcenter/settings.vue @@ -0,0 +1,297 @@ + + + + + 审批设置 + 配置 OA 各业务的审批流程模板、表单与权限 + + + 新建流程 + + + + + + + + + + + + {{ moduleName(row.module_code) }} + + + {{ bizTypeLabel(row.biz_type) }} + + + + + 默认 + - + + + + + toggleFlowStatus(row, val)" + /> + + + + + 编辑 + 删除 + + + + + + + + + + + + + + + + + + + + + {{ bizTypeLabel(row.biz_type) }} + + + + + + + + + + + savePerm(row, { is_default: val ? 1 : 0 })" + /> + + + + + 保存部门 + + + + + + 适用部门为空表示全公司通用;同一业务类型下仅一条默认流程,发起时自动命中。 + + + + + + + + + + diff --git a/go/controllers/app_oa_expense.go b/go/controllers/app_oa_expense.go index 0473d10..4ca4892 100644 --- a/go/controllers/app_oa_expense.go +++ b/go/controllers/app_oa_expense.go @@ -307,6 +307,97 @@ func (c *AppOaController) ExpenseRecords() { 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 费用类型下拉(全局通用 + 本租户专有,仅启用)。 func (c *AppOaController) ExpenseTypes() { claims, err := c.oaClaims() diff --git a/go/controllers/backend_approval.go b/go/controllers/backend_approval.go index 6620b0d..9e88b52 100644 --- a/go/controllers/backend_approval.go +++ b/go/controllers/backend_approval.go @@ -380,6 +380,73 @@ func (c *BackendApprovalController) FlowDelete() { _ = 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 // 编排界面数据源:当前租户的功能模块 + 审批人候选(用户 / 角色 / 部门 / 职位)。 func (c *BackendApprovalController) Options() { diff --git a/go/controllers/backend_approval_center.go b/go/controllers/backend_approval_center.go new file mode 100644 index 0000000..c45e4bd --- /dev/null +++ b/go/controllers/backend_approval_center.go @@ -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") +} diff --git a/go/controllers/backend_reimburse.go b/go/controllers/backend_reimburse.go index 4812114..90a57a1 100644 --- a/go/controllers/backend_reimburse.go +++ b/go/controllers/backend_reimburse.go @@ -2,6 +2,7 @@ package controllers import ( "encoding/json" + "errors" "fmt" "io" "path" @@ -13,6 +14,7 @@ import ( "server/models" "server/pkg/jwtutil" "server/services" + "server/services/approval" "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" @@ -355,8 +357,39 @@ func reimburseListData(claims *jwtutil.Claims, page, pageSize int, keyword strin if pageSize < 1 || pageSize > 100 { pageSize = 10 } + qs := models.Orm.QueryTable(new(models.BackendReimbursement)). 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 != "" { qs = qs.Filter("description__icontains", keyword) } @@ -827,8 +860,99 @@ func (c *BackendReimburseController) Submit() { 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 提交审批共享逻辑:仅草稿可提交,需租户已配置启用审批流, -// 提交后状态置为审批中(1),并写入 action=1 的审批记录。 +// 提交后状态置为审批中(1)并写入 action=1 的审批记录。 +// 流程匹配与实例/任务创建走运行期引擎(services/approval): +// 实例落 yz_approval_instance,首节点待办落 yz_approval_task, +// 审批人在「我的待办」里处理(reimburseActCore)。 func reimburseSubmitCore(claims *jwtutil.Claims, id uint64) (int, string) { row, err := findOwnedReimbursement(claims, id) if err != nil { @@ -837,31 +961,189 @@ func reimburseSubmitCore(claims *jwtutil.Claims, id uint64) (int, string) { if row.Status != 0 { return 400, "当前状态不可提交" } - var flow models.BackendApprovalFlow - err = models.Orm.QueryTable(new(models.BackendApprovalFlow)). - Filter("tid", claims.TenantId).Filter("is_active", 1).Filter("is_deleted", 0). - OrderBy("step_order").One(&flow) - if err != nil { + + var deptID uint64 + if row.DepartmentID != nil { + deptID = *row.DepartmentID + } + + // 依次尝试回退类型匹配流程;有部门限制的流程未命中时由引擎继续尝试通用流 + var instID uint64 + var submitErr error + for _, bizType := range reimburseFallbackBizTypes { + instID, submitErr = approval.Submit(approval.SubmitInput{ + Scope: models.ApprovalScopeTenant, + Tid: uint64(claims.TenantId), + BizTid: uint64(claims.TenantId), + BizType: bizType, + BizID: id, + BizTitle: claims.Username + "的报销单", + SubmitUID: uint64(claims.UserID), + SubmitName: claims.Username, + DeptID: deptID, + }) + if submitErr == nil { + break + } + var noFlow *approval.ErrNoFlow + if !errors.As(submitErr, &noFlow) { + return 400, submitErr.Error() + } + } + if submitErr != nil { return 400, "尚未配置审批流程" } + + // 实例可能无审批节点直接通过(如流程只有通知),按实例实际状态落单据状态 + var inst models.ApprovalInstance + if err := models.Orm.QueryTable(new(models.ApprovalInstance)). + Filter("id", instID).One(&inst); err != nil { + return 500, "查询审批实例失败" + } + newStatus := int8(1) // 审批中 + if inst.Status == models.ApprovalInstanceApproved { + newStatus = 2 + } + now := time.Now() - _, err = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).Update(orm.Params{ - "status": 1, "current_step": flow.StepOrder, "current_approver_id": 0, "update_time": now, - }) - if err != nil { + if _, err := models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).Update(orm.Params{ + "status": newStatus, "current_approver_id": 0, "update_time": now, + }); err != nil { return 500, "提交失败" } - _, err = models.Orm.Insert(&models.BackendApprovalRecord{ + if _, err := models.Orm.Insert(&models.BackendApprovalRecord{ Tid: uint64(claims.TenantId), UID: uint64Ptr(uint64(claims.UserID)), ReimbursementID: id, ApproverID: uint64(claims.UserID), ApproverName: claims.Username, Action: 1, - Step: flow.StepOrder, PrevStep: 0, - }) - if err != nil { + }); err != nil { return 500, "提交审批记录失败" } return 200, "" } +// findRunningReimburseInstance 按报销单 ID 查运行中的审批实例 +// (实例的 biz_type 取发起时命中的类型,按回退顺序逐一查找)。 +func findRunningReimburseInstance(tid, id uint64) (*models.ApprovalInstance, error) { + for _, bizType := range reimburseFallbackBizTypes { + var inst models.ApprovalInstance + err := models.Orm.QueryTable(new(models.ApprovalInstance)). + Filter("tid", tid).Filter("biz_type", bizType).Filter("biz_id", id). + Filter("status", models.ApprovalInstanceRunning).Filter("is_deleted", 0). + OrderBy("-id").One(&inst) + if err == nil { + return &inst, nil + } + if err != orm.ErrNoRows { + return nil, err + } + } + return nil, orm.ErrNoRows +} + +// reimburseMyTaskCore 查当前用户在指定报销单上的待审批任务(无则返回 nil)。 +func reimburseMyTaskCore(claims *jwtutil.Claims, id uint64) (*models.ApprovalTask, error) { + inst, err := findRunningReimburseInstance(uint64(claims.TenantId), id) + if err != nil { + return nil, nil // 无运行中实例:不是「错误」,只是没有待办 + } + var task models.ApprovalTask + err = models.Orm.QueryTable(new(models.ApprovalTask)). + Filter("instance_id", inst.ID). + Filter("approver_id", claims.UserID). + Filter("status", models.ApprovalTaskPending). + Filter("is_deleted", 0). + One(&task) + if err == orm.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return &task, nil +} + +// reimburseActCore 审批操作共享逻辑:当前用户必须是该单当前节点的待办审批人, +// 通过/驳回经运行期引擎推进(或签/会签/多级节点均由引擎处理), +// 实例结束时同步报销单状态,并写审批记录保持时间线兼容。 +// 返回 (HTTP 状态码, 错误信息, 成功提示),错误信息为空串表示成功。 +func reimburseActCore(claims *jwtutil.Claims, id uint64, pass bool, comment string) (int, string, string) { + inst, err := findRunningReimburseInstance(uint64(claims.TenantId), id) + if err != nil { + return 404, "该报销单不在审批中", "" + } + + var task models.ApprovalTask + err = models.Orm.QueryTable(new(models.ApprovalTask)). + Filter("instance_id", inst.ID). + Filter("approver_id", claims.UserID). + Filter("status", models.ApprovalTaskPending). + Filter("is_deleted", 0). + One(&task) + if err == orm.ErrNoRows { + return 400, "您没有该单据的待审批任务", "" + } + if err != nil { + return 500, "查询审批任务失败", "" + } + + result, err := approval.Approve(task.ID, approval.ApproveInput{ + ApproverID: uint64(claims.UserID), + Pass: pass, + Comment: comment, + }) + if err != nil { + return 400, err.Error(), "" + } + + // 业务回调:写时间线记录 + 实例结束时同步单据状态 + applyReimburseApprovalResult(uint64(claims.TenantId), id, + uint64(claims.UserID), claims.Username, task.Sort, + pass, comment, result.Finished, result.InstanceStat) + + hint := result.Message + if hint == "" { + hint = "操作成功" + } + return 200, "", hint +} + +// isReimburseBizType 业务类型是否归属报销(提交与实例落库的 biz_type 均可能命中)。 +func isReimburseBizType(bizType string) bool { + for _, bt := range reimburseFallbackBizTypes { + if bt == bizType { + return true + } + } + return false +} + +// applyReimburseApprovalResult 报销业务回调(报销页与审批中心两处审批入口共用): +// - 始终写一条审批记录(action: 2通过 3驳回),与既有时间线保持兼容; +// - 实例结束时同步报销单状态:通过→2,驳回→3。 +func applyReimburseApprovalResult(tid, bizID, approverID uint64, approverName string, + taskSort int, pass bool, comment string, finished bool, instStat int8) { + + action := int8(models.ApprovalActionApprove) + if !pass { + action = models.ApprovalActionReject + } + if _, err := models.Orm.Insert(&models.BackendApprovalRecord{ + Tid: tid, UID: uint64Ptr(approverID), ReimbursementID: bizID, + ApproverID: approverID, ApproverName: approverName, Action: action, + Comment: &comment, Step: int8(taskSort), + }); err != nil { + return + } + + if finished { + newStatus := int8(3) + if instStat == models.ApprovalInstanceApproved { + newStatus = 2 + } + _, _ = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", bizID). + Update(orm.Params{"status": newStatus, "update_time": time.Now()}) + } +} + type reimbursementStatusPayload struct { Status int8 `json:"status"` } diff --git a/go/routers/api/api.go b/go/routers/api/api.go index f32d57a..e0f7f08 100644 --- a/go/routers/api/api.go +++ b/go/routers/api/api.go @@ -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/delete/:id/:item_id", &controllers.AppOaController{}, "delete:ExpenseDeleteItem") 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/records/:id", &controllers.AppOaController{}, "get:ExpenseRecords") diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go index 08c39e6..3b628fe 100644 --- a/go/routers/backend/backend.go +++ b/go/routers/backend/backend.go @@ -33,6 +33,12 @@ func RegisterApprovalRoutes() { beego.Router("/backend/approval/flow/delete/:id", &controllers.BackendApprovalController{}, "delete:FlowDelete") beego.Router("/backend/approval/flow/pick", &controllers.BackendApprovalController{}, "get:Pick") beego.Router("/backend/approval/flow/available", &controllers.BackendApprovalController{}, "get:AvailableFlows") + + // 审批中心:待我审批/我已审批/抄送我的/我发起的 + 通用审批操作 + 实例进度 + beego.Router("/backend/approval/center/task/list", &controllers.ApprovalCenterController{}, "get:TaskList") + beego.Router("/backend/approval/center/task/act", &controllers.ApprovalCenterController{}, "post:TaskAct") + beego.Router("/backend/approval/center/instance/list", &controllers.ApprovalCenterController{}, "get:InstanceList") + beego.Router("/backend/approval/center/instance/detail/:id", &controllers.ApprovalCenterController{}, "get:InstanceDetail") } // RegisterProductRoutes 注册租户端「产品购买」路由。 @@ -296,6 +302,9 @@ func RegisterAuthRoutes() { 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/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/:item_id", &controllers.BackendReimburseController{}, "post:UpdateItem;delete:DeleteItem") beego.Router("/backend/reimbursements/:id", &controllers.BackendReimburseController{}, "get:Detail;post:Update;delete:Delete") diff --git a/uniapp/api/expense.js b/uniapp/api/expense.js index b11f197..cae4ee7 100644 --- a/uniapp/api/expense.js +++ b/uniapp/api/expense.js @@ -253,6 +253,21 @@ export function resolveImageUrl(url) { 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 拦截不显示), * 需要走 openDocument 打开。 diff --git a/uniapp/manifest.json b/uniapp/manifest.json index 234e099..7f52e44 100644 --- a/uniapp/manifest.json +++ b/uniapp/manifest.json @@ -81,6 +81,11 @@ }, "vueVersion" : "3", "h5" : { - "title" : "云泽系统" + "title" : "云泽系统", + "router" : { + "mode" : "hash", + "base" : "/h5/" + }, + "publicPath" : "/h5/" } } diff --git a/uniapp/pages/tools/expense/detail.vue b/uniapp/pages/tools/expense/detail.vue index 783629f..93b21e8 100644 --- a/uniapp/pages/tools/expense/detail.vue +++ b/uniapp/pages/tools/expense/detail.vue @@ -97,7 +97,7 @@ - + @@ -109,6 +109,20 @@ + + + + 我是当前节点「{{ myTask.node_name }}」审批人 + + + + + + + + + + @@ -120,6 +134,9 @@ import { deleteExpense, submitExpense, fetchExpenseRecords, + fetchMyApproveTask, + approveExpense, + rejectExpense, getExpenseStatusLabel, getExpenseStatusType, getExpenseActionLabel, @@ -132,6 +149,7 @@ const expenseId = ref('') const detail = ref(null) const items = ref([]) const records = ref([]) +const myTask = ref(null) const loading = ref(false) const isDraft = computed(() => Number(detail.value?.status) === EXPENSE_STATUS.DRAFT) @@ -170,6 +188,14 @@ async function loadDetail() { detail.value = data items.value = data.items || [] 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) { uni.showToast({ title: '加载失败', icon: 'none' }) } finally { @@ -244,6 +270,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 封装已弹出服务端错误提示 + } + } + }) +}
查看我参与的全部审批记录
处理与我相关的 OA 审批事项
配置 OA 各业务的审批流程模板、表单与权限