实现Table控件,修复详情页子表无法正常显示的问题

This commit is contained in:
jingrow 2025-10-17 00:15:43 +08:00
parent 50a027516c
commit 64b03ebfb1

View File

@ -1,14 +1,248 @@
<script setup lang="ts">
defineProps<{ df: any; record: Record<string, any>; canEdit: boolean; ctx: any }>()
import { ref, computed, onMounted } from 'vue'
import axios from 'axios'
import { get_session_api_headers } from '@/shared/api/auth'
const props = defineProps<{ df: any; record: Record<string, any>; canEdit: boolean; ctx: any }>()
type Field = { label: string; fieldname: string; fieldtype: string; in_list_view?: boolean; columns?: number }
const loading = ref(false)
const childFields = ref<Field[]>([])
const rows = computed<any[]>(() => {
const value = (props.record as any)?.[props.df.fieldname]
return Array.isArray(value) ? value : []
})
const selectedRows = ref<Set<number>>(new Set())
const selectAll = ref(false)
const t = (key: string) => (props.ctx?.t ? props.ctx.t(key) : key)
async function loadChildMeta() {
const pagetype = props.df?.options
if (!pagetype) return
loading.value = true
try {
const res = await axios.get(`/api/data/PageType/${encodeURIComponent(pagetype)}` , { headers: get_session_api_headers(), withCredentials: true })
const fields: Field[] = res.data?.data?.fields || []
childFields.value = fields
} finally {
loading.value = false
}
}
function getTableColumns() {
// get_table_columns No. in_list_view
const table_fields = childFields.value
const columns: Array<[Partial<Field>, number]> = []
let total = 1
columns.push([{ label: t('No.') }, 1])
for (const tf of table_fields) {
const isLayout = ['Section Break', 'Column Break', 'Tab Break'].includes(tf.fieldtype)
if (isLayout) continue
if (!tf.in_list_view || !tf.label) continue
let colsize = tf.columns ? Number(tf.columns) : defaultCol(tf)
if (total > 11 + colsize) continue
total += colsize
columns.push([{ label: tf.label, fieldname: tf.fieldname, fieldtype: tf.fieldtype }, colsize])
}
adjust(total, true)
adjust(total, false)
function defaultCol(f: Field) {
//
const narrow = ['Int', 'Check', 'Percent']
const wide = ['Text', 'Long Text']
if (narrow.includes(f.fieldtype)) return 1
if (['Float', 'Currency', 'Date', 'Datetime'].includes(f.fieldtype)) return 2
if (wide.includes(f.fieldtype)) return 3
return 2
}
function adjust(_total: number, increase?: boolean) {
let passes = 0
let totalRef = _total
const startCond = () => (increase ? totalRef < 11 : totalRef >= 11)
while (startCond() && passes < 11) {
for (let i = 0; i < columns.length; i++) {
const col = columns[i]
const size = col[1]
const fieldname = (col[0] as any).fieldname
if (!fieldname) continue
if (size > 1 && size < 11) {
if (increase) {
col[1] += 1
totalRef++
} else {
col[1] -= 1
totalRef--
}
}
if (increase && totalRef > 9) break
if (!increase && totalRef < 11) break
}
passes++
}
}
return columns
}
const columns = computed(() => getTableColumns())
function addRow() {
if (!props.canEdit) return
const arr = Array.isArray((props.record as any)[props.df.fieldname]) ? (props.record as any)[props.df.fieldname] : (((props.record as any)[props.df.fieldname] = []), (props.record as any)[props.df.fieldname])
const idx = (arr?.length || 0) + 1
const row: any = { idx }
//
for (const [meta] of columns.value.slice(1)) {
const fn = (meta as any).fieldname
if (fn && !(fn in row)) row[fn] = ''
}
arr.push(row)
}
function removeRow(i: number) {
if (!props.canEdit) return
const arr = rows.value
if (!arr?.length) return
arr.splice(i, 1)
arr.forEach((r: any, j: number) => (r.idx = j + 1))
//
selectedRows.value.clear()
selectAll.value = false
}
function toggleSelectAll() {
if (selectAll.value) {
selectedRows.value.clear()
} else {
selectedRows.value = new Set(Array.from({ length: rows.value.length }, (_, i) => i))
}
selectAll.value = !selectAll.value
}
function toggleRowSelect(index: number) {
if (selectedRows.value.has(index)) {
selectedRows.value.delete(index)
} else {
selectedRows.value.add(index)
}
//
selectAll.value = selectedRows.value.size === rows.value.length
}
function batchDelete() {
if (!props.canEdit || selectedRows.value.size === 0) return
const arr = rows.value
const indicesToDelete = Array.from(selectedRows.value).sort((a, b) => b - a) //
indicesToDelete.forEach(i => arr.splice(i, 1))
arr.forEach((r: any, j: number) => (r.idx = j + 1))
//
selectedRows.value.clear()
selectAll.value = false
}
onMounted(() => {
loadChildMeta()
})
</script>
<template>
<div class="table-container">
<span class="field-value">{{ record[df.fieldname] ?? '—' }}</span>
<template v-if="columns.length > 0">
<div class="table-grid">
<div class="table-head">
<div class="table-cell head checkbox" v-if="canEdit" style="width: 4%">
<input type="checkbox" :checked="selectAll" @change="toggleSelectAll" />
</div>
<div
v-for="([meta, size], i) in columns"
:key="i"
class="table-cell head"
:style="{ width: (size * 8) + '%' }"
>
{{ (meta as any).label }}
</div>
<div class="table-cell head actions" v-if="canEdit" style="width: 8%">{{ t('Actions') }}</div>
</div>
<div class="table-body" v-if="rows.length">
<div class="table-row" v-for="(r, i) in rows" :key="i">
<div class="table-cell checkbox" v-if="canEdit" style="width: 4%">
<input type="checkbox" :checked="selectedRows.has(i)" @change="toggleRowSelect(i)" />
</div>
<div
v-for="([meta, size], k) in columns"
:key="k"
class="table-cell"
:style="{ width: (size * 8) + '%' }"
>
<template v-if="k === 0">{{ i + 1 }}</template>
<template v-else>{{ r[(meta as any).fieldname] ?? '' }}</template>
</div>
<div class="table-cell actions" v-if="canEdit" style="width: 8%">
<button class="link-btn" type="button" @click="removeRow(i)">
<i class="fa-regular fa-trash-can"></i>
</button>
</div>
</div>
</div>
<div class="grid-empty" v-else>
<span>{{ t('No Data') }}</span>
</div>
</div>
<div class="grid-footer" v-if="canEdit">
<div class="footer-actions">
<button
v-if="selectedRows.size > 0"
class="btn btn-danger"
type="button"
@click="batchDelete"
>
{{ t('Delete') }} ({{ selectedRows.size }})
</button>
</div>
<div class="footer-add">
<button class="btn btn-primary" type="button" @click="addRow">{{ t('Add Row') }}</button>
</div>
</div>
</template>
<template v-else>
<span class="field-value">{{ loading ? t('Loading') + '…' : '—' }}</span>
</template>
</div>
</template>
<style scoped>
.table-container { width: 100%; }
.table-grid { width: 100%; }
.table-head { display: flex; background: #fff; border: 1px solid var(--table-border-color, #e5e7eb); border-top-left-radius: 6px; border-top-right-radius: 6px; }
.table-body { border: 1px solid var(--table-border-color, #e5e7eb); border-top: none; border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; }
.table-row { display: flex; border-top: 1px solid var(--table-border-color, #e5e7eb); }
.table-cell { padding: 8px 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-left: 1px solid var(--table-border-color, #e5e7eb); }
.table-cell:first-child { border-left: none; }
.table-cell.head { background: #fff; font-weight: 600; }
.table-cell.actions { text-align: center; }
.table-cell.checkbox { text-align: center; padding: 8px 4px; }
.table-cell.checkbox input[type="checkbox"] { margin: 0; }
.grid-empty { padding: 16px; text-align: center; color: #6b7280; border: 1px dashed #e5e7eb; border-radius: 6px; background: #fff; }
.grid-footer { margin-top: 12px; display: flex; justify-content: space-between; align-items: center; }
.footer-actions { flex: 1; }
.footer-add { flex: 0 0 auto; }
.btn { padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; border: 1px solid #e5e7eb; }
.btn-primary { background: #3b82f6; color: white; border-color: #3b82f6; }
.btn-primary:hover { background: #2563eb; }
.btn-danger { background: #ef4444; color: white; border-color: #ef4444; }
.btn-danger:hover { background: #dc2626; }
.link-btn { background: transparent; border: none; color: #ef4444; cursor: pointer; padding: 2px; }
</style>