feat: implement MediaFormModal for adding new media entries, update Media interface to make 'id' optional, and refactor MediaItem to use utility for star ratings
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
import request from './lib/request';
|
import request from './lib/request';
|
||||||
import type { Media } from './lib/interfaces';
|
import type { Media } from './lib/interfaces';
|
||||||
import MediaItem from './lib/MediaItem.svelte';
|
import MediaItem from './lib/MediaItem.svelte';
|
||||||
|
import MediaFormModal from './lib/MediaFormModal.svelte';
|
||||||
// 类型定义
|
// 类型定义
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
interface ApiResponse<T> {
|
||||||
@@ -32,6 +33,9 @@
|
|||||||
let totalItems = $state(0);
|
let totalItems = $state(0);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
|
||||||
|
// 模态框状态
|
||||||
|
let showCreateModal = $state(false);
|
||||||
|
|
||||||
// 类别选项
|
// 类别选项
|
||||||
const categories = [
|
const categories = [
|
||||||
{ id: 'game', label: '游戏' },
|
{ id: 'game', label: '游戏' },
|
||||||
@@ -109,6 +113,25 @@
|
|||||||
localStorage.removeItem('auth');
|
localStorage.removeItem('auth');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理创建新媒体
|
||||||
|
async function handleCreate(media: Media) {
|
||||||
|
try {
|
||||||
|
const response = await request.post<ApiResponse<Media>>('/media/create', {
|
||||||
|
...media,
|
||||||
|
type: currentCategory
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
showCreateModal = false;
|
||||||
|
await fetchMediaList();
|
||||||
|
} else {
|
||||||
|
error = response.data.message || 'Failed to create media';
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
error = e.message || 'Failed to create media';
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -177,15 +200,23 @@
|
|||||||
<!-- 类别选择栏 -->
|
<!-- 类别选择栏 -->
|
||||||
<div class="bg-white shadow rounded-lg mb-6">
|
<div class="bg-white shadow rounded-lg mb-6">
|
||||||
<div class="px-6 py-4">
|
<div class="px-6 py-4">
|
||||||
<div class="flex space-x-6">
|
<div class="flex justify-between items-center">
|
||||||
{#each categories as category}
|
<div class="flex space-x-6">
|
||||||
<button
|
{#each categories as category}
|
||||||
class="px-4 py-2 rounded-md text-sm font-medium {currentCategory === category.id ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
|
<button
|
||||||
onclick={() => currentCategory = category.id}
|
class="px-4 py-2 rounded-md text-sm font-medium {currentCategory === category.id ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
|
||||||
>
|
onclick={() => currentCategory = category.id}
|
||||||
{category.label}
|
>
|
||||||
</button>
|
{category.label}
|
||||||
{/each}
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 bg-indigo-600 text-white rounded-md text-sm font-medium hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
onclick={() => showCreateModal = true}
|
||||||
|
>
|
||||||
|
添加新作品
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,6 +264,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<MediaFormModal
|
||||||
|
show={showCreateModal}
|
||||||
|
handleClose={() => showCreateModal = false}
|
||||||
|
submitMedia={handleCreate}
|
||||||
|
mode="add"
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:global(body) {
|
:global(body) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
154
src/lib/MediaFormModal.svelte
Normal file
154
src/lib/MediaFormModal.svelte
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Media } from './interfaces';
|
||||||
|
import { fade, scale } from 'svelte/transition';
|
||||||
|
|
||||||
|
let {show, mode, submitMedia, handleClose} = $props();
|
||||||
|
let media: Media = $state({
|
||||||
|
title: '',
|
||||||
|
type: '',
|
||||||
|
status: 'plan_to_watch',
|
||||||
|
date: '',
|
||||||
|
rating: 0,
|
||||||
|
platform: '',
|
||||||
|
notes: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: 'plan_to_watch', label: '计划中' },
|
||||||
|
{ value: 'in_progress', label: '进行中' },
|
||||||
|
{ value: 'completed', label: '已完成' }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function handleSubmit(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
// 暂时留空,后续处理
|
||||||
|
submitMedia(media);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if show}
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||||
|
transition:fade={{ duration: 200 }}
|
||||||
|
onclick={handleClose}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-lg p-6 w-full mx-4 max-w-[800px]"
|
||||||
|
transition:scale={{ duration: 200 }}
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 id="modal-title" class="text-xl font-semibold text-gray-900">
|
||||||
|
{mode === 'add' ? '添加新作品' : '编辑作品'}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
class="text-gray-400 hover:text-gray-500"
|
||||||
|
onclick={handleClose}
|
||||||
|
type="button"
|
||||||
|
aria-label="关闭"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onsubmit={handleSubmit} class="space-y-4">
|
||||||
|
<div class="flex justify-between items-center gap-6">
|
||||||
|
<label class="font-medium text-gray-700 whitespace-nowrap" for="title">标题</label>
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
type="text"
|
||||||
|
bind:value={media.title}
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="请输入标题"
|
||||||
|
required
|
||||||
|
aria-required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center gap-6">
|
||||||
|
<label class="font-medium text-gray-700 whitespace-nowrap" for="status">状态</label>
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
bind:value={media.status}
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
required
|
||||||
|
aria-required="true"
|
||||||
|
>
|
||||||
|
{#each statusOptions as option}
|
||||||
|
<option value={option.value}>{option.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center gap-6">
|
||||||
|
<label class="font-medium text-gray-700 whitespace-nowrap" for="date">日期</label>
|
||||||
|
<input
|
||||||
|
id="date"
|
||||||
|
type="date"
|
||||||
|
bind:value={media.date}
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
aria-label="选择日期"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center gap-6">
|
||||||
|
<label class="font-medium text-gray-700 whitespace-nowrap" for="rating">评分</label>
|
||||||
|
<input
|
||||||
|
id="rating"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="10"
|
||||||
|
step="0.5"
|
||||||
|
bind:value={media.rating}
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
aria-label="输入评分,范围0-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center gap-6">
|
||||||
|
<label class="font-medium text-gray-700 whitespace-nowrap" for="platform">平台</label>
|
||||||
|
<input
|
||||||
|
id="platform"
|
||||||
|
type="text"
|
||||||
|
bind:value={media.platform}
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="例如:Steam、Netflix、Kindle等"
|
||||||
|
aria-label="输入平台名称"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center gap-6">
|
||||||
|
<label class="font-medium text-gray-700 whitespace-nowrap" for="notes">备注</label>
|
||||||
|
<textarea
|
||||||
|
id="notes"
|
||||||
|
bind:value={media.notes}
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
rows="3"
|
||||||
|
placeholder="添加一些备注..."
|
||||||
|
aria-label="添加备注信息"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3 mt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
|
||||||
|
onclick={handleClose}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
{mode === 'add' ? '添加' : '保存'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -12,36 +12,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 星星评分组件
|
// 星星评分组件
|
||||||
function StarRating({ rating }: { rating: number }) {
|
import { StarRating } from './utils';
|
||||||
const maxStars = 5;
|
|
||||||
const stars = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= maxStars; i++) {
|
|
||||||
const fill = i <= Math.floor(rating) ? '#FFD700' : 'white';
|
|
||||||
const isHalfStar = i - 0.5 <= rating && rating < i;
|
|
||||||
|
|
||||||
if (isHalfStar) {
|
|
||||||
stars.push(`
|
|
||||||
<div class="relative w-4 h-4">
|
|
||||||
<svg class="absolute w-4 h-4" viewBox="0 0 24 24" fill="white" stroke="currentColor">
|
|
||||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
|
||||||
</svg>
|
|
||||||
<svg class="absolute w-4 h-4" viewBox="0 0 24 24" fill="#FFD700" stroke="currentColor" style="clip-path: inset(0 50% 0 0)">
|
|
||||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
} else {
|
|
||||||
stars.push(`
|
|
||||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="${fill}" stroke="currentColor">
|
|
||||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
|
||||||
</svg>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stars.join('');
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border rounded-lg p-4 hover:bg-gray-50" transition:fade>
|
<div class="border rounded-lg p-4 hover:bg-gray-50" transition:fade>
|
||||||
@@ -62,7 +33,7 @@
|
|||||||
<div class="px-2 py-1 bg-gray-100 rounded text-sm text-gray-700 flex items-center gap-1">
|
<div class="px-2 py-1 bg-gray-100 rounded text-sm text-gray-700 flex items-center gap-1">
|
||||||
<span>评分:</span>
|
<span>评分:</span>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
{@html StarRating({ rating: media.rating / 2 })}
|
{@html StarRating(media.rating / 2)}
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-1">({media.rating}/10)</span>
|
<span class="ml-1">({media.rating}/10)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface Media {
|
export interface Media {
|
||||||
id: number;
|
id?: number;
|
||||||
title: string;
|
title: string;
|
||||||
type: string;
|
type: string;
|
||||||
status: 'completed' | 'in_progress' | 'plan_to_watch';
|
status: 'completed' | 'in_progress' | 'plan_to_watch';
|
||||||
@@ -7,6 +7,4 @@ export interface Media {
|
|||||||
notes?: string;
|
notes?: string;
|
||||||
platform?: string;
|
platform?: string;
|
||||||
date?: string;
|
date?: string;
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/lib/utils.ts
Normal file
35
src/lib/utils.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* @Date: 2025-05-26 17:39:36
|
||||||
|
* @LastEditors: 陈子健
|
||||||
|
* @LastEditTime: 2025-05-26 17:39:47
|
||||||
|
* @FilePath: /my-score/frontend/src/lib/utils.ts
|
||||||
|
*/
|
||||||
|
export const StarRating = (rating: number, maxStars: number = 5) => {
|
||||||
|
const stars = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= maxStars; i++) {
|
||||||
|
const fill = i <= Math.floor(rating) ? '#FFD700' : 'white';
|
||||||
|
const isHalfStar = i - 0.5 <= rating && rating < i;
|
||||||
|
|
||||||
|
if (isHalfStar) {
|
||||||
|
stars.push(`
|
||||||
|
<div class="relative w-4 h-4">
|
||||||
|
<svg class="absolute w-4 h-4" viewBox="0 0 24 24" fill="white" stroke="currentColor">
|
||||||
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||||
|
</svg>
|
||||||
|
<svg class="absolute w-4 h-4" viewBox="0 0 24 24" fill="#FFD700" stroke="currentColor" style="clip-path: inset(0 50% 0 0)">
|
||||||
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
stars.push(`
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="${fill}" stroke="currentColor">
|
||||||
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||||
|
</svg>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stars.join('');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user