Files
oa/src/components/app-vuero/ComVFlexTable.vue
Kasi a70bfa10aa func : 가격조사 상세폼 양식추가
- 이메일, 사업자번호, 금액, 전송여부, 재전송여부
2025-05-28 17:14:56 +09:00

596 lines
14 KiB
Vue

<script setup lang="ts">
import { type VNode } from 'vue'
import { flewTableWrapperSymbol } from '../base/VFlexTableWrapper.vue'
export interface VFlexTableColumn {
key: string
label: string
format: (value: any, row: any, index: number) => any
renderHeader?: () => VNode
renderRow?: (row: any, column: VFlexTableColumn, index: number) => VNode
align?: 'start' | 'center' | 'end'
bold?: boolean
inverted?: boolean
scrollX?: boolean
scrollY?: boolean
grow?: boolean | 'lg' | 'xl'
media?: boolean
cellClass?: string
}
export interface VFlexTableProps {
data?: any[]
columns?: Record<string, string | Partial<VFlexTableColumn>>
printObjects?: boolean
reactive?: boolean
compact?: boolean
rounded?: boolean
separators?: boolean
clickable?: boolean
subtable?: boolean
noHeader?: boolean
usePaymentHeader?: boolean
}
const emits = defineEmits<{
(e: 'rowClick', row: any, index: number): void
}>()
const props = withDefaults(defineProps<VFlexTableProps>(), {
columns: undefined,
usePaymentHeader: false,
data: () => [],
})
const wrapper = inject(flewTableWrapperSymbol, null)
const data = computed(() => {
if (wrapper?.data) return wrapper.data
if (props.reactive) {
if (isReactive(props.data)) {
return props.data
}
else {
return reactive(props.data)
}
}
return toRaw(props.data)
})
const defaultFormatter = (value: any) => value
const columns = computed(() => {
const columnsSrc = wrapper?.columns ?? props.columns
let columns: VFlexTableColumn[] = []
if (columnsSrc) {
for (const [key, label] of Object.entries(columnsSrc)) {
if (typeof label === 'string') {
columns.push({
format: defaultFormatter,
label,
key,
})
}
else {
columns.push({
format: defaultFormatter,
label: key,
key,
...(label as any),
})
}
}
}
else if (data.value.length > 0) {
for (const [key] of Object.entries(data.value[0])) {
columns.push({
format: defaultFormatter,
label: key,
key,
})
}
}
return columns
})
</script>
<template>
<div
class="flex-table"
:class="[
props.compact && 'is-compact',
props.rounded && 'is-rounded',
props.separators && 'with-separators',
props.noHeader && 'no-header',
props.clickable && 'is-table-clickable',
props.subtable && 'sub-table',
]"
>
<slot v-if="props.usePaymentHeader" name="payment-header">
<div
v-if="!props.noHeader"
class="flex-table-header"
>
<template
v-for="column in columns"
:key="'col' + column.key"
>
<slot
name="header-column"
:column="column"
>
<component
:is="{ render: column.renderHeader } as any"
v-if="column.renderHeader"
:class="[
column.grow === true && 'is-grow',
column.grow === 'lg' && 'is-grow-lg',
column.grow === 'xl' && 'is-grow-xl',
column.align === 'end' && 'cell-end',
column.align === 'center' && 'cell-center',
]"
/>
<span
v-else
:class="[
column.grow === true && 'is-grow',
column.grow === 'lg' && 'is-grow-lg',
column.grow === 'xl' && 'is-grow-xl',
column.align === 'end' && 'cell-end',
column.align === 'center' && 'cell-center',
]"
>{{ column.label }}</span>
</slot>
</template>
</div>
</slot>
<slot v-else name="header">
<div
v-if="!props.noHeader"
class="flex-table-header"
>
<template
v-for="column in columns"
:key="'col' + column.key"
>
<slot
name="header-column"
:column="column"
>
<component
:is="{ render: column.renderHeader } as any"
v-if="column.renderHeader"
:class="[
column.grow === true && 'is-grow',
column.grow === 'lg' && 'is-grow-lg',
column.grow === 'xl' && 'is-grow-xl',
column.align === 'end' && 'cell-end',
column.align === 'center' && 'cell-center',
]"
/>
<span
v-else
:class="[
column.grow === true && 'is-grow',
column.grow === 'lg' && 'is-grow-lg',
column.grow === 'xl' && 'is-grow-xl',
column.align === 'end' && 'cell-end',
column.align === 'center' && 'cell-center',
]"
>{{ column.label }}</span>
</slot>
</template>
</div>
</slot>
<slot name="body">
<template
v-for="(row, index) in data"
:key="index"
>
<slot
name="body-row-pre"
:row="row"
:columns="columns"
:index="index"
/>
<!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
<div
class="flex-table-item"
:class="[props.clickable && 'is-clickable']"
:tabindex="props.clickable ? 0 : undefined"
:role="props.clickable ? 'button' : undefined"
@keydown.enter.prevent="
() => {
props.clickable && emits('rowClick', row, index)
}
"
@click="
() => {
props.clickable && emits('rowClick', row, index)
}
"
>
<slot
name="body-row"
:row="row"
:columns="columns"
:index="index"
>
<template
v-for="column in columns"
:key="'row' + column.key"
>
<VFlexTableCell :column="column">
<slot
:name="`body-cell-${column.key}`"
:row="row"
:column="column"
:index="index"
:value="column.format(row[column.key], row, index)"
>
<component
:is="
{
render: () => column.renderRow?.(row, column, index),
} as any
"
v-if="column.renderRow"
/>
<span
v-else-if="
typeof column.format(row[column.key], row, index) === 'object'
"
:class="[
column.cellClass,
column.inverted && 'dark-inverted',
// !column.inverted && (column.bold ? 'dark-text' : 'light-text'),
]"
>
<details v-if="printObjects">
<div class="language-json py-4">
<pre><code>{{ column.format(row[column.key], row, index) }}</code></pre>
</div>
</details>
</span>
<span
v-else
:class="[
column.cellClass,
column.inverted && 'dark-inverted',
// !column.inverted && (column.bold ? 'dark-text' : 'light-text'),
]"
>
{{ column.format(row[column.key], row, index) }}
</span>
</slot>
</VFlexTableCell>
</template>
</slot>
</div>
<slot
name="body-row-post"
:row="row"
:columns="columns"
:index="index"
/>
</template>
</slot>
</div>
</template>
<style lang="scss">
.flex-table {
.flex-table-header {
display: flex;
align-items: center;
padding: 20px 10px;
background-color: var(--primary);
> span,
.text {
flex: 1 1 0;
display: flex;
align-items: center;
font-size: 1.0rem;
font-weight: 600;
color: var(--smoke-white);
text-transform: uppercase;
padding: 0 10px;
justify-content: center;
align-items: center;
display: flex;
&.is-checkbox {
display: flex;
justify-content: center;
align-items: center;
width: 30px;
max-width: 30px;
.checkbox {
padding: 0;
> span {
height: 22px;
}
}
}
&.cell-center {
justify-content: center;
}
&.cell-end {
justify-content: flex-end;
}
&.is-grow {
flex-grow: 2;
}
&.is-grow-lg {
flex-grow: 3;
}
&.is-grow-xl {
flex-grow: 6;
}
a {
color: var(--muted-grey);
}
}
.checkbox {
padding-bottom: 10px;
padding-top: 0;
> span {
min-height: 20px;
}
}
}
.flex-table-item {
display: flex;
align-items: stretch;
width: 100%;
min-height: 60px;
background: var(--white);
border: 1px solid color-mix(in oklab, var(--fade-grey), black 3%);
padding: 8px;
margin-bottom: 6px;
&.is-row {
border: none;
background: transparent;
}
}
&.sub-table {
.flex-table-item {
padding-top: 0;
padding-bottom: 0;
margin-bottom: 0;
min-height: 40px;
border: none;
background: transparent;
.table-label {
font-family: var(--font);
text-transform: uppercase;
font-size: 0.8rem;
color: var(--light-text);
}
.table-total {
font-family: var(--font);
color: var(--dark-text);
font-weight: 500;
&.is-bigger {
font-size: 1.2rem;
font-weight: 600;
}
}
}
}
&.is-compact {
.flex-table-item {
margin-bottom: 0;
border-radius: 0;
&:not(:last-child) {
border-bottom: none;
}
}
&.is-rounded {
&:not(.no-header) {
.flex-table-item {
&:nth-of-type(2) {
border-radius: 8px 8px 0 0;
}
&:last-child {
margin-bottom: 6px;
border-radius: 0 0 8px 8px;
}
}
}
&.no-header {
.flex-table-item {
&:first-child {
border-radius: 8px 8px 0 0;
}
&:last-child {
margin-bottom: 6px;
border-radius: 0 0 8px 8px;
}
}
}
}
}
&:not(.is-compact) {
&.is-rounded {
.flex-table-item {
border-radius: 8px;
}
}
}
&.is-table-clickable {
.flex-table-item {
&:hover,
&:focus-within {
background: var(--widget-grey) !important;
}
}
}
&.with-separators {
.flex-table-item {
.flex-table-cell {
&:not(:first-of-type) {
border-inline-start: solid 1px color-mix(in oklab, var(--fade-grey), black 3%);
}
}
}
}
}
/* ==========================================================================
2. Flex Table Dark mode
========================================================================== */
.is-dark {
.flex-table {
&:not(.sub-table) {
.flex-table-item {
background: color-mix(in oklab, var(--dark-sidebar), white 6%);
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
}
}
&.with-separators {
.flex-table-item {
.flex-table-cell {
&:not(:first-of-type) {
border-inline-start: dashed 1px color-mix(in oklab, var(--dark-sidebar), white 12%);
}
}
}
}
&.is-table-clickable {
.flex-table-item {
&:hover,
&:focus-within {
background: color-mix(in oklab, var(--dark-sidebar), white 12%) !important;
}
}
}
}
}
/* ==========================================================================
3. Media Queries
========================================================================== */
@media (width <= 767px) {
.flex-table {
.flex-table-header {
display: none;
}
.flex-table-item {
flex-direction: column;
justify-content: center;
width: 100% !important;
padding: 20px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
> div {
border: none !important;
}
}
&:not(.sub-table) {
.flex-table-item {
.flex-table-cell {
> span,
> small,
> strong,
> p,
> div,
> .is-pushed-mobile,
> .text {
margin-inline-start: auto;
&.no-push {
margin-inline-start: 0 !important;
}
}
}
&:not(:first-child) {
.flex-table-cell {
&[data-th] {
&::before {
content: attr(data-th);
font-size: 0.9rem;
text-transform: uppercase;
font-weight: 500;
color: var(--muted-grey);
}
}
}
}
}
}
}
}
@media only screen and (width <= 767px) {
.flex-table {
&.sub-table {
padding-top: 16px;
.is-vhidden {
display: none !important;
}
.flex-table-item:not(.is-vhidden) {
flex-direction: revert !important;
}
}
}
}
.paymentColumn1 {
min-width: 20%;
}
.paymentColumn2 {
min-width: 40%;
}
.paymentColumn3 {
min-width: 10%;
}
.paymentColumn4 {
min-width: 20%;
}
.paymentColumn5 {
min-width: 10%;
}
</style>