first commit

This commit is contained in:
Warunee Tamkoo 2023-09-06 14:51:44 +07:00
commit eb2f504652
32490 changed files with 5731109 additions and 0 deletions

27
node_modules/structure-chart/README.md generated vendored Normal file
View file

@ -0,0 +1,27 @@
# Structure Chart
## วิธีใช้
```
// import Structure Chart Component
import { StructChart } from 'structure-chart'
import 'structure-chart/structure-chart.css' // or import 'structure-chart/dist/style.css'
// import Data from static file or API
import chartData from '@/assets/structChartData'
const dataSource = ref(chartData)
// onClick Event
const chartClick = (data: any) => {
console.log(data.value)
}
<StructChart
:dataSource="data"
@onElementClick="chartClick"
:config="{
'minZoom': 0.5,
'maxZoom': 3
}"
/>
```

View file

@ -0,0 +1,15 @@
import type { PropType } from 'vue';
import type { IStructConfig } from '../stores/StructStore';
declare const _sfc_main: import("vue").DefineComponent<{
config: PropType<IStructConfig>;
dataSource: ObjectConstructor;
}, {
savePNG: (scale?: number) => Promise<void>;
savePDF: (scale?: number) => Promise<void>;
}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, "onElementClick"[], "onElementClick", import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
config: PropType<IStructConfig>;
dataSource: ObjectConstructor;
}>> & {
onOnElementClick?: ((...args: any[]) => any) | undefined;
}, {}>;
export default _sfc_main;

View file

@ -0,0 +1,6 @@
declare const _sfc_main: import("vue").DefineComponent<{
dataSource: ObjectConstructor;
}, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
dataSource: ObjectConstructor;
}>>, {}>;
export default _sfc_main;

13990
node_modules/structure-chart/dist/index-d950fc13.js generated vendored Normal file

File diff suppressed because one or more lines are too long

2
node_modules/structure-chart/dist/index.d.ts generated vendored Normal file
View file

@ -0,0 +1,2 @@
import StructChart from './components/StructChart.vue';
export { StructChart };

5768
node_modules/structure-chart/dist/index.es-ebdebaec.js generated vendored Normal file

File diff suppressed because it is too large Load diff

470
node_modules/structure-chart/dist/purify.es-4f9a0368.js generated vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,30 @@
/**
* @interface IStructConfig
* Configuration of StructureChart
*/
export interface IStructConfig {
'minZoom'?: number;
'maxZoom'?: number;
}
export declare const useStructStore: import("pinia").StoreDefinition<"StructStore", import("pinia")._UnwrapAll<Pick<{
config: import("vue").Ref<{
minZoom?: number | undefined;
maxZoom?: number | undefined;
}>;
dataSource: import("vue").Ref<any>;
selectedElement: import("vue").Ref<any>;
}, "dataSource" | "config" | "selectedElement">>, Pick<{
config: import("vue").Ref<{
minZoom?: number | undefined;
maxZoom?: number | undefined;
}>;
dataSource: import("vue").Ref<any>;
selectedElement: import("vue").Ref<any>;
}, never>, Pick<{
config: import("vue").Ref<{
minZoom?: number | undefined;
maxZoom?: number | undefined;
}>;
dataSource: import("vue").Ref<any>;
selectedElement: import("vue").Ref<any>;
}, never>>;

6
node_modules/structure-chart/dist/structure-chart.js generated vendored Normal file
View file

@ -0,0 +1,6 @@
import { S as p } from "./index-d950fc13.js";
import "vue";
import "pinia";
export {
p as StructChart
};

File diff suppressed because one or more lines are too long

1
node_modules/structure-chart/dist/style.css generated vendored Normal file
View file

