ui: mobile-first records card list, responsive filters/paddings across pages, grades table scroll guard
This commit is contained in:
@@ -91,7 +91,7 @@ export function DashboardPage() {
|
||||
const metricCards = (s?.metrics ?? []).slice(0, 3);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[1080px] px-6 py-5">
|
||||
<div className="mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<MetricCard label="总记录" value={s?.total ?? 0} />
|
||||
|
||||
@@ -74,7 +74,7 @@ export function GradesPage() {
|
||||
const semesters = [...(s?.semesters ?? [])].sort((a, b) => b.term.localeCompare(a.term));
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[1080px] px-6 py-5">
|
||||
<div className="mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5">
|
||||
{/* 总览指标 */}
|
||||
<div className="mb-4 flex flex-wrap gap-3">
|
||||
<MetricCard label="总学分" value={s?.overall.credits ?? 0} />
|
||||
@@ -297,7 +297,7 @@ function SemesterCard({ sem }: { sem: SemesterGrades }) {
|
||||
/>
|
||||
<CardBody className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse text-[13px]">
|
||||
<div className="-mx-1 overflow-x-auto px-1"><table className="w-full border-collapse text-[13px]">
|
||||
<thead>
|
||||
<tr>
|
||||
{['课程', '学分', '分数', '等级', '绩点', ''].map(h => (
|
||||
@@ -335,7 +335,7 @@ function SemesterCard({ sem }: { sem: SemesterGrades }) {
|
||||
))}
|
||||
<CourseAddRow sem={sem} />
|
||||
</tbody>
|
||||
</table>
|
||||
</table></div>
|
||||
</div>
|
||||
</CardBody>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@/lib/api';
|
||||
import type { FootprintRow } from '@/lib/types';
|
||||
import {
|
||||
Button, Confirm, Dropdown, DropdownItem, Empty, Input, Select, Table, TableWrap, Td, Th, Tr, useToast
|
||||
Badge, Button, Confirm, Dropdown, DropdownItem, Empty, Input, Select, Table, TableWrap, Td, Th, Tr, useToast
|
||||
} from '@/components/ui';
|
||||
import { StatusBadge, LevelBadge, RoleBadge, GradeBadge } from '@/components/common/badges';
|
||||
import { RecordEditorDialog } from '@/components/records/RecordEditorDialog';
|
||||
@@ -85,7 +85,7 @@ export function RecordsPage() {
|
||||
const openDetail = (id: number) => { setDetailId(id); setDetailOpen(true); };
|
||||
|
||||
return (
|
||||
<div className="px-6 py-5">
|
||||
<div className="px-3 py-4 sm:px-6 sm:py-5">
|
||||
<div className="mb-3 flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
className="w-[220px]"
|
||||
@@ -93,15 +93,16 @@ export function RecordsPage() {
|
||||
value={q}
|
||||
onChange={e => setQ(e.target.value)}
|
||||
/>
|
||||
<Select className="w-[130px]" value={typeId} onChange={e => setTypeId(e.target.value)}
|
||||
<div className="grid w-full grid-cols-2 gap-2 sm:flex sm:w-auto sm:items-center sm:gap-2">
|
||||
<Select className="w-full sm:w-[130px]" value={typeId} onChange={e => setTypeId(e.target.value)}
|
||||
options={[{ value: '', label: '全部类型' }, ...(typesQ.data?.items ?? []).map(t => ({ value: String(t.id), label: t.name }))]} />
|
||||
<Select className="w-[110px]" value={year} onChange={e => setYear(e.target.value)}
|
||||
<Select className="w-full sm:w-[110px]" value={year} onChange={e => setYear(e.target.value)}
|
||||
options={[{ value: '', label: '全部年份' }, ...(yearsQ.data?.years ?? []).map(y => ({ value: String(y), label: y + ' 年' }))]} />
|
||||
<Select className="w-[110px]" value={level} onChange={e => setLevel(e.target.value)}
|
||||
<Select className="w-full sm:w-[110px]" value={level} onChange={e => setLevel(e.target.value)}
|
||||
options={[{ value: '', label: '全部级别' }, ...(taxQ.data?.items ?? []).filter(t => t.kind === 'level').map(t => ({ value: t.value, label: t.value }))]} />
|
||||
<Select className="w-[100px]" value={role} onChange={e => setRole(e.target.value)}
|
||||
<Select className="w-full sm:w-[100px]" value={role} onChange={e => setRole(e.target.value)}
|
||||
options={[{ value: '', label: '全部角色' }, ...(taxQ.data?.items ?? []).filter(t => t.kind === 'role').map(t => ({ value: t.value, label: t.value }))]} />
|
||||
<Select className="w-[110px]" value={status} onChange={e => setStatus(e.target.value)}
|
||||
<Select className="w-full sm:w-[110px]" value={status} onChange={e => setStatus(e.target.value)}
|
||||
options={[
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'draft', label: '草稿' },
|
||||
@@ -109,21 +110,24 @@ export function RecordsPage() {
|
||||
{ value: 'submitted', label: '已提交待果' },
|
||||
{ value: 'finished', label: '已完结' }
|
||||
]} />
|
||||
<Select className="w-[130px]" value={sort} onChange={e => setSort(e.target.value)}
|
||||
<Select className="w-full sm:w-[130px]" value={sort} onChange={e => setSort(e.target.value)}
|
||||
options={[
|
||||
{ value: 'date_desc', label: '时间从新到旧' },
|
||||
{ value: 'date_asc', label: '时间从旧到新' },
|
||||
{ value: 'updated_desc', label: '最近修改' },
|
||||
{ value: 'weight_desc', label: '重要度优先' }
|
||||
]} />
|
||||
<span className="num ml-auto text-[12px] text-ink-3">共 {total} 条</span>
|
||||
<Button variant="primary" size="md" onClick={openNew}>
|
||||
<Plus size={14} />
|
||||
新建
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between gap-2 sm:contents">
|
||||
<span className="num text-[12px] text-ink-3 sm:ml-auto">共 {total} 条</span>
|
||||
<Button variant="primary" size="md" onClick={openNew}>
|
||||
<Plus size={14} />
|
||||
新建
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-line bg-surface shadow-card">
|
||||
<div className="hidden rounded-lg border border-line bg-surface shadow-card md:block">
|
||||
<TableWrap className="max-h-[calc(100vh-260px)] overflow-y-auto">
|
||||
<Table>
|
||||
<thead>
|
||||
@@ -194,8 +198,35 @@ export function RecordsPage() {
|
||||
)}
|
||||
</TableWrap>
|
||||
|
||||
{total > 0 && (
|
||||
<div className="flex h-11 items-center justify-end gap-3 border-t border-line px-4 text-[12px] text-ink-3">
|
||||
{/* 手机端:卡片列表(表格在窄屏不可用) */}
|
||||
<div className="divide-y divide-line rounded-lg border border-line bg-surface md:hidden">
|
||||
{(listQ.data?.items ?? []).map(row => (
|
||||
<button key={row.id} onClick={() => openDetail(row.id)} className="block w-full px-3 py-3 text-left transition-colors active:bg-surface-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-2 shrink-0 rounded-full" style={{ backgroundColor: row.type_color }} />
|
||||
<span className="min-w-0 flex-1 truncate text-[13.5px] font-medium text-ink-1">{row.title}</span>
|
||||
{row.attachment_count > 0 && <span className="num shrink-0 text-[11px] text-ink-3">{row.attachment_count} 附件</span>}
|
||||
</div>
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-1.5">
|
||||
<Badge color={row.type_color}>{row.type_name}</Badge>
|
||||
<StatusBadge status={row.status} />
|
||||
{row.level && <LevelBadge level={row.level} />}
|
||||
{row.award_grade && <GradeBadge grade={row.award_grade} />}
|
||||
</div>
|
||||
<div className="num mt-1 text-[11.5px] text-ink-3">{fmtRange(row.start_date, row.end_date)}</div>
|
||||
</button>
|
||||
))}
|
||||
{!listQ.isLoading && !listQ.data?.items.length && (
|
||||
<Empty
|
||||
title="没有匹配的记录"
|
||||
description="调整筛选条件,或新建一条记录。"
|
||||
action={<Button variant="primary" onClick={openNew}><Plus size={14} /> 新建记录</Button>}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{total > 0 && (
|
||||
<div className="flex h-11 items-center justify-end gap-3 border-t border-line bg-surface px-4 text-[12px] text-ink-3">
|
||||
<span className="num">第 {page} / {totalPages} 页</span>
|
||||
<Button variant="secondary" size="sm" disabled={page <= 1} onClick={() => setPage(p => p - 1)}>上一页</Button>
|
||||
<Button variant="secondary" size="sm" disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>下一页</Button>
|
||||
|
||||
@@ -8,7 +8,7 @@ const PALETTE = ['#3056d3', '#7c3aed', '#0891b2', '#16a34a', '#d97706', '#dc2626
|
||||
|
||||
export function TaxonomyPage() {
|
||||
return (
|
||||
<div className="mx-auto max-w-[1080px] px-6 py-5">
|
||||
<div className="mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5">
|
||||
<p className="mb-4 text-[12px] text-ink-3">
|
||||
字典驱动录入表单中的下拉选项;维度是自定义的成长分类轴,一条记录可挂多个维度。
|
||||
</p>
|
||||
|
||||
@@ -44,7 +44,7 @@ export function TimelinePage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[880px] px-6 py-5">
|
||||
<div className="mx-auto max-w-[880px] px-3 py-4 sm:px-6 sm:py-5">
|
||||
<div className="mb-4 flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
className="w-[220px]"
|
||||
@@ -90,7 +90,7 @@ export function TimelinePage() {
|
||||
<div className="absolute bottom-0 left-[7px] top-0 w-px bg-line" aria-hidden />
|
||||
{tlQ.data.groups.map(group => (
|
||||
<section key={group.year} className="mb-2">
|
||||
<div className="sticky top-0 z-10 -mx-6 flex items-center gap-2 bg-bg/90 px-6 py-2 backdrop-blur">
|
||||
<div className="sticky top-0 z-10 -mx-3 flex items-center gap-2 bg-bg/90 px-3 py-2 backdrop-blur sm:-mx-6 sm:px-6">
|
||||
<span className="num text-[16px] font-semibold text-ink-1">{group.year}</span>
|
||||
<span className="text-[12px] text-ink-3">{group.count} 条记录</span>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@ export function TypesPage() {
|
||||
const items = typesQ.data?.items ?? [];
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[1080px] px-6 py-5">
|
||||
<div className="mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-[12px] text-ink-3">
|
||||
每个类型可配置专属字段模板,录入表单、详情页与检索列随之联动。内置类型不可删除,可改名或停用。
|
||||
|
||||
Reference in New Issue
Block a user