198 lines
5.0 KiB
Vue
198 lines
5.0 KiB
Vue
<template>
|
|
<v-dialog v-model="dialogModel" max-width="480" v-bind="$attrs">
|
|
<v-card>
|
|
<v-card-title class="text-subtitle-1 font-weight-medium">
|
|
<slot name="title">
|
|
{{ props.titleText }}
|
|
</slot>
|
|
</v-card-title>
|
|
|
|
<v-card-text class="pt-2">
|
|
<slot :form="form" name="content" :permission="formPermission" :status="formStatus">
|
|
<div class="d-flex flex-column ga-4">
|
|
<v-select
|
|
v-if="props.showStatus"
|
|
v-model="formStatus"
|
|
density="comfortable"
|
|
hide-details
|
|
item-title="title"
|
|
item-value="value"
|
|
:items="normalizedStatusOptions"
|
|
:label="props.statusLabelText"
|
|
variant="outlined"
|
|
/>
|
|
|
|
<v-select
|
|
v-if="props.showPermission"
|
|
v-model="formPermission"
|
|
density="comfortable"
|
|
hide-details
|
|
item-title="title"
|
|
item-value="value"
|
|
:items="normalizedPermissionOptions"
|
|
:label="props.permissionLabelText"
|
|
variant="outlined"
|
|
/>
|
|
|
|
<slot :form="form" name="fields"></slot>
|
|
</div>
|
|
</slot>
|
|
</v-card-text>
|
|
|
|
<v-card-actions class="px-4 pb-4">
|
|
<slot :cancel="handleCancel" name="actions" :submit="handleSubmit">
|
|
<v-spacer />
|
|
<v-btn :disabled="props.loading" variant="text" @click="handleCancel">
|
|
{{ props.cancelText }}
|
|
</v-btn>
|
|
<v-btn color="primary" :loading="props.loading" @click="handleSubmit">
|
|
{{ props.confirmText }}
|
|
</v-btn>
|
|
</slot>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, reactive, watch } from 'vue'
|
|
|
|
type OptionValue = string | number
|
|
type Option = { title: string; value: OptionValue }
|
|
|
|
type GenericRecord = Record<string, unknown>
|
|
|
|
interface Props {
|
|
modelValue: boolean
|
|
item: GenericRecord | null
|
|
statusKey?: string
|
|
permissionKey?: string
|
|
showStatus?: boolean
|
|
showPermission?: boolean
|
|
statusOptions?: Array<Option | string | number>
|
|
permissionOptions?: Array<Option | string | number>
|
|
titleText?: string
|
|
statusLabelText?: string
|
|
permissionLabelText?: string
|
|
cancelText?: string
|
|
confirmText?: string
|
|
loading?: boolean
|
|
closeOnSubmit?: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
statusKey: 'status',
|
|
permissionKey: 'permission',
|
|
showStatus: true,
|
|
showPermission: true,
|
|
statusOptions: () => [],
|
|
permissionOptions: () => [],
|
|
titleText: '編輯',
|
|
statusLabelText: '狀態',
|
|
permissionLabelText: '權限',
|
|
cancelText: '取消',
|
|
confirmText: '確認',
|
|
loading: false,
|
|
closeOnSubmit: true,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', value: boolean): void
|
|
(e: 'submit', value: GenericRecord): void
|
|
(e: 'cancel'): void
|
|
}>()
|
|
|
|
const dialogModel = computed({
|
|
get: () => props.modelValue,
|
|
set: (v: boolean) => emit('update:modelValue', v),
|
|
})
|
|
|
|
function normalizeOptions(options: Array<Option | 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 normalizedPermissionOptions = computed(() => normalizeOptions(props.permissionOptions))
|
|
|
|
const form = reactive<GenericRecord>({})
|
|
|
|
function resetForm(next: GenericRecord) {
|
|
for (const key of Object.keys(form)) {
|
|
delete form[key]
|
|
}
|
|
Object.assign(form, next)
|
|
}
|
|
|
|
const getDefaultStatus = (): OptionValue | '' => normalizedStatusOptions.value[0]?.value ?? ''
|
|
function getDefaultPermission(): OptionValue | '' {
|
|
return normalizedPermissionOptions.value[0]?.value ?? ''
|
|
}
|
|
|
|
const formStatus = computed<OptionValue | ''>({
|
|
get: () => {
|
|
const current = form[props.statusKey] as OptionValue | undefined
|
|
return current ?? getDefaultStatus()
|
|
},
|
|
set: (v) => {
|
|
form[props.statusKey] = v
|
|
},
|
|
})
|
|
|
|
const formPermission = computed<OptionValue | ''>({
|
|
get: () => {
|
|
const current = form[props.permissionKey] as OptionValue | undefined
|
|
return current ?? getDefaultPermission()
|
|
},
|
|
set: (v) => {
|
|
form[props.permissionKey] = v
|
|
},
|
|
})
|
|
|
|
function syncFromItem() {
|
|
const item = props.item ?? {}
|
|
resetForm({ ...item })
|
|
|
|
if (props.showStatus) {
|
|
const status = item[props.statusKey] as OptionValue | undefined
|
|
form[props.statusKey] = status ?? getDefaultStatus()
|
|
}
|
|
|
|
if (props.showPermission) {
|
|
const permission = item[props.permissionKey] as OptionValue | undefined
|
|
form[props.permissionKey] = permission ?? getDefaultPermission()
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(open) => {
|
|
if (open) syncFromItem()
|
|
}
|
|
)
|
|
|
|
watch(
|
|
() => props.item,
|
|
() => {
|
|
if (props.modelValue) syncFromItem()
|
|
}
|
|
)
|
|
|
|
function handleCancel() {
|
|
emit('cancel')
|
|
dialogModel.value = false
|
|
}
|
|
|
|
function handleSubmit() {
|
|
emit('submit', { ...form })
|
|
|
|
if (props.closeOnSubmit) {
|
|
dialogModel.value = false
|
|
}
|
|
}
|
|
</script>
|