@ -0,0 +1 @@
@charset "UTF-8";.node-container[data-v-2fe285ed]{border-collapse:collapse;margin:0 auto}.root-node td[data-v-2fe285ed]{text-align:center;width:100%}.element-container[data-v-2fe285ed]{background:white;border:1px solid #f0efef;border-radius:5px;box-sizing:border-box;box-shadow:0 20px 20px -20px #a3a5ae36;display:inline-flex;flex-direction:column;position:relative;margin:0 1px 2px;min-width:9rem;text-align:left;transition-duration:.25s}.element-container[data-v-2fe285ed]:hover{box-shadow:0 6px 10px #7c7c7c;cursor:pointer;transform:scale(1.05);z-index:20}.root-element[data-v-2fe285ed]{border-top:3px solid hsl(212,86%,64%)}.child-element[data-v-2fe285ed]{border-top:3px solid hsl(180,62%,55%);width:99%}.subchild-element[data-v-2fe285ed]{border-top:3px solid #995511}.section-primary[data-v-2fe285ed],.section-secondary[data-v-2fe285ed],.section-list[data-v-2fe285ed]{font-family:sans-serif;display:flex;align-items:center;padding:6px 15px 6px 20px;bottom:0;justify-content:space-between}.section-primary .column-content[data-v-2fe285ed],.section-secondary .column-content[data-v-2fe285ed],.section-list .column-content[data-v-2fe285ed]{min-width:120px}.section-primary .column-side[data-v-2fe285ed],.section-secondary .column-side[data-v-2fe285ed],.section-list .column-side[data-v-2fe285ed]{margin-left:20px}.section-primary .column-side .side-button[data-v-2fe285ed],.section-secondary .column-side .side-button[data-v-2fe285ed],.section-list .column-side .side-button[data-v-2fe285ed]{background-color:#d9d9d9;border:1px solid white;border-radius:8px;color:#717181;display:inline-grid;align-items:center;min-width:32px;max-width:auto;padding:0 8px;height:36px;text-align:center}.section-primary .header[data-v-2fe285ed],.section-secondary .header[data-v-2fe285ed],.section-tertiary .header[data-v-2fe285ed]{color:#334454;font-size:1.1rem;line-height:1.5rem;margin:0}.section-primary .header[data-v-2fe285ed]{font-weight:700!important}.section-secondary[data-v-2fe285ed],.section-tertiary[data-v-2fe285ed]{border-top:1px solid #d9d9d9}.section-secondary .header[data-v-2fe285ed]{font-weight:500!important}.section-tertiary[data-v-2fe285ed]{background:#fafafa;padding-left:10px}.section-tertiary .header[data-v-2fe285ed]{font-weight:300!important}.section-tertiary .header[data-v-2fe285ed]:before{content:"\2022";color:#394b5c;font-weight:700;display:inline-block;width:.9em}.node-children[data-v-2fe285ed]{vertical-align:top}.nodeline[data-v-2fe285ed]{height:1.75rem}.nodeline-down[data-v-2fe285ed]{background:#B0B4B0;margin-left:auto;margin-right:auto;margin-top:-1rem;margin-bottom:-1rem;height:1.9rem;width:.125rem;float:none}.nodeline-top[data-v-2fe285ed]{border-top-color:#b0b4b0;border-top-style:solid;border-top-width:2px}.nodeline-bottom[data-v-2fe285ed]{border-bottom-color:#b0b4b0;border-bottom-style:solid;border-bottom-width:2px}.nodeline-right[data-v-2fe285ed]{border-right-color:#b0b4b0;border-right-style:solid;border-right-width:2px}.nodeline-left[data-v-2fe285ed]{border-left-color:#b0b4b0;border-left-style:solid;border-left-width:2px}.subchild-section[data-v-2fe285ed]{margin-left:1rem;border-collapse:collapse}.subchild-section-nodeline[data-v-2fe285ed]{min-width:20px;max-width:30px}.subchild-more[data-v-2fe285ed]{background:transparent url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPgo8c3ZnIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDE2IDE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiMwMDAwMDAiIGNsYXNzPSJiaSBiaS1jYXJldC1kb3duLWZpbGwiPgogIDxwYXRoIGQ9Ik03LjI0NyAxMS4xNCAyLjQ1MSA1LjY1OEMxLjg4NSA1LjAxMyAyLjM0NSA0IDMuMjA0IDRoOS41OTJhMSAxIDAgMCAxIC43NTMgMS42NTlsLTQuNzk2IDUuNDhhMSAxIDAgMCAxLTEuNTA2IDB6Ii8+Cjwvc3ZnPg==) no-repeat center bottom;background-size:16px;border:1px solid white;border-radius:5px;cursor:pointer;padding:5px 10px;color:#300;width:36px;height:40px;text-align:center}.struct-chart[data-v-70ed3ff9]{width:100vw;height:100vh;background-color:#fff;user-select:none}.struct-chart-container[data-v-70ed3ff9]{width:auto;height:auto}

59
node_modules/structure-chart/package.json generated vendored Normal file
View file

@ -0,0 +1,59 @@
{
"name": "structure-chart",
"version": "0.0.9",
"description": "Structure Chart",
"keywords": [
"vue",
"vue3",
"structure",
"chart"
],
"author": "Suchin Prasongbundit",
"type": "module",
"main": "./dist/structure-chart.umd.js",
"module": "./dist/structure-chart.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/structure-chart.js",
"require": "./dist/structure-chart.umd.js"
},
"./structure-chart.css": "./dist/style.css",
"./dist/*": "./dist/*"
},
"files": [
"dist",
"src"
],
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"build-npm": "vue-tsc && vite build",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"panzoom": "^9.4.3",
"pinia": "^2.0.32",
"vue": "^3.2.47"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@types/node": "^18.14.2",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-typescript": "^11.0.2",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.34.0",
"eslint-plugin-vue": "^9.9.0",
"npm-run-all": "^4.1.5",
"sass": "^1.61.0",
"typescript": "~4.8.4",
"vite": "^4.1.4",
"vite-plugin-dts": "^2.2.0",
"vue-tsc": "^1.2.0"
}
}

