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

+44
View File
@@ -71,6 +71,50 @@ export function fetchReplaceOaRoleUsers(roleId: string, userIds: string[]) {
})
}
// ---------------------------------------------------------------------------
// 收款账户
// ---------------------------------------------------------------------------
export function fetchOaReceiveAccountList(params?: {
keyword?: string
accountType?: string
page?: number
pageSize?: number
}) {
return request.get<{ list: any[]; total: number }>({
url: `${BASE}/receive-accounts`,
params
})
}
export function fetchOaReceiveAccountOptions() {
return request.get<{ list: any[] }>({ url: `${BASE}/receive-accounts/options` })
}
export function fetchOaReceiveAccountDetail(id: string) {
return request.get<any>({ url: `${BASE}/receive-accounts/${id}` })
}
export function fetchCreateOaReceiveAccount(data: Record<string, unknown>) {
return request.post({ url: `${BASE}/receive-accounts`, data, showSuccessMessage: true })
}
export function fetchUpdateOaReceiveAccount(id: string, data: Record<string, unknown>) {
return request.put({ url: `${BASE}/receive-accounts/${id}`, data, showSuccessMessage: true })
}
export function fetchDeleteOaReceiveAccount(id: string) {
return request.del({ url: `${BASE}/receive-accounts/${id}`, showSuccessMessage: true })
}
export function fetchBatchDeleteOaReceiveAccounts(ids: string[]) {
return request.post({
url: `${BASE}/receive-accounts/batch-delete`,
data: { ids },
showSuccessMessage: true
})
}
// ---------------------------------------------------------------------------
// 分组 / 应用 / 草稿 / 发布
// ---------------------------------------------------------------------------
@@ -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>
+2
View File
@@ -20,6 +20,7 @@ import { setupOaRelatedApproval } from '@/utils/oa-form-create-related-approval'
import { setupOaDeptSelect } from '@/utils/oa-form-create-dept'
import { setupOaMoneyField } from '@/utils/oa-form-create-money'
import { setupOaPersonSelect } from '@/utils/oa-form-create-person'
import { setupOaReceiveAccountSelect } from '@/utils/oa-form-create-receive-account'
// FormCreate 按字符串解析 el-input / fc-radio 等,主应用是按需自动导入,不会全局注册;
// 必须挂上官方 auto-import,否则发起审批/详情里动态表单控件会残缺(如单选项只剩文字)。
@@ -30,6 +31,7 @@ setupOaRelatedApproval(FcDesigner.formCreate)
setupOaDeptSelect(FcDesigner.formCreate)
setupOaMoneyField(FcDesigner.formCreate)
setupOaPersonSelect(FcDesigner.formCreate)
setupOaReceiveAccountSelect(FcDesigner.formCreate)
function maskSecret(value?: string) {
if (!value) return '(empty)'
+2
View File
@@ -17,6 +17,7 @@ import { setupOaRelatedApproval } from '@/utils/oa-form-create-related-approval'
import { setupOaDeptSelect } from '@/utils/oa-form-create-dept'
import { setupOaMoneyField } from '@/utils/oa-form-create-money'
import { setupOaPersonSelect } from '@/utils/oa-form-create-person'
import { setupOaReceiveAccountSelect } from '@/utils/oa-form-create-receive-account'
import App from './App.vue'
const app = createApp(App)
@@ -37,4 +38,5 @@ setupOaRelatedApproval(FcDesigner.formCreate)
setupOaDeptSelect(FcDesigner.formCreate)
setupOaMoneyField(FcDesigner.formCreate)
setupOaPersonSelect(FcDesigner.formCreate)
setupOaReceiveAccountSelect(FcDesigner.formCreate)
app.mount('#app')
+7
View File
@@ -15,6 +15,7 @@ import { formatProgressPersonLines } from '@/utils/oa-progress-person-lines'
import { formatDeptDisplay } from '@/utils/oa-dept-value'
import { formatPersonDisplay } from '@/utils/oa-person-value'
import { formatMoneyDisplay } from '@/utils/money-format'
import { formatReceiveAccountDisplay } from '@/utils/oa-receive-account-value'
import { toRmbUpper } from '@/utils/rmb-upper'
export const OA_PRINT_COMPANY_NAME = '安徽中科兴皖医学研究有限公司'
@@ -274,6 +275,12 @@ async function walkRules(
continue
}
if (type === 'receiveAccount') {
const text = formatReceiveAccountDisplay(value)
out.push({ label, text: text || '-' })
continue
}
if (UPLOAD_TYPES.has(type) || String(rule.name || '').toLowerCase().includes('upload')) {
const urls = extractUrls(value)
out.push({ label, urls: urls.length ? urls : [], text: urls.length ? undefined : '-' })
@@ -0,0 +1,19 @@
import FcDesigner from '@form-create/designer'
import OaReceiveAccountSelect from '@/components/oa-approval/OaReceiveAccountSelect.vue'
import { receiveAccountSelectDragRule } from '@/utils/oa-receive-account-rule'
const FLAG = '__oaReceiveAccountSelectConfigured'
export function setupOaReceiveAccountSelect(formCreate?: {
component?: (name: string, c: unknown) => void
}) {
if ((FcDesigner as any)[FLAG]) return
FcDesigner.component('receiveAccount', OaReceiveAccountSelect)
if (typeof formCreate?.component === 'function') {
formCreate.component('receiveAccount', OaReceiveAccountSelect)
}
if (typeof FcDesigner.addDragRule === 'function') {
FcDesigner.addDragRule(receiveAccountSelectDragRule)
}
;(FcDesigner as any)[FLAG] = true
}
@@ -0,0 +1,45 @@
/** 与 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) +
'r'
)
}
/** FormCreate Designer DragRule:收款账户选择 */
export const receiveAccountSelectDragRule = {
menu: 'main' as const,
icon: 'icon-input',
label: '收款账户',
name: 'receiveAccount',
input: true,
mask: true,
validate: ['string', 'array', 'object'],
event: ['change'],
rule() {
return {
type: 'receiveAccount',
field: uniqueId(),
title: '收款账户',
info: '',
$required: false,
props: {
multiple: true
}
}
},
props() {
return [
{
type: 'input',
field: 'formCreateInfo',
title: '提示信息'
}
]
}
}