changes
This commit is contained in:
@@ -144,8 +144,8 @@ export default defineNuxtPlugin(() => {
|
||||
},
|
||||
{
|
||||
name: "dealersetting",
|
||||
url: "data/Biz_Setting/",
|
||||
url_detail: "data-detail/Biz_Setting/",
|
||||
url: "data/Common/",
|
||||
url_detail: "data-detail/Common/",
|
||||
params: { sort: "index" },
|
||||
},
|
||||
{
|
||||
@@ -250,8 +250,8 @@ export default defineNuxtPlugin(() => {
|
||||
{
|
||||
name: "common",
|
||||
commit: "common",
|
||||
url: "data/Biz_Setting/",
|
||||
url_detail: "data-detail/Biz_Setting/",
|
||||
url: "data/Common/",
|
||||
url_detail: "data-detail/Common/",
|
||||
params: { sort: "index" },
|
||||
},
|
||||
{ name: "upload", url: "upload/", params: {} },
|
||||
@@ -302,8 +302,8 @@ export default defineNuxtPlugin(() => {
|
||||
{
|
||||
name: "bizsetting",
|
||||
commit: "bizsetting",
|
||||
url: "data/Biz_Setting/",
|
||||
url_detail: "data-detail/Biz_Setting/",
|
||||
url: "data/Common/",
|
||||
url_detail: "data-detail/Common/",
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
@@ -1285,7 +1285,7 @@ export default defineNuxtPlugin(() => {
|
||||
/**
|
||||
* @param {'edit' | 'view'} right
|
||||
* @param {{ code?: string, category?: string }} options
|
||||
* `code` & `category` from `Biz_Setting`
|
||||
* `code` & `category` from `Common`
|
||||
* - If passed options, returns edit rights for `code` (useful for components used in modals in many different tabs)
|
||||
* - If not passed, returns edit rights for current tab & subtab
|
||||
* @returns {boolean}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
const { $getapi, $readyapi } = useNuxtApp()
|
||||
let connlist = $readyapi(['moneyunit', 'datatype', 'filterchoice', 'colorchoice', 'textalign', 'placement', 'colorscheme',
|
||||
'filtertype', 'sorttype', 'tablesetting', 'settingchoice', 'sharechoice', 'menuchoice', 'settingtype', 'settingclass',
|
||||
'sex', 'legaltype', 'cart'])
|
||||
let filter = connlist.filter(v=>!v.ready)
|
||||
if(filter.length>0) await $getapi(filter)
|
||||
})
|
||||
export default defineNuxtPlugin(async () => {
|
||||
const { $getapi, $readyapi } = useNuxtApp();
|
||||
let connlist = $readyapi(["common", "tablesetting"]);
|
||||
let filter = connlist.filter((v) => !v.ready);
|
||||
if (filter.length > 0) await $getapi(filter);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { defineNuxtPlugin } from "#app";
|
||||
import Employee from '@/components/viewer/Employee.vue';
|
||||
import Kpi from '@/components/viewer/Kpi.vue';
|
||||
import Salary from '@/components/viewer/Salary.vue';
|
||||
import Dashboard from '@/components/viewer/Dashboard.vue';
|
||||
import HRM from '@/components/viewer/HRM.vue';
|
||||
import Rollcall from '@/components/viewer/Rollcall.vue';
|
||||
import Employee from "@/components/viewer/Employee.vue";
|
||||
import Kpi from "@/components/viewer/Kpi.vue";
|
||||
import Salary from "@/components/viewer/Salary.vue";
|
||||
import Dashboard from "@/components/viewer/Dashboard.vue";
|
||||
import HRM from "@/components/viewer/HRM.vue";
|
||||
import Rollcall from "@/components/viewer/Rollcall.vue";
|
||||
import Notebox from "~/components/common/Notebox.vue";
|
||||
import ProductCountbox from "~/components/common/ProductCountbox.vue";
|
||||
import SvgIcon from "~/components/SvgIcon.vue";
|
||||
@@ -20,13 +20,9 @@ import ChipImage from "~/components/media/ChipImage.vue";
|
||||
import Avatarbox from "~/components/common/Avatarbox.vue";
|
||||
import Email from "~/components/marketing/email/Email.vue";
|
||||
import ViewList from "~/components/common/ViewList.vue";
|
||||
import InternalEntry from "~/components/modal/InternalEntry.vue"
|
||||
|
||||
import Configuration from "~/components/maintab/Configuration.vue";
|
||||
import DebtView from "~/components/accounting/DebtView.vue";
|
||||
import ReportDaily from "~/components/report/Daily.vue";
|
||||
import ReportFormTo from "~/components/report/FromTo.vue";
|
||||
import ReportMonthly from "~/components/report/Monthly.vue";
|
||||
|
||||
//format
|
||||
import FormatNumber from "~/components/datatable/format/FormatNumber.vue";
|
||||
@@ -50,36 +46,18 @@ import ScrollBox from "~/components/datatable/ScrollBox.vue";
|
||||
import Product from "~/components/product/Product.vue";
|
||||
import Reservation from "~/components/modal/Reservation.vue";
|
||||
import UserMainTab from "~/components/modal/UserMainTab.vue";
|
||||
import TransactionFiles from "~/components/transaction/TransactionFiles.vue";
|
||||
import PaymentSchedule from "~/components/application/PaymentSchedule.vue";
|
||||
import TransactionView from "~/components/transaction/TransactionView.vue";
|
||||
import ContractPaymentUpload from "~/components/application/ContractPaymentUpload.vue";
|
||||
import CountWithAdd from "~/components/common/CountWithAdd.vue";
|
||||
import CalculationView from "~/components/application/CalculationView.vue";
|
||||
import InternalAccount from "~/components/accounting/InternalAccount.vue";
|
||||
import MenuAccount from "~/components/menu/MenuAccount.vue";
|
||||
import PhaseAdvance from "~/components/application/PhaseAdvance.vue";
|
||||
import ImageLayout from '@/components/media/ImageLayout.vue';
|
||||
import ProjectDocuments from '~/components/product/ProjectDocuments.vue';
|
||||
import ProductEdit from '~/components/product/ProductEdit.vue';
|
||||
|
||||
import ProductHandover from '~/components/product/ProductHandover.vue'
|
||||
import Cart from '~/components/product/Cart.vue'
|
||||
import CountdownTimer from '~/components/common/CountdownTimer.vue'
|
||||
import CustomerInfo2 from '~/components/customer/CustomerInfo2.vue'
|
||||
import MenuFile from '~/components/menu/MenuFile.vue'
|
||||
import DebtProduct from '~/components/accounting/DebtProduct.vue'
|
||||
import DebtCustomer from '~/components/accounting/DebtCustomer.vue'
|
||||
import Due from '~/components/debt/Due.vue';
|
||||
import Overdue from '@/components/debt/Overdue.vue';
|
||||
import DebtSendEmail from '@/components/debt/DebtSendEmail.vue';
|
||||
import DebtCheckbox from '@/components/debt/DebtCheckbox.vue';
|
||||
import Allocate from '@/components/transaction/Allocate.vue';
|
||||
import Handover from '~/components/handover/Handover.vue'
|
||||
import HandoverSendEmail from '@/components/handover/HandoverSendEmail.vue';
|
||||
import HandoverCheckbox from '@/components/handover/HandoverCheckbox.vue';
|
||||
import HandoverStatus from '@/components/handover/HandoverStatus.vue';
|
||||
import CountDown from '@/components/dialog/CountDown.vue';
|
||||
import ImageLayout from "@/components/media/ImageLayout.vue";
|
||||
import Cart from "~/components/product/Cart.vue";
|
||||
import CountdownTimer from "~/components/common/CountdownTimer.vue";
|
||||
import CustomerInfo2 from "~/components/customer/CustomerInfo2.vue";
|
||||
import MenuFile from "~/components/menu/MenuFile.vue";
|
||||
import Rights from "@/components/viewer/Rights.vue";
|
||||
|
||||
const components = {
|
||||
DebtView,
|
||||
@@ -88,14 +66,10 @@ const components = {
|
||||
CustomerInfo2,
|
||||
CountdownTimer,
|
||||
PhaseAdvance,
|
||||
InternalEntry,
|
||||
ViewList,
|
||||
ColorText,
|
||||
CalculationView,
|
||||
CountWithAdd,
|
||||
ContractPaymentUpload,
|
||||
TransactionView,
|
||||
TransactionFiles,
|
||||
Reservation,
|
||||
Notebox,
|
||||
ProductCountbox,
|
||||
@@ -118,9 +92,6 @@ const components = {
|
||||
MenuParam,
|
||||
ScrollBox,
|
||||
MenuPayment,
|
||||
ReportDaily,
|
||||
ReportFormTo,
|
||||
ReportMonthly,
|
||||
DataModel,
|
||||
FormatNumber,
|
||||
FormatDate,
|
||||
@@ -134,27 +105,15 @@ const components = {
|
||||
InternalAccount,
|
||||
MenuAccount,
|
||||
ImageLayout,
|
||||
ProjectDocuments,
|
||||
Cart,
|
||||
MenuFile,
|
||||
DebtProduct,
|
||||
DebtCustomer,
|
||||
Due,
|
||||
Overdue,
|
||||
DebtSendEmail,
|
||||
DebtCheckbox,
|
||||
ProductHandover,
|
||||
Allocate,
|
||||
Handover,
|
||||
HandoverSendEmail,
|
||||
HandoverCheckbox,
|
||||
HandoverStatus,
|
||||
Dashboard,
|
||||
HRM,
|
||||
Rollcall,
|
||||
Salary,
|
||||
Kpi,
|
||||
Employee
|
||||
Employee,
|
||||
Rights,
|
||||
};
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { defineNuxtPlugin } from '#app';
|
||||
import { CkeditorPlugin } from '@ckeditor/ckeditor5-vue';
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.use(CkeditorPlugin);
|
||||
});
|
||||
@@ -1,323 +0,0 @@
|
||||
/**
|
||||
* plugins/excelExporter.js
|
||||
*
|
||||
* Xuất Excel từ DataTable với đầy đủ:
|
||||
* - Group header (2 dòng, colspan/rowspan merge)
|
||||
* - Stack split (array → nhiều sub-row, mỗi item 1 dòng)
|
||||
* - splitTemplate, splitExpression, splitField, splitType currency
|
||||
* - Template: FormatNumber, dayjs date, điều kiện row.type===x, Math.abs diff
|
||||
* - Lọc cột action (DebtCheckbox, SvgIcon, modal...)
|
||||
* - Màu header từ headerStyle / tablesetting
|
||||
*
|
||||
* Không cần cài thêm package — SheetJS load từ CDN lần đầu dùng (lazy).
|
||||
*/
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Import SheetJS từ package đã cài (npm install xlsx)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
import * as _XLSX from 'xlsx'
|
||||
|
||||
async function getXLSX() {
|
||||
return _XLSX
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function stripHtml(str) {
|
||||
if (!str) return ''
|
||||
return String(str).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
function fmtCurrency(v) {
|
||||
if (v === null || v === undefined || v === '') return ''
|
||||
const n = Number(v)
|
||||
if (isNaN(n)) return String(v)
|
||||
return n.toLocaleString('vi-VN')
|
||||
}
|
||||
|
||||
function fmtDate(raw, fmt = 'DD/MM/YYYY') {
|
||||
if (!raw) return ''
|
||||
try {
|
||||
const d = new Date(raw)
|
||||
const dd = String(d.getDate()).padStart(2, '0')
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
||||
return fmt.replace('DD', dd).replace('MM', mm).replace('YYYY', String(d.getFullYear()))
|
||||
} catch { return String(raw) }
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
// eslint-disable-next-line no-new-func
|
||||
return Function('"use strict"; return (' + expr + ')')()
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Resolve text của 1 item trong stack array
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function getStackItemText(field, item) {
|
||||
if (field.splitExpression) {
|
||||
const val = evalSplitExpression(field.splitExpression, item)
|
||||
if (val === null || val === undefined) return '—'
|
||||
return field.splitType === 'currency' ? fmtCurrency(val) : String(val)
|
||||
}
|
||||
if (field.splitTemplate) return resolveSplitTemplate(field.splitTemplate, item)
|
||||
const val = item[field.splitField]
|
||||
if (val === null || val === undefined) return '—'
|
||||
return field.splitType === 'currency' ? fmtCurrency(val) : String(val)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Resolve text của 1 cell bình thường
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function getCellText(field, row) {
|
||||
if (field.name === '__stt') return row.__stt ?? ''
|
||||
|
||||
if (field.split === 'stack') {
|
||||
const arr = row[field.name]
|
||||
if (!Array.isArray(arr) || arr.length === 0) return '—'
|
||||
// Gom tất cả item thành 1 chuỗi ngăn cách bởi newline
|
||||
return arr.map(item => getStackItemText(field, item)).join('\n')
|
||||
}
|
||||
|
||||
if (field.template) return evalTemplate(field.template, row)
|
||||
|
||||
if (field.format === 'date' && row[field.name]) return fmtDate(row[field.name])
|
||||
|
||||
if (field.format === 'number' || field.type === 'currency') {
|
||||
return row[field.name] !== null && row[field.name] !== undefined
|
||||
? fmtCurrency(row[field.name]) : ''
|
||||
}
|
||||
|
||||
return row[field.name] ?? ''
|
||||
}
|
||||
|
||||
function evalTemplate(template, row) {
|
||||
// <FormatNumber v-bind="{value: row.xxx}" />
|
||||
const fnMatch = template.match(/FormatNumber[^}]*value:\s*row\.(\w+)/)
|
||||
if (fnMatch) {
|
||||
const v = row[fnMatch[1]]
|
||||
return v === null || v === undefined ? '' : fmtCurrency(v)
|
||||
}
|
||||
|
||||
// row.type === N ? row.xxx : null
|
||||
const condMatch = template.match(/row\.(\w+)\s*===\s*(\d+)\s*\?\s*row\.(\w+)\s*:\s*null/)
|
||||
if (condMatch) {
|
||||
const v = row[condMatch[1]] === Number(condMatch[2]) ? row[condMatch[3]] : null
|
||||
return v === null || v === undefined ? '' : fmtCurrency(v)
|
||||
}
|
||||
|
||||
// $dayjs(row.xxx).format('...')
|
||||
const djMatch = template.match(/\$dayjs\(row\.(\w+)\)\.format\(['"]([^'"]+)['"]\)/)
|
||||
if (djMatch) return fmtDate(row[djMatch[1]], djMatch[2])
|
||||
|
||||
// Math.abs($dayjs().startOf('day').diff(row.xxx, 'day'))
|
||||
const diffMatch = template.match(/Math\.abs\(.*\.diff\(row\.(\w+),\s*'day'\)\)/)
|
||||
if (diffMatch) {
|
||||
try {
|
||||
const d = new Date(row[diffMatch[1]]); d.setHours(0, 0, 0, 0)
|
||||
const today = new Date(); today.setHours(0, 0, 0, 0)
|
||||
return String(Math.abs(Math.round((today - d) / 86400000)))
|
||||
} catch { return '' }
|
||||
}
|
||||
|
||||
// {{row.xxx}} nội suy đơn giản
|
||||
const interpolated = template.replace(/\{\{row\.(\w+)\}\}/g, (_, k) => row[k] ?? '')
|
||||
return stripHtml(interpolated)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Xây dựng group header row (mirror DataTable.vue groupHeaderRow computed)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function buildGroupHeaderRow(fields) {
|
||||
const result = []
|
||||
let i = 0
|
||||
while (i < fields.length) {
|
||||
const f = fields[i]
|
||||
if (!f.groupHeader) {
|
||||
result.push({ label: stripHtml(f.label), colspan: 1, rowspan: 2, isGroup: false })
|
||||
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 })
|
||||
i += count
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Style helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function hexFromStyle(headerStyle) {
|
||||
const m = (headerStyle || '').match(/background-color:\s*#([0-9a-fA-F]{6})/i)
|
||||
return m ? m[1].toUpperCase() : '204853'
|
||||
}
|
||||
|
||||
function thinBorder() {
|
||||
const s = { style: 'thin', color: { rgb: 'FFD0D0D0' } }
|
||||
return { top: s, bottom: s, left: s, right: s }
|
||||
}
|
||||
|
||||
function headerCellStyle(bgHex, fgHex) {
|
||||
return {
|
||||
fill: { patternType: 'solid', fgColor: { rgb: 'FF' + bgHex } },
|
||||
font: { bold: true, color: { rgb: 'FF' + fgHex }, sz: 10, name: 'Arial' },
|
||||
alignment: { horizontal: 'center', vertical: 'center', wrapText: true },
|
||||
border: thinBorder(),
|
||||
}
|
||||
}
|
||||
|
||||
function dataCellStyle(alignRight, wrapText) {
|
||||
return {
|
||||
font: { sz: 10, name: 'Arial' },
|
||||
alignment: { horizontal: alignRight ? 'right' : 'left', vertical: 'top', wrapText },
|
||||
border: thinBorder(),
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Hàm xuất Excel chính
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function exportToExcel({ fields, data, tablesetting = [], filename = 'export' }) {
|
||||
const XLSX = await getXLSX()
|
||||
|
||||
const hasGroup = fields.some(f => f.groupHeader)
|
||||
const HEADER_ROWS = hasGroup ? 2 : 1
|
||||
|
||||
const fgHex = (tablesetting.find(s => s.code === 'header-font-color')?.detail || '#FFFFFF')
|
||||
.replace('#', '').toUpperCase()
|
||||
|
||||
// ── Xây dựng AOA ──────────────────────────────────────────────────────────
|
||||
const aoa = []
|
||||
|
||||
if (hasGroup) {
|
||||
const groupRow = buildGroupHeaderRow(fields)
|
||||
const row1 = []
|
||||
const row2 = []
|
||||
let colCursor = 0
|
||||
|
||||
groupRow.forEach(col => {
|
||||
if (!col.isGroup) {
|
||||
row1.push(col.label)
|
||||
row2.push('') // merge về row1 sau
|
||||
colCursor++
|
||||
} else {
|
||||
row1.push(col.label)
|
||||
for (let x = 1; x < col.colspan; x++) row1.push('')
|
||||
const groupFields = fields.slice(colCursor, colCursor + col.colspan)
|
||||
groupFields.forEach(f => row2.push(stripHtml(f.label)))
|
||||
colCursor += col.colspan
|
||||
}
|
||||
})
|
||||
aoa.push(row1)
|
||||
aoa.push(row2)
|
||||
} else {
|
||||
aoa.push(fields.map(f => stripHtml(f.label)))
|
||||
}
|
||||
|
||||
// ── Dữ liệu ────────────────────────────────────────────────────────────────
|
||||
data.forEach((row, idx) => {
|
||||
if (row.__stt === undefined) row.__stt = idx + 1
|
||||
|
||||
// Đếm số sub-row cần (bằng max length của các stack field)
|
||||
const maxStack = Math.max(1, ...fields.map(f =>
|
||||
f.split === 'stack' && Array.isArray(row[f.name]) ? (row[f.name].length || 1) : 1
|
||||
))
|
||||
|
||||
for (let si = 0; si < maxStack; si++) {
|
||||
aoa.push(fields.map(f => {
|
||||
// Cột stack: lấy item[si]
|
||||
if (f.split === 'stack') {
|
||||
const arr = Array.isArray(row[f.name]) ? row[f.name] : []
|
||||
const item = arr[si]
|
||||
return item ? getStackItemText(f, item) : ''
|
||||
}
|
||||
// Cột thường: chỉ ghi ở sub-row đầu
|
||||
return si === 0 ? String(getCellText(f, row) ?? '') : ''
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
// ── Worksheet ──────────────────────────────────────────────────────────────
|
||||
const ws = XLSX.utils.aoa_to_sheet(aoa)
|
||||
|
||||
// ── Merge cells ────────────────────────────────────────────────────────────
|
||||
if (hasGroup) {
|
||||
const merges = []
|
||||
const groupRow = buildGroupHeaderRow(fields)
|
||||
let c = 0
|
||||
groupRow.forEach(col => {
|
||||
if (!col.isGroup) {
|
||||
merges.push({ s: { r: 0, c }, e: { r: 1, c } }) // rowspan=2
|
||||
c++
|
||||
} else {
|
||||
if (col.colspan > 1)
|
||||
merges.push({ s: { r: 0, c }, e: { r: 0, c: c + col.colspan - 1 } }) // colspan
|
||||
c += col.colspan
|
||||
}
|
||||
})
|
||||
ws['!merges'] = merges
|
||||
}
|
||||
|
||||
// ── Độ rộng cột ────────────────────────────────────────────────────────────
|
||||
ws['!cols'] = fields.map(f => {
|
||||
const labelLen = stripHtml(f.label).length
|
||||
const minWch = Math.max(f.minwidth ? f.minwidth / 7 : 10, labelLen + 2)
|
||||
const maxWch = f.maxwidth ? f.maxwidth / 7 : 40
|
||||
return { wch: Math.min(minWch, maxWch) }
|
||||
})
|
||||
|
||||
// ── Chiều cao dòng header ──────────────────────────────────────────────────
|
||||
ws['!rows'] = Array.from({ length: HEADER_ROWS }, () => ({ hpt: 36 }))
|
||||
|
||||
// ── Style từng cell ────────────────────────────────────────────────────────
|
||||
const wsRange = XLSX.utils.decode_range(ws['!ref'])
|
||||
for (let R = wsRange.s.r; R <= wsRange.e.r; R++) {
|
||||
for (let C = wsRange.s.c; C <= wsRange.e.c; C++) {
|
||||
const addr = XLSX.utils.encode_cell({ r: R, c: C })
|
||||
if (!ws[addr]) ws[addr] = { t: 's', v: '' }
|
||||
const f = fields[C]
|
||||
if (R < HEADER_ROWS) {
|
||||
ws[addr].s = headerCellStyle(hexFromStyle(f?.headerStyle), fgHex)
|
||||
} else {
|
||||
const isRight = f?.textalign === 'right' || f?.splitType === 'currency'
|
||||
const hasStack = f?.split === 'stack'
|
||||
ws[addr].s = dataCellStyle(isRight, hasStack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ghi file ───────────────────────────────────────────────────────────────
|
||||
const wb = XLSX.utils.book_new()
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Data')
|
||||
XLSX.writeFile(wb, `${filename}.xlsx`)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Nuxt Plugin
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
return {
|
||||
provide: {
|
||||
exportTableExcel: exportToExcel,
|
||||
},
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user