693 lines
25 KiB
Vue
693 lines
25 KiB
Vue
<template>
|
|
<div class="field is-grouped is-grouped-multiline pl-2" v-if="filters?.length > 0">
|
|
<div class="control mr-5">
|
|
<a class="button is-primary is-small has-text-white has-text-weight-bold"
|
|
@click="updateData({ filters: [] })">Xóa lọc</a>
|
|
</div>
|
|
<div class="control pr-2 mr-5">
|
|
<span class="icon-text">
|
|
<SvgIcon v-bind="{ name: 'sigma.svg', type: 'primary', size: 20 }"></SvgIcon>
|
|
<span class="fsb-18 has-text-primary">{{ totalRows }}</span>
|
|
</span>
|
|
</div>
|
|
<div class="control" v-for="(v, i) in filters" :key="i">
|
|
<div class="tags has-addons is-marginless">
|
|
<a class="tag is-primary has-text-white is-marginless" @click="showCondition(v)">
|
|
{{ v.label?.indexOf('>') >= 0 ? $stripHtml(v.label, 30) : v.label }}
|
|
</a>
|
|
<a class="tag is-delete is-marginless has-text-black-bis" @click="removeFilter(i)"></a>
|
|
</div>
|
|
<span class="help has-text-black-bis">
|
|
{{ v.sort ?? (v.select
|
|
? '[' + (v.select[0] ? $stripHtml(v.select[0], 20) : '') + '…' + v.select.length + ']'
|
|
: v.condition) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-container mb-0 dtp-wrap" ref="container">
|
|
<table class="table is-fullwidth is-bordered is-narrow is-hoverable dtp-table" :style="tableStyle">
|
|
|
|
<!-- ═══════════ THEAD ═══════════ -->
|
|
<thead>
|
|
|
|
<!-- ROW 1: group headers (chỉ render nếu có ít nhất 1 field có groupHeader) -->
|
|
<tr v-if="hasGroupHeaders" class="dtp-header-group">
|
|
<th
|
|
v-for="(col, i) in groupHeaderRow"
|
|
:key="i"
|
|
:colspan="col.colspan"
|
|
:rowspan="col.rowspan"
|
|
:style="col.style"
|
|
:class="['dtp-th-group', col.isGroup ? 'dtp-th-group--label' : 'dtp-th-group--empty']"
|
|
>
|
|
<div v-if="col.isGroup">{{ col.label }}</div>
|
|
<div v-else :style="col.dropStyle" @click="showField(col.field)">
|
|
<a v-if="col.label?.indexOf('<') < 0">{{ col.label }}</a>
|
|
<a v-else>
|
|
<component :is="dynamicComponent(col.label)" :row="{}"
|
|
@clickevent="clickEvent($event, {}, col.field)" />
|
|
</a>
|
|
</div>
|
|
</th>
|
|
</tr>
|
|
|
|
<!-- ROW 2: chỉ render fields CÓ groupHeader (field không có groupHeader đã rowspan=2 ở Row 1) -->
|
|
<tr class="dtp-header-fields" v-if="hasGroupHeaders">
|
|
<th
|
|
v-for="(field, i) in displayFields.filter(f => f.groupHeader)"
|
|
:key="i"
|
|
:style="field.headerStyle"
|
|
:class="{ 'dtp-th--sorted': isSorted(field) }"
|
|
@click="showField(field)"
|
|
>
|
|
<div :style="field.dropStyle">
|
|
<a v-if="field.label?.indexOf('<') < 0">{{ field.label }}</a>
|
|
<a v-else>
|
|
<component :is="dynamicComponent(field.label)" :row="{}"
|
|
@clickevent="clickEvent($event, {}, field)" />
|
|
</a>
|
|
</div>
|
|
</th>
|
|
</tr>
|
|
<!-- ROW đơn (không có groupHeader nào): render bình thường -->
|
|
<tr class="dtp-header-fields" v-else>
|
|
<th
|
|
v-for="(field, i) in displayFields"
|
|
:key="i"
|
|
:style="field.headerStyle"
|
|
:class="{ 'dtp-th--sorted': isSorted(field) }"
|
|
@click="showField(field)"
|
|
>
|
|
<div :style="field.dropStyle">
|
|
<a v-if="field.label?.indexOf('<') < 0">{{ field.label }}</a>
|
|
<a v-else>
|
|
<component :is="dynamicComponent(field.label)" :row="{}"
|
|
@clickevent="clickEvent($event, {}, field)" />
|
|
</a>
|
|
</div>
|
|
</th>
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<!-- ═══════════ TBODY ═══════════ -->
|
|
<tbody>
|
|
<tr v-for="(v, i) in displayData" :key="i">
|
|
<td
|
|
v-for="(field, j) in displayFields"
|
|
:key="j"
|
|
:style="tdStyle(field, v)"
|
|
:class="[
|
|
'dtp-td',
|
|
field.split === 'stack' ? 'dtp-td--stack' : '',
|
|
field.split === 'columns' ? 'dtp-td--columns' : '',
|
|
field.textalign === 'right' ? 'dtp-td--right' : '',
|
|
]"
|
|
@dblclick="doubleClick(field, v)"
|
|
>
|
|
<!-- ── Custom template ── -->
|
|
<component
|
|
v-if="field.template"
|
|
:is="dynamicComponent(field.template)"
|
|
:row="v"
|
|
@clickevent="clickEvent($event, v, field)"
|
|
/>
|
|
|
|
<!-- ── split: "stack" ── -->
|
|
<template v-else-if="field.split === 'stack'">
|
|
<div class="dtp-stack">
|
|
<template v-if="v[field.name]?.length > 0">
|
|
<div
|
|
class="dtp-stack-row"
|
|
v-for="(item, si) in v[field.name]"
|
|
:key="si"
|
|
>
|
|
<!-- splitExpression: "{inv_amount} - {plan_amount}" → tính toán số học -->
|
|
<span v-if="field.splitExpression">
|
|
<CellValue
|
|
:value="evalSplitExpression(field.splitExpression, item)"
|
|
:type="field.splitType || 'number'"
|
|
:align="field.textalign"
|
|
/>
|
|
</span>
|
|
<!-- splitTemplate: "Đợt {cycle} - {code}" → ghép nhiều field -->
|
|
<span v-else-if="field.splitTemplate" class="dtp-split-tpl">
|
|
{{ resolveSplitTemplate(field.splitTemplate, item) }}
|
|
</span>
|
|
<CellValue
|
|
v-else
|
|
:value="item[field.splitField]"
|
|
:type="field.splitType || detectType(item[field.splitField])"
|
|
:align="field.textalign"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<div v-else class="dtp-stack-row dtp-stack-row--empty">
|
|
<span class="dtp-null">—</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- ── split: "columns" ── -->
|
|
<template v-else-if="field.split === 'columns'">
|
|
<div v-if="!v[field.name] || v[field.name].length === 0"
|
|
class="dtp-cols-empty">Không có dữ liệu</div>
|
|
<table v-else class="dtp-minitable">
|
|
<thead v-if="field.showSubHeader !== false">
|
|
<tr>
|
|
<th v-for="(sf, si) in resolveSubfields(field, v[field.name])" :key="si">
|
|
{{ sf.label }}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(item, si) in v[field.name]" :key="si">
|
|
<td
|
|
v-for="(sf, sj) in resolveSubfields(field, v[field.name])"
|
|
:key="sj"
|
|
:class="['dtp-minitd', sf.type === 'currency' || sf.type === 'number' ? 'dtp-minitd--right' : '']"
|
|
>
|
|
<CellValue :value="item[sf.name]" :type="sf.type" />
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
<!-- mini footer sum -->
|
|
<tfoot v-if="field.showSubFooter !== false && hasSummableSubs(field, v[field.name])">
|
|
<tr>
|
|
<td
|
|
v-for="(sf, si) in resolveSubfields(field, v[field.name])"
|
|
:key="si"
|
|
:class="sf.type === 'currency' || sf.type === 'number' ? 'dtp-minitd--right' : ''"
|
|
class="dtp-minifoot"
|
|
>
|
|
<template v-if="si === 0">
|
|
<span class="dtp-null">∑</span>
|
|
</template>
|
|
<template v-else-if="sf.type === 'currency' || sf.type === 'number'">
|
|
<CellValue :value="sumSubField(v[field.name], sf.name)" :type="sf.type" />
|
|
</template>
|
|
</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</template>
|
|
|
|
<!-- ── Normal cell (giữ nguyên logic gốc) ── -->
|
|
<template v-else>
|
|
<CellValue
|
|
v-if="field.type"
|
|
:value="v[field.name]"
|
|
:type="field.type"
|
|
:field="field"
|
|
/>
|
|
<span v-else>{{ v[field.name] ?? '' }}</span>
|
|
</template>
|
|
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
|
|
<!-- ═══════════ TFOOT (summary) ═══════════ -->
|
|
<tfoot v-if="showSummary">
|
|
<tr>
|
|
<td
|
|
v-for="(field, i) in displayFields"
|
|
:key="i"
|
|
:style="field.headerStyle"
|
|
class="dtp-foot"
|
|
:class="field.textalign === 'right' ? 'dtp-td--right' : ''"
|
|
>
|
|
<span v-if="i === 0" class="dtp-foot-label">∑</span>
|
|
<CellValue
|
|
v-else-if="(field.summary === 'sum' || field.type === 'currency') && !field.split"
|
|
:value="sumColumn(field)"
|
|
type="currency"
|
|
/>
|
|
<span v-else-if="field.summary === 'count'">{{ totalRows }}</span>
|
|
</td>
|
|
</tr>
|
|
</tfoot>
|
|
|
|
</table>
|
|
|
|
<DatatablePagination
|
|
v-bind="{ data: data, perPage: perPage }"
|
|
@changepage="changePage"
|
|
v-if="showPaging"
|
|
/>
|
|
</div>
|
|
|
|
<Modal
|
|
@close="close"
|
|
@selected="doSelect"
|
|
@confirm="confirmRemove"
|
|
v-bind="showmodal"
|
|
v-if="showmodal"
|
|
/>
|
|
</template>
|
|
|
|
<!-- ════════════════════════ CELL VALUE COMPONENT ════════════════════════ -->
|
|
<script>
|
|
export const CellValue = {
|
|
name: 'CellValue',
|
|
props: {
|
|
value: { default: null },
|
|
type: { type: String, default: 'string' },
|
|
field: { type: Object, default: () => ({}) },
|
|
align: { type: String, default: '' },
|
|
},
|
|
setup(props) {
|
|
const { $formatNumber } = useNuxtApp()
|
|
function fmtCurrency(v) {
|
|
if (v === null || v === undefined) return null
|
|
return Number(v).toLocaleString('vi-VN')
|
|
}
|
|
return { fmtCurrency }
|
|
},
|
|
template: `
|
|
<span v-if="value === null || value === undefined" class="dtp-null">—</span>
|
|
<span v-else-if="type === 'currency'"
|
|
:class="['dtp-currency', Number(value) < 0 ? 'dtp-currency--neg' : Number(value) === 0 ? 'dtp-currency--zero' : '']"
|
|
>{{ fmtCurrency(value) }}</span>
|
|
<span v-else-if="type === 'number'"
|
|
class="dtp-number">{{ Number(value).toLocaleString('vi-VN') }}</span>
|
|
<span v-else-if="type === 'date'"
|
|
class="dtp-date">{{ value }}</span>
|
|
<span v-else-if="type === 'code'"
|
|
class="dtp-code">{{ value }}</span>
|
|
<span v-else-if="type === 'badge' && field.badgeMap"
|
|
:class="['dtp-badge', field.badgeColorMap?.[value] ?? '']"
|
|
>{{ field.badgeMap?.[value] ?? value }}</span>
|
|
<span v-else>{{ value }}</span>
|
|
`
|
|
}
|
|
</script>
|
|
|
|
<script setup>
|
|
import { ref, computed, reactive, watch } from "vue/dist/vue.esm-bundler.js"
|
|
import { defineComponent } from "vue"
|
|
import { useStore } from "~/stores/index"
|
|
|
|
const emit = defineEmits(["edit", "insert", "dataevent", "changedata", "changefilter", "displayDataChange"])
|
|
const {
|
|
$calc, $calculate, $calculateData, $copy, $deleterow, $empty,
|
|
$find, $getEditRights, $formatNumber, $multiSort, $remove, $stripHtml, $unique,
|
|
} = useNuxtApp()
|
|
const store = useStore()
|
|
const props = defineProps({ pagename: String })
|
|
|
|
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
function dynamicComponent(html) {
|
|
return defineComponent({
|
|
template: html,
|
|
props: { row: { type: Object, default: () => ({}) } },
|
|
})
|
|
}
|
|
|
|
function detectType(value) {
|
|
if (typeof value === 'number') return 'number'
|
|
return 'string'
|
|
}
|
|
|
|
function resolveSubfields(field, rows) {
|
|
if (field.subfields) return field.subfields
|
|
if (!rows?.length) return []
|
|
return Object.keys(rows[0]).map(k => ({
|
|
name: k, label: k,
|
|
type: typeof rows[0][k] === 'number' ? 'number' : 'string',
|
|
}))
|
|
}
|
|
|
|
function hasSummableSubs(field, rows) {
|
|
if (!rows?.length) return false
|
|
return resolveSubfields(field, rows).some(sf => sf.type === 'currency' || sf.type === 'number')
|
|
}
|
|
|
|
function sumSubField(rows, name) {
|
|
return rows.reduce((s, r) => s + (Number(r[name]) || 0), 0)
|
|
}
|
|
|
|
function sumColumn(field) {
|
|
return (data || []).reduce((s, r) => s + (Number(r[field.name]) || 0), 0)
|
|
}
|
|
|
|
function isSorted(field) {
|
|
return filters?.some(f => f.name === field.name && f.sort)
|
|
}
|
|
|
|
function resolveSplitTemplate(template, item) {
|
|
return template.replace(/\{(\w+)\}/g, (_, key) => item[key] ?? '')
|
|
}
|
|
|
|
function evalSplitExpression(expression, item) {
|
|
try {
|
|
const expr = expression.replace(/\{(\w+)\}/g, (_, key) => {
|
|
const v = item[key]
|
|
return (v === null || v === undefined) ? 'null' : Number(v)
|
|
})
|
|
const exprFull = expr.replace(/\b([a-zA-Z_]\w*)\b/g, (match) => {
|
|
if (!(match in item)) return match
|
|
const v = item[match]
|
|
return (v === null || v === undefined) ? 'null' : Number(v)
|
|
})
|
|
return Function('"use strict"; return (' + exprFull + ')')()
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
// ── state ────────────────────────────────────────────────────────────────────
|
|
var timer
|
|
var showPaging = ref(false)
|
|
var totalRows = ref(0)
|
|
var currentPage = 1
|
|
var displayFields = ref([])
|
|
var displayData = ref([])
|
|
var pagedata = store[props.pagename]
|
|
var tablesetting = $copy(pagedata.tablesetting || store.tablesetting)
|
|
var perPage = Number($find(tablesetting, { code: 'per-page' }, 'detail')) || 20
|
|
var filters = $copy(pagedata.filters || [])
|
|
var currentField
|
|
var filterData = []
|
|
var currentRow
|
|
var data = $copy(pagedata.data)
|
|
var showmodal = ref()
|
|
|
|
const hasGroupHeaders = computed(() =>
|
|
displayFields.value.some(f => f.groupHeader)
|
|
)
|
|
|
|
const groupHeaderRow = computed(() => {
|
|
const fields = displayFields.value
|
|
if (!fields.length) return []
|
|
|
|
const result = []
|
|
let i = 0
|
|
while (i < fields.length) {
|
|
const f = fields[i]
|
|
if (!f.groupHeader) {
|
|
result.push({
|
|
label: f.label,
|
|
colspan: 1,
|
|
rowspan: 2,
|
|
isGroup: false,
|
|
style: f.headerStyle || '',
|
|
dropStyle: f.dropStyle || '',
|
|
field: f,
|
|
})
|
|
i++
|
|
} else {
|
|
let count = 1
|
|
while (i + count < fields.length && fields[i + count].groupHeader === f.groupHeader) {
|
|
count++
|
|
}
|
|
result.push({
|
|
label: f.groupHeader,
|
|
colspan: count,
|
|
rowspan: 1,
|
|
isGroup: true,
|
|
style: f.headerStyle || '',
|
|
})
|
|
i += count
|
|
}
|
|
}
|
|
return result
|
|
})
|
|
|
|
// ── summary ──────────────────────────────────────────────────────────────────
|
|
const showSummary = computed(() =>
|
|
pagedata?.showSummary !== false &&
|
|
displayFields.value.some(f => f.summary || f.type === 'currency')
|
|
)
|
|
|
|
function tdStyle(field, record) {
|
|
let val = getStyle(field, record)
|
|
if (field.split === 'stack' || field.split === 'columns') {
|
|
val = val.replace(/padding:[^;]+;?/g, '') + 'padding:0;vertical-align:top;'
|
|
}
|
|
return val
|
|
}
|
|
|
|
// ── watch ────────────────────────────────────────────────────────────────────
|
|
watch(() => store[props.pagename], () => { updateChange() })
|
|
|
|
function updateChange() {
|
|
pagedata = store[props.pagename]
|
|
if (!pagedata.update) return
|
|
if (pagedata.update.data) data = $copy(pagedata.update.data)
|
|
if (pagedata.update.filters) { doFilter(pagedata.update.filters); updateShow(); return }
|
|
if (filters.length > 0) doFilter(filters)
|
|
if (pagedata.update.fields || pagedata.update.data) updateShow()
|
|
}
|
|
|
|
const updateShow = function (full_data) {
|
|
const allowedFns = { '$getEditRights()': $getEditRights }
|
|
const arr = pagedata.fields.filter(({ show }) => {
|
|
if (typeof show === 'boolean') return show
|
|
if (show === 'true') return true
|
|
if (show === 'false') return false
|
|
return allowedFns[show]?.() || false
|
|
})
|
|
|
|
if (full_data === false) {
|
|
displayData.value = $copy(data)
|
|
} else {
|
|
displayData.value = $copy(
|
|
data.filter((_, idx) =>
|
|
idx >= (currentPage - 1) * perPage && idx < currentPage * perPage
|
|
)
|
|
)
|
|
}
|
|
|
|
displayData.value.forEach((v, idx) => {
|
|
v.__stt = (currentPage - 1) * perPage + idx + 1
|
|
})
|
|
arr.forEach(v => {
|
|
v.headerStyle = getSettingStyle('header', v)
|
|
v.dropStyle = getSettingStyle('dropdown', v)
|
|
})
|
|
displayFields.value = arr
|
|
showPagination()
|
|
}
|
|
|
|
function confirmRemove() {
|
|
$deleterow(pagedata.api.name, currentRow.id, props.pagename, true)
|
|
}
|
|
|
|
const clickEvent = (event, row, field) => {
|
|
const name = typeof event === 'string' ? event : event.name
|
|
const evdata = typeof event === 'string' ? event : event.data
|
|
if (name === 'remove') {
|
|
currentRow = row
|
|
showmodal.value = {
|
|
component: 'dialog/Confirm',
|
|
vbind: { content: 'Bạn có muốn xóa bản ghi này không?', duration: 10 },
|
|
title: 'Xác nhận', width: '500px', height: '100px',
|
|
}
|
|
}
|
|
emit(name, row, field, evdata)
|
|
}
|
|
|
|
const showField = async (field) => {
|
|
if (pagedata.contextMenu === false || field.menu === 'no') return
|
|
currentField = field
|
|
filterData = $unique(pagedata.data, [field.name])
|
|
showmodal.value = {
|
|
vbind: { pagename: props.pagename, field, filters, filterData, width: 100 },
|
|
component: 'datatable/ContextMenu',
|
|
title: field.name, width: '650px', height: '500px',
|
|
}
|
|
}
|
|
|
|
const getStyle = (field, record) => {
|
|
let val = (tablesetting.find(v => v.code === 'td-border')?.detail ?? '') + ';'
|
|
let stop = false
|
|
if (!Array.isArray(field.bgcolor) && field.bgcolor) {
|
|
val += ` background-color:${field.bgcolor};`
|
|
} else if (Array.isArray(field.bgcolor)) {
|
|
for (const v of field.bgcolor) {
|
|
if (stop) break
|
|
const match = v.type === 'search'
|
|
? record[field.name]?.toLowerCase().includes(v.keyword?.toLowerCase())
|
|
: $calculate(record, v.tags, v.expression)?.value
|
|
if (match) { val += ` background-color:${v.color};`; stop = true }
|
|
}
|
|
}
|
|
stop = false
|
|
if (!Array.isArray(field.color) && field.color) {
|
|
val += ` color:${field.color};`
|
|
} else if (Array.isArray(field.color)) {
|
|
for (const v of field.color) {
|
|
if (stop) break
|
|
if ($calculate(record, v.tags, v.expression)?.value) { val += ` color:${v.color};`; stop = true }
|
|
}
|
|
}
|
|
const fs = tablesetting.find(v => v.code === 'table-font-size')?.detail ?? '12'
|
|
val += ` font-size:${field.textsize ?? fs}px;`
|
|
if (field.textalign) val += ` text-align:${field.textalign};`
|
|
if (field.minwidth) val += ` min-width:${field.minwidth}px;`
|
|
if (field.maxwidth) val += ` max-width:${field.maxwidth}px;`
|
|
return val
|
|
}
|
|
|
|
const getSettingStyle = (name, field) => {
|
|
let val = ''
|
|
if (name === 'header') {
|
|
val += `background-color:${tablesetting.find(v => v.code === 'header-background')?.detail};`
|
|
if (field?.minwidth) val += ` min-width:${field.minwidth}px;`
|
|
if (field?.maxwidth) val += ` max-width:${field.maxwidth}px;`
|
|
} else if (name === 'dropdown') {
|
|
val += `font-size:${tablesetting.find(v => v.code === 'header-font-size')?.detail}px;`
|
|
val += filters.find(v => v.name === field?.name)
|
|
? `color:${tablesetting.find(v => v.code === 'header-filter-color')?.detail};`
|
|
: `color:${tablesetting.find(v => v.code === 'header-font-color')?.detail};`
|
|
}
|
|
return val
|
|
}
|
|
|
|
const changePage = (page) => { currentPage = page; updateShow() }
|
|
|
|
const showPagination = () => {
|
|
showPaging.value = pagedata.pagination !== false
|
|
totalRows.value = data.length
|
|
if (showPaging.value && pagedata.api) {
|
|
if (pagedata.api.full_data === false) totalRows.value = pagedata.api.total_rows
|
|
showPaging.value = totalRows.value > perPage
|
|
}
|
|
}
|
|
|
|
const close = () => { showmodal.value = undefined }
|
|
|
|
const frontendFilter = (newVal) => {
|
|
newVal = $copy(newVal)
|
|
let filtered = $copy(pagedata.data)
|
|
newVal.filter(m => m.select || m.filter).forEach(v => {
|
|
if (v.select) {
|
|
filtered = filtered.filter(x =>
|
|
v.select.findIndex(y => ($empty(y) ? $empty(x[v.name]) : y === x[v.name])) > -1
|
|
)
|
|
} else if (v.filter) {
|
|
filtered = filtered.filter(x => {
|
|
if ($empty(x[v.name])) return false
|
|
let text = ''
|
|
v.filter.forEach((y, k) => {
|
|
text += `${k > 0 ? (v.filter[k-1].operator === 'and' ? ' &&' : ' ||') : ''}
|
|
${$formatNumber(x[v.name])} ${y.condition === '=' ? '==' : y.condition === '<>' ? '!==' : y.condition} ${$formatNumber(y.value)}`
|
|
})
|
|
return $calc(text)
|
|
})
|
|
}
|
|
})
|
|
const sortList = filters.filter(x => x.sort)
|
|
if (!sortList.length) return filtered
|
|
const sort = {}, format = {}
|
|
sortList.forEach(v => { sort[v.name] = v.sort === 'az' ? 'asc' : 'desc'; format[v.name] = v.format })
|
|
return $multiSort(filtered, sort, format)
|
|
}
|
|
|
|
const doFilter = (newVal, nonset) => {
|
|
if (currentPage > 1 && nonset !== true) currentPage = 1
|
|
if (pagedata.api?.full_data) {
|
|
data = frontendFilter(newVal)
|
|
pagedata.dataFilter = $copy(data)
|
|
store.commit(props.pagename, pagedata)
|
|
emit('changedata', newVal)
|
|
}
|
|
pagedata.filters = newVal
|
|
store.commit(props.pagename, pagedata)
|
|
emit('changefilter', newVal?.length > 0)
|
|
}
|
|
|
|
const doSelect = (value) => {
|
|
showmodal.value = undefined
|
|
const field = currentField
|
|
let found = filters.find(v => v.name === field.name)
|
|
if (found) {
|
|
if (!found.select) found.select = []
|
|
const idx = found.select.findIndex(x => x === value)
|
|
idx >= 0 ? $remove(found.select, idx) : found.select.push(value)
|
|
if (found.select.length === 0) {
|
|
const i = filters.findIndex(v => v.name === field.name)
|
|
if (i >= 0) $remove(filters, i)
|
|
}
|
|
} else {
|
|
filters.push({ name: field.name, label: field.label, select: [value], format: field.format })
|
|
}
|
|
doFilter(filters)
|
|
updateShow()
|
|
}
|
|
|
|
const removeFilter = (i) => { $remove(filters, i); doFilter(filters); updateShow() }
|
|
|
|
const updateData = (newVal) => {
|
|
if (newVal.filters) filters = $copy(newVal.filters)
|
|
else if (pagedata.filters) filters = $copy(pagedata.filters)
|
|
if (newVal.data || newVal.fields || newVal.filters) doFilter(filters)
|
|
if (newVal.data || newVal.fields || newVal.filters || newVal.tablesetting) updateShow()
|
|
}
|
|
|
|
const doubleClick = (field, v) => { currentField = field; doSelect(v[field.name]) }
|
|
|
|
var tableStyle = getSettingStyle('table')
|
|
setTimeout(() => updateShow(), 200)
|
|
|
|
watch(displayData, (val) => {
|
|
emit('displayDataChange', toRaw(val))
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* ── Group headers ─────────────────────────────────────────────── */
|
|
.dtp-th-group--label {
|
|
text-align: center;
|
|
font-weight: 700;
|
|
letter-spacing: 0.04em;
|
|
color:#ffffff;
|
|
border-bottom: 2px solid rgba(255,255,255,0.3) !important;
|
|
}
|
|
|
|
/* ── Hover (giữ nguyên) ───────────────────────────────────────── */
|
|
:deep(.table tbody tr:hover td, .table tbody tr:hover th) {
|
|
background-color: hsl(0, 0%, 78%);
|
|
color: rgb(0, 0, 0);
|
|
}
|
|
|
|
/* ── Stack split ──────────────────────────────────────────────── */
|
|
.dtp-td--stack { padding: 0 !important; vertical-align: top; }
|
|
.dtp-stack { display: flex; flex-direction: column; }
|
|
.dtp-stack-row {
|
|
padding: 4px 8px;
|
|
min-height: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
border-bottom: 1px solid #e8ecf0;
|
|
}
|
|
.dtp-stack-row:last-child { border-bottom: none; }
|
|
.dtp-stack-row--empty { color: #bbb; font-style: italic; }
|
|
.dtp-split-tpl { font-size: 11px; }
|
|
|
|
/* ── Columns split (mini-table) ───────────────────────────────── */
|
|
.dtp-td--columns { padding: 0 !important; vertical-align: top; }
|
|
.dtp-cols-empty { padding: 6px 10px; color: #bbb; font-style: italic; }
|
|
.dtp-minitable { width: 100%; border-collapse: collapse; }
|
|
.dtp-minitable thead th {
|
|
padding: 3px 8px;
|
|
border-right: 1px solid #dde2e8;
|
|
border-bottom: 1px solid #c8cdd3;
|
|
white-space: nowrap;
|
|
text-transform: uppercase;
|
|
}
|
|
.dtp-minitd {
|
|
padding: 4px 8px;
|
|
border-right: 1px solid #eef1f4;
|
|
border-bottom: 1px solid #eef1f4;
|
|
}
|
|
.dtp-minitd--right { text-align: right; }
|
|
.dtp-minitable tbody tr:last-child .dtp-minitd { border-bottom: none; }
|
|
.dtp-minifoot { font-weight: 700; border-top: 1px solid #c8cdd3 !important; padding: 4px 8px; }
|
|
|
|
/* ── Số âm / zero cho currency ────────────────────────────────── */
|
|
.dtp-currency--neg { color: #ef4444; }
|
|
.dtp-currency--zero { color: #b0b7c3; }
|
|
</style> |