264 lines
7.2 KiB
Vue
264 lines
7.2 KiB
Vue
<template>
|
|
<v-sheet class="bg-background" v-bind="$attrs">
|
|
<!-- Search Filter Section -->
|
|
<SKSearchFilter
|
|
v-if="showSearchFilter"
|
|
:collapse-btn-text="props.collapseBtnText"
|
|
:expand-btn-text="props.expandBtnText"
|
|
:fields="searchFields"
|
|
:reset-btn-text="props.resetBtnText"
|
|
:search-btn-text="props.searchBtnText"
|
|
:show-expand="true"
|
|
:visible-when-collapsed="['roleName', 'roleId']"
|
|
@reset="$emit('reset')"
|
|
@search="$emit('search', $event)"
|
|
/>
|
|
|
|
<!-- Data Table Section -->
|
|
<v-card class="bg-surface">
|
|
<SKTableActionBar
|
|
v-model:settings-selected-keys="visibleHeaderKeys"
|
|
:create-btn-text="props.createBtnText"
|
|
:search-visible="showSearchFilter"
|
|
:settings-items="headerSettingsItems"
|
|
:show-search-toggle="true"
|
|
:title="props.listTitle"
|
|
@create="emit('create')"
|
|
@refresh="emit('refresh')"
|
|
@settings="emit('settings')"
|
|
@toggle-search="showSearchFilter = !showSearchFilter"
|
|
/>
|
|
<v-divider></v-divider>
|
|
<v-data-table
|
|
class="role-table"
|
|
:headers="filteredHeaders"
|
|
hover
|
|
:items="props.roles"
|
|
:items-per-page="10"
|
|
:items-per-page-options="itemsPerPageOptions"
|
|
:items-per-page-text="props.itemsPerPageText"
|
|
:loading="props.loading"
|
|
>
|
|
<!-- Status Slot -->
|
|
<template #[`item.status`]="{ item }">
|
|
<v-switch
|
|
color="primary"
|
|
density="compact"
|
|
hide-details
|
|
:model-value="item.status"
|
|
@update:model-value="emit('update:status', item, $event)"
|
|
></v-switch>
|
|
</template>
|
|
|
|
<!-- Actions Slot -->
|
|
<template #[`item.actions`]="{ item }">
|
|
<v-btn
|
|
class="px-1"
|
|
color="primary"
|
|
size="small"
|
|
variant="text"
|
|
@click="emit('edit', item)"
|
|
>
|
|
{{ props.editActionText }}
|
|
</v-btn>
|
|
<v-divider
|
|
class="mx-1 d-inline-block"
|
|
style="height: 12px; vertical-align: middle"
|
|
vertical
|
|
></v-divider>
|
|
<v-btn
|
|
class="px-1"
|
|
color="error"
|
|
size="small"
|
|
variant="text"
|
|
@click="emit('delete', item)"
|
|
>
|
|
{{ props.deleteActionText }}
|
|
</v-btn>
|
|
</template>
|
|
</v-data-table>
|
|
</v-card>
|
|
</v-sheet>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watchEffect } from 'vue'
|
|
import SKSearchFilter from './base/SKSearchFilter.vue'
|
|
import SKTableActionBar from './base/SKTableActionBar.vue'
|
|
|
|
export interface RoleItem {
|
|
name: string
|
|
id: string
|
|
status: boolean
|
|
note: string
|
|
createTime: string
|
|
}
|
|
|
|
const showSearchFilter = ref(true)
|
|
|
|
const props = defineProps({
|
|
// Filter Labels & Placeholders
|
|
roleNameLabel: { type: String, default: '角色名稱' },
|
|
roleNamePlaceholder: { type: String, default: '請輸入' },
|
|
roleIdLabel: { type: String, default: '角色ID' },
|
|
roleIdPlaceholder: { type: String, default: '請輸入' },
|
|
statusLabel: { type: String, default: '狀態' },
|
|
statusPlaceholder: { type: String, default: '請選擇' },
|
|
statusOptions: { type: Array, default: () => ['已啟用', '已禁用'] },
|
|
noteLabel: { type: String, default: '備註' },
|
|
notePlaceholder: { type: String, default: '請輸入' },
|
|
createTimeLabel: { type: String, default: '建立時間' },
|
|
startDateLabel: { type: String, default: '開始日期' },
|
|
endDateLabel: { type: String, default: '結束日期' },
|
|
datePlaceholder: { type: String, default: '請選擇' },
|
|
|
|
// Button Texts
|
|
searchBtnText: { type: String, default: '搜尋' },
|
|
resetBtnText: { type: String, default: '重置' },
|
|
expandBtnText: { type: String, default: '展開' },
|
|
collapseBtnText: { type: String, default: '收起' },
|
|
createBtnText: { type: String, default: '新增角色' },
|
|
|
|
// Table Texts
|
|
listTitle: { type: String, default: '權限列表' },
|
|
roleNameHeader: { type: String, default: '角色名稱' },
|
|
roleIdHeader: { type: String, default: '角色ID' },
|
|
statusHeader: { type: String, default: '狀態' },
|
|
noteHeader: { type: String, default: '備註' },
|
|
createTimeHeader: { type: String, default: '建立時間' },
|
|
actionsHeader: { type: String, default: '操作' },
|
|
editActionText: { type: String, default: '修改' },
|
|
deleteActionText: { type: String, default: '刪除' },
|
|
|
|
// Data Table Footer Texts
|
|
itemsPerPageText: { type: String, default: '每頁筆數:' },
|
|
itemsPerPageAllText: { type: String, default: '全部' },
|
|
|
|
// Data
|
|
roles: {
|
|
type: Array as () => RoleItem[],
|
|
default: () => [],
|
|
},
|
|
loading: { type: Boolean, default: false },
|
|
})
|
|
|
|
const emit = defineEmits([
|
|
'search',
|
|
'reset',
|
|
'create',
|
|
'edit',
|
|
'delete',
|
|
'update:status',
|
|
'refresh',
|
|
'settings',
|
|
])
|
|
|
|
// --- Search Fields Configuration ---
|
|
const searchFields = computed(() => [
|
|
{
|
|
key: 'roleName',
|
|
type: 'text' as const,
|
|
label: props.roleNameLabel,
|
|
placeholder: props.roleNamePlaceholder,
|
|
meta: {
|
|
cols: 12,
|
|
md: 4,
|
|
lg: 3,
|
|
},
|
|
},
|
|
{
|
|
key: 'roleId',
|
|
type: 'text' as const,
|
|
label: props.roleIdLabel,
|
|
placeholder: props.roleIdPlaceholder,
|
|
meta: {
|
|
cols: 12,
|
|
md: 4,
|
|
lg: 3,
|
|
},
|
|
},
|
|
{
|
|
key: 'status',
|
|
type: 'select' as const,
|
|
label: props.statusLabel,
|
|
placeholder: props.statusPlaceholder,
|
|
items: props.statusOptions,
|
|
meta: {
|
|
cols: 12,
|
|
md: 4,
|
|
lg: 3,
|
|
},
|
|
},
|
|
{
|
|
key: 'note',
|
|
type: 'text' as const,
|
|
label: props.noteLabel,
|
|
placeholder: props.notePlaceholder,
|
|
meta: {
|
|
cols: 12,
|
|
md: 4,
|
|
lg: 3,
|
|
},
|
|
},
|
|
{
|
|
type: 'date' as const,
|
|
key: 'startDate',
|
|
label: props.startDateLabel,
|
|
placeholder: props.datePlaceholder,
|
|
meta: {
|
|
cols: 12,
|
|
md: 4,
|
|
lg: 3,
|
|
},
|
|
},
|
|
{
|
|
type: 'date' as const,
|
|
key: 'endDate',
|
|
label: props.endDateLabel,
|
|
placeholder: props.datePlaceholder,
|
|
meta: {
|
|
cols: 12,
|
|
md: 4,
|
|
lg: 3,
|
|
},
|
|
},
|
|
])
|
|
|
|
// --- Table Config ---
|
|
|
|
const formattedHeaders = computed(() => [
|
|
{ title: props.roleNameHeader, key: 'name', align: 'start' as const },
|
|
{ title: props.roleIdHeader, key: 'id', align: 'start' as const },
|
|
{ title: props.statusHeader, key: 'status', align: 'start' as const },
|
|
{ title: props.noteHeader, key: 'note', align: 'start' as const },
|
|
{ title: props.createTimeHeader, key: 'createTime', align: 'start' as const },
|
|
{ title: props.actionsHeader, key: 'actions', align: 'end' as const, sortable: false },
|
|
])
|
|
|
|
const visibleHeaderKeys = ref<string[]>([])
|
|
|
|
watchEffect(() => {
|
|
if (visibleHeaderKeys.value.length > 0) {
|
|
return
|
|
}
|
|
visibleHeaderKeys.value = formattedHeaders.value.map((h) => String(h.key))
|
|
})
|
|
|
|
const headerSettingsItems = computed(() =>
|
|
formattedHeaders.value
|
|
.filter((h) => h.key !== 'actions')
|
|
.map((h) => ({ key: String(h.key), label: String(h.title) }))
|
|
)
|
|
|
|
const filteredHeaders = computed(() =>
|
|
formattedHeaders.value.filter((h) => visibleHeaderKeys.value.includes(String(h.key)))
|
|
)
|
|
|
|
const itemsPerPageOptions = computed(() =>
|
|
[10, 25, 50, 100, -1].map((value) => ({
|
|
value,
|
|
title: value === -1 ? props.itemsPerPageAllText : String(value),
|
|
}))
|
|
)
|
|
</script>
|