Implement artist management functionality: add modal form, table display, and save logic

This commit is contained in:
lucast committed 2026-02-03 14:52:29 +01:00
1 parent 602d1a7b3c
commit 57d4b62f22
4 files changed
+197 -7

No files matched your search

+21 -3
View File
@@ -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
View File
@@ -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>
+10
View File
@@ -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
View File
@@ -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 {