fix(04): service tree view
This commit is contained in:
parent
55a6e03bcf
commit
76a08ac368
3 changed files with 159 additions and 14 deletions
|
|
@ -5,17 +5,21 @@ import { moveItemUp, moveItemDown, deleteItem, dialog } from 'stores/utils';
|
||||||
import NoData from 'components/NoData.vue';
|
import NoData from 'components/NoData.vue';
|
||||||
import WorkManagementComponent from './WorkManagementComponent.vue';
|
import WorkManagementComponent from './WorkManagementComponent.vue';
|
||||||
import AddButton from '../button/AddButton.vue';
|
import AddButton from '../button/AddButton.vue';
|
||||||
import { WorkItems } from 'stores/product-service/types';
|
import { ServiceCreate, WorkItems } from 'stores/product-service/types';
|
||||||
|
import TreeView from '../shared/TreeView.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const workItems = defineModel<WorkItems[]>('workItems', { default: [] });
|
const workItems = defineModel<WorkItems[]>('workItems', { default: [] });
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
|
service?: ServiceCreate;
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
outlined?: boolean;
|
outlined?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
separator?: boolean;
|
separator?: boolean;
|
||||||
|
treeView?: boolean;
|
||||||
|
|
||||||
priceDisplay?: {
|
priceDisplay?: {
|
||||||
price: boolean;
|
price: boolean;
|
||||||
|
|
@ -30,6 +34,22 @@ defineEmits<{
|
||||||
(e: 'workProperties', index: number): void;
|
(e: 'workProperties', index: number): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const nodes = ref([
|
||||||
|
{
|
||||||
|
title: props.service?.name,
|
||||||
|
subtitle: props.service?.code,
|
||||||
|
selected: false,
|
||||||
|
children: workItems.value.map((v) => ({
|
||||||
|
title: v.name,
|
||||||
|
subtitle: ' ',
|
||||||
|
children: v.product.map((x) => ({
|
||||||
|
title: x.name,
|
||||||
|
subtitle: x.code,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
function addWork() {
|
function addWork() {
|
||||||
workItems.value.push({
|
workItems.value.push({
|
||||||
id: '',
|
id: '',
|
||||||
|
|
@ -75,7 +95,7 @@ function confirmDelete(items: unknown[], index: number) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="workItems.length > 0" class="q-gutter-y-md row">
|
<div v-if="workItems.length > 0 && !treeView" class="q-gutter-y-md row">
|
||||||
<WorkManagementComponent
|
<WorkManagementComponent
|
||||||
class="col-12"
|
class="col-12"
|
||||||
v-for="(work, index) in workItems"
|
v-for="(work, index) in workItems"
|
||||||
|
|
@ -101,6 +121,39 @@ function confirmDelete(items: unknown[], index: number) {
|
||||||
<div class="col-12" style="height: 12px"></div>
|
<div class="col-12" style="height: 12px"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="workItems.length > 0 && treeView"
|
||||||
|
class="row rounded bordered surface-2 col q-pa-md"
|
||||||
|
>
|
||||||
|
<TreeView
|
||||||
|
expandable
|
||||||
|
hideCheckBox
|
||||||
|
icon-size="2.5rem"
|
||||||
|
class="full-width"
|
||||||
|
v-model:nodes="nodes"
|
||||||
|
:decoration="[
|
||||||
|
{
|
||||||
|
level: 0,
|
||||||
|
icon: 'mdi-server-outline',
|
||||||
|
bg: 'hsla(var(--orange-5-hsl)/0.1)',
|
||||||
|
fg: 'var(--orange-5)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 1,
|
||||||
|
icon: 'mdi-briefcase-outline',
|
||||||
|
bg: 'hsla(var(--violet-11-hsl)/0.1)',
|
||||||
|
fg: 'var(--violet-11)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 2,
|
||||||
|
icon: 'mdi-shopping-outline',
|
||||||
|
bg: 'hsla(var(--teal-10-hsl)/0.1)',
|
||||||
|
fg: 'var(--teal-10)',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="col row rounded bordered surface-2 justify-center items-center"
|
class="col row rounded bordered surface-2 justify-center items-center"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue';
|
import { Icon } from '@iconify/vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
type Node = {
|
type Node = {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
|
@ -13,6 +14,8 @@ type Props = {
|
||||||
keyTitle?: string;
|
keyTitle?: string;
|
||||||
keySubtitle?: string;
|
keySubtitle?: string;
|
||||||
expandable?: boolean;
|
expandable?: boolean;
|
||||||
|
hideCheckBox?: boolean;
|
||||||
|
iconSize?: string;
|
||||||
decoration?: {
|
decoration?: {
|
||||||
level?: number;
|
level?: number;
|
||||||
bg?: string;
|
bg?: string;
|
||||||
|
|
@ -27,6 +30,11 @@ const nodes = defineModel<Node[]>('nodes', { required: true });
|
||||||
const emits = defineEmits<{ (e: 'checked'): void }>();
|
const emits = defineEmits<{ (e: 'checked'): void }>();
|
||||||
|
|
||||||
const dec = props.decoration?.find((v) => v.level === (props.level || 0));
|
const dec = props.decoration?.find((v) => v.level === (props.level || 0));
|
||||||
|
const maxLevel = computed(() =>
|
||||||
|
props.decoration?.reduce((max, v) => {
|
||||||
|
return v.level && v.level > max ? v.level : max;
|
||||||
|
}, 0),
|
||||||
|
);
|
||||||
|
|
||||||
function recursiveDeselect(node: Node) {
|
function recursiveDeselect(node: Node) {
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
|
|
@ -48,8 +56,22 @@ function toggleExpand(node: Node) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="tree-container">
|
<div
|
||||||
<div v-for="(node, i) in nodes" class="tree-item" :key="i">
|
class="tree-container"
|
||||||
|
:class="{
|
||||||
|
'q-pl-lg': level && level > 0,
|
||||||
|
'last-children': level && level === maxLevel,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(node, i) in nodes"
|
||||||
|
class="tree-item"
|
||||||
|
:class="{
|
||||||
|
'q-pt-sm': level !== 0 && level !== maxLevel && i !== 0,
|
||||||
|
'q-pt-xs': level === maxLevel && i === 0,
|
||||||
|
}"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
<slot
|
<slot
|
||||||
v-if="$slots['item']"
|
v-if="$slots['item']"
|
||||||
name="item"
|
name="item"
|
||||||
|
|
@ -61,7 +83,23 @@ function toggleExpand(node: Node) {
|
||||||
class="item__content row items-center no-wrap"
|
class="item__content row items-center no-wrap"
|
||||||
@click="toggleExpand(node)"
|
@click="toggleExpand(node)"
|
||||||
>
|
>
|
||||||
<label class="flex items-center item__checkbox" @click.stop>
|
<div
|
||||||
|
v-if="level !== maxLevel"
|
||||||
|
class="q-mr-md"
|
||||||
|
style="color: var(--stone-4)"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="mdi-chevron-down-circle"
|
||||||
|
size="sm"
|
||||||
|
:style="`transform: rotate(${node.opened ? '180deg' : '0'})`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label
|
||||||
|
v-if="!hideCheckBox"
|
||||||
|
class="flex items-center item__checkbox"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="node.selected"
|
v-model="node.selected"
|
||||||
|
|
@ -71,9 +109,17 @@ function toggleExpand(node: Node) {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="item__icon flex items-center justify-center"
|
class="item__icon flex items-center justify-center"
|
||||||
:style="`background: ${dec?.bg}; color: ${dec?.fg}`"
|
:style="`background: ${dec?.bg}; color: ${dec?.fg}; height: ${iconSize}; width: ${iconSize}`"
|
||||||
>
|
>
|
||||||
<Icon v-if="dec && dec.icon" :icon="dec.icon" />
|
<div
|
||||||
|
:style="`height: calc(${iconSize} - 40%); width: calc(${iconSize} - 40%)`"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
v-if="dec && dec.icon"
|
||||||
|
:icon="dec.icon"
|
||||||
|
class="full-width full-height"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column">
|
<div class="column">
|
||||||
|
|
@ -86,14 +132,13 @@ function toggleExpand(node: Node) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<q-separator v-if="!level"></q-separator>
|
<q-separator v-if="!level" spaced="md"></q-separator>
|
||||||
|
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div
|
<div v-if="node.opened && node.children && node.children.length > 0">
|
||||||
class="q-pl-lg q-pt-sm"
|
|
||||||
v-if="node.opened && node.children && node.children.length > 0"
|
|
||||||
>
|
|
||||||
<TreeView
|
<TreeView
|
||||||
|
:iconSize
|
||||||
|
:hideCheckBox
|
||||||
class="item__children"
|
class="item__children"
|
||||||
v-if="node.children"
|
v-if="node.children"
|
||||||
v-model:nodes="node.children"
|
v-model:nodes="node.children"
|
||||||
|
|
@ -153,6 +198,10 @@ function toggleExpand(node: Node) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.last-children {
|
||||||
|
margin-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
.slide-enter-active {
|
.slide-enter-active {
|
||||||
transition: all 0.1s ease-out;
|
transition: all 0.1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,8 @@ const dialogService = ref(false);
|
||||||
const dialogProductEdit = ref(false);
|
const dialogProductEdit = ref(false);
|
||||||
const dialogServiceEdit = ref(false);
|
const dialogServiceEdit = ref(false);
|
||||||
|
|
||||||
|
const serviceTreeView = ref(false);
|
||||||
|
|
||||||
const statusToggle = ref(false);
|
const statusToggle = ref(false);
|
||||||
const profileSubmit = ref(false);
|
const profileSubmit = ref(false);
|
||||||
const infoProductEdit = ref(false);
|
const infoProductEdit = ref(false);
|
||||||
|
|
@ -4019,10 +4021,49 @@ watch(
|
||||||
id="group-form"
|
id="group-form"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="surface-1 rounded q-my-md q-mx-lg row"
|
class="surface-1 rounded q-my-md q-mx-lg row items-center"
|
||||||
style="position: absolute; z-index: 999; top: 0; right: 0"
|
style="position: absolute; z-index: 999; top: 0; right: 0"
|
||||||
v-if="actionDisplay && !currentNoAction"
|
v-if="actionDisplay && !currentNoAction"
|
||||||
>
|
>
|
||||||
|
<div class="bordered rounded q-mr-md" v-if="serviceTab === 2">
|
||||||
|
<q-btn
|
||||||
|
icon="mdi-file-tree-outline"
|
||||||
|
flat
|
||||||
|
square
|
||||||
|
:class="{
|
||||||
|
' surface-3': serviceTreeView,
|
||||||
|
'app-text-muted-2': serviceTreeView,
|
||||||
|
'app-text-muted': !serviceTreeView,
|
||||||
|
}"
|
||||||
|
size="sm"
|
||||||
|
padding="6px 10px"
|
||||||
|
title="Tree"
|
||||||
|
style="
|
||||||
|
border-top-left-radius: var(--radius-2);
|
||||||
|
border-bottom-left-radius: var(--radius-2);
|
||||||
|
"
|
||||||
|
@click="serviceTreeView = true"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
icon="mdi-view-list-outline"
|
||||||
|
flat
|
||||||
|
square
|
||||||
|
:class="{
|
||||||
|
' surface-3': !serviceTreeView,
|
||||||
|
'app-text-muted-2': !serviceTreeView,
|
||||||
|
'app-text-muted': serviceTreeView,
|
||||||
|
}"
|
||||||
|
size="sm"
|
||||||
|
padding="6px 10px"
|
||||||
|
title="List"
|
||||||
|
style="
|
||||||
|
border-top-right-radius: var(--radius-2);
|
||||||
|
border-bottom-right-radius: var(--radius-2);
|
||||||
|
"
|
||||||
|
@click="serviceTreeView = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<UndoButton
|
<UndoButton
|
||||||
v-if="infoServiceEdit"
|
v-if="infoServiceEdit"
|
||||||
id="btn-info-basic-undo"
|
id="btn-info-basic-undo"
|
||||||
|
|
@ -4104,6 +4145,8 @@ watch(
|
||||||
|
|
||||||
<FormServiceWork
|
<FormServiceWork
|
||||||
v-if="serviceTab === 2"
|
v-if="serviceTab === 2"
|
||||||
|
:service="formDataProductService"
|
||||||
|
:tree-view="serviceTreeView"
|
||||||
:readonly="!infoServiceEdit"
|
:readonly="!infoServiceEdit"
|
||||||
v-model:work-items="workItems"
|
v-model:work-items="workItems"
|
||||||
dense
|
dense
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue