254 lines
7.9 KiB
Vue
254 lines
7.9 KiB
Vue
<template>
|
|
<v-sheet class="bg-surface" v-bind="$attrs">
|
|
<v-card border elevation="0">
|
|
<!-- Top Action Bar -->
|
|
<SKTableActionBar
|
|
:create-btn-text="props.createBtnText"
|
|
:title="props.listTitle"
|
|
@create="emit('create')"
|
|
@refresh="emit('refresh')"
|
|
@settings="emit('settings')"
|
|
/>
|
|
|
|
<SKFormEditDialog
|
|
v-model="addSubDialogOpen"
|
|
:cancel-text="props.dialogCancelText"
|
|
:confirm-text="props.dialogConfirmText"
|
|
:item="addSubDraftItem"
|
|
:loading="props.dialogLoading"
|
|
:show-permission="false"
|
|
:status-label-text="props.dialogStatusLabelText"
|
|
:status-options="props.statusOptions"
|
|
:title-text="props.addSubDialogTitleText"
|
|
@submit="onAddSubSubmit"
|
|
>
|
|
<template #fields="{ form }">
|
|
<v-text-field
|
|
v-model="form.name"
|
|
density="comfortable"
|
|
hide-details
|
|
:label="props.dialogNameLabelText"
|
|
variant="outlined"
|
|
/>
|
|
<v-textarea
|
|
v-model="form.note"
|
|
density="comfortable"
|
|
hide-details
|
|
:label="props.dialogNoteLabelText"
|
|
rows="3"
|
|
variant="outlined"
|
|
/>
|
|
</template>
|
|
</SKFormEditDialog>
|
|
|
|
<SKFormEditDialog
|
|
v-model="editDialogOpen"
|
|
:cancel-text="props.dialogCancelText"
|
|
:confirm-text="props.dialogConfirmText"
|
|
:item="selectedItem"
|
|
:loading="props.dialogLoading"
|
|
:show-permission="false"
|
|
:status-label-text="props.dialogStatusLabelText"
|
|
:status-options="props.statusOptions"
|
|
:title-text="props.editDialogTitleText"
|
|
@submit="onEditSubmit"
|
|
>
|
|
<template #fields="{ form }">
|
|
<v-text-field
|
|
v-model="form.name"
|
|
density="comfortable"
|
|
hide-details
|
|
:label="props.dialogNameLabelText"
|
|
variant="outlined"
|
|
/>
|
|
<v-textarea
|
|
v-model="form.note"
|
|
density="comfortable"
|
|
hide-details
|
|
:label="props.dialogNoteLabelText"
|
|
rows="3"
|
|
variant="outlined"
|
|
/>
|
|
</template>
|
|
</SKFormEditDialog>
|
|
|
|
<!-- Tree Table -->
|
|
<SKTreeTable
|
|
:headers="formattedHeaders"
|
|
:items="props.items"
|
|
:loading="props.loading"
|
|
table-class="dept-table"
|
|
:tree-column-keys="['name']"
|
|
@toggle-expand="emit('toggle-expand', $event)"
|
|
>
|
|
<!-- Status Column -->
|
|
<template #[`item.status`]="{ item }">
|
|
<v-chip
|
|
:color="isEnabledStatus(item.status) ? 'success' : 'error'"
|
|
label
|
|
size="small"
|
|
variant="tonal"
|
|
>
|
|
{{ isEnabledStatus(item.status) ? props.statusEnabledText : props.statusDisabledText }}
|
|
</v-chip>
|
|
</template>
|
|
|
|
<!-- Actions Column -->
|
|
<template #[`item.actions`]="{ item }">
|
|
<v-btn class="px-1" color="primary" size="small" variant="text" @click="openAddSub(item)">
|
|
{{ props.addSubActionText }}
|
|
</v-btn>
|
|
<v-btn class="px-1" color="primary" size="small" variant="text" @click="openEdit(item)">
|
|
{{ props.editActionText }}
|
|
</v-btn>
|
|
<v-btn
|
|
class="px-1"
|
|
color="error"
|
|
size="small"
|
|
variant="text"
|
|
@click="emit('delete', item)"
|
|
>
|
|
{{ props.deleteActionText }}
|
|
</v-btn>
|
|
</template>
|
|
</SKTreeTable>
|
|
</v-card>
|
|
</v-sheet>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { PropType } from 'vue'
|
|
import { computed, ref } from 'vue'
|
|
import SKFormEditDialog from './base/SKFormEditDialog.vue'
|
|
import SKTableActionBar from './base/SKTableActionBar.vue'
|
|
import SKTreeTable from './base/SKTreeTable.vue'
|
|
|
|
export interface DeptItem {
|
|
id: string | number
|
|
name: string
|
|
status: string | number
|
|
createTime: string
|
|
note: string
|
|
children?: DeptItem[]
|
|
[key: string]: unknown
|
|
}
|
|
|
|
const props = defineProps({
|
|
items: { type: Array as () => DeptItem[], default: () => [] },
|
|
loading: { type: Boolean, default: false },
|
|
|
|
// Text Props
|
|
listTitle: { type: String, default: '部門列表' },
|
|
createBtnText: { type: String, default: '新增部門' },
|
|
addSubActionText: { type: String, default: '新增下級' },
|
|
editActionText: { type: String, default: '修改' },
|
|
deleteActionText: { type: String, default: '刪除' },
|
|
statusEnabledText: { type: String, default: '已啟用' },
|
|
statusDisabledText: { type: String, default: '已禁用' },
|
|
|
|
// Dialog Props
|
|
addSubDialogTitleText: { type: String, default: '新增下級' },
|
|
editDialogTitleText: { type: String, default: '編輯' },
|
|
dialogStatusLabelText: { type: String, default: '狀態' },
|
|
dialogNameLabelText: { type: String, default: '部門名稱' },
|
|
dialogNoteLabelText: { type: String, default: '備註' },
|
|
dialogCancelText: { type: String, default: '取消' },
|
|
dialogConfirmText: { type: String, default: '確認' },
|
|
dialogLoading: { type: Boolean, default: false },
|
|
statusEnabledValue: { type: [String, Number] as PropType<string | number>, default: undefined },
|
|
statusOptions: {
|
|
type: Array as () => Array<string | number | { title: string; value: string | number }>,
|
|
default: () => [],
|
|
},
|
|
|
|
// Header Texts
|
|
nameHeader: { type: String, default: '部門名稱' },
|
|
statusHeader: { type: String, default: '狀態' },
|
|
createTimeHeader: { type: String, default: '創建時間' },
|
|
noteHeader: { type: String, default: '備註' },
|
|
actionsHeader: { type: String, default: '操作' },
|
|
})
|
|
|
|
const emit = defineEmits([
|
|
'create',
|
|
'add-sub',
|
|
'edit',
|
|
'delete',
|
|
'refresh',
|
|
'settings',
|
|
'toggle-expand',
|
|
])
|
|
|
|
function normalizeOptions (options: Array<string | number | { title: string; value: string | number }>) {
|
|
return options.map((o) => {
|
|
if (typeof o === 'string' || typeof o === 'number') {
|
|
return { title: String(o), value: o }
|
|
}
|
|
return o
|
|
})
|
|
}
|
|
|
|
const normalizedStatusOptions = computed(() => normalizeOptions(props.statusOptions))
|
|
|
|
const resolvedStatusEnabledValue = computed(() => {
|
|
if (props.statusEnabledValue !== undefined) return props.statusEnabledValue
|
|
return normalizedStatusOptions.value[0]?.value ?? 'enable'
|
|
})
|
|
|
|
const isEnabledStatus = (status: unknown) => status === resolvedStatusEnabledValue.value
|
|
|
|
const addSubDialogOpen = ref(false)
|
|
const editDialogOpen = ref(false)
|
|
const addSubParentItem = ref<DeptItem | null>(null)
|
|
const addSubDraftItem = ref<Record<string, unknown> | null>(null)
|
|
const selectedItem = ref<DeptItem | null>(null)
|
|
|
|
function openAddSub (item: DeptItem) {
|
|
addSubParentItem.value = item
|
|
addSubDraftItem.value = {
|
|
name: '',
|
|
note: '',
|
|
status: resolvedStatusEnabledValue.value,
|
|
}
|
|
addSubDialogOpen.value = true
|
|
}
|
|
|
|
function openEdit (item: DeptItem) {
|
|
selectedItem.value = item
|
|
editDialogOpen.value = true
|
|
}
|
|
|
|
function onAddSubSubmit (payload: Record<string, unknown>) {
|
|
if (!addSubParentItem.value) return
|
|
const newItem: DeptItem = {
|
|
id: Date.now(),
|
|
name: String(payload.name ?? ''),
|
|
note: String(payload.note ?? ''),
|
|
status: (payload.status as string | number | undefined) ?? resolvedStatusEnabledValue.value,
|
|
createTime: new Date().toISOString(),
|
|
children: [],
|
|
}
|
|
emit('add-sub', addSubParentItem.value, newItem)
|
|
}
|
|
|
|
function onEditSubmit (updated: Record<string, unknown>) {
|
|
emit('edit', updated)
|
|
}
|
|
|
|
// --- Table Config ---
|
|
|
|
const formattedHeaders = computed(() => [
|
|
{ title: props.nameHeader, key: 'name', align: 'start' as const, minWidth: '250px' },
|
|
{ title: props.statusHeader, key: 'status', align: 'center' as const, width: '100px' },
|
|
{ title: props.createTimeHeader, key: 'createTime', align: 'start' as const },
|
|
{ title: props.noteHeader, key: 'note', align: 'start' as const },
|
|
{
|
|
title: props.actionsHeader,
|
|
key: 'actions',
|
|
align: 'center' as const,
|
|
width: '250px',
|
|
sortable: false,
|
|
},
|
|
])
|
|
</script>
|