tab, toast grid 추가

This commit is contained in:
2025-08-14 11:00:48 +09:00
parent 332b4b2d4b
commit fd6fe43498
15 changed files with 1044 additions and 54 deletions

View File

@@ -1,6 +1,8 @@
<template>
<NuxtLayout>
<NuxtPage />
<keepAlive>
<NuxtPage :keepalive="true" />
</keepAlive>
</NuxtLayout>
</template>
@@ -8,7 +10,8 @@
@import "./assets/css/main.css";
</style>
<script setup>
<script setup lang="ts">
onMounted(() => {
const script = document.createElement('script')
script.src = '/dist/cy_custom.js'
@@ -22,3 +25,5 @@
document.head.appendChild(script)
})
</script>

View File

@@ -0,0 +1,95 @@
<template>
<div class="wrapper">
<!-- 경로 -->
<nav class="breadcrumb">{{ breadcrumb }}</nav>
<!-- 화면 + 버튼 영역 -->
<header class="header">
<h1 class="title">{{ pageTitle }}</h1>
<div class="header-actions">
<slot name="actions" />
</div>
</header>
<!-- 메인 콘텐츠 -->
<main class="content">
<slot />
</main>
</div>
</template>
<script setup lang="ts">
import { useRoute } from '#imports'
const route = useRoute()
// 경로(메뉴 경로)
const breadcrumb = computed(() => route.path)
// 화면명(meta.title 값)
const pageTitle = computed(() => route.meta.title || 'Untitled Page')
</script>
<style scoped>
.wrapper {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 16px;
background: #f9f9f9;
min-height: 100%;
}
.breadcrumb {
font-size: 14px;
color: #666;
}
/* 화면명과 버튼을 좌우 끝으로 배치 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.title {
margin: 0;
font-size: 20px;
font-weight: bold;
}
.header-actions {
display: flex;
gap: 8px;
}
.content {
flex: 1;
padding: 12px;
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
/* 버튼 공통 스타일 */
.header-actions ::v-deep button {
background-color: #4CAF50;
color: white;
font-size: 14px;
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.header-actions ::v-deep button:hover {
background-color: #45a049;
}
.header-actions ::v-deep button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
</style>

47
components/ToastGrid.vue Normal file
View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import { ref, defineExpose } from 'vue';
import type { OptColumn, OptRow } from 'tui-grid/types/options';
import type { TuiGridElement } from 'vue3-tui-grid';
import type Grid from 'tui-grid';
interface TreeColumnOptions {
name: string;
useCascadingCheckbox?: boolean;
}
const tuiGridRef = ref<TuiGridElement>();
const props = defineProps<{
data: OptRow[];
// editor: https://github.com/nhn/tui.grid/blob/master/packages/toast-ui.grid/docs/v4.0-migration-guide-kor.md
columns: OptColumn[];
treeColumnOptions?: TreeColumnOptions;
rowHeaders?: string[];
rowKey?: string;
}>();
// grid api : https://nhn.github.io/tui.grid/latest/Grid
// const ref = ref<InstanceType<typeof ToastGrid>>();
// ref.value?.api()?.clear();
// ref.value?.api()?.getModifiedRows();
defineExpose({
api: (): Grid | undefined => tuiGridRef.value?.gridInstance,
clearGrid: () => clearGrid(),
});
function clearGrid() {
tuiGridRef.value?.gridInstance.clear();
}
</script>
<template>
<tui-grid
ref="tuiGridRef"
:data="props.data"
:columns="props.columns"
:treeColumnOptions="props.treeColumnOptions"
:rowHeaders="props.rowHeaders"
:rowKey="props.rowKey"
/>
</template>

View File

@@ -0,0 +1,188 @@
import type { OptColumn } from 'tui-grid/types/options';
export const colDefs: OptColumn[] = [
{
name: 'seq',
header: 'seq',
width: 50,
align: 'center',
hidden: true,
},
{
name: 'parentCode',
header: '부모 코드',
width: 200,
editor: 'text',
align: 'center',
filter: { type: 'text' },
},
{
name: 'level',
header: '레벨',
width: 100,
editor: 'text',
align: 'center',
filter: { type: 'number' },
},
{
name: 'code',
header: '코드',
minWidth: 250,
editor: 'text',
align: 'center',
},
{
name: 'name',
header: '이름',
minWidth: 250,
editor: 'text',
align: 'center',
},
{
name: 'useFlag',
header: '사용 여부',
width: 150,
filter: { type: 'text' },
align: 'center',
},
{
name: 'menuFlag',
header: '메뉴 여부',
width: 150,
filter: { type: 'text' },
align: 'center',
},
{
name: 'apiFlag',
header: 'API 여부',
width: 150,
filter: { type: 'text' },
align: 'center',
},
{
name: 'authExceptionFlag',
header: '예외 허용 여부',
width: 150,
filter: { type: 'text' },
align: 'center',
},
{
name: 'sortOrder',
header: '표시 순서',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'uri',
header: 'uri',
width: 300,
editor: 'text',
align: 'center',
},
{
name: 'field1',
header: '필드1',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'field2',
header: '필드2',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'field3',
header: '필드3',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'field4',
header: '필드4',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'field5',
header: '필드5',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'userButton1',
header: '사용자 버튼1',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'userButton2',
header: '사용자 버튼2',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'userButton3',
header: '사용자 버튼3',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'userButton4',
header: '사용자 버튼4',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'userButton5',
header: '사용자 버튼5',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'userButton6',
header: '사용자 버튼6',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'userButton7',
header: '사용자 버튼7',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'userButton8',
header: '사용자 버튼8',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'userButton9',
header: '사용자 버튼9',
width: 200,
editor: 'text',
align: 'center',
},
{
name: 'userButton10',
header: '사용자 버튼10',
width: 200,
editor: 'text',
align: 'center',
},
];

View File

@@ -8,6 +8,7 @@ export default withNuxt(
"vue/html-self-closing": "off",
"vue/html-closing-bracket-newline": "off",
"@typescript-eslint/no-explicit-any": "off",
'import/no-duplicates': 'off'
},
}
);

