feat: enhance UI components with improved styling, transitions, and layout adjustments across App, MediaFormModal, MediaItem, and StarRating components

This commit is contained in:
ethan.chen
2025-06-03 17:41:10 +08:00
parent 701021c112
commit 35e0df26fe
5 changed files with 70 additions and 77 deletions

View File

@@ -273,16 +273,16 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="min-h-screen bg-gray-100" transition:fade> <div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100" transition:fade>
<div class="min-w-[1200px] mx-auto px-6 py-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 类别选择栏 --> <!-- 类别选择栏 -->
<div class="bg-white shadow rounded-lg mb-6"> <div class="bg-white/80 backdrop-blur-sm shadow-sm rounded-xl mb-6 border border-gray-200">
<div class="px-6 py-4"> <div class="px-6 py-4">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex space-x-6"> <div class="flex space-x-4">
{#each categories as category} {#each categories as category}
<button <button
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'}" class="px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 {currentCategory === category.id ? 'bg-gradient-to-r from-indigo-500 to-indigo-600 text-white shadow-sm' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
onclick={() => currentCategory = category.id} onclick={() => currentCategory = category.id}
> >
{category.label} {category.label}
@@ -290,7 +290,7 @@
{/each} {/each}
</div> </div>
<button <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" class="px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-indigo-500 to-indigo-600 rounded-lg hover:from-indigo-600 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-all duration-200 shadow-sm hover:shadow-md"
onclick={() => showCreateModal = true} onclick={() => showCreateModal = true}
> >
添加新作品 添加新作品
@@ -300,14 +300,14 @@
</div> </div>
<!-- 媒体列表 --> <!-- 媒体列表 -->
<div class="bg-white shadow rounded-lg"> <div class="bg-white/80 backdrop-blur-sm shadow-sm rounded-xl border border-gray-200">
<div class="px-6 py-4"> <div class="px-6 py-4">
{#if loading} {#if loading}
<div class="text-center py-8"> <div class="text-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600 mx-auto"></div> <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600 mx-auto"></div>
</div> </div>
{:else} {:else}
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{#each mediaList as media} {#each mediaList as media}
<MediaItem {media} onEdit={handleEditClick} onDelete={handleDeleteClick} /> <MediaItem {media} onEdit={handleEditClick} onDelete={handleDeleteClick} />
{/each} {/each}
@@ -315,19 +315,19 @@
<!-- 分页控制 --> <!-- 分页控制 -->
<div class="mt-6 flex justify-between items-center"> <div class="mt-6 flex justify-between items-center">
<div class="text-sm text-gray-700"> <div class="text-sm text-gray-600">
Showing {(currentPage - 1) * pageSize + 1} to {Math.min(currentPage * pageSize, totalItems)} of {totalItems} items Showing {(currentPage - 1) * pageSize + 1} to {Math.min(currentPage * pageSize, totalItems)} of {totalItems} items
</div> </div>
<div class="flex space-x-2"> <div class="flex space-x-3">
<button <button
class="px-4 py-2 rounded-md text-sm font-medium {currentPage === 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50 border'}" class="px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 {currentPage === 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-200 shadow-sm hover:shadow-md'}"
disabled={currentPage === 1} disabled={currentPage === 1}
onclick={() => currentPage = currentPage - 1} onclick={() => currentPage = currentPage - 1}
> >
Previous Previous
</button> </button>
<button <button
class="px-4 py-2 rounded-md text-sm font-medium {currentPage * pageSize >= totalItems ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50 border'}" class="px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 {currentPage * pageSize >= totalItems ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-200 shadow-sm hover:shadow-md'}"
disabled={currentPage * pageSize >= totalItems} disabled={currentPage * pageSize >= totalItems}
onclick={() => currentPage = currentPage + 1} onclick={() => currentPage = currentPage + 1}
> >

View File

@@ -50,23 +50,23 @@
{#if show} {#if show}
<div <div
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" class="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50"
transition:fade={{ duration: 200 }} transition:fade={{ duration: 200 }}
onclick={handleClose} onclick={handleClose}
role="presentation" role="presentation"
> >
<div <div
class="bg-white rounded-lg p-6 w-full mx-4 max-w-[800px]" class="bg-gradient-to-br from-white to-gray-50 rounded-xl p-6 w-full mx-4 max-w-[800px] shadow-2xl"
transition:scale={{ duration: 200 }} transition:scale={{ duration: 200 }}
onclick={(e) => e.stopPropagation()} onclick={(e) => e.stopPropagation()}
role="presentation" role="presentation"
> >
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h2 id="modal-title" class="text-xl font-semibold text-gray-900"> <h2 id="modal-title" class="text-xl font-semibold bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">
{mode === 'add' ? '添加新作品' : '编辑作品'} {mode === 'add' ? '添加新作品' : '编辑作品'}
</h2> </h2>
<button <button
class="text-gray-400 hover:text-gray-500" class="text-gray-400 hover:text-gray-600 transition-colors duration-200"
onclick={handleClose} onclick={handleClose}
type="button" type="button"
aria-label="关闭" aria-label="关闭"
@@ -84,7 +84,7 @@
id="title" id="title"
type="text" type="text"
bind:value={media.title} 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" class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white/50 backdrop-blur-sm transition-all duration-200"
placeholder="请输入标题" placeholder="请输入标题"
required required
aria-required="true" aria-required="true"
@@ -96,7 +96,7 @@
<select <select
id="type" id="type"
bind:value={media.type} bind:value={media.type}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white/50 backdrop-blur-sm transition-all duration-200"
required required
aria-required="true" aria-required="true"
> >
@@ -131,7 +131,7 @@
id="platform" id="platform"
type="text" type="text"
bind:value={media.platform} 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" class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white/50 backdrop-blur-sm transition-all duration-200"
placeholder="例如Steam、Netflix、Kindle等" placeholder="例如Steam、Netflix、Kindle等"
aria-label="输入平台名称" aria-label="输入平台名称"
/> />
@@ -142,7 +142,7 @@
<textarea <textarea
id="notes" id="notes"
bind:value={media.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" class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white/50 backdrop-blur-sm transition-all duration-200"
rows="10" rows="10"
placeholder="添加一些备注..." placeholder="添加一些备注..."
aria-label="添加备注信息" aria-label="添加备注信息"
@@ -152,7 +152,7 @@
<div class="flex justify-end gap-3 mt-6"> <div class="flex justify-end gap-3 mt-6">
<button <button
type="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" class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200"
onclick={handleClose} onclick={handleClose}
> >
取消 取消
@@ -160,7 +160,7 @@
<button <button
type="button" type="button"
onclick={() => submitMedia(media)} onclick={() => submitMedia(media)}
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" class="px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-indigo-500 to-indigo-600 rounded-lg hover:from-indigo-600 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-all duration-200 shadow-sm hover:shadow-md"
> >
{mode === 'add' ? '添加' : '保存'} {mode === 'add' ? '添加' : '保存'}
</button> </button>

View File

@@ -1,66 +1,58 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition';
import type { Media } from './interfaces'; import type { Media } from './interfaces';
let {media, onEdit, onDelete}: {media: Media, onEdit: (media: Media) => void, onDelete: (media: Media) => void} = $props(); let {media, onEdit, onDelete}: {media: Media, onEdit: (media: Media) => void, onDelete: (media: Media) => void} = $props();
// 状态映射
const statusMap = {
'completed': '已完成',
'in_progress': '进行中',
'plan_to_watch': '计划中'
};
// 星星评分组件 // 星星评分组件
import { StarRating } from './utils'; import { StarRating } from './utils';
</script> </script>
<div class="border rounded-lg p-4 hover:bg-gray-50 relative" transition:fade> <div class="bg-white/90 backdrop-blur-sm rounded-xl shadow-[0_2px_8px_-2px_rgba(0,0,0,0.1)] hover:shadow-[0_4px_12px_-4px_rgba(0,0,0,0.15)] transition-all duration-200 border border-gray-100/50 overflow-hidden">
<button <div class="p-6">
class="delete-button absolute top-0 right-0 p-2 text-gray-400/50 hover:text-red-500 hover:bg-red-50 rounded-full transition-all duration-200" <div class="flex justify-between items-center mb-1">
onclick={(e) => { <span>
e.stopPropagation(); <h2 class="text-lg font-semibold text-gray-800 line-clamp-1">
onDelete(media);
}}
aria-label="删除"
>
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
<div class="flex justify-between items-start" role="presentation" onclick={() => onEdit(media)}>
<div class="space-y-2 flex-1">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900 truncate max-w-[70%]">
{media.title} {media.title}
{#if media.platform} {#if media.platform}
<span class="px-2 py-1 bg-gray-100 rounded text-sm text-gray-700"> <span class="px-2 py-1 text-gray-600 text-sm bg-gray-50/80 rounded-full">{`(${media.platform})`}</span>
平台:{media.platform}
</span>
{/if}
</h3>
<div class="flex flex-wrap gap-2 ml-auto mr-2">
{#if media.rating}
<div class="px-2 py-1 bg-gray-100 rounded text-sm text-gray-700 flex items-center gap-1">
<span>评分:</span>
<div class="flex items-center">
{@html StarRating(media.rating / 2)}
</div>
<span class="ml-1">({media.rating}/10)</span>
</div>
{/if} {/if}
</div> </h2>
<div class="text-sm text-gray-500"> </span>
{media.date ? new Date(media.date).toLocaleDateString() : '未设置日期'} <div class="flex items-center gap-2">
</div> <button
onclick={() => onEdit(media)}
class="text-gray-400 hover:text-indigo-400 transition-colors duration-200"
aria-label="编辑"
>
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
<button
onclick={() => onDelete(media)}
class="text-gray-400 hover:text-rose-400 transition-colors duration-200"
aria-label="删除"
>
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div> </div>
{#if media.notes}
<div class="mt-2 text-sm text-gray-600 rounded">
<div class="whitespace-pre-wrap text-left line-clamp-4">{media.notes}</div>
</div>
{/if}
</div> </div>
<div class="space-y-2 text-sm text-gray-600 flex justify-between">
<p class="flex items-center gap-2">
<span class="text-gray-500">评分:</span>
<span class="flex items-center gap-1">{@html StarRating(media.rating ?? 0)}</span>
</p>
<p class="flex items-center gap-2">
<span class="text-gray-500">日期:</span>
<span class="text-gray-700">{media.date}</span>
</p>
</div>
{#if media.notes}
<div class="mt-1 pt-1 border-t border-gray-100/50">
<p class="text-sm text-gray-600 line-clamp-3">{media.notes}</p>
</div>
{/if}
</div> </div>
</div> </div>

View File

@@ -18,9 +18,9 @@
<div class="flex items-center gap-1" onmouseleave={handleMouseLeave} role="presentation"> <div class="flex items-center gap-1" onmouseleave={handleMouseLeave} role="presentation">
{#each Array(maxStars) as _, i} {#each Array(maxStars) as _, i}
<div <div
class="text-l focus:outline-none transition-colors duration-200 cursor-pointer" class="text-xl focus:outline-none transition-all duration-200 cursor-pointer hover:scale-110"
class:text-yellow-400={i <(hoverValue || value)} class:text-yellow-400={i < (hoverValue || value)}
class:text-gray-300={i >= (hoverValue || value)} class:text-gray-200={i >= (hoverValue || value)}
onclick={(e) => { onclick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@@ -33,6 +33,6 @@
</div> </div>
{/each} {/each}
{#if value > 0} {#if value > 0}
<span class="ml-2 text-sm text-gray-600">{value}</span> <span class="ml-2 text-sm font-medium bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">{value}</span>
{/if} {/if}
</div> </div>

View File

@@ -1,7 +1,7 @@
/* /*
* @Date: 2025-05-26 17:39:36 * @Date: 2025-05-26 17:39:36
* @LastEditors: 陈子健 * @LastEditors: 陈子健
* @LastEditTime: 2025-05-26 17:39:47 * @LastEditTime: 2025-05-29 11:03:14
* @FilePath: /my-score/frontend/src/lib/utils.ts * @FilePath: /my-score/frontend/src/lib/utils.ts
*/ */
export const StarRating = (rating: number, maxStars: number = 5) => { export const StarRating = (rating: number, maxStars: number = 5) => {
@@ -12,6 +12,7 @@ export const StarRating = (rating: number, maxStars: number = 5) => {
const isHalfStar = i - 0.5 <= rating && rating < i; const isHalfStar = i - 0.5 <= rating && rating < i;
if (isHalfStar) { if (isHalfStar) {
console.log('isHalfStar', isHalfStar);
stars.push(` stars.push(`
<div class="relative w-4 h-4"> <div class="relative w-4 h-4">
<svg class="absolute w-4 h-4" viewBox="0 0 24 24" fill="white" stroke="currentColor"> <svg class="absolute w-4 h-4" viewBox="0 0 24 24" fill="white" stroke="currentColor">