feat(oa): add owner person/dept fields with initiator defaults

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-30 17:54:48 +08:00
1 parent 3b46b10604
commit 4ebc2dbe82
18 files changed
+791 -15

No files matched your search

@@ -76,4 +76,15 @@
._fc-exceed .el-upload {
display: none;
}
/* 请假/加班套件时长:左对齐(EP 默认 center) */
.oa-leave-kit .el-input-number .el-input__inner,
.oa-leave-duration.el-input-number .el-input__inner,
.oa-leave-duration .el-input__inner,
.oa-overtime-kit .el-input-number .el-input__inner,
.oa-overtime-duration.el-input-number .el-input__inner,
.oa-overtime-duration .el-input__inner,
.el-input-number.is-left .el-input__inner {
text-align: left !important;
}
}
@@ -0,0 +1,45 @@
<!-- FormCreate:归属部门(默认当前登录人主部门;allowModify 控制可否改选) -->
<template>
<OaDeptSelect
:model-value="modelValue"
:disabled="effectiveDisabled"
:multiple="false"
:form-create-inject="formCreateInject"
@update:model-value="onUpdate"
@change="onChange"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import OaDeptSelect from '@/components/oa-approval/OaDeptSelect.vue'
import type { OaDeptValueItem } from '@/utils/oa-dept-value'
defineOptions({ name: 'ownerDept' })
const props = withDefaults(
defineProps<{
modelValue?: unknown
disabled?: boolean
/** 是否允许改选;默认 false(只读展示默认值) */
allowModify?: boolean
formCreateInject?: Record<string, unknown>
}>(),
{ allowModify: false }
)
const emit = defineEmits<{
(e: 'update:modelValue', v: OaDeptValueItem | OaDeptValueItem[] | null): void
(e: 'change', v: OaDeptValueItem | OaDeptValueItem[] | null): void
}>()
const effectiveDisabled = computed(() => !!props.disabled || !props.allowModify)
function onUpdate(v: OaDeptValueItem | OaDeptValueItem[] | null) {
emit('update:modelValue', v)
}
function onChange(v: OaDeptValueItem | OaDeptValueItem[] | null) {
emit('change', v)
}
</script>
@@ -0,0 +1,45 @@
<!-- FormCreate:归属人(默认当前登录人;allowModify 控制可否改选) -->
<template>
<OaPersonSelect
:model-value="modelValue"
:disabled="effectiveDisabled"
:multiple="false"
:form-create-inject="formCreateInject"
@update:model-value="onUpdate"
@change="onChange"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import OaPersonSelect from '@/components/oa-approval/OaPersonSelect.vue'
import type { OaPersonValueItem } from '@/utils/oa-person-value'
defineOptions({ name: 'ownerPerson' })
const props = withDefaults(
defineProps<{
modelValue?: unknown
disabled?: boolean
/** 是否允许改选;默认 false(只读展示默认值) */
allowModify?: boolean
formCreateInject?: Record<string, unknown>
}>(),
{ allowModify: false }
)
const emit = defineEmits<{
(e: 'update:modelValue', v: OaPersonValueItem | OaPersonValueItem[] | null): void
(e: 'change', v: OaPersonValueItem | OaPersonValueItem[] | null): void
}>()
const effectiveDisabled = computed(() => !!props.disabled || !props.allowModify)
function onUpdate(v: OaPersonValueItem | OaPersonValueItem[] | null) {
emit('update:modelValue', v)
}
function onChange(v: OaPersonValueItem | OaPersonValueItem[] | null) {
emit('change', v)
}
</script>
@@ -172,7 +172,7 @@
function isPersonLikeField(field: string) {
const t = meta(field)?.type
return t === 'person' || t === 'initiator'
return t === 'person' || t === 'ownerPerson' || t === 'initiator'
}
function isPostField(field: string) {
@@ -186,7 +186,9 @@
}
function defaultValueForType(type: string, opts?: ConditionCandidate['options']) {
if (type === 'person' || type === 'post' || type === 'initiator') return [] as string[]
if (type === 'person' || type === 'ownerPerson' || type === 'post' || type === 'initiator') {
return [] as string[]
}
return opts?.[0]?.value ?? ''
}
@@ -335,7 +337,7 @@
}
onMounted(() => {
if (props.candidates.some((c) => c.type === 'person' || c.type === 'initiator')) {
if (props.candidates.some((c) => c.type === 'person' || c.type === 'ownerPerson' || c.type === 'initiator')) {
loadUsers()
}
if (props.candidates.some((c) => c.type === 'post')) {
@@ -344,7 +346,10 @@
})
watch(
() => props.candidates.some((c) => c.type === 'person' || c.type === 'initiator'),
() =>
props.candidates.some(
(c) => c.type === 'person' || c.type === 'ownerPerson' || c.type === 'initiator'
),
(need) => {
if (need && !userOptions.value.length) loadUsers()
}
@@ -5,6 +5,7 @@ export const CONDITION_ELIGIBLE_TYPES = new Set([
'number',
'money',
'person',
'ownerPerson',
'post',
'radio',
'select',
@@ -32,7 +33,7 @@ export interface ConditionCandidate {
}
export function opsForFieldType(type: string): Array<{ value: ConditionOp; label: string }> {
if (type === 'person' || type === 'post' || type === 'initiator') {
if (type === 'person' || type === 'ownerPerson' || type === 'post' || type === 'initiator') {
return [{ value: 'contains', label: '包含' }]
}
// 单选/下拉为离散选项,仅等于/不等于;输入框常填数字参与区间比较,与数字/金额/日期同套运算符
@@ -0,0 +1,19 @@
import FcDesigner from '@form-create/designer'
import OaOwnerDeptSelect from '@/components/oa-approval/OaOwnerDeptSelect.vue'
import { ownerDeptSelectDragRule } from '@/utils/oa-owner-dept-select-rule'
const FLAG = '__oaOwnerDeptSelectConfigured'
export function setupOaOwnerDeptSelect(formCreate?: {
component?: (name: string, c: unknown) => void
}) {
if ((FcDesigner as any)[FLAG]) return
FcDesigner.component('ownerDept', OaOwnerDeptSelect)
if (typeof formCreate?.component === 'function') {
formCreate.component('ownerDept', OaOwnerDeptSelect)
}
if (typeof FcDesigner.addDragRule === 'function') {
FcDesigner.addDragRule(ownerDeptSelectDragRule)
}
;(FcDesigner as any)[FLAG] = true
}
@@ -0,0 +1,19 @@
import FcDesigner from '@form-create/designer'
import OaOwnerPersonSelect from '@/components/oa-approval/OaOwnerPersonSelect.vue'
import { ownerPersonSelectDragRule } from '@/utils/oa-owner-person-select-rule'
const FLAG = '__oaOwnerPersonSelectConfigured'
export function setupOaOwnerPersonSelect(formCreate?: {
component?: (name: string, c: unknown) => void
}) {
if ((FcDesigner as any)[FLAG]) return
FcDesigner.component('ownerPerson', OaOwnerPersonSelect)
if (typeof formCreate?.component === 'function') {
formCreate.component('ownerPerson', OaOwnerPersonSelect)
}
if (typeof FcDesigner.addDragRule === 'function') {
FcDesigner.addDragRule(ownerPersonSelectDragRule)
}
;(FcDesigner as any)[FLAG] = true
}
@@ -0,0 +1,52 @@
/** 与 FormCreate 官方 unique 同类:生成不重复 field */
let _uidSeq = 0
function uniqueId() {
const num = 370 + ++_uidSeq
return (
'F' +
Math.random().toString(36).slice(3, 6) +
Number(`${Date.now()}`).toString(36) +
num.toString(36) +
'od'
)
}
/** FormCreate Designer DragRule:归属部门(默认当前登录人主部门) */
export const ownerDeptSelectDragRule = {
menu: 'main' as const,
icon: 'icon-tree',
label: '归属部门',
name: 'ownerDept',
input: true,
mask: true,
validate: ['string', 'array', 'object'],
event: ['change'],
rule() {
return {
type: 'ownerDept',
field: uniqueId(),
title: '归属部门',
info: '',
$required: false,
props: {
allowModify: false
}
}
},
props() {
return [
{
type: 'switch',
field: 'allowModify',
title: '允许修改',
value: false,
props: { activeText: '是', inactiveText: '否' }
},
{
type: 'input',
field: 'formCreateInfo',
title: '提示信息'
}
]
}
}
@@ -0,0 +1,160 @@
import { fetchDeptTree } from '@/api/admin'
import { isDeptValueItem, type OaDeptValueItem } from '@/utils/oa-dept-value'
import { isPersonValueItem, type OaPersonValueItem } from '@/utils/oa-person-value'
import { buildPathLabelMapFromTreeRoots } from '@/utils/tree-path'
export type OwnerUserSnapshot = {
id?: string
userId?: string
realName?: string
userName?: string
username?: string
regionId?: string
deptId?: string
regionName?: string
deptName?: string
}
function walkRules(rules: unknown[], out: Record<string, any>[]) {
for (const r of rules) {
if (!r || typeof r !== 'object') continue
const rule = r as Record<string, any>
out.push(rule)
const children = rule.children || rule.props?.rule || []
if (Array.isArray(children)) walkRules(children, out)
}
}
function flattenRules(schemaOrRules: unknown): Record<string, any>[] {
if (Array.isArray(schemaOrRules)) {
const out: Record<string, any>[] = []
walkRules(schemaOrRules, out)
return out
}
if (schemaOrRules && typeof schemaOrRules === 'object') {
const root = schemaOrRules as any
const rules = root.rule || root.rules || []
if (Array.isArray(rules)) {
const out: Record<string, any>[] = []
walkRules(rules, out)
return out
}
}
return []
}
function isEmptyOwnerValue(raw: unknown): boolean {
if (raw == null || raw === '') return true
if (Array.isArray(raw)) return raw.length === 0
if (typeof raw === 'object') {
const o = raw as Record<string, unknown>
return !String(o.id || '').trim()
}
return false
}
export function buildCurrentOwnerPerson(user: OwnerUserSnapshot | null | undefined): OaPersonValueItem | null {
if (!user) return null
const id = String(user.id || user.userId || '').trim()
if (!id) return null
const name = String(user.realName || user.userName || user.username || '').trim() || id
return { id, name }
}
export function buildCurrentOwnerDeptSync(
user: OwnerUserSnapshot | null | undefined
): OaDeptValueItem | null {
if (!user) return null
const id = String(user.regionId || user.deptId || '').trim()
if (!id) return null
const name = String(user.regionName || user.deptName || '').trim() || id
return { id, name }
}
/** 用部门树解析全路径名称;失败则回退 id / 登录态名称 */
export async function resolveCurrentOwnerDept(
user: OwnerUserSnapshot | null | undefined
): Promise<OaDeptValueItem | null> {
const base = buildCurrentOwnerDeptSync(user)
if (!base) return null
try {
const tree = (await fetchDeptTree()) || []
const map = buildPathLabelMapFromTreeRoots(tree as any, { nameKey: 'deptName' })
const path = map.get(base.id)
if (path) return { id: base.id, name: path }
} catch {
/* 无部门树权限时仍用 id/名称回退 */
}
return base
}
/**
* 发起页:写入当前登录人 / 主部门。
* - allowModify 为假:始终覆盖为当前用户(防展示旧值 / 复制发起)
* - allowModify 为真:仅空字段补默认
*/
export async function applyOwnerFieldDefaults(
schemaOrRules: unknown,
formData: Record<string, any>,
user: OwnerUserSnapshot | null | undefined
): Promise<Record<string, any>> {
const rules = flattenRules(schemaOrRules)
const hasPerson = rules.some((r) => String(r.type || '') === 'ownerPerson')
const hasDept = rules.some((r) => String(r.type || '') === 'ownerDept')
if (!hasPerson && !hasDept) return { ...formData }
const person = hasPerson ? buildCurrentOwnerPerson(user) : null
const dept = hasDept ? await resolveCurrentOwnerDept(user) : null
const next = { ...formData }
for (const rule of rules) {
const type = String(rule.type || '')
const field = String(rule.field || rule.name || '').trim()
if (!field) continue
const allowModify = rule.props?.allowModify === true
if (type === 'ownerPerson' && person) {
if (!allowModify || isEmptyOwnerValue(next[field])) {
next[field] = person
}
}
if (type === 'ownerDept' && dept) {
if (!allowModify || isEmptyOwnerValue(next[field])) {
next[field] = dept
}
}
}
return next
}
/** 确保 props.allowModify 有明确布尔(缺省 false) */
export function ensureOwnerFieldRuleProps(rules: unknown): any[] {
if (!Array.isArray(rules)) return []
return rules.map((raw) => {
if (!raw || typeof raw !== 'object') return raw
const rule = { ...(raw as any) }
const type = String(rule.type || '')
if (type === 'ownerPerson' || type === 'ownerDept') {
const props = { ...(rule.props || {}) }
if (props.allowModify !== true) props.allowModify = false
rule.props = props
}
if (Array.isArray(rule.children)) {
rule.children = ensureOwnerFieldRuleProps(rule.children)
}
if (rule.props && Array.isArray(rule.props.rule)) {
rule.props = {
...rule.props,
rule: ensureOwnerFieldRuleProps(rule.props.rule)
}
}
return rule
})
}
export function isOwnerPersonValue(v: unknown): v is OaPersonValueItem {
return isPersonValueItem(v)
}
export function isOwnerDeptValue(v: unknown): v is OaDeptValueItem {
return isDeptValueItem(v)
}
@@ -0,0 +1,52 @@
/** 与 FormCreate 官方 unique 同类:生成不重复 field */
let _uidSeq = 0
function uniqueId() {
const num = 370 + ++_uidSeq
return (
'F' +
Math.random().toString(36).slice(3, 6) +
Number(`${Date.now()}`).toString(36) +
num.toString(36) +
'op'
)
}
/** FormCreate Designer DragRule:归属人(默认当前登录人) */
export const ownerPersonSelectDragRule = {
menu: 'main' as const,
icon: 'icon-input',
label: '归属人',
name: 'ownerPerson',
input: true,
mask: true,
validate: ['string', 'array', 'object'],
event: ['change'],
rule() {
return {
type: 'ownerPerson',
field: uniqueId(),
title: '归属人',
info: '',
$required: false,
props: {
allowModify: false
}
}
},
props() {
return [
{
type: 'switch',
field: 'allowModify',
title: '允许修改',
value: false,
props: { activeText: '是', inactiveText: '否' }
},
{
type: 'input',
field: 'formCreateInfo',
title: '提示信息'
}
]
}
}
@@ -211,6 +211,7 @@
ensureOaUploadRuleProps
} from '@/utils/oa-form-create-upload'
import { ensureLeaveKitRuleProps } from '@/utils/oa-leave-kit-runtime'
import { ensureOvertimeKitRuleProps } from '@/utils/oa-overtime-kit-runtime'
import { ensureConfirmationKitVisibility } from '@/utils/oa-confirmation-kit-runtime'
import { ensureOwnerFieldRuleProps } from '@/utils/oa-owner-field-defaults'
@@ -344,7 +345,11 @@
viewFormData.value = { ...(data || {}) }
const rule = Array.isArray(schema?.rule) ? schema.rule : Array.isArray(schema) ? schema : []
viewRule.value = ensureConfirmationKitVisibility(
ensureOwnerFieldRuleProps(ensureLeaveKitRuleProps(ensureOaUploadRuleProps(rule)))
ensureOwnerFieldRuleProps(
ensureOvertimeKitRuleProps(
ensureLeaveKitRuleProps(ensureOaUploadRuleProps(rule))
)
)
)
const savedGutter = Number(schema?.option?.row?.gutter)
viewOption.value = {