|
|
@@ -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>
|