feat(mobile-oa-h5): implement P1 person/dept/post and owner field selects
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
bb86eb7ad2
commit
d5e61a1162
10 files changed
+832
-14
No files matched your search
@@ -20,6 +20,12 @@
|
||||
- `receiveAccount`:底部多选弹层 + options API
|
||||
- `relatedApproval`:关联列表 + 候选弹层(mine/done/cc)
|
||||
|
||||
## P1 真实组件(已落地)
|
||||
|
||||
- `person` / `dept` / `post`:底部选择弹层,值 `{ id, name }`(部门展示全路径)
|
||||
- `ownerPerson` / `ownerDept`:单选封装,`allowModify` 控制可否改选
|
||||
- 仍为 stub:各 Kit(请假/加班/转正/离职/差旅)
|
||||
|
||||
## 本地联调提示
|
||||
|
||||
1. `cd mobile-oa-h5 && npm run dev`
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { get, post, request } from '../utils/http.js'
|
||||
|
||||
export function fetchUserList(params) {
|
||||
return get('/web/admin/users', params)
|
||||
}
|
||||
|
||||
export function fetchDeptTree(params) {
|
||||
return get('/web/admin/depts', params)
|
||||
}
|
||||
|
||||
export function fetchPostList(params) {
|
||||
return get('/web/admin/posts', params)
|
||||
}
|
||||
|
||||
export function fetchReceiveAccountOptions() {
|
||||
return get('/web/admin/oa-approval/receive-accounts/options')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div class="oa-pick">
|
||||
<van-field
|
||||
:model-value="displayText"
|
||||
readonly
|
||||
is-link
|
||||
:disabled="disabled"
|
||||
placeholder="请选择部门"
|
||||
@click="openPicker"
|
||||
/>
|
||||
<van-popup v-model:show="visible" position="bottom" round :style="{ height: '70%' }">
|
||||
<div class="oa-pick__sheet">
|
||||
<div class="oa-pick__hd">
|
||||
<span @click="visible = false">取消</span>
|
||||
<strong>选择部门</strong>
|
||||
<span class="oa-pick__ok" @click="confirm">确定</span>
|
||||
</div>
|
||||
<van-search v-model="keyword" placeholder="搜索部门名称" />
|
||||
<div class="oa-pick__list">
|
||||
<template v-if="multiple">
|
||||
<van-checkbox-group v-model="pendingIds">
|
||||
<van-cell
|
||||
v-for="d in filtered"
|
||||
:key="d.id"
|
||||
clickable
|
||||
:title="d.label"
|
||||
@click="toggle(d.id)"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-checkbox :name="d.id" @click.stop />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-checkbox-group>
|
||||
</template>
|
||||
<template v-else>
|
||||
<van-radio-group v-model="pendingSingle">
|
||||
<van-cell
|
||||
v-for="d in filtered"
|
||||
:key="d.id"
|
||||
clickable
|
||||
:title="d.label"
|
||||
@click="pendingSingle = d.id"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-radio :name="d.id" @click.stop />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
<van-empty v-if="!loading && !filtered.length" description="暂无部门" />
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import { fetchDeptTree } from '../../api/oa.js'
|
||||
import {
|
||||
formatIdNameDisplay,
|
||||
normalizeIdNameModelValue,
|
||||
selectedIdsFromNormalized
|
||||
} from '../../utils/id-name-value.js'
|
||||
|
||||
defineOptions({ name: 'dept' })
|
||||
|
||||
const SEP = ' / '
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Array, Object], default: null },
|
||||
disabled: { type: Boolean, default: false },
|
||||
multiple: { type: Boolean, default: true },
|
||||
max: { type: [Number, String], default: null },
|
||||
formCreateInject: { type: Object, default: null }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const pendingIds = ref([])
|
||||
const pendingSingle = ref('')
|
||||
const nameMap = ref(new Map())
|
||||
const flatOptions = ref([])
|
||||
|
||||
const multiple = computed(() => !!props.multiple)
|
||||
const normalized = computed(() =>
|
||||
normalizeIdNameModelValue(props.modelValue, multiple.value)
|
||||
)
|
||||
const displayText = computed(() => formatIdNameDisplay(normalized.value))
|
||||
const filtered = computed(() => {
|
||||
const q = keyword.value.trim().toLowerCase()
|
||||
if (!q) return flatOptions.value
|
||||
return flatOptions.value.filter((d) => d.label.toLowerCase().includes(q))
|
||||
})
|
||||
|
||||
function flattenTree(nodes, ancestors = [], out = []) {
|
||||
for (const n of nodes || []) {
|
||||
const id = String(n.id || '').trim()
|
||||
const name = String(n.deptName || n.name || id).trim()
|
||||
if (!id) continue
|
||||
const path = [...ancestors, name]
|
||||
const label = path.join(SEP)
|
||||
out.push({ id, label, name: label })
|
||||
if (n.children?.length) flattenTree(n.children, path, out)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function emitFromIds(ids) {
|
||||
const items = []
|
||||
const seen = new Set()
|
||||
for (const id of ids) {
|
||||
const tid = String(id || '').trim()
|
||||
if (!tid || seen.has(tid)) continue
|
||||
seen.add(tid)
|
||||
items.push({ id: tid, name: nameMap.value.get(tid) || tid })
|
||||
}
|
||||
if (multiple.value) {
|
||||
const max = props.max != null && Number(props.max) > 0 ? Number(props.max) : null
|
||||
if (max != null && items.length > max) {
|
||||
showToast(`最多可选 ${max} 个部门`)
|
||||
return
|
||||
}
|
||||
emit('update:modelValue', items)
|
||||
emit('change', items)
|
||||
} else {
|
||||
const one = items[0] || null
|
||||
emit('update:modelValue', one)
|
||||
emit('change', one)
|
||||
}
|
||||
}
|
||||
|
||||
function openPicker() {
|
||||
if (props.disabled) return
|
||||
const ids = selectedIdsFromNormalized(normalized.value, multiple.value)
|
||||
if (multiple.value) pendingIds.value = ids
|
||||
else pendingSingle.value = ids[0] || ''
|
||||
keyword.value = ''
|
||||
visible.value = true
|
||||
if (!flatOptions.value.length) loadDepts()
|
||||
}
|
||||
|
||||
function toggle(id) {
|
||||
const set = new Set(pendingIds.value)
|
||||
if (set.has(id)) set.delete(id)
|
||||
else set.add(id)
|
||||
pendingIds.value = [...set]
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
emitFromIds(multiple.value ? pendingIds.value : pendingSingle.value ? [pendingSingle.value] : [])
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
async function loadDepts() {
|
||||
loading.value = true
|
||||
try {
|
||||
const tree = await fetchDeptTree()
|
||||
const list = Array.isArray(tree) ? tree : tree?.list || []
|
||||
const flat = flattenTree(list)
|
||||
const map = new Map()
|
||||
for (const d of flat) map.set(d.id, d.label)
|
||||
const kept = Array.isArray(normalized.value)
|
||||
? normalized.value
|
||||
: normalized.value
|
||||
? [normalized.value]
|
||||
: []
|
||||
for (const x of kept) {
|
||||
if (!map.has(x.id) && x.name) map.set(x.id, x.name)
|
||||
}
|
||||
nameMap.value = map
|
||||
flatOptions.value = flat
|
||||
} catch (e) {
|
||||
showToast(e?.message || '加载部门失败')
|
||||
nameMap.value = new Map()
|
||||
flatOptions.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDepts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oa-pick__sheet {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
.oa-pick__hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border, #dcebe4);
|
||||
}
|
||||
.oa-pick__ok {
|
||||
color: var(--brand-green, #0a9f6d);
|
||||
font-weight: 600;
|
||||
}
|
||||
.oa-pick__list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<OaDeptSelect
|
||||
:model-value="modelValue"
|
||||
:disabled="effectiveDisabled"
|
||||
:multiple="false"
|
||||
:form-create-inject="formCreateInject"
|
||||
@update:model-value="onUpdate"
|
||||
@change="onChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import OaDeptSelect from './OaDeptSelect.vue'
|
||||
|
||||
defineOptions({ name: 'ownerDept' })
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Array, Object], default: null },
|
||||
disabled: { type: Boolean, default: false },
|
||||
allowModify: { type: Boolean, default: false },
|
||||
formCreateInject: { type: Object, default: null }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const effectiveDisabled = computed(() => !!props.disabled || !props.allowModify)
|
||||
|
||||
function onUpdate(v) {
|
||||
emit('update:modelValue', v)
|
||||
}
|
||||
function onChange(v) {
|
||||
emit('change', v)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<OaPersonSelect
|
||||
:model-value="modelValue"
|
||||
:disabled="effectiveDisabled"
|
||||
:multiple="false"
|
||||
:form-create-inject="formCreateInject"
|
||||
@update:model-value="onUpdate"
|
||||
@change="onChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import OaPersonSelect from './OaPersonSelect.vue'
|
||||
|
||||
defineOptions({ name: 'ownerPerson' })
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Array, Object], default: null },
|
||||
disabled: { type: Boolean, default: false },
|
||||
allowModify: { type: Boolean, default: false },
|
||||
formCreateInject: { type: Object, default: null }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const effectiveDisabled = computed(() => !!props.disabled || !props.allowModify)
|
||||
|
||||
function onUpdate(v) {
|
||||
emit('update:modelValue', v)
|
||||
}
|
||||
function onChange(v) {
|
||||
emit('change', v)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="oa-pick">
|
||||
<van-field
|
||||
:model-value="displayText"
|
||||
readonly
|
||||
is-link
|
||||
:disabled="disabled"
|
||||
placeholder="请选择人员"
|
||||
@click="openPicker"
|
||||
/>
|
||||
<van-popup v-model:show="visible" position="bottom" round :style="{ height: '70%' }">
|
||||
<div class="oa-pick__sheet">
|
||||
<div class="oa-pick__hd">
|
||||
<span @click="visible = false">取消</span>
|
||||
<strong>选择人员</strong>
|
||||
<span class="oa-pick__ok" @click="confirm">确定</span>
|
||||
</div>
|
||||
<van-search v-model="keyword" placeholder="搜索姓名或工号" />
|
||||
<div class="oa-pick__list">
|
||||
<template v-if="multiple">
|
||||
<van-checkbox-group v-model="pendingIds">
|
||||
<van-cell
|
||||
v-for="u in filtered"
|
||||
:key="u.id"
|
||||
clickable
|
||||
:title="u.label"
|
||||
:label="u.sub || undefined"
|
||||
@click="toggle(u.id)"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-checkbox :name="u.id" @click.stop />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-checkbox-group>
|
||||
</template>
|
||||
<template v-else>
|
||||
<van-radio-group v-model="pendingSingle">
|
||||
<van-cell
|
||||
v-for="u in filtered"
|
||||
:key="u.id"
|
||||
clickable
|
||||
:title="u.label"
|
||||
:label="u.sub || undefined"
|
||||
@click="pendingSingle = u.id"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-radio :name="u.id" @click.stop />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
<van-empty v-if="!loading && !filtered.length" description="暂无人员" />
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import { fetchUserList } from '../../api/oa.js'
|
||||
import {
|
||||
formatIdNameDisplay,
|
||||
normalizeIdNameModelValue,
|
||||
selectedIdsFromNormalized
|
||||
} from '../../utils/id-name-value.js'
|
||||
|
||||
defineOptions({ name: 'person' })
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Array, Object], default: null },
|
||||
disabled: { type: Boolean, default: false },
|
||||
multiple: { type: Boolean, default: true },
|
||||
max: { type: [Number, String], default: null },
|
||||
formCreateInject: { type: Object, default: null }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const pendingIds = ref([])
|
||||
const pendingSingle = ref('')
|
||||
const nameMap = ref(new Map())
|
||||
const userOptions = ref([])
|
||||
|
||||
const multiple = computed(() => !!props.multiple)
|
||||
const normalized = computed(() =>
|
||||
normalizeIdNameModelValue(props.modelValue, multiple.value)
|
||||
)
|
||||
const displayText = computed(() => formatIdNameDisplay(normalized.value))
|
||||
const filtered = computed(() => {
|
||||
const q = keyword.value.trim().toLowerCase()
|
||||
if (!q) return userOptions.value
|
||||
return userOptions.value.filter(
|
||||
(u) =>
|
||||
u.label.toLowerCase().includes(q) ||
|
||||
u.sub.toLowerCase().includes(q) ||
|
||||
u.id.toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
function emitFromIds(ids) {
|
||||
const items = []
|
||||
const seen = new Set()
|
||||
for (const id of ids) {
|
||||
const tid = String(id || '').trim()
|
||||
if (!tid || seen.has(tid)) continue
|
||||
seen.add(tid)
|
||||
items.push({ id: tid, name: nameMap.value.get(tid) || tid })
|
||||
}
|
||||
if (multiple.value) {
|
||||
const max = props.max != null && Number(props.max) > 0 ? Number(props.max) : null
|
||||
if (max != null && items.length > max) {
|
||||
showToast(`最多可选 ${max} 个人员`)
|
||||
return
|
||||
}
|
||||
emit('update:modelValue', items)
|
||||
emit('change', items)
|
||||
} else {
|
||||
const one = items[0] || null
|
||||
emit('update:modelValue', one)
|
||||
emit('change', one)
|
||||
}
|
||||
}
|
||||
|
||||
function openPicker() {
|
||||
if (props.disabled) return
|
||||
const ids = selectedIdsFromNormalized(normalized.value, multiple.value)
|
||||
if (multiple.value) pendingIds.value = ids
|
||||
else pendingSingle.value = ids[0] || ''
|
||||
keyword.value = ''
|
||||
visible.value = true
|
||||
if (!userOptions.value.length) loadUsers()
|
||||
}
|
||||
|
||||
function toggle(id) {
|
||||
const set = new Set(pendingIds.value)
|
||||
if (set.has(id)) set.delete(id)
|
||||
else set.add(id)
|
||||
pendingIds.value = [...set]
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
emitFromIds(multiple.value ? pendingIds.value : pendingSingle.value ? [pendingSingle.value] : [])
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
loading.value = true
|
||||
try {
|
||||
const pageSize = 200
|
||||
const raw = []
|
||||
let page = 1
|
||||
for (;;) {
|
||||
const res = await fetchUserList({ page, pageSize, status: '1' })
|
||||
const list = res?.list || []
|
||||
raw.push(...list)
|
||||
if (list.length < pageSize) break
|
||||
if (typeof res?.total === 'number' && raw.length >= res.total) break
|
||||
page += 1
|
||||
if (page > 50) break
|
||||
}
|
||||
const map = new Map()
|
||||
const opts = []
|
||||
for (const u of raw) {
|
||||
const id = String(u.id || '').trim()
|
||||
if (!id) continue
|
||||
const nick = String(u.nickName || '').trim()
|
||||
const userNo = String(u.userNo || '').trim()
|
||||
const label = nick || userNo || id
|
||||
map.set(id, label)
|
||||
opts.push({
|
||||
id,
|
||||
label,
|
||||
sub: nick && userNo && nick !== userNo ? userNo : ''
|
||||
})
|
||||
}
|
||||
const kept = Array.isArray(normalized.value)
|
||||
? normalized.value
|
||||
: normalized.value
|
||||
? [normalized.value]
|
||||
: []
|
||||
for (const x of kept) {
|
||||
if (!map.has(x.id) && x.name) map.set(x.id, x.name)
|
||||
}
|
||||
nameMap.value = map
|
||||
userOptions.value = opts
|
||||
} catch (e) {
|
||||
showToast(e?.message || '加载人员失败')
|
||||
nameMap.value = new Map()
|
||||
userOptions.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oa-pick__sheet {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
.oa-pick__hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border, #dcebe4);
|
||||
}
|
||||
.oa-pick__ok {
|
||||
color: var(--brand-green, #0a9f6d);
|
||||
font-weight: 600;
|
||||
}
|
||||
.oa-pick__list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div class="oa-pick">
|
||||
<van-field
|
||||
:model-value="displayText"
|
||||
readonly
|
||||
is-link
|
||||
:disabled="disabled"
|
||||
placeholder="请选择职位"
|
||||
@click="openPicker"
|
||||
/>
|
||||
<van-popup v-model:show="visible" position="bottom" round :style="{ height: '70%' }">
|
||||
<div class="oa-pick__sheet">
|
||||
<div class="oa-pick__hd">
|
||||
<span @click="visible = false">取消</span>
|
||||
<strong>选择职位</strong>
|
||||
<span class="oa-pick__ok" @click="confirm">确定</span>
|
||||
</div>
|
||||
<van-search v-model="keyword" placeholder="搜索职位名称或编码" />
|
||||
<div class="oa-pick__list">
|
||||
<template v-if="multiple">
|
||||
<van-checkbox-group v-model="pendingIds">
|
||||
<van-cell
|
||||
v-for="p in filtered"
|
||||
:key="p.id"
|
||||
clickable
|
||||
:title="p.label"
|
||||
:label="p.sub || undefined"
|
||||
@click="toggle(p.id)"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-checkbox :name="p.id" @click.stop />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-checkbox-group>
|
||||
</template>
|
||||
<template v-else>
|
||||
<van-radio-group v-model="pendingSingle">
|
||||
<van-cell
|
||||
v-for="p in filtered"
|
||||
:key="p.id"
|
||||
clickable
|
||||
:title="p.label"
|
||||
:label="p.sub || undefined"
|
||||
@click="pendingSingle = p.id"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-radio :name="p.id" @click.stop />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
<van-empty v-if="!loading && !filtered.length" description="暂无职位" />
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import { fetchPostList } from '../../api/oa.js'
|
||||
import {
|
||||
formatIdNameDisplay,
|
||||
normalizeIdNameModelValue,
|
||||
selectedIdsFromNormalized
|
||||
} from '../../utils/id-name-value.js'
|
||||
|
||||
defineOptions({ name: 'post' })
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Array, Object], default: null },
|
||||
disabled: { type: Boolean, default: false },
|
||||
multiple: { type: Boolean, default: true },
|
||||
max: { type: [Number, String], default: null },
|
||||
formCreateInject: { type: Object, default: null }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const pendingIds = ref([])
|
||||
const pendingSingle = ref('')
|
||||
const nameMap = ref(new Map())
|
||||
const postOptions = ref([])
|
||||
|
||||
const multiple = computed(() => !!props.multiple)
|
||||
const normalized = computed(() =>
|
||||
normalizeIdNameModelValue(props.modelValue, multiple.value)
|
||||
)
|
||||
const displayText = computed(() => formatIdNameDisplay(normalized.value))
|
||||
const filtered = computed(() => {
|
||||
const q = keyword.value.trim().toLowerCase()
|
||||
if (!q) return postOptions.value
|
||||
return postOptions.value.filter(
|
||||
(p) =>
|
||||
p.label.toLowerCase().includes(q) ||
|
||||
p.sub.toLowerCase().includes(q) ||
|
||||
p.id.toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
function emitFromIds(ids) {
|
||||
const items = []
|
||||
const seen = new Set()
|
||||
for (const id of ids) {
|
||||
const tid = String(id || '').trim()
|
||||
if (!tid || seen.has(tid)) continue
|
||||
seen.add(tid)
|
||||
items.push({ id: tid, name: nameMap.value.get(tid) || tid })
|
||||
}
|
||||
if (multiple.value) {
|
||||
const max = props.max != null && Number(props.max) > 0 ? Number(props.max) : null
|
||||
if (max != null && items.length > max) {
|
||||
showToast(`最多可选 ${max} 个职位`)
|
||||
return
|
||||
}
|
||||
emit('update:modelValue', items)
|
||||
emit('change', items)
|
||||
} else {
|
||||
const one = items[0] || null
|
||||
emit('update:modelValue', one)
|
||||
emit('change', one)
|
||||
}
|
||||
}
|
||||
|
||||
function openPicker() {
|
||||
if (props.disabled) return
|
||||
const ids = selectedIdsFromNormalized(normalized.value, multiple.value)
|
||||
if (multiple.value) pendingIds.value = ids
|
||||
else pendingSingle.value = ids[0] || ''
|
||||
keyword.value = ''
|
||||
visible.value = true
|
||||
if (!postOptions.value.length) loadPosts()
|
||||
}
|
||||
|
||||
function toggle(id) {
|
||||
const set = new Set(pendingIds.value)
|
||||
if (set.has(id)) set.delete(id)
|
||||
else set.add(id)
|
||||
pendingIds.value = [...set]
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
emitFromIds(multiple.value ? pendingIds.value : pendingSingle.value ? [pendingSingle.value] : [])
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
async function loadPosts() {
|
||||
loading.value = true
|
||||
try {
|
||||
const pageSize = 200
|
||||
const raw = []
|
||||
let page = 1
|
||||
for (;;) {
|
||||
const res = await fetchPostList({ page, pageSize })
|
||||
const list = res?.list || []
|
||||
raw.push(...list)
|
||||
if (list.length < pageSize) break
|
||||
if (typeof res?.total === 'number' && raw.length >= res.total) break
|
||||
page += 1
|
||||
if (page > 50) break
|
||||
}
|
||||
const map = new Map()
|
||||
const opts = []
|
||||
for (const p of raw) {
|
||||
const id = String(p.id || '').trim()
|
||||
if (!id) continue
|
||||
const name = String(p.postName || '').trim()
|
||||
const code = String(p.postCode || '').trim()
|
||||
const label = name || code || id
|
||||
map.set(id, label)
|
||||
opts.push({ id, label, sub: name && code && name !== code ? code : '' })
|
||||
}
|
||||
const kept = Array.isArray(normalized.value)
|
||||
? normalized.value
|
||||
: normalized.value
|
||||
? [normalized.value]
|
||||
: []
|
||||
for (const x of kept) {
|
||||
if (!map.has(x.id) && x.name) map.set(x.id, x.name)
|
||||
}
|
||||
nameMap.value = map
|
||||
postOptions.value = opts
|
||||
} catch (e) {
|
||||
showToast(e?.message || '加载职位失败')
|
||||
nameMap.value = new Map()
|
||||
postOptions.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPosts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oa-pick__sheet {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
.oa-pick__hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border, #dcebe4);
|
||||
}
|
||||
.oa-pick__ok {
|
||||
color: var(--brand-green, #0a9f6d);
|
||||
font-weight: 600;
|
||||
}
|
||||
.oa-pick__list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -34,21 +34,41 @@ export const kitchenSinkRule = [
|
||||
title: '收款账户 receiveAccount',
|
||||
value: []
|
||||
},
|
||||
{ type: 'dept', field: 'deptId', title: '部门 dept', value: '' },
|
||||
{ type: 'person', field: 'personIds', title: '人员 person', value: [] },
|
||||
{
|
||||
type: 'dept',
|
||||
field: 'deptId',
|
||||
title: '部门 dept',
|
||||
value: [],
|
||||
props: { multiple: true }
|
||||
},
|
||||
{
|
||||
type: 'person',
|
||||
field: 'personIds',
|
||||
title: '人员 person',
|
||||
value: [],
|
||||
props: { multiple: true }
|
||||
},
|
||||
{
|
||||
type: 'ownerPerson',
|
||||
field: 'ownerPerson',
|
||||
title: '所属人 ownerPerson',
|
||||
value: ''
|
||||
title: '所属人 ownerPerson(可改选)',
|
||||
value: null,
|
||||
props: { allowModify: true }
|
||||
},
|
||||
{
|
||||
type: 'ownerDept',
|
||||
field: 'ownerDept',
|
||||
title: '所属部门 ownerDept',
|
||||
value: ''
|
||||
title: '所属部门 ownerDept(可改选)',
|
||||
value: null,
|
||||
props: { allowModify: true }
|
||||
},
|
||||
{
|
||||
type: 'post',
|
||||
field: 'postId',
|
||||
title: '岗位 post',
|
||||
value: [],
|
||||
props: { multiple: true }
|
||||
},
|
||||
{ type: 'post', field: 'postId', title: '岗位 post', value: '' },
|
||||
{ type: 'leaveKit', field: 'leaveKit', title: '请假套件 leaveKit', value: {} },
|
||||
{
|
||||
type: 'overtimeKit',
|
||||
|
||||
@@ -3,21 +3,26 @@ import OaMoneyField from './components/OaMoneyField.vue'
|
||||
import OaUploadField from './components/OaUploadField.vue'
|
||||
import OaReceiveAccountSelect from './components/OaReceiveAccountSelect.vue'
|
||||
import OaRelatedApproval from './components/OaRelatedApproval.vue'
|
||||
import OaPersonSelect from './components/OaPersonSelect.vue'
|
||||
import OaDeptSelect from './components/OaDeptSelect.vue'
|
||||
import OaPostSelect from './components/OaPostSelect.vue'
|
||||
import OaOwnerPersonSelect from './components/OaOwnerPersonSelect.vue'
|
||||
import OaOwnerDeptSelect from './components/OaOwnerDeptSelect.vue'
|
||||
|
||||
/** P0 真实组件;其余仍用 stub,阶段二继续替换 */
|
||||
/** P0 + P1 真实组件;套件仍用 stub */
|
||||
const REAL_COMPONENTS = {
|
||||
money: OaMoneyField,
|
||||
upload: OaUploadField,
|
||||
receiveAccount: OaReceiveAccountSelect,
|
||||
relatedApproval: OaRelatedApproval
|
||||
relatedApproval: OaRelatedApproval,
|
||||
person: OaPersonSelect,
|
||||
dept: OaDeptSelect,
|
||||
post: OaPostSelect,
|
||||
ownerPerson: OaOwnerPersonSelect,
|
||||
ownerDept: OaOwnerDeptSelect
|
||||
}
|
||||
|
||||
const STUB_TYPES = [
|
||||
'dept',
|
||||
'person',
|
||||
'ownerPerson',
|
||||
'ownerDept',
|
||||
'post',
|
||||
'leaveKit',
|
||||
'overtimeKit',
|
||||
'confirmationKit',
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/** person / dept / post 共用 { id, name } 值结构 */
|
||||
|
||||
export function isIdNameItem(v) {
|
||||
if (!v || typeof v !== 'object') return false
|
||||
return String(v.id || '').trim() !== '' && String(v.name || '').trim() !== ''
|
||||
}
|
||||
|
||||
export function normalizeIdNameModelValue(raw, multiple) {
|
||||
if (multiple) {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw.filter(isIdNameItem).map((x) => ({
|
||||
id: String(x.id).trim(),
|
||||
name: String(x.name).trim()
|
||||
}))
|
||||
}
|
||||
if (isIdNameItem(raw)) {
|
||||
return [{ id: String(raw.id).trim(), name: String(raw.name).trim() }]
|
||||
}
|
||||
return []
|
||||
}
|
||||
if (isIdNameItem(raw)) {
|
||||
return { id: String(raw.id).trim(), name: String(raw.name).trim() }
|
||||
}
|
||||
if (Array.isArray(raw) && raw.length === 1 && isIdNameItem(raw[0])) {
|
||||
return { id: String(raw[0].id).trim(), name: String(raw[0].name).trim() }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function formatIdNameDisplay(raw) {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw
|
||||
.filter(isIdNameItem)
|
||||
.map((x) => x.name)
|
||||
.filter(Boolean)
|
||||
.join('、')
|
||||
}
|
||||
if (isIdNameItem(raw)) return raw.name
|
||||
return ''
|
||||
}
|
||||
|
||||
export function selectedIdsFromNormalized(normalized, multiple) {
|
||||
if (multiple) {
|
||||
return Array.isArray(normalized) ? normalized.map((x) => x.id) : []
|
||||
}
|
||||
return normalized && !Array.isArray(normalized) ? [normalized.id] : []
|
||||
}
|
||||
Reference in new issue
Block a user