feat: ship OA drafts/badges, role dual-track, and test-env deploy tooling.
Add approval instance drafts with menu badges, dual-track user/role read model and role bound names, form-designer outline docs, and bare-metal test deploy scripts. Ignore local test-server credential file. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
7af53fc344
commit
de22678c87
56 files changed
+11861
-6082
No files matched your search
@@ -210,6 +210,93 @@ export function fetchStartOaInstance(data: {
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 发起审批草稿箱
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function fetchOaInstanceDraftList(params?: {
|
||||
keyword?: string
|
||||
groupId?: string
|
||||
updateTimeFrom?: string
|
||||
updateTimeTo?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}) {
|
||||
return request.get<{ list: any[]; total: number }>({
|
||||
url: `${BASE}/drafts`,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchOaInstanceDraftCount() {
|
||||
return request.get<{ count: number }>({ url: `${BASE}/drafts/count` })
|
||||
}
|
||||
|
||||
export function fetchOaInstanceDraftDetail(id: string) {
|
||||
return request.get<any>({ url: `${BASE}/drafts/${id}` })
|
||||
}
|
||||
|
||||
export function fetchCreateOaInstanceDraft(data: {
|
||||
appId: string
|
||||
formData?: Record<string, any>
|
||||
title?: string
|
||||
initiatorDeptId?: string
|
||||
initiatorSelectByNode?: Record<string, string[]>
|
||||
}) {
|
||||
return request.post({
|
||||
url: `${BASE}/drafts`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUpdateOaInstanceDraft(
|
||||
id: string,
|
||||
data: {
|
||||
formData?: Record<string, any>
|
||||
title?: string
|
||||
initiatorDeptId?: string
|
||||
initiatorSelectByNode?: Record<string, string[]>
|
||||
}
|
||||
) {
|
||||
return request.put({
|
||||
url: `${BASE}/drafts/${id}`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchDeleteOaInstanceDraft(id: string) {
|
||||
return request.del({
|
||||
url: `${BASE}/drafts/${id}`,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchBatchDeleteOaInstanceDrafts(ids: string[]) {
|
||||
return request.post({
|
||||
url: `${BASE}/drafts/batch-delete`,
|
||||
data: { ids },
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSubmitOaInstanceDraft(
|
||||
id: string,
|
||||
data?: {
|
||||
formData?: Record<string, any>
|
||||
title?: string
|
||||
initiatorDeptId?: string
|
||||
initiatorSelectByNode?: Record<string, string[]>
|
||||
}
|
||||
) {
|
||||
return request.post({
|
||||
url: `${BASE}/drafts/${id}/submit`,
|
||||
data: data || {},
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
/** 详情审批进度投影项(后端 build_progress_items) */
|
||||
export interface OaProgressPerson {
|
||||
id?: string
|
||||
@@ -254,6 +341,10 @@ export function fetchOaTasks(
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchOaPendingTaskCount() {
|
||||
return request.get<{ count: number }>({ url: `${BASE}/tasks/pending/count` })
|
||||
}
|
||||
|
||||
export function fetchOaCcUnreadCount() {
|
||||
return request.get<{ count: number }>({ url: `${BASE}/tasks/cc/unread-count` })
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
class="view-kv"
|
||||
:class="{ 'view-kv-span-2': field.span }"
|
||||
>
|
||||
<dt>{{ field.label }}</dt>
|
||||
<dt v-if="field.label">{{ field.label }}</dt>
|
||||
<dd>
|
||||
<el-tag v-if="field.tagType" :type="field.tagType" size="small">{{
|
||||
formatViewValue(field.value)
|
||||
|
||||
@@ -662,7 +662,7 @@
|
||||
cfg.postIds = [...(ac.postIds || [])]
|
||||
cfg.signMode = ac.signMode || 'or'
|
||||
memberPickBuffer.value = ''
|
||||
rolePickBuffer.value = ''
|
||||
postPickBuffer.value = ''
|
||||
if (showUsersPicker.value) await loadUsers()
|
||||
if (showPostsPicker.value) await loadPosts()
|
||||
drawerVisible.value = true
|
||||
|
||||
@@ -662,4 +662,46 @@
|
||||
.oa-fc-host--left-kit ._fc-l-group > ._fc-l-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* 画布描边 · 方案 E:布局容器淡虚线常驻,叶子字段默认无框(悬停/选中才显)
|
||||
* 权威:docs/superpowers/specs/2026-08-06-oa-form-designer-outline-style-e-design.md
|
||||
* Demo:docs/demos/oa-form-designer-outline-styles.html
|
||||
*/
|
||||
.oa-fc-host ._fd-drag-tool {
|
||||
outline: 1px dashed transparent;
|
||||
}
|
||||
|
||||
.oa-fc-host ._fd-drag-tool:hover {
|
||||
outline-color: rgba(16, 185, 129, 0.35);
|
||||
outline-style: solid;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.oa-fc-host ._fd-drag-tool.active {
|
||||
outline: 1px solid #10b981;
|
||||
}
|
||||
|
||||
/* 含嵌套子组件的布局容器:极淡虚线,便于辨认层级 */
|
||||
.oa-fc-host ._fd-drag-tool:has(._fd-drag-tool) {
|
||||
outline: 1px dashed rgba(16, 185, 129, 0.18);
|
||||
}
|
||||
|
||||
.oa-fc-host ._fd-drag-tool:has(._fd-drag-tool):hover {
|
||||
outline-color: rgba(16, 185, 129, 0.35);
|
||||
outline-style: dashed;
|
||||
}
|
||||
|
||||
.oa-fc-host ._fd-drag-tool:has(._fd-drag-tool).active {
|
||||
outline: 1px solid #10b981;
|
||||
}
|
||||
|
||||
/* 拖拽操作钮:库默认蓝 → Mint */
|
||||
.oa-fc-host ._fd-drag-btn {
|
||||
background-color: #10b981;
|
||||
}
|
||||
|
||||
.oa-fc-host ._fd-drag-btn._fd-drag-danger {
|
||||
background-color: #ff2e2e;
|
||||
}
|
||||
</style>
|
||||
@@ -1374,17 +1374,6 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
menuCode: 'oa:apps:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'OA000000000000000000000000000108',
|
||||
name: 'OaApprovalReceiveAccounts',
|
||||
path: 'receive-accounts',
|
||||
component: '/oa-approval/receive-accounts/index',
|
||||
meta: {
|
||||
title: '收款账户管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'oa:receive-accounts:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'OA000000000000000000000000000103',
|
||||
name: 'OaApprovalStart',
|
||||
@@ -1396,6 +1385,17 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
menuCode: 'oa:start'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'OA000000000000000000000000000109',
|
||||
name: 'OaApprovalDrafts',
|
||||
path: 'drafts',
|
||||
component: '/oa-approval/drafts/index',
|
||||
meta: {
|
||||
title: '草稿箱',
|
||||
keepAlive: true,
|
||||
menuCode: 'oa:drafts'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'OA000000000000000000000000000104',
|
||||
name: 'OaApprovalPending',
|
||||
@@ -1440,6 +1440,17 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
menuCode: 'oa:cc'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'OA000000000000000000000000000108',
|
||||
name: 'OaApprovalReceiveAccounts',
|
||||
path: 'receive-accounts',
|
||||
component: '/oa-approval/receive-accounts/index',
|
||||
meta: {
|
||||
title: '收款账户管理',
|
||||
keepAlive: true,
|
||||
menuCode: 'oa:receive-accounts:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'OA000000000000000000000000000201',
|
||||
name: 'OaApprovalDesigner',
|
||||
|
||||
@@ -3,6 +3,8 @@ import { ref } from 'vue'
|
||||
import { formatOaCcUnreadBadgeCount } from '@/api/oaApproval'
|
||||
|
||||
export const OA_MENU_CODE_CC = 'oa:cc'
|
||||
export const OA_MENU_CODE_DRAFTS = 'oa:drafts'
|
||||
export const OA_MENU_CODE_PENDING = 'oa:pending'
|
||||
|
||||
export const useOaMenuBadgeStore = defineStore('oaMenuBadgeStore', () => {
|
||||
const badgeByMenuCode = ref<Record<string, number>>({})
|
||||
|
||||
@@ -22,6 +22,7 @@ import { resetRouterState } from '@/router/guards/beforeEach'
|
||||
import { useMenuStore } from './menu'
|
||||
import { useQcMenuBadgeStore } from './qc-menu-badge'
|
||||
import { useProductionMenuBadgeStore } from './production-menu-badge'
|
||||
import { useOaMenuBadgeStore } from './oa-menu-badge'
|
||||
import { StorageConfig } from '@/utils/storage/storage-config'
|
||||
import { fetchLogout } from '@/api/auth'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
@@ -149,6 +150,7 @@ export const useUserStore = defineStore(
|
||||
useMenuStore().setHomePath('')
|
||||
useQcMenuBadgeStore().clearBadges()
|
||||
useProductionMenuBadgeStore().clearBadges()
|
||||
useOaMenuBadgeStore().clearBadges()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+8
@@ -57,6 +57,12 @@ declare namespace Api {
|
||||
deptName?: string
|
||||
roleIds?: string[]
|
||||
postIds?: string[]
|
||||
/** 经启用职位继承的启用角色 ID */
|
||||
postInheritedRoleIds?: string[]
|
||||
/** 有效角色 ID(启用直接绑定 ∪ 职位继承) */
|
||||
effectiveRoleIds?: string[]
|
||||
/** 有效角色名称 */
|
||||
effectiveRoleNames?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,6 +639,8 @@ declare namespace Api {
|
||||
tenantNo: string
|
||||
orderNum: number
|
||||
remark: string
|
||||
boundUserNames?: string[]
|
||||
boundPostNames?: string[]
|
||||
createTime: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
import { fetchOaCcUnreadCount } from '@/api/oaApproval'
|
||||
import { OA_MENU_CODE_CC, useOaMenuBadgeStore } from '@/store/modules/oa-menu-badge'
|
||||
import {
|
||||
fetchOaCcUnreadCount,
|
||||
fetchOaInstanceDraftCount,
|
||||
fetchOaPendingTaskCount
|
||||
} from '@/api/oaApproval'
|
||||
import {
|
||||
OA_MENU_CODE_CC,
|
||||
OA_MENU_CODE_DRAFTS,
|
||||
OA_MENU_CODE_PENDING,
|
||||
useOaMenuBadgeStore
|
||||
} from '@/store/modules/oa-menu-badge'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
|
||||
const OA_BADGE_CODES = new Set([OA_MENU_CODE_CC, OA_MENU_CODE_DRAFTS, OA_MENU_CODE_PENDING])
|
||||
|
||||
export function resolveOaMenuBadgeCode(item: AppRouteRecord): string | undefined {
|
||||
const code = (item.meta.menuCode || '').trim()
|
||||
if (code === OA_MENU_CODE_CC) return code
|
||||
if (String(item.name || '') === 'OaApprovalCc') return OA_MENU_CODE_CC
|
||||
if (OA_BADGE_CODES.has(code)) return code
|
||||
const name = String(item.name || '')
|
||||
if (name === 'OaApprovalCc') return OA_MENU_CODE_CC
|
||||
if (name === 'OaApprovalDrafts') return OA_MENU_CODE_DRAFTS
|
||||
if (name === 'OaApprovalPending') return OA_MENU_CODE_PENDING
|
||||
// path 兜底仅限 oa: 菜单,避免命中旧工作流 pending
|
||||
if (!code.startsWith('oa:')) return undefined
|
||||
const path = String(item.path || '')
|
||||
if (path.endsWith('/cc') || path === 'cc') return OA_MENU_CODE_CC
|
||||
if (path.endsWith('/drafts') || path === 'drafts') return OA_MENU_CODE_DRAFTS
|
||||
if (path.endsWith('/pending') || path === 'pending') return OA_MENU_CODE_PENDING
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -29,11 +47,31 @@ export async function refreshOaMenuBadges() {
|
||||
const menuStore = useMenuStore()
|
||||
const badgeStore = useOaMenuBadgeStore()
|
||||
const visibleCodes = collectVisibleOaMenuBadgeCodes(menuStore.menuList)
|
||||
if (!visibleCodes.includes(OA_MENU_CODE_CC)) return
|
||||
try {
|
||||
const data = await fetchOaCcUnreadCount()
|
||||
badgeStore.setBadge(OA_MENU_CODE_CC, data?.count ?? 0)
|
||||
} catch {
|
||||
badgeStore.setBadge(OA_MENU_CODE_CC, 0)
|
||||
if (!visibleCodes.length) return
|
||||
|
||||
const jobs: Promise<void>[] = []
|
||||
|
||||
if (visibleCodes.includes(OA_MENU_CODE_CC)) {
|
||||
jobs.push(
|
||||
fetchOaCcUnreadCount()
|
||||
.then((data) => badgeStore.setBadge(OA_MENU_CODE_CC, data?.count ?? 0))
|
||||
.catch(() => badgeStore.setBadge(OA_MENU_CODE_CC, 0))
|
||||
)
|
||||
}
|
||||
if (visibleCodes.includes(OA_MENU_CODE_DRAFTS)) {
|
||||
jobs.push(
|
||||
fetchOaInstanceDraftCount()
|
||||
.then((data) => badgeStore.setBadge(OA_MENU_CODE_DRAFTS, data?.count ?? 0))
|
||||
.catch(() => badgeStore.setBadge(OA_MENU_CODE_DRAFTS, 0))
|
||||
)
|
||||
}
|
||||
if (visibleCodes.includes(OA_MENU_CODE_PENDING)) {
|
||||
jobs.push(
|
||||
fetchOaPendingTaskCount()
|
||||
.then((data) => badgeStore.setBadge(OA_MENU_CODE_PENDING, data?.count ?? 0))
|
||||
.catch(() => badgeStore.setBadge(OA_MENU_CODE_PENDING, 0))
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(jobs)
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
<!-- OA 草稿箱:发起审批暂存记录 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">草稿箱</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="流程名称">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入流程名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="保存时间">
|
||||
<el-date-picker
|
||||
v-model="searchForm.updateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板分组">
|
||||
<el-select v-model="searchForm.groupId" placeholder="全部" clearable>
|
||||
<el-option
|
||||
v-for="g in groupOptions"
|
||||
:key="g.id"
|
||||
:label="g.groupName"
|
||||
:value="g.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="indexMethod"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="appName"
|
||||
label="流程名称"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.appName || row.title || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="versionNo"
|
||||
label="模板版本"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.versionNo != null ? `v${row.versionNo}` : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="updateTime"
|
||||
label="最近保存"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.updateTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" min-width="100" align="center">
|
||||
<template #default>
|
||||
<el-tag size="small" type="warning">草稿</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="320" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="openEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:loading="submittingId === row.id"
|
||||
@click="handleSubmit(row)"
|
||||
>
|
||||
<el-icon><Promotion /></el-icon>
|
||||
提交审批
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
@size-change="loadList"
|
||||
@current-change="loadList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onActivated, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Delete, Edit, Promotion, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
fetchBatchDeleteOaInstanceDrafts,
|
||||
fetchDeleteOaInstanceDraft,
|
||||
fetchOaGroupList,
|
||||
fetchOaInstanceDraftList,
|
||||
fetchSubmitOaInstanceDraft
|
||||
} from '@/api/oaApproval'
|
||||
import { refreshOaMenuBadges } from '@/utils/oa-menu-badge'
|
||||
|
||||
defineOptions({ name: 'OaApprovalDrafts' })
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const selectedRows = ref<any[]>([])
|
||||
const groupOptions = ref<Array<{ id: string; groupName: string }>>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const submittingId = ref('')
|
||||
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
groupId: '' as string,
|
||||
updateRange: null as string[] | null
|
||||
})
|
||||
|
||||
function indexMethod(index: number) {
|
||||
return (page.value - 1) * pageSize.value + index + 1
|
||||
}
|
||||
|
||||
function onSelectionChange(rows: any[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
try {
|
||||
const res = await fetchOaGroupList()
|
||||
groupOptions.value = res?.list || []
|
||||
} catch {
|
||||
groupOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const range = searchForm.updateRange || []
|
||||
const res = await fetchOaInstanceDraftList({
|
||||
keyword: searchForm.keyword || undefined,
|
||||
groupId: searchForm.groupId || undefined,
|
||||
updateTimeFrom: range[0] || undefined,
|
||||
updateTimeTo: range[1] || undefined,
|
||||
page: page.value,
|
||||
pageSize: pageSize.value
|
||||
})
|
||||
tableData.value = res?.list || []
|
||||
total.value = res?.total || 0
|
||||
selectedRows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.groupId = ''
|
||||
searchForm.updateRange = null
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function openEdit(row: any) {
|
||||
if (!row?.id) return
|
||||
router.push({
|
||||
path: '/system-manage/oa-approval/start',
|
||||
query: { draftId: row.id }
|
||||
})
|
||||
}
|
||||
|
||||
async function handleSubmit(row: any) {
|
||||
if (!row?.id) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确认提交该草稿并发起审批吗?', '提交审批', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '提交',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
submittingId.value = row.id
|
||||
try {
|
||||
const data: any = await fetchSubmitOaInstanceDraft(row.id)
|
||||
await loadList()
|
||||
void refreshOaMenuBadges()
|
||||
if (data?.id) {
|
||||
router.push({ path: `/system-manage/oa-approval/instances/${data.id}` })
|
||||
}
|
||||
} catch {
|
||||
openEdit(row)
|
||||
} finally {
|
||||
submittingId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
if (!row?.id) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该草稿吗?删除后不可恢复。', '删除草稿', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await fetchDeleteOaInstanceDraft(row.id)
|
||||
await loadList()
|
||||
void refreshOaMenuBadges()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id).filter(Boolean)
|
||||
if (!ids.length) return
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除选中的 ${ids.length} 条草稿吗?`, '批量删除', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await fetchBatchDeleteOaInstanceDrafts(ids)
|
||||
await loadList()
|
||||
void refreshOaMenuBadges()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadGroups()
|
||||
await loadList()
|
||||
void refreshOaMenuBadges()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
loadList()
|
||||
void refreshOaMenuBadges()
|
||||
})
|
||||
</script>
|
||||
@@ -206,6 +206,7 @@
|
||||
import OaProgressChain from '@/components/oa-approval/OaProgressChain.vue'
|
||||
import { MAX_ADMIN_LIST_PAGE_SIZE } from '@/utils/fetch-paged-list'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { refreshOaMenuBadges } from '@/utils/oa-menu-badge'
|
||||
import {
|
||||
OA_FC_DEFAULT_GUTTER,
|
||||
ensureOaUploadRuleProps
|
||||
@@ -398,6 +399,7 @@
|
||||
if (kind === 'approve') await fetchApproveOaTask(task.id, comment || undefined)
|
||||
else await fetchRejectOaTask(task.id, comment || undefined)
|
||||
await load()
|
||||
void refreshOaMenuBadges()
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
@@ -409,6 +411,7 @@
|
||||
try {
|
||||
await fetchWithdrawOaInstance(String(route.params.id || ''))
|
||||
await load()
|
||||
void refreshOaMenuBadges()
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
@@ -433,6 +436,7 @@
|
||||
try {
|
||||
await fetchReturnOaTask(task.id, comment)
|
||||
await load()
|
||||
void refreshOaMenuBadges()
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
@@ -497,6 +501,7 @@
|
||||
})
|
||||
addSignVisible.value = false
|
||||
await load()
|
||||
void refreshOaMenuBadges()
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
@@ -524,6 +529,7 @@
|
||||
})
|
||||
transferVisible.value = false
|
||||
await load()
|
||||
void refreshOaMenuBadges()
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
import { fetchOaTasks } from '@/api/oaApproval'
|
||||
import { refreshOaMenuBadges } from '@/utils/oa-menu-badge'
|
||||
|
||||
defineOptions({ name: 'OaApprovalPending' })
|
||||
|
||||
@@ -131,6 +132,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
onActivated(load)
|
||||
onMounted(() => {
|
||||
load()
|
||||
void refreshOaMenuBadges()
|
||||
})
|
||||
onActivated(() => {
|
||||
load()
|
||||
void refreshOaMenuBadges()
|
||||
})
|
||||
</script>
|
||||
@@ -126,10 +126,14 @@
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dlgVisible = false">
|
||||
<el-button class="btn-ghost" @click="closeDialog">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="savingDraft" @click="saveDraft">
|
||||
<el-icon><Document /></el-icon>
|
||||
{{ editingDraftId ? '保存草稿' : '暂存草稿' }}
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="submitting" @click="submit">
|
||||
<el-icon><Check /></el-icon>
|
||||
提交
|
||||
@@ -143,21 +147,26 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Check, Close, Promotion, Search } from '@element-plus/icons-vue'
|
||||
import { Check, Close, Document, Promotion, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { fetchUserList } from '@/api/admin'
|
||||
import {
|
||||
fetchCreateOaInstanceDraft,
|
||||
fetchOaFlowPreview,
|
||||
fetchOaGroupList,
|
||||
fetchOaInstanceDetail,
|
||||
fetchOaInstanceDraftDetail,
|
||||
fetchOaTemplateDetail,
|
||||
fetchOaTemplates,
|
||||
fetchStartOaInstance,
|
||||
fetchSubmitOaInstanceDraft,
|
||||
fetchUpdateOaInstanceDraft,
|
||||
type OaFlowPreviewStep
|
||||
} from '@/api/oaApproval'
|
||||
import OaAppIcon from '@/components/oa-approval/OaAppIcon.vue'
|
||||
import OaFlowPreview from '@/components/oa-approval/OaFlowPreview.vue'
|
||||
import { MAX_ADMIN_LIST_PAGE_SIZE } from '@/utils/fetch-paged-list'
|
||||
import { refreshOaMenuBadges } from '@/utils/oa-menu-badge'
|
||||
import {
|
||||
OA_FC_DEFAULT_GUTTER,
|
||||
ensureOaUploadRuleProps
|
||||
@@ -214,6 +223,8 @@
|
||||
})
|
||||
const fapi = ref<any>(null)
|
||||
const submitting = ref(false)
|
||||
const savingDraft = ref(false)
|
||||
const editingDraftId = ref('')
|
||||
const needInitiatorSelect = ref(false)
|
||||
const initiatorSelectByNode = ref<Record<string, string[]>>({})
|
||||
const userOptions = ref<Array<{ id: string; nickName: string; userNo: string }>>([])
|
||||
@@ -334,6 +345,7 @@
|
||||
}
|
||||
|
||||
async function onPick(t: any) {
|
||||
editingDraftId.value = ''
|
||||
picked.value = t
|
||||
formData.value = {}
|
||||
formRule.value = []
|
||||
@@ -418,6 +430,77 @@
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
dlgVisible.value = false
|
||||
editingDraftId.value = ''
|
||||
}
|
||||
|
||||
async function openDraft(draftId: string) {
|
||||
const detail = await fetchOaInstanceDraftDetail(draftId)
|
||||
const appId = String(detail?.appId || '').trim()
|
||||
if (!appId) {
|
||||
ElMessage.error('草稿缺少审批模板')
|
||||
return
|
||||
}
|
||||
let row = templates.value.find((t: any) => t.id === appId)
|
||||
if (!row) {
|
||||
row = {
|
||||
id: appId,
|
||||
appName: detail?.appName || '发起审批'
|
||||
}
|
||||
}
|
||||
await onPick(row)
|
||||
editingDraftId.value = draftId
|
||||
const saved = { ...(detail?.formData || {}) }
|
||||
formData.value = await applyOwnerFieldDefaults(
|
||||
formRule.value,
|
||||
saved,
|
||||
(userStore.info || userStore.getUserInfo) as any
|
||||
)
|
||||
formData.value = applyOvertimeKitDefaults(
|
||||
{ rule: formRule.value },
|
||||
formData.value,
|
||||
(userStore.info || userStore.getUserInfo) as any
|
||||
)
|
||||
const linked = applyConfirmationKitDateLinkage(
|
||||
{ rule: formRule.value },
|
||||
formData.value
|
||||
)
|
||||
formData.value = linked.data
|
||||
confirmationKitPrevSnapshot = { ...linked.data }
|
||||
leaveKitPrevSnapshot = { ...formData.value }
|
||||
tripKitPrevSnapshot = { ...formData.value }
|
||||
const sel = detail?.initiatorSelectByNode
|
||||
initiatorSelectByNode.value =
|
||||
sel && typeof sel === 'object' && !Array.isArray(sel) ? { ...sel } : {}
|
||||
await loadFlowPreview(appId)
|
||||
}
|
||||
|
||||
async function saveDraft() {
|
||||
if (!picked.value?.id) return
|
||||
savingDraft.value = true
|
||||
try {
|
||||
const payload = {
|
||||
formData: { ...formData.value },
|
||||
initiatorSelectByNode: { ...initiatorSelectByNode.value },
|
||||
title: picked.value.appName || ''
|
||||
}
|
||||
if (editingDraftId.value) {
|
||||
await fetchUpdateOaInstanceDraft(editingDraftId.value, payload)
|
||||
} else {
|
||||
const created: any = await fetchCreateOaInstanceDraft({
|
||||
appId: picked.value.id,
|
||||
...payload
|
||||
})
|
||||
if (created?.id) editingDraftId.value = created.id
|
||||
}
|
||||
void refreshOaMenuBadges()
|
||||
closeDialog()
|
||||
} finally {
|
||||
savingDraft.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function syncLeaveKitDurations() {
|
||||
if (leaveKitSyncing) return
|
||||
if (!formRule.value.length) return
|
||||
@@ -604,12 +687,24 @@
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const data: any = await fetchStartOaInstance({
|
||||
appId: picked.value.id,
|
||||
const payload = {
|
||||
formData: { ...formData.value },
|
||||
initiatorSelectByNode: { ...initiatorSelectByNode.value }
|
||||
})
|
||||
dlgVisible.value = false
|
||||
}
|
||||
let data: any
|
||||
if (editingDraftId.value) {
|
||||
data = await fetchSubmitOaInstanceDraft(editingDraftId.value, {
|
||||
...payload,
|
||||
title: picked.value.appName || ''
|
||||
})
|
||||
} else {
|
||||
data = await fetchStartOaInstance({
|
||||
appId: picked.value.id,
|
||||
...payload
|
||||
})
|
||||
}
|
||||
closeDialog()
|
||||
void refreshOaMenuBadges()
|
||||
if (data?.id) {
|
||||
router.push({ path: `/system-manage/oa-approval/instances/${data.id}` })
|
||||
}
|
||||
@@ -624,9 +719,16 @@
|
||||
const [tpl, groupsRes] = await Promise.all([fetchOaTemplates(), fetchOaGroupList()])
|
||||
templates.value = tpl?.list || []
|
||||
groupOptions.value = groupsRes?.list || []
|
||||
const qDraftId = String(route.query.draftId || '').trim()
|
||||
const qAppId = String(route.query.appId || '').trim()
|
||||
const qCopyFrom = String(route.query.copyFromInstanceId || '').trim()
|
||||
if (qAppId) {
|
||||
if (qDraftId) {
|
||||
try {
|
||||
await openDraft(qDraftId)
|
||||
} catch {
|
||||
ElMessage.error('无法打开草稿')
|
||||
}
|
||||
} else if (qAppId) {
|
||||
const row = templates.value.find((t: any) => t.id === qAppId)
|
||||
if (row) {
|
||||
await onPick(row)
|
||||
|
||||
@@ -72,7 +72,46 @@
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
class="no-auto-icon"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleView(row)"
|
||||
>
|
||||
{{ row.roleName || '-' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="拥有用户"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="role-bound-names-cell">{{
|
||||
(row.boundUserNames || []).length
|
||||
? (row.boundUserNames || []).join('、')
|
||||
: '—'
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="关联职位"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="role-bound-names-cell">{{
|
||||
(row.boundPostNames || []).length
|
||||
? (row.boundPostNames || []).join('、')
|
||||
: '—'
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="dataScope"
|
||||
label="数据权限"
|
||||
@@ -1054,6 +1093,15 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.role-bound-names-cell {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.role-dialog-form,
|
||||
.data-scope-form {
|
||||
:deep(.el-form-item) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<section class="panel">
|
||||
<div class="flex gap-4">
|
||||
<!-- 左侧:组织树 -->
|
||||
<div class="user-dept-aside w-[280px] shrink-0 border-r border-[var(--mint-border)] pr-4">
|
||||
<div class="user-dept-aside w-[200px] shrink-0 border-r border-[var(--mint-border)] pr-4">
|
||||
<!-- 标题与搜索同处 panel-top:避免标题栏底边与独立输入框之间出现不可控的视觉断层 -->
|
||||
<div class="panel-top user-dept-aside__head">
|
||||
<h2>组织</h2>
|
||||
@@ -131,7 +131,14 @@
|
||||
<template #default="{ row }">
|
||||
<div class="user-nick-cell">
|
||||
<el-avatar :size="24" :src="resolveUserAvatarFromInfo(row)" />
|
||||
<span class="user-nick-cell__name">{{ row.nickName }}</span>
|
||||
<el-button
|
||||
class="user-nick-cell__name no-auto-icon"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleView(row)"
|
||||
>
|
||||
{{ row.nickName || '-' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -146,6 +153,26 @@
|
||||
{{ formatUserDeptDisplay(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="职位"
|
||||
min-width="140"
|
||||
align="left"
|
||||
header-align="left"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="user-multi-label-cell">{{ formatUserPostDisplay(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="角色"
|
||||
min-width="160"
|
||||
align="left"
|
||||
header-align="left"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="user-multi-label-cell">{{ formatUserEffectiveRoleDisplay(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="phoneNo"
|
||||
label="手机号"
|
||||
@@ -357,22 +384,6 @@
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" style="grid-column: 1 / -1">
|
||||
<el-select
|
||||
v-model="formData.roleIds"
|
||||
multiple
|
||||
filterable
|
||||
placeholder="请选择角色"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in roleOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="职位" style="grid-column: 1 / -1">
|
||||
<el-select
|
||||
v-model="formData.postIds"
|
||||
@@ -511,15 +522,28 @@
|
||||
return deptPathLabelMap.value.get(id) || row.deptName || '—'
|
||||
}
|
||||
|
||||
function formatUserRoleDisplay(row: Api.User.UserItem) {
|
||||
const ids = row.roleIds || []
|
||||
if (!ids.length) return '—'
|
||||
function formatRoleIdsDisplay(ids: string[] | undefined | null) {
|
||||
if (!ids?.length) return '—'
|
||||
const labels = ids
|
||||
.map((id) => roleOptions.value.find((o) => o.value === id)?.label)
|
||||
.filter((name): name is string => Boolean(name))
|
||||
return labels.length ? labels.join('、') : '—'
|
||||
}
|
||||
|
||||
function formatUserEffectiveRoleDisplay(row: Api.User.UserItem) {
|
||||
const names = row.effectiveRoleNames || []
|
||||
if (names.length) return names.join('、')
|
||||
return formatRoleIdsDisplay(row.effectiveRoleIds)
|
||||
}
|
||||
|
||||
function formatUserDirectRoleDisplay(row: Api.User.UserItem) {
|
||||
return formatRoleIdsDisplay(row.roleIds)
|
||||
}
|
||||
|
||||
function formatUserPostInheritedRoleDisplay(row: Api.User.UserItem) {
|
||||
return formatRoleIdsDisplay(row.postInheritedRoleIds)
|
||||
}
|
||||
|
||||
function formatUserPostDisplay(row: Api.User.UserItem) {
|
||||
const ids = row.postIds || []
|
||||
if (!ids.length) return '—'
|
||||
@@ -558,7 +582,6 @@
|
||||
status: '1',
|
||||
orderNum: 0,
|
||||
remark: '',
|
||||
roleIds: [] as string[],
|
||||
postIds: [] as string[]
|
||||
})
|
||||
|
||||
@@ -667,7 +690,6 @@
|
||||
formData.status = '1'
|
||||
formData.orderNum = 0
|
||||
formData.remark = ''
|
||||
formData.roleIds = []
|
||||
formData.postIds = []
|
||||
supervisorUserOptions.value = []
|
||||
editingId.value = null
|
||||
@@ -708,7 +730,6 @@
|
||||
formData.status = detail.status || '1'
|
||||
formData.orderNum = detail.orderNum ?? 0
|
||||
formData.remark = detail.remark || ''
|
||||
formData.roleIds = detail.roleIds ? [...detail.roleIds] : []
|
||||
formData.postIds = detail.postIds ? [...detail.postIds] : []
|
||||
if (formData.supervisorUserId) {
|
||||
await remoteSearchSupervisorUsers(detail.nickName || '')
|
||||
@@ -744,7 +765,6 @@
|
||||
status: formData.status,
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined,
|
||||
roleIds: [...formData.roleIds],
|
||||
postIds: [...formData.postIds]
|
||||
})
|
||||
} else {
|
||||
@@ -763,7 +783,6 @@
|
||||
status: formData.status,
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined,
|
||||
roleIds: formData.roleIds.length ? formData.roleIds : undefined,
|
||||
postIds: formData.postIds.length ? formData.postIds : undefined
|
||||
})
|
||||
}
|
||||
@@ -882,7 +901,9 @@
|
||||
value: row.status === '1' ? '正常' : '停用',
|
||||
tagType: row.status === '1' ? 'success' : 'info'
|
||||
},
|
||||
{ label: '角色', value: formatUserRoleDisplay(row), span: true },
|
||||
{ label: '有效角色', value: formatUserEffectiveRoleDisplay(row), span: true },
|
||||
{ label: '直接绑定角色', value: formatUserDirectRoleDisplay(row), span: true },
|
||||
{ label: '职位继承角色', value: formatUserPostInheritedRoleDisplay(row), span: true },
|
||||
{ label: '职位', value: formatUserPostDisplay(row), span: true },
|
||||
{ label: '创建时间', value: row.createTime },
|
||||
{ label: '备注', value: row.remark, span: true }
|
||||
@@ -964,9 +985,24 @@
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
/* 职位 / 角色多项用顿号分隔,列宽不足时自然换行 */
|
||||
.user-multi-label-cell {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
line-height: 1.5;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user