Implement artist management functionality: add modal form, table display, and save logic
This commit is contained in:
1 parent
602d1a7b3c
commit
57d4b62f22
4 files changed
+197
-7
No files matched your search
@@ -27,16 +27,34 @@ interface ModalFormProps<T> {
|
|||||||
item?: T
|
item?: T
|
||||||
fields: Field<T>[]
|
fields: Field<T>[]
|
||||||
title?: string
|
title?: string
|
||||||
|
onSave: (value: any) => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<ModalFormProps<any>>()
|
const props = defineProps<ModalFormProps<any>>()
|
||||||
const open = defineModel("open", { required: true })
|
const open = defineModel("open", { required: true })
|
||||||
const emit = defineEmits<{ (e: "save", value: any): boolean }>()
|
|
||||||
|
|
||||||
let localItem = reactive<any>({})
|
let localItem = reactive<any>({})
|
||||||
|
|
||||||
const save = () => {
|
watch(
|
||||||
if (emit("save", localItem)) open.value = false
|
() => props.item,
|
||||||
|
(item) => {
|
||||||
|
Object.keys(localItem).forEach((k) => delete localItem[k])
|
||||||
|
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(item)) {
|
||||||
|
localItem[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of props.fields) {
|
||||||
|
if (field.type == "file") localItem[field.key as string] = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
if (await props.onSave(localItem)) open.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+131
-1
@@ -1 +1,131 @@
|
|||||||
<template></template>
|
<script setup lang="ts">
|
||||||
|
import type { Field } from "~/components/Admin/ModalForm.vue"
|
||||||
|
import type { Row } from "~/ui/Table.vue"
|
||||||
|
|
||||||
|
const { data, refresh } = useApiFetch<Track[]>("/all/artists")
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const tableRows: Row[] = [
|
||||||
|
{
|
||||||
|
name: "_id",
|
||||||
|
displayName: "ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file",
|
||||||
|
displayName: "Cover",
|
||||||
|
type: "image",
|
||||||
|
imagePrefix: `${import.meta.env.VITE_PUBLIC_BACKEND}/static/`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
displayName: "Name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "action",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const modalFields: Field[] = [
|
||||||
|
{ key: "name", label: "Name" },
|
||||||
|
{ key: "file", label: "Cover file", type: "file" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const showModal = ref<boolean>(false)
|
||||||
|
const item = ref<Track | undefined>(undefined)
|
||||||
|
|
||||||
|
const save = async (value: any) => {
|
||||||
|
if (value._id) {
|
||||||
|
try {
|
||||||
|
await cfetch("/admin/artist", {
|
||||||
|
method: "PATCH",
|
||||||
|
data: value,
|
||||||
|
params: { id: value._id },
|
||||||
|
forceFormData: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.show("Artist updated successfully", "success")
|
||||||
|
} catch {
|
||||||
|
toast.show("Failed to update artist", "error")
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await cfetch("/admin/artist", {
|
||||||
|
method: "POST",
|
||||||
|
data: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.show("Artist added successfully", "success")
|
||||||
|
} catch {
|
||||||
|
toast.show("Failed to add artist", "error")
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param id Set it to a value to make it edit instead of creating a new one
|
||||||
|
*/
|
||||||
|
const show = (id?: string) => {
|
||||||
|
if (!id) {
|
||||||
|
item.value = undefined
|
||||||
|
showModal.value = true
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const curItem: any = data.value.filter((i) => i._id == id)[0]
|
||||||
|
|
||||||
|
if (!curItem) return
|
||||||
|
|
||||||
|
item.value = curItem
|
||||||
|
|
||||||
|
showModal.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1 class="title">Manage artists</h1>
|
||||||
|
|
||||||
|
<Table :rows="tableRows" :data="data" class="table">
|
||||||
|
<template #action="{ item, index }">
|
||||||
|
<div class="action">
|
||||||
|
<Button @click="show(item._id)">Edit</Button>
|
||||||
|
<Button @click="">Delete</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<Button @click="show()">Add a new artist</Button>
|
||||||
|
|
||||||
|
<AdminModalForm
|
||||||
|
v-model:open="showModal"
|
||||||
|
@save="save"
|
||||||
|
:fields="modalFields"
|
||||||
|
:item="item"
|
||||||
|
:title="item ? 'Edit artist' : 'Add new artist'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/util" as *;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@include fontSize(24px);
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,6 +4,7 @@ export type Row = {
|
|||||||
class?: string
|
class?: string
|
||||||
width?: string
|
width?: string
|
||||||
type?: "image"
|
type?: "image"
|
||||||
|
imagePrefix?: string
|
||||||
displayName?: string
|
displayName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +35,10 @@ const { rows, data } = defineProps<TableProps>()
|
|||||||
<td v-for="(row, j) in rows" :key="j">
|
<td v-for="(row, j) in rows" :key="j">
|
||||||
<slot :name="row.name" :item="item" :index="i">
|
<slot :name="row.name" :item="item" :index="i">
|
||||||
<p v-if="row.type != 'image'">{{ item[row.name] }}</p>
|
<p v-if="row.type != 'image'">{{ item[row.name] }}</p>
|
||||||
|
<nuxt-img
|
||||||
|
v-else-if="row.type == 'image'"
|
||||||
|
:src="`${row.imagePrefix}${item[row.name]}`"
|
||||||
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -63,5 +68,10 @@ const { rows, data } = defineProps<TableProps>()
|
|||||||
|
|
||||||
& thead {
|
& thead {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& img {
|
||||||
|
height: 60px;
|
||||||
|
border-radius: $border-radius-standard;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+35
-3
@@ -1,10 +1,25 @@
|
|||||||
import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"
|
import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"
|
||||||
import defu from "defu"
|
import defu from "defu"
|
||||||
|
|
||||||
|
const hasFile = (value: any): boolean => {
|
||||||
|
if (!value || typeof value !== "object") return false
|
||||||
|
|
||||||
|
return Object.values(value).some(
|
||||||
|
(v) =>
|
||||||
|
v instanceof File ||
|
||||||
|
v instanceof Blob ||
|
||||||
|
(Array.isArray(v) && v.some((i) => i instanceof File)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestConfig = {
|
||||||
|
forceFormData?: boolean
|
||||||
|
} & AxiosRequestConfig
|
||||||
|
|
||||||
export const cfetch = async (
|
export const cfetch = async (
|
||||||
url: string,
|
url: string,
|
||||||
options: AxiosRequestConfig = {},
|
options: RequestConfig = {},
|
||||||
authorize: boolean = true
|
authorize: boolean = true,
|
||||||
): Promise<AxiosResponse> => {
|
): Promise<AxiosResponse> => {
|
||||||
const baseUrl = import.meta.env.VITE_PUBLIC_BACKEND
|
const baseUrl = import.meta.env.VITE_PUBLIC_BACKEND
|
||||||
|
|
||||||
@@ -26,7 +41,24 @@ export const cfetch = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: AxiosRequestConfig = defu(options, defaults)
|
const config: RequestConfig = defu(options, defaults)
|
||||||
|
|
||||||
|
if (config.data && (hasFile(config.data) || config.forceFormData)) {
|
||||||
|
const form = new FormData()
|
||||||
|
|
||||||
|
for (const [key, val] of Object.entries(config.data)) {
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
val.forEach((v) => form.append(`${key}[]`, v))
|
||||||
|
} else if (val !== undefined && val !== null) {
|
||||||
|
form.append(key, val as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.data = form
|
||||||
|
|
||||||
|
if (!config.headers) config.headers = {}
|
||||||
|
delete config.headers["Content-Type"]
|
||||||
|
}
|
||||||
|
|
||||||
let response
|
let response
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user