25
node_modules/structure-chart/src/App.vue generated vendored Normal file
View file

@ -0,0 +1,25 @@
<script setup lang="ts">
import { ref } from 'vue'
import StructChart from './components/StructChart.vue'
import chartData from './assets/structChartData2'
const dataSource = ref(chartData)
const chartRef = ref()
const savePNG = () => {
chartRef.value.savePNG()
}
const savePDF = () => {
chartRef.value.savePDF()
}
</script>
<template>
<div>
<button @click="savePNG()">Save PNG</button>
<button @click="savePDF()">Save PDF</button>
<StructChart ref="chartRef" :dataSource="dataSource"/>
</div>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,272 @@
/**
* Root TreeList TreeList
* - Object
* - deptID : Unique ID // Int String
* - departmentName : ชื่อสำนัก//
* - totalPositionCount : จำนวนตำแหน่งทั้งหมดในหน่วยงาน
* - totalPositionVacant : จำนวนตำแหน่งว่างทั้งหมดในหน่วยงาน
* - Object 3
* - heads : เป็น Array Object
* - offcier : เป็น Array Object
* - children : เป็น Array Root children Object
* - Object
* - positionID : Unique ID deptID
* - positionName : ชื่อตำแหน่ง
* - positionNum : เลขที่ประจำตำแหน่ง .
* - totalPositionCount : จำนวนตำแหน่งทั้งหมด
* - totalPositionVacant : จำนวนตำแหน่งว่างทั้งหมด
*/
const chartData = { // root ของข้อมูลเป็น Object (ไม่ใช่ Array แบบ TreeList)
deptID: 1, // Unique ID ของสำนัก/ฝ่าย/กอง (ถ้ามี?)
departmentName: "สำนักงานเลขานุการผู้ว่าราชการกรุงเทพมหานคร", // ชื่อสำนัก/ฝ่าย/กอง
totalPositionCount: 75, // จำนวนตำแหน่งทั้งหมดในหน่วยงาน
totalPositionVacant: 2, // จำนวนตำแหน่งว่างทั้งหมดในหน่วยงาน
heads: [ // Array ของผู้บริหารในหน่วยงานนั้น ๆ (ผู้อำนวยการ/หัวหน้า)
{
positionID: 2, // Unique ID ของประเภทตำแหน่งนั้น (ถ้ามี?)
positionName: "ผู้อำนวยการสูง", // ชื่อตำแหน่ง
positionNum: "(หัวหน้าสำนักงาน)", // เลขที่ประจำตำแหน่ง (ถ้ามี) เช่นพวก กทข.
totalPositionCount: 1, // จำนวนตำแหน่งทั้งหมด
totalPositionVacant: 0 // จำนวนตำแหน่งว่างทั้งหมด
}
],
officer: [ // Array ของเจ้าหน้าที่ในหน่วยงาน (รายการตำแหน่งในหน่วยงานที่ขึ้นตรงกับ deptID นั้น ๆ)
],
children: [ // Array ของหน่วยงานย่อย เช่น ฝ่าย กอง
{ // โครงสร้างที่เหลือจะเหมือนกับส่วน Root ทุกอย่าง คือแต่ละหน่วยงานย่อย มี heads ไว้ระบุหัวหน้า ผู้อำนวยการ มี Officer ไว้ระบุรายการตำแหน่งในหน่วยงาน มี Children ไว้ระบุหน่วยงานย่อย
deptID: 3,
departmentName: "ฝ่ายบริหารทั่วไป",
totalPositionCount: 14,
totalPositionVacant: 0,
heads: [
{
positionID: 2,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ชพ. (หัวหน้าฝ่าย)",
totalPositionCount: 1,
totalPositionVacant: 0
}
],
officer: [
{
positionID: 3,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ปก./ชก",
totalPositionCount: 1,
totalPositionVacant: 0
},
{
positionID: 4,
positionName: "นักทรัพยากรบุคคล",
positionNum: "ปก./ชก.",
totalPositionCount: 1,
totalPositionVacnt: 0
},
{
positionID: 5,
positionName: "นักวิชาการเงินและบัญชี",
positionNum: "ปก./ชก.",
totalPositionCount: 2,
totalPositionVacnt: 0
},
{
positionID: 6,
positionName: "นักวิชาการพัสดุ",
positionNum: "ปก./ชก.",
totalPositionCount: 1,
totalPositionVacnt: 0
},
{
positionID: 7,
positionName: "เจ้าพนักงานการเงินและบัญชี",
positionNum: "ปง./ชง.",
totalPositionCount: 3,
totalPositionVacnt: 0
},
{
positionID: 8,
positionName: "เจ้าพนักงานพัสดุ",
positionNum: "ปง./ชง.",
totalPositionCount: 1,
totalPositionVacnt: 0
},
{
positionID: 9,
positionName: "เจ้าพนักงานธุรการ",
positionNum: "ปง./ชง.",
totalPositionCount: 4,
totalPositionVacnt: 0
}
]
},
{
deptID: 4,
departmentName: "ส่วนประสานนโยบาย",
totalPositionCount: 15,
totalPositionVacant: 0,
heads: [
{
positionID: 10,
positionName: "ผู้อำนวยการต้น",
positionNum: "(ผู้อำนวยการส่วน)",
totalPositionCount: 1,
totalPositionVacant: 0,
}
],
children: [
{
deptID: 11,
departmentName: "กลุ่มงานประชุม",
totalPositionCount: 6,
totalPositionVacant: 0,
heads: [
{
positionID: 11,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ชพ. (หัวหน้ากลุ่มงาน)",
totalPositionCount: 1,
totalPositionVacant: 0,
}
],
officer: [
{
positionID: 12,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ปก./ชก.",
totalPositionCount: 4,
totalPositionVacant: 0
},
{
positionID: 13,
positionName: "เจ้าพนักงานธุรการ",
positionNum: "ปง./ชง.",
totalPositionCount: 1,
totalPositionVacant: 0
}
]
},
{
deptID: 12,
departmentName: "กลุ่มงานการเมืองและประสานนโยบาย",
totalPositionCount: 8,
totalPositionVacant: 0,
heads: [
{
positionID: 14,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ชพ. (หัวหน้ากลุ่มงาน)",
totalPositionCount: 1,
totalPositionVacant: 0,
}
],
officer: [
{
positionID: 15,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ปก./ชก.",
totalPositionCount: 4,
totalPositionVacant: 0
},
{
positionID: 16,
positionName: "เจ้าพนักงานสถิติ",
positionNum: "ปง./ชง.",
totalPositionCount: 1,
totalPositionVacant: 0
},
{
positionID: 17,
positionName: "เจ้าพนักงานธุรการ",
positionNum: "ปง./ชง.",
totalPositionCount: 2,
totalPositionVacant: 0
}
]
}
]
},
{
deptID: 14,
departmentName: "ส่วนเรื่องราวร้องทุกข์",
totalPositionCount: 15,
totalPositionVacant: 0,
heads: [
{
positionID: 18,
positionName: "ผู้อำนวยการต้น",
positionNum: "(ผู้อำนวยการส่วน)",
totalPositionCount: 1,
totalPositionVacant: 0
}
],
children: [
{
deptID: 15,
departmentName: "กลุ่มงานรับเรื่องราวร้องทุกข์",
totalPositionCount: 7,
totalPositionVacant: 0,
heads: [
{
positionID: 19,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ชพ. (หัวหน้ากลุ่มงาน)",
totalPositionCount: 1,
totalPositionVacant: 0
}
],
officer: [
{
positionID: 20,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ปก./ชก.",
totalPositionCount: 4,
totalPositionVacant: 0
},
{
positionID: 21,
positionName: "เจ้าพนักงานธุรการ",
positionNum: "ปง./ชง.",
totalPositionCount: 2,
totalPositionVacant: 0
}
]
},
{
deptID: 16,
departmentName: "กลุ่มงานตรวจสอบ ติดตามและประมวลผล",
totalPositionCount: 7,
totalPositionVacant: 0,
heads: [
{
positionID: 22,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ชพ. (หัวหน้ากลุ่มงาน)",
totalPositionCount: 1,
totalPositionVacant: 0
}
],
officer: [
{
positionID: 23,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ปก./ชก.",
totalPositionCount: 4,
totalPositionVacant: 0
},
{
positionID: 24,
positionName: "เจ้าพนักงานธุรการ",
positionNum: "ปง./ชง.",
totalPositionCount: 2,
totalPositionVacant: 0
}
]
}
]
}
]
}
export default chartData

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,153 @@
<script setup lang="ts">
/**
* import Vue's Stuff
*/
import { computed, watch, onMounted } from 'vue'
import type { PropType } from 'vue'
import panzoom from 'panzoom'
import html2canvas from 'html2canvas'
import { jsPDF } from "jspdf"
/**
* import Chart Node componenet
*/
import StructChartNode from './StructChartNode.vue'
/**
* import Pinia Store
*/
import { storeToRefs } from 'pinia'
import { useStructStore } from '../stores/StructStore'
import type { IStructConfig } from '../stores/StructStore'
const { config, dataSource, selectedElement } = storeToRefs(useStructStore())
/**
* define Prop (Pass down value)
*/
const props = defineProps({
config: Object as PropType<IStructConfig>, // Chart configuration
dataSource: Object, // Data Source
})
/**
* define Emit (Child to Parent value)
*/
const emit = defineEmits([
'onElementClick', // Event call when user click each element
])
/**
* Computed Property
* - minZoom : if parent don't send minZoom value, use default from StructStore (0.5)
* - maxZoom : if parent don't send maxZoom value, use default from StructStore (2)
*/
const minZoom = computed(() => props.config?.minZoom ? props.config.minZoom : config.value.minZoom)
const maxZoom = computed(() => props.config?.maxZoom ? props.config.maxZoom : config.value.maxZoom)
/**
* บภาพหนาจอออกมาเป canvas object งใหงกนอ นำไปประมวลผลต
* @param scale ขนาดของภาพทองการ าพนฐานค 3 เทาของหนาจอ
*/
const captureChart = async(scale: number = 3) => {
return await html2canvas(document.querySelector('.struct-chart-container') as HTMLElement, {
scale: scale,
width: (document.querySelector('.struct-chart-container') as HTMLElement).scrollWidth,
height: (document.querySelector('.struct-chart-container') as HTMLElement).scrollHeight
})
}
/**
* บภาพหนาจอออกมาเป PNG แลวดาวนโหลด
* @param scale ขนาดของภาพทองการ าพนฐานค 3 เทาของหนาจอ
*/
const savePNG = async(scale: number = 3) => {
let canvas = await captureChart(scale)
let anchor = document.createElement('a')
anchor.href = canvas.toDataURL('image/png')
anchor.download = 'structure-chart.png'
anchor.click()
anchor.remove()
}
/**
* savePDF - save canvas to PNG format and embed in PDF file then download.
*/
const savePDF = async(scale: number = 3) => {
let canvas = await captureChart(scale)
const doc = new jsPDF('l', 'px', 'a4')
// const doc = new jsPDF('p', 'px', 'a4')
let pageWidth = doc.internal.pageSize.getWidth()
let pageHeight = doc.internal.pageSize.getHeight()
let widthRatio = pageWidth / canvas.width
let heightRatio = pageHeight / canvas.height
let canvasRatio = widthRatio > heightRatio ? heightRatio : widthRatio
let canvasWidth = canvas.width * canvasRatio;
let canvasHeight = canvas.height * canvasRatio;
let marginX = (pageWidth - canvasWidth) / 2
let marginY = (pageHeight - canvasHeight) / 2
doc.addImage(canvas.toDataURL('image/png'), 'PNG', marginX, marginY, canvasWidth, canvasHeight);
doc.save('structure-chart.pdf');
}
// export method to public
defineExpose({ savePNG, savePDF })
/**
* onMounted:
* - assign dataSource to internal Store
* - init panzoom
*/
onMounted(async () => {
dataSource.value = props.dataSource
// PanZoom
// let element = document.querySelector('.struct-chart-container')
// panzoom(element as HTMLElement, { // or let instance = panzoom(...)
// smoothScroll: true,
// minZoom: minZoom.value,
// maxZoom: maxZoom.value
// })
})
/**
* watch if dataSource change -> update internal Store
*/
watch(() => props.dataSource, (newVal) => {
dataSource.value = newVal
// if (dataSourceLock.value === undefined) dataSourceLock.value = newVal
})
/**
* watch if user click element (selectedElement in internal Store change value)
*/
watch(selectedElement, () => {
// emit onElementClick when user select element
emit('onElementClick', selectedElement)
})
</script>
<template>
<div class="struct-chart">
<div class="struct-chart-container">
<StructChartNode :dataSource="props.dataSource" />
</div>
</div>
</template>
<style scoped lang="scss">
.struct-chart {
width: 100vw;
height: 100vh;
background-color: white;
// cursor: grab;
user-select: none;
}
.struct-chart-container {
width: auto;
height: auto;
}
</style>

