feat(web-admin): register receiveAccount FormCreate control.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-30 13:49:43 +08:00
1 parent 4e805f9625
commit cf61ce340d
7 files changed
+435

No files matched your search

@@ -0,0 +1,316 @@
<!-- FormCreate 自定义控件:收款账户选择(Fresh Mint · PC 下拉 + 窄屏抽屉;预览画布全宽) -->
<template>
<div class="oa-receive-account-select">
<template v-if="disabled">
<span class="oa-receive-account-select__readonly">{{ displayText || '—' }}</span>
</template>
<template v-else-if="isNarrow">
<button type="button" class="oa-receive-account-select__trigger" @click="openDrawer">
<span v-if="displayText" class="oa-receive-account-select__trigger-text">{{
displayText
}}</span>
<span v-else class="oa-receive-account-select__placeholder">请选择收款账户</span>
<span class="oa-receive-account-select__chev">▾</span>
</button>
<el-drawer
v-model="drawerVisible"
title="选择收款账户"
direction="btt"
size="75%"
append-to-body
:close-on-click-modal="false"
class="oa-receive-account-drawer"
modal-class="protocol-dialog-mask"
>
<el-input
v-model="keyword"
clearable
placeholder="搜索账户名称或账号"
class="oa-receive-account-select__search"
/>
<div class="oa-receive-account-select__list">
<label
v-for="a in filteredOptions"
:key="a.id"
class="oa-receive-account-select__row"
>
<el-checkbox
:model-value="pendingIds.includes(a.id)"
@change="(v: boolean | string | number) => onNarrowCheck(a.id, !!v)"
/>
<span class="oa-receive-account-select__row-label">{{ a.label }}</span>
</label>
<div v-if="!filteredOptions.length" class="oa-receive-account-select__empty">
暂无收款账户
</div>
</div>
<template #footer>
<el-button @click="drawerVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button type="primary" @click="confirmDrawer">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-drawer>
</template>
<el-select
v-else
:model-value="selectModel"
class="oa-receive-account-select__select"
filterable
clearable
multiple
placeholder="请选择收款账户"
@update:model-value="onSelectChange"
>
<el-option
v-for="a in accountOptions"
:key="a.id"
:label="a.label"
:value="a.id"
/>
</el-select>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { Check, Close } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { fetchOaReceiveAccountOptions } from '@/api/oaApproval'
import {
formatReceiveAccountDisplay,
normalizeReceiveAccountModelValue,
optionLabelFromRow,
snapshotFromRow,
type ReceiveAccountSnapshot
} from '@/utils/oa-receive-account-value'
defineOptions({ name: 'receiveAccount' })
const props = defineProps<{
modelValue?: unknown
disabled?: boolean
formCreateInject?: Record<string, unknown>
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: ReceiveAccountSnapshot[]): void
(e: 'change', v: ReceiveAccountSnapshot[]): void
}>()
type AccountOpt = { id: string; label: string }
const snapshotMap = ref(new Map<string, ReceiveAccountSnapshot>())
const accountOptions = ref<AccountOpt[]>([])
const isNarrow = ref(false)
const drawerVisible = ref(false)
const keyword = ref('')
const pendingIds = ref<string[]>([])
let mql: MediaQueryList | null = null
const normalized = computed(() => normalizeReceiveAccountModelValue(props.modelValue))
const displayText = computed(() => formatReceiveAccountDisplay(normalized.value))
const selectModel = computed(() => normalized.value.map((x) => x.id))
const filteredOptions = computed(() => {
const q = keyword.value.trim().toLowerCase()
if (!q) return accountOptions.value
return accountOptions.value.filter(
(a) => a.label.toLowerCase().includes(q) || a.id.toLowerCase().includes(q)
)
})
function emitFromIds(ids: string[]) {
const items: ReceiveAccountSnapshot[] = []
const seen = new Set<string>()
const prevMap = new Map(normalized.value.map((x) => [x.id, x]))
for (const id of ids) {
const tid = String(id || '').trim()
if (!tid || seen.has(tid)) continue
seen.add(tid)
const snap = snapshotMap.value.get(tid) || prevMap.get(tid)
if (snap) items.push(snap)
}
emit('update:modelValue', items)
emit('change', items)
}
function onSelectChange(val: string | string[] | null | undefined) {
emitFromIds(Array.isArray(val) ? val.map(String) : [])
}
function openDrawer() {
pendingIds.value = normalized.value.map((x) => x.id)
keyword.value = ''
drawerVisible.value = true
}
function onNarrowCheck(id: string, checked: boolean) {
const set = new Set(pendingIds.value)
if (checked) set.add(id)
else set.delete(id)
pendingIds.value = [...set]
}
function confirmDrawer() {
emitFromIds(pendingIds.value)
drawerVisible.value = false
}
async function loadOptions() {
try {
const res = await fetchOaReceiveAccountOptions()
const list = res?.list || []
const map = new Map<string, ReceiveAccountSnapshot>()
const opts: AccountOpt[] = []
for (const row of list) {
const snap = snapshotFromRow(row)
if (!snap.id) continue
map.set(snap.id, snap)
opts.push({ id: snap.id, label: optionLabelFromRow(snap) })
}
// 保留已选快照(主数据可能已删除该账户)
for (const x of normalized.value) {
if (!map.has(x.id)) map.set(x.id, x)
}
snapshotMap.value = map
accountOptions.value = opts
} catch {
snapshotMap.value = new Map()
accountOptions.value = []
ElMessage.error('加载收款账户选项失败')
}
}
function syncNarrow(e?: MediaQueryList | MediaQueryListEvent) {
const matches = e && 'matches' in e ? e.matches : !!mql?.matches
isNarrow.value = matches
}
onMounted(() => {
loadOptions()
mql = window.matchMedia('(max-width: 767px)')
syncNarrow(mql)
mql.addEventListener('change', syncNarrow)
})
onBeforeUnmount(() => {
mql?.removeEventListener('change', syncNarrow)
})
</script>
<style scoped lang="scss">
.oa-receive-account-select {
box-sizing: border-box;
width: 100%;
max-width: 100%;
min-width: 0;
}
.oa-receive-account-select__select {
width: 100%;
max-width: 100%;
}
.oa-receive-account-select__select :deep(.el-select),
.oa-receive-account-select__select :deep(.el-select__wrapper),
.oa-receive-account-select__select :deep(.el-tooltip__trigger) {
width: 100%;
max-width: 100%;
min-width: 0;
}
.oa-receive-account-select__select :deep(.el-select__selection) {
flex-wrap: wrap;
}
.oa-receive-account-select__select :deep(.el-select__wrapper) {
min-height: 40px;
}
.oa-receive-account-select__trigger {
display: flex;
box-sizing: border-box;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 100%;
min-height: 40px;
padding: 4px 12px;
border: 1px solid var(--mint-border, #cbd5e1);
border-radius: 8px;
background: #fff;
text-align: left;
cursor: pointer;
}
.oa-receive-account-select__readonly {
color: var(--mint-text, #0f172a);
font-size: 14px;
line-height: 1.5;
white-space: pre-line;
word-break: break-all;
}
.oa-receive-account-select__placeholder {
color: var(--mint-text-muted, #94a3b8);
}
.oa-receive-account-select__trigger-text {
min-width: 0;
color: var(--mint-text, #0f172a);
white-space: pre-line;
word-break: break-all;
}
.oa-receive-account-select__chev {
flex-shrink: 0;
margin-left: 8px;
color: var(--mint-text-muted, #94a3b8);
font-size: 11px;
}
.oa-receive-account-select__search {
margin-bottom: 10px;
}
.oa-receive-account-select__list {
max-height: 50vh;
overflow: auto;
}
.oa-receive-account-select__row {
display: flex;
gap: 8px;
align-items: center;
min-height: 44px;
padding: 6px 4px;
cursor: pointer;
}
.oa-receive-account-select__row-label {
min-width: 0;
color: var(--mint-text, #0f172a);
}
.oa-receive-account-select__empty {
padding: 16px;
color: var(--mint-text-muted, #94a3b8);
text-align: center;
}
@media (max-width: 767px) {
.oa-receive-account-select__select :deep(.el-select__wrapper),
.oa-receive-account-select__trigger {
min-height: 44px;
}
}
</style>