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:
zlwuandCursor committed 2026-08-06 13:58:47 +08:00
1 parent 7af53fc344
commit de22678c87
56 files changed
+11861 -6082

No files matched your search

+91
View File
@@ -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
+42
View File
@@ -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>
+22 -11
View File
@@ -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>>({})
+2
View File
@@ -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()
}
/**
+8
View File
@@ -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
}
+48 -10
View File
@@ -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>
+109 -7
View File
@@ -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)
+49 -1
View File
@@ -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) {
+63 -27
View File
@@ -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>