View file

@ -0,0 +1,449 @@
<script setup lang="ts">
import { toRefs, watch, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useStructStore } from '../stores/StructStore'
const { dataSource, selectedElement } = storeToRefs(useStructStore())
const props = defineProps({
dataSource: Object
})
const propDataSource = toRefs(props).dataSource
onMounted(() => {
// console.log(propDataSource?.value)
})
watch(() => props.dataSource, (newVal) => {
// console.log(newVal)
})
const onElementSelected = (data: any) => {
selectedElement.value = data
}
const onSideElementClick = (data: any) => {
console.log(data)
}
const numberFormat = (data: number) => {
return new Intl.NumberFormat().format(data)
}
</script>
<template>
<table class="node-container">
<tbody>
<!-- วนน Root Node เช "สำนักงานเลขานุการผู้ว่าราชการกรุงเทพมหานคร" -->
<template v-if="propDataSource">
<tr class="root-node">
<td :colspan="propDataSource.children?.length * 2">
<div class="element-container root-element" @click="onElementSelected(propDataSource?.deptID)">
<div class="section-primary">
<div class="column-content">
<span class="header">{{ propDataSource.departmentName }}</span>
</div>
<div class="column-side">
<div class="side-button" @click.stop="onSideElementClick('test')">
{{ numberFormat(propDataSource.totalPositionCount) }}
</div>
</div>
</div>
<!-- ามรายการตำแหนงของ ผอ. -->
<template v-if="propDataSource.heads.length > 0">
<template v-for="head in propDataSource.heads" :key="head.positionID">
<div class="section-secondary">
<div class="column-content">
<span class="header">{{ head.positionName }} {{ head.positionNum }}</span>
</div>
<div class="column-side">
<div class="side-button">
{{ numberFormat(head.totalPositionCount) }}
</div>
</div>
</div>
</template>
</template>
<!-- ามรายการตำแหนงของเจาหนาท -->
<template v-if="propDataSource.officer.length > 0">
<div class="section-tertiary">
<template v-for="officer in propDataSource.officer" :key="officer.positionID">
<div class="section-list">
<div class="column-content">
<span class="header">
{{ officer.positionName }} {{ officer.positionNum }}
</span>
</div>
<div class="column-side">
<div class="side-button">
{{ numberFormat(officer.totalPositionCount) }}
</div>
</div>
</div>
</template>
</div>
</template>
</div>
</td>
</tr>
</template>
<!-- เสนแบ Node -->
<template v-if="propDataSource?.children?.length > 0">
<tr class="nodeline">
<td :colspan="propDataSource?.children?.length * 2">
<div class="nodeline-down"></div>
</td>
</tr>
<tr class="nodeline">
<td class="nodeline-right"></td>
<template v-for="n in propDataSource?.children?.length - 1" v-bind:key="n">
<td class="nodeline-left nodeline-top"></td>
<td class="nodeline-right nodeline-top"></td>
</template>
<td class="nodeline-left"></td>
</tr>
<!-- Child Node งหมดอย -->
<tr class="node-children">
<!-- วนรอบเพอแสดง Root ของ Child Node เปนคอลมนาง การแสดงผลจะเปนแบบแนวนอนสำหร Child Node ลำดบแรก จากนนจะเปนแบบแนวตงในลำดบถ ไป -->
<template v-for="child in propDataSource?.children" :key="child.deptID">
<td colspan="2">
<!-- วนน Root ของ Child Node เช "ฝ่ายบริหารทั่วไป" -->
<div class="element-container child-element" @click="onElementSelected(child?.deptID)">
<div class="section-primary">
<div class="column-content">
<span class="header">{{ child.departmentName }}</span>
</div>
<div class="column-side">
<div class="side-button">
{{ numberFormat(child.totalPositionCount) }}
</div>
</div>
</div>
<!-- ามรายการตำแหน ผอ. -->
<template v-if="child.heads?.length > 0">
<template v-for="head in child.heads" :key="head.positionID">
<div class="section-secondary">
<div class="column-content">
<span class="header">{{ head.positionName }} {{ head.positionNum }}</span>
</div>
<div class="column-side">
<div class="side-button">
{{ numberFormat(head.totalPositionCount) }}
</div>
</div>
</div>
</template>
</template>
<!-- ามรายการตำแหนงพนกงาน -->
<template v-if="child.officer?.length > 0">
<div class="section-tertiary">
<template v-for="officer in child.officer" :key="officer.positionID">
<div class="section-list">
<div class="column-content">
<span class="header">
{{ officer.positionName }} {{ officer.positionNum }}
</span>
</div>
<div class="column-side">
<div class="side-button">
{{ numberFormat(officer.totalPositionCount) }}
</div>
</div>
</div>
</template>
</div>
</template>
</div>
<!-- ามกล/กองยอยภายใต Child Node เช "กลุ่มงานประชุม" -->
<template v-if="child.children?.length > 0">
<table class="subchild-section">
<tbody>
<template v-for="(group, index) in child.children" :key="group.deptID">
<tr>
<td class="nodeline-left nodeline-bottom subchild-section-nodeline"></td>
<td rowspan="2">
<div class="element-container subchild-element" @click="onElementSelected(group?.deptID)" >
<div class="section-primary">
<div class="column-content">
<span class="header">
{{ group.departmentName }}
</span>
</div>
<div class="column-side">
<div class="side-button">
{{ numberFormat(group.totalPositionCount) }}
</div>
<template v-if="group.children?.length > 0">
<span class="subchild-more"></span>
</template>
</div>
</div>
<template v-if="group.heads?.length > 0">
<template v-for="groupHead in group.heads"
:key="groupHead.positionID">
<div class="section-secondary">
<div class="column-content">
<span class="header">{{ groupHead.positionName }} {{
groupHead.positionNum }}</span>
</div>
<div class="column-side">
<div class="side-button">
{{ numberFormat(groupHead.totalPositionCount) }}
</div>
</div>
</div>
</template>
</template>
<template v-if="group.officer?.length > 0">
<div class="section-tertiary">
<template v-for="groupOfficer in group.officer"
:key="groupOfficer.positionID">
<div class="section-list">
<div class="column-content">
<span class="header">
{{ groupOfficer.positionName }} {{
groupOfficer.positionNum }}
</span>
</div>
<div class="column-side">
<div class="side-button">
{{ numberFormat(groupOfficer.totalPositionCount) }}
</div>
</div>
</div>
</template>
</div>
</template>
</div>
</td>
</tr>
<tr>
<template v-if="index != child.children.length - 1">
<td class="nodeline-left nodeline-top subchild-section-nodeline"></td>
</template>
<template v-else>
<td></td>
</template>
</tr>
</template>
</tbody>
</table>
</template>
</td>
</template>
</tr>
</template>
</tbody>
</table>
</template>
<style scoped lang="scss">
/**
* 1 level = 1 node
* แตละ node งทเรยกว element
* 1 element = 1 person/dept
*/
// container node (level)
.node-container {
border-collapse: collapse;
margin: 0 auto 0 auto;
}
// root node element
.root-node {
td {
text-align: center;
width: 100%;
}
}
// container element
.element-container {
background: white;
border: 1px solid #f0efef;
border-radius: 5px;
box-sizing: border-box;
box-shadow: 0px 20px 20px -20px hsla(229, 6%, 66%, 0.212);
display: inline-flex;
flex-direction: column;
position: relative;
margin: 0 1px 2px 1px;
min-width: 9rem;
text-align: left;
transition-duration: 250ms;
&:hover {
box-shadow: 0 6px 10px #7c7c7c;
cursor: pointer;
transform: scale(1.05);
z-index: 20;
}
}
// root element
.root-element {
border-top: 3px solid hsl(212, 86%, 64%);
}
// child element
.child-element {
border-top: 3px solid hsl(180, 62%, 55%);
width: 99%;
}
// sub child element
.subchild-element {
border-top: 3px solid #995511;
}
.section-primary,
.section-secondary,
.section-list {
font-family: sans-serif;
display: flex;
align-items: center;
padding: 6px 15px 6px 20px;
bottom: 0;
justify-content: space-between;
.column-content {
min-width: 120px;
}
.column-side {
margin-left: 20px;
.side-button {
background-color: #d9d9d9;
border: 1px solid white;
// border-radius: 50%;
border-radius: 8px;
color: #717181;
display: inline-grid;
align-items: center;
min-width: 32px;
max-width: auto;
padding: 0 8px;
height: 36px;
text-align: center;
}
}
}
.section-primary, .section-secondary, .section-tertiary {
.header {
color: #334454;
font-size: 1.1rem;
line-height: 1.5rem;
margin: 0;
}
}
.section-primary {
.header {
font-weight: 700 !important;
}
}
.section-secondary,
.section-tertiary {
border-top: 1px solid #d9d9d9;
}
.section-secondary {
.header {
font-weight: 500 !important;
}
}
.section-tertiary {
background: #fafafa;
padding-left: 10px;
.header {
font-weight: 300 !important;
}
.header::before {
content: "\2022";
color: #394b5c;
font-weight: bold;
display: inline-block;
width: 0.9em;
}
}
.node-children {
vertical-align: top;
}
.nodeline {
height: 1.75rem;
}
// $line-color: #D2D6DB;
$line-color: #B0B4B0;
.nodeline-down {
background: $line-color;
margin-left: auto;
margin-right: auto;
margin-top: -1rem;
margin-bottom: -1rem;
height: 1.9rem;
width: 0.125rem;
float: none;
}
.nodeline-top {
border-top-color: $line-color;
border-top-style: solid;
border-top-width: 2px;
}
.nodeline-bottom {
border-bottom-color: $line-color;
border-bottom-style: solid;
border-bottom-width: 2px;
}
.nodeline-right {
border-right-color: $line-color;
border-right-style: solid;
border-right-width: 2px;
}
.nodeline-left {
border-left-color: $line-color;
border-left-style: solid;
border-left-width: 2px;
}
.subchild-section {
margin-left: 1rem;
border-collapse: collapse;
}
.subchild-section-nodeline {
min-width: 20px;
max-width: 30px;
}
.subchild-more {
background: transparent url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPgo8c3ZnIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDE2IDE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiMwMDAwMDAiIGNsYXNzPSJiaSBiaS1jYXJldC1kb3duLWZpbGwiPgogIDxwYXRoIGQ9Ik03LjI0NyAxMS4xNCAyLjQ1MSA1LjY1OEMxLjg4NSA1LjAxMyAyLjM0NSA0IDMuMjA0IDRoOS41OTJhMSAxIDAgMCAxIC43NTMgMS42NTlsLTQuNzk2IDUuNDhhMSAxIDAgMCAxLTEuNTA2IDB6Ii8+Cjwvc3ZnPg==) no-repeat center bottom;
background-size: 16px;
border: 1px solid white;
border-radius: 5px;
cursor: pointer;
padding: 5px 10px;
color: #330000;
width: 36px;
height: 40px;
text-align: center;
}
</style>

3
node_modules/structure-chart/src/index.ts generated vendored Normal file
View file

@ -0,0 +1,3 @@
import StructChart from './components/StructChart.vue'
export { StructChart }

7
node_modules/structure-chart/src/main.ts generated vendored Normal file
View file

@ -0,0 +1,7 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')

32
node_modules/structure-chart/src/stores/StructStore.ts generated vendored Normal file
View file

@ -0,0 +1,32 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
/**
* @interface IStructConfig
* Configuration of StructureChart
*/
export interface IStructConfig {
'minZoom'?: number,
'maxZoom'?: number
}
export const useStructStore = defineStore('StructStore', () => {
// StructureChart configuration
const config = ref<IStructConfig>({
'minZoom': 0.5,
'maxZoom': 2.0
})
// ข้อมูลที่กำลังแสดงอยู่
const dataSource = ref()
// Current (Active) Node
const selectedElement = ref()
return {
config,
dataSource,
selectedElement
}
})