实现Table控件,修复详情页子表无法正常显示的问题
This commit is contained in:
parent
50a027516c
commit
64b03ebfb1
@ -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>
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user