View File

@@ -1,42 +1,18 @@
<template>
<div class="layout">
<AppHeader v-model="activeMenu" @update:model-value="onMenuClick" />
<nav
v-if="subMenus && subMenus.length && showSubmenuBar"
class="submenu-bar"
@click.stop
>
<NuxtLink
v-for="sub in subMenus"
:key="sub.key"
:to="sub.to"
class="submenu-btn"
:class="{ active: $route.path === sub.to }"
>
{{ sub.label }}
</NuxtLink>
</nav>
<main class="main">
<slot />
</main>
<footer class="footer">
<p>&copy; 2024 Nuxt.js App</p>
</footer>
</div>
</template>
<script setup lang="ts">
import AppHeader from "../components/AppHeader.vue";
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useTabsStore } from "../stores/tab";
const router = useRouter();
const route = useRoute();
const activeMenu = ref("home");
const showSubmenuBar = ref(false);
const tabsStore = useTabsStore();
// HOME 메뉴가 선택되었을 때 최상단 경로로 이동
watch(activeMenu, newValue => {
watch(activeMenu, (newValue) => {
if (newValue === "home") {
router.push("/");
}
@@ -46,6 +22,7 @@ watch(route, () => {
showSubmenuBar.value = false;
});
// 서브메뉴 정의
const subMenus = computed(() => {
if (activeMenu.value === "test") {
return [
@@ -58,16 +35,10 @@ const subMenus = computed(() => {
{ key: "pathway4", label: "pathway4", to: "/cultureGraph/pathway4" },
{ key: "pathwayjson", label: "pathwayjson", to: "/test/pathwayjson" },
{ key: "cultureGraph", label: "배양그래프", to: "/test/culture-graph" },
{
key: "cultureGraphMulti",
label: "배양그래프 멀티",
to: "/test/culture-graph-multi",
},
{
key: "cultureGraphTab",
label: "배양그래프 탭",
to: "/test/culture-graph-tab",
},
{ key: "cultureGraphMulti", label: "배양그래프 멀티", to: "/test/culture-graph-multi" },
{ key: "cultureGraphTab", label: "배양그래프 탭", to: "/test/culture-graph-tab" },
{ key: "tui-grid", label: "tui-grid", to: "/tui" },
{ key: "리소스", label: "리소스", to: "/admin/resource" },
];
} else if (activeMenu.value === "admin") {
return [
@@ -76,7 +47,6 @@ const subMenus = computed(() => {
{ key: "programs", label: "프로그램", to: "/admin/programs" },
];
}
// HOME 메뉴일 때는 서브메뉴 없음
return [];
});
@@ -85,9 +55,14 @@ function onMenuClick(menu: string) {
showSubmenuBar.value = true;
}
// 서브메뉴 클릭 시 탭 추가
function onSubMenuClick(sub: { key: string; label: string; to: string, componentName:string }) {
tabsStore.addTab(sub);
router.push(sub.to);
}
function handleClickOutsideSubmenuBar(event: MouseEvent) {
const submenu = document.querySelector(".submenu-bar");
// menu-btn(대메뉴) 클릭 시에는 닫히지 않도록 예외 처리
if (
submenu &&
!submenu.contains(event.target as Node) &&
@@ -103,14 +78,53 @@ onMounted(() => {
onBeforeUnmount(() => {
window.removeEventListener("click", handleClickOutsideSubmenuBar);
});
/*
useHead({
title: "Integrated Bio Foundry Platform",
meta: [{ name: "description", content: "Integrated Bio Foundry Platform" }],
});
*/
</script>
<template>
<div class="layout">
<AppHeader v-model="activeMenu" @update:model-value="onMenuClick" />
<!-- 서브메뉴 -->
<nav
v-if="subMenus && subMenus.length && showSubmenuBar"
class="submenu-bar"
@click.stop
>
<button
v-for="sub in subMenus"
:key="sub.key"
class="submenu-btn"
:class="{ active: $route.path === sub.to }"
@click="onSubMenuClick({...sub, componentName : sub.key})"
>
{{ sub.label }}
</button>
</nav>
<!-- 동적 -->
<div v-if="tabsStore.tabs.length" class="tab-bar">
<div
v-for="tab in tabsStore.tabs"
:key="tab.key"
class="tab-item"
:class="{ active: tabsStore.activeTab === tab.key }"
@click="tabsStore.setActiveTab(tab.key); router.push(tab.to)"
>
{{ tab.label }}
<span class="close-btn" @click.stop="tabsStore.removeTab(tab.key)">×</span>
</div>
</div>
<main class="main">
<slot />
</main>
<footer class="footer">
<p>&copy; 2024 Nuxt.js App</p>
</footer>
</div>
</template>
<style scoped>
.layout {
min-height: 100vh;
@@ -162,4 +176,29 @@ useHead({
background: #e6f0fa;
color: #1976d2;
}
/* 탭바 스타일 */
.tab-bar {
display: flex;
gap: 6px;
padding: 0.4rem 0.8rem;
background: #fff;
border-bottom: 1px solid #ddd;
}
.tab-item {
padding: 0.3rem 0.8rem;
background: #f2f2f2;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
}
.tab-item.active {
background: #1976d2;
color: white;
}
.close-btn {
margin-left: 6px;
cursor: pointer;
}
</style>

View File

@@ -39,5 +39,10 @@ export default defineNuxtConfig({
public: {
apiBase: 'http://localhost'
}
}
},
typescript: {
shim: false,
strict: true,
},
plugins: ['~/plugins/vue3-tui-grid.client.ts']
});

188
package-lock.json generated
View File

@@ -22,8 +22,11 @@
"eslint": "^9.29.0",
"nuxt": "^3.17.5",
"pinia": "^3.0.3",
"tui-code-snippet": "^2.3.3",
"tui-grid": "^4.21.22",
"vue": "^3.5.17",
"vue-router": "^4.5.1"
"vue-router": "^4.5.1",
"vue3-tui-grid": "^0.1.51"
},
"devDependencies": {
"autoprefixer": "^10.4.21",
@@ -4451,6 +4454,22 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/adler-32": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz",
"integrity": "sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==",
"license": "Apache-2.0",
"dependencies": {
"exit-on-epipe": "~1.0.1",
"printj": "~1.1.0"
},
"bin": {
"adler32": "bin/adler32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/ag-charts-types": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-12.0.0.tgz",
@@ -5172,6 +5191,28 @@
],
"license": "CC-BY-4.0"
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cfb/node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -5367,6 +5408,15 @@
"node": ">=0.10.0"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -6330,6 +6380,12 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/dompurify": {
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
"integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==",
"license": "(MPL-2.0 OR Apache-2.0)"
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@@ -7185,6 +7241,15 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/exit-on-epipe": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
"integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -7545,6 +7610,15 @@
"node": ">=12.20.0"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -11436,6 +11510,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/printj": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==",
"license": "Apache-2.0",
"bin": {
"printj": "bin/printj.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -12549,6 +12635,18 @@
"node": ">=0.10.0"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
@@ -13092,6 +13190,45 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tui-code-snippet": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tui-code-snippet/-/tui-code-snippet-2.3.3.tgz",
"integrity": "sha512-5NEHTDFKillDNPy6MCgpXDNBTB7SZkHBFOF6vXfCDIFZcBdFYFXTd2xvAVvIeM3UYFRWu27xUf/Kxl5f9+RooQ==",
"license": "MIT"
},
"node_modules/tui-date-picker": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/tui-date-picker/-/tui-date-picker-4.3.3.tgz",
"integrity": "sha512-/2YoLnj5c1e+Ag1ZZYOgzEs2o0v7Ol7c5UAnBj438zGlkwkMxyH0HwP2pVqqIYX05WE7K0+6nTWVMybS8otBgw==",
"license": "MIT",
"dependencies": {
"tui-time-picker": "^2.1.6"
}
},
"node_modules/tui-grid": {
"version": "4.21.22",
"resolved": "https://registry.npmjs.org/tui-grid/-/tui-grid-4.21.22.tgz",
"integrity": "sha512-RxkFcveR2tWf3QrtuW9/zejXjeXL3SdrQPK89DbrrM6N7TlyUcb5HAf0U3MBn/hYT1p5kt5wyOwHPCQJuIBrsA==",
"license": "MIT",
"dependencies": {
"dompurify": "^2.3.9",
"tui-date-picker": "^4.1.0",
"tui-pagination": "^3.4.0",
"xlsx": "^0.17.1"
}
},
"node_modules/tui-pagination": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/tui-pagination/-/tui-pagination-3.4.1.tgz",
"integrity": "sha512-W09L0wPMSFstthBhQjcLNDnN1yuCEDn/tIXmaKdTpNFYa11eNrNo/rOwXzrugXvP2arZ60KmVhkFzoAOEuV0Sg==",
"license": "MIT"
},
"node_modules/tui-time-picker": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/tui-time-picker/-/tui-time-picker-2.1.6.tgz",
"integrity": "sha512-4Jmo3wjGS+Ii4/qQgt5DaFEohHpB3U6BzWeTODVVFHD9sx3NOsbomY9K0xMobSLODi+tQEH7wfOtNU0IJmcQ6Q==",
"license": "MIT"
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -14017,6 +14154,16 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue3-tui-grid": {
"version": "0.1.51",
"resolved": "https://registry.npmjs.org/vue3-tui-grid/-/vue3-tui-grid-0.1.51.tgz",
"integrity": "sha512-UY9Ebli+3NmA9HheXbtQSsjNSmXn5H+Qv3lKQv4y35cDm+dME/sW7O5cXESdrfDFM0sSKFgTSBZF5+iWfmlE4g==",
"license": "MIT",
"dependencies": {
"tui-grid": "^4.21.2",
"vue": "^3.2.37"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
@@ -14139,6 +14286,24 @@
"node": ">= 6"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -14276,6 +14441,27 @@
}
}
},
"node_modules/xlsx": {
"version": "0.17.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.17.5.tgz",
"integrity": "sha512-lXNU0TuYsvElzvtI6O7WIVb9Zar1XYw7Xb3VAx2wn8N/n0whBYrCnHMxtFyIiUU1Wjf09WzmLALDfBO5PqTb1g==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.2.0",
"cfb": "^1.1.4",
"codepage": "~1.15.0",
"crc-32": "~1.2.0",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",

View File

@@ -19,16 +19,19 @@
"@pinia/nuxt": "^0.11.1",
"ag-grid-community": "^34.0.0",
"ag-grid-vue3": "^34.0.0",
"echarts": "^5.6.0",
"chart.js": "^4.5.0",
"cytoscape": "^3.32.0",
"cytoscape-layers": "^3.0.0",
"cytoscape-overlays": "^2.0.0",
"echarts": "^5.6.0",
"eslint": "^9.29.0",
"nuxt": "^3.17.5",
"pinia": "^3.0.3",
"tui-code-snippet": "^2.3.3",
"tui-grid": "^4.21.22",
"vue": "^3.5.17",
"vue-router": "^4.5.1"
"vue-router": "^4.5.1",
"vue3-tui-grid": "^0.1.51"
},
"devDependencies": {
"autoprefixer": "^10.4.21",

41
pages/admin/resource.vue Normal file
View File

@@ -0,0 +1,41 @@
<template>
<ContentsWrapper>
<template #actions>
<button @click="onAddClick">추가</button>
<button @click="onUpdateClick">저장</button>
</template>
<input type="text" >
<ToastGrid
ref="grid1Ref"
:data="data"
:columns="colDefs"
/>
</ContentsWrapper>
</template>
<script setup lang="ts">
import {colDefs} from '../../composables/grids/resourceGrid'
definePageMeta({
title: '리소스 관리'
})
const data = [{}]
const grid1Ref = ref();
onMounted(async () => {
await nextTick() // DOM 및 컴포넌트 렌더링 완료 대기
grid1Ref.value?.api()?.setBodyHeight('700')
})
function onAddClick() {
grid1Ref.value?.api()?.appendRow({});
}
function onUpdateClick() {
//grid1Ref.value?.clearGrid();
console.log(grid1Ref.value?.api()?.getModifiedRows());
}
</script>

323
pages/tui.vue Normal file
View File

@@ -0,0 +1,323 @@
<script setup lang="ts">
import ToastGrid from '../components/ToastGrid.vue';
const data = [
{
id: 549731,
name: 'Beautiful Lies',
artist: 'Birdy',
release: '2016.03.26',
type: 'Deluxe',
typeCode: '1',
genre: 'Pop',
genreCode: '1',
grade: '4',
price: 10000,
downloadCount: 1000,
listenCount: 5000,
_attributes: {
expanded: true,
},
_children: [
{
id: 491379,
name: 'Chaos And The Calm',
artist: 'James Bay',
release: '2015.03.23',
type: 'EP',
typeCode: '2',
genre: 'Pop,Rock',
genreCode: '1,2',
grade: '5',
price: 12000,
downloadCount: 1000,
listenCount: 5000,
_children: [],
},
{
id: 498896,
name: 'The Magic Whip',
artist: 'Blur',
release: '2015.04.27',
type: 'EP',
typeCode: '2',
genre: 'Rock',
genreCode: '2',
grade: '3',
price: 15000,
downloadCount: 1000,
listenCount: 5000,
_attributes: {
expanded: false,
},
},
{
id: 450720,
name: "I'm Not The Only One",
artist: 'Sam Smith',
release: '2014.09.15',
type: 'Single',
typeCode: '3',
genre: 'Pop,R&B',
genreCode: '1,3',
grade: '4',
price: 8000,
downloadCount: 1000,
listenCount: 5000,
_attributes: {
expanded: true,
},
_children: [
{
id: 587871,
name: 'This Is Acting',
artist: 'Sia',
release: '2016.10.22',
type: 'EP',
typeCode: '2',
genre: 'Pop',
genreCode: '1',
grade: '3',
price: 20000,
downloadCount: 1000,
listenCount: 5000,
_attributes: {
expanded: true,
},
_children: [
{
id: 490500,
name: 'Blue Skies',
release: '2015.03.18',
artist: 'Lenka',
type: 'Single',
typeCode: '3',
genre: 'Pop,Rock',
genreCode: '1,2',
grade: '5',
price: 6000,
downloadCount: 1000,
listenCount: 5000,
},
{
id: 317659,
name: "I Won't Give Up",
artist: 'Jason Mraz',
release: '2012.01.03',
type: 'Single',
typeCode: '3',
genre: 'Pop',
genreCode: '1',
grade: '2',
price: 7000,
downloadCount: 1000,
listenCount: 5000,
},
{
id: 583551,
name: 'Following My Intuition',
artist: 'Craig David',
release: '2016.10.01',
type: 'Deluxe',
typeCode: '1',
genre: 'R&B,Electronic',
genreCode: '3,4',
grade: '5',
price: 15000,
downloadCount: 1000,
listenCount: 5000,
},
],
},
],
},
],
},
{
id: 436461,
name: 'X',
artist: 'Ed Sheeran',
release: '2014.06.24',
type: 'Deluxe',
typeCode: '1',
genre: 'Pop',
genreCode: '1',
grade: '5',
price: 20000,
downloadCount: 1000,
listenCount: 5000,
},
{
id: 295651,
name: 'Moves Like Jagger',
release: '2011.08.08',
artist: 'Maroon5',
type: 'Single',
typeCode: '3',
genre: 'Pop,Rock',
genreCode: '1,2',
grade: '2',
price: 7000,
downloadCount: 1000,
listenCount: 5000,
},
{
id: 541713,
name: 'A Head Full Of Dreams',
artist: 'Coldplay',
release: '2015.12.04',
type: 'Deluxe',
typeCode: '1',
genre: 'Rock',
genreCode: '2',
grade: '3',
price: 25000,
downloadCount: 1000,
listenCount: 5000,
_attributes: {
expanded: false,
},
_children: [
{
id: 294574,
name: '4',
artist: 'Beyoncé',
release: '2011.07.26',
type: 'Deluxe',
typeCode: '1',
genre: 'Pop',
genreCode: '1',
grade: '3',
price: 12000,
downloadCount: 1000,
listenCount: 5000,
},
{
id: 265289,
name: '21',
artist: 'Adele',
release: '2011.01.21',
type: 'Deluxe',
typeCode: '1',
genre: 'Pop,R&B',
genreCode: '1,3',
grade: '5',
price: 15000,
downloadCount: 1000,
listenCount: 5000,
},
],
},
{
id: 555871,
name: 'Warm On A Cold Night',
artist: 'HONNE',
release: '2016.07.22',
type: 'EP',
typeCode: '1',
genre: 'R&B,Electronic',
genreCode: '3,4',
grade: '4',
price: 11000,
downloadCount: 1000,
listenCount: 5000,
},
{
id: 550571,
name: 'Take Me To The Alley',
artist: 'Gregory Porter',
release: '2016.09.02',
type: 'Deluxe',
typeCode: '1',
genre: 'Jazz',
genreCode: '5',
grade: '3',
price: 30000,
downloadCount: 1000,
listenCount: 5000,
},
{
id: 544128,
name: 'Make Out',
artist: 'LANY',
release: '2015.12.11',
type: 'EP',
typeCode: '2',
genre: 'Electronic',
genreCode: '4',
grade: '2',
price: 12000,
downloadCount: 1000,
listenCount: 5000,
},
{
id: 366374,
name: 'Get Lucky',
artist: 'Daft Punk',
release: '2013.04.23',
type: 'Single',
typeCode: '3',
genre: 'Pop,Funk',
genreCode: '1,5',
grade: '3',
price: 9000,
downloadCount: 1000,
listenCount: 5000,
},
{
id: 8012747,
name: 'Valtari',
artist: 'Sigur Rós',
release: '2012.05.31',
type: 'EP',
typeCode: '3',
genre: 'Rock',
genreCode: '2',
grade: '5',
price: 10000,
downloadCount: 1000,
listenCount: 5000,
},
];
const columns = [
{ header: 'Name', name: 'name', width: 300 },
{ header: 'Artist', name: 'artist' },
{ header: 'Type', name: 'type' },
{ header: 'Release', name: 'release' },
{ header: 'Genre', name: 'genre' },
{ header: 'checkbox', name: 'checkbox', editor:{ type: 'checkbox', options: {
listItems: [
{ text: 'true', value: true },
]
}}}
];
const treeColumnOptions = { name: 'name', useCascadingCheckbox: true };
const grid1Ref = ref();
function onClearClick() {
//grid1Ref.value?.clearGrid();
grid1Ref.value?.api()?.clear();
}
function onUpdateClick() {
//grid1Ref.value?.clearGrid();
console.log(grid1Ref.value?.api()?.getModifiedRows());
}
</script>
<template>
<div>
<button @click="onClearClick">clear api</button>
<br>
<button @click="onUpdateClick">update list</button>
<ToastGrid
ref="grid1Ref"
:data="data"
:columns="columns"
:treeColumnOptions="treeColumnOptions"
/>
</div>
</template>

View File

@@ -0,0 +1,7 @@
import { defineNuxtPlugin } from '#app'
import TuiGrid from 'vue3-tui-grid'
import 'tui-grid/dist/tui-grid.css'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(TuiGrid)
})

36
stores/tab.ts Normal file
View File

@@ -0,0 +1,36 @@
// stores/tabs.ts
import { defineStore } from 'pinia'
export interface TabItem {
key: string
label: string
to: string
componentName: string
}
export const useTabsStore = defineStore('tabs', {
state: () => ({
activeTab: '' as string,
tabs: [] as { key: string; label: string; to: string; componentName: string }[]
}),
actions: {
addTab(tab: TabItem) {
if (!this.tabs.find(t => t.key === tab.key)) {
this.tabs.push(tab)
}
this.activeTab = tab.key
},
removeTab(key: string) {
const idx = this.tabs.findIndex(t => t.key === key)
if (idx !== -1) {
this.tabs.splice(idx, 1)
if (this.activeTab === key && this.tabs.length) {
this.activeTab = this.tabs[Math.max(idx - 1, 0)].key
}
}
},
setActiveTab(key: string) {
this.activeTab = key
}
}
})

View File

@@ -1,4 +1,9 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"typeRoots": ["./src/types", "./node_modules/@types"],
"allowJs": true,
"skipLibCheck": true
}
}

9
types/vue3-tui-grid.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
// src/types/vue3-tui-grid.d.ts
declare module 'vue3-tui-grid' {
import type { Plugin } from 'vue';
const TuiGrid: Plugin;
export default TuiGrid;
export type TuiGridElement = any;
export type GridEvent = any;
}