feat: add project icon picker component and integrate with project management
- Implemented ProjectIconPicker component for selecting project icons. - Created ProjectIcon component to display selected icons. - Updated calendar page to remove unused PageHeading component. - Modified growth page to enhance UI and improve text clarity. - Refactored index page to include project icons and improve layout. - Added mine page for user profile management with avatar selection. - Introduced projects-sort page for managing project order with drag-and-drop functionality. - Enhanced projects page to allow icon selection when adding or editing projects. - Updated record page to streamline user experience and improve form actions. - Adjusted practice service to support project icon management. - Added tests for project saving functionality to ensure correct handling of icons. - Updated global type definitions to support SVG and PNG imports.
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>芭蕾岛 · 项目图标库</title>
|
||||
<link rel="icon" type="image/svg+xml" href="pointe-shoes.svg">
|
||||
<style>
|
||||
:root { color-scheme: light; --ink: #3e3435; --muted: #827572; --accent: #9c5268; --surface: #fffdfa; --line: #e9dfd9; --sample: #f4ece8; }
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
body { margin: 0; background: #faf7f3; color: var(--ink); font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; line-height: 1.55; }
|
||||
button { font: inherit; cursor: pointer; }
|
||||
button:focus-visible, a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
||||
main { width: min(1080px, calc(100% - 64px)); margin: 0 auto; padding: 32px 0 24px; }
|
||||
.topline, .header, .preview-strip, .controls, .control, .project-row, .sample-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||
.topline { padding-bottom: 17px; border-bottom: 1px solid var(--line); font-size: 12px; color: var(--muted); }
|
||||
.brand { font-size: 14px; font-weight: 600; letter-spacing: 2px; color: var(--ink); }
|
||||
.header { margin: 22px 0; align-items: flex-end; }
|
||||
h1 { margin: 0 0 5px; font-size: 29px; font-weight: 600; letter-spacing: -.7px; }
|
||||
p { margin: 0; }
|
||||
.muted { font-size: 13px; color: var(--muted); }
|
||||
.palette { display: flex; gap: 6px; padding-bottom: 6px; }
|
||||
.palette span { width: 20px; height: 20px; border-radius: 50%; border: 1px solid #00000006; }
|
||||
.preview-strip { padding: 17px; border: 1px solid var(--line); border-radius: 17px; background: var(--surface); }
|
||||
.project-preview { width: 390px; max-width: 100%; }
|
||||
.preview-label { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 11px; color: var(--muted); }
|
||||
.project-row { justify-content: flex-start; gap: 12px; min-height: 52px; padding: 5px 16px; border: 1px solid #e7ded8; border-radius: 14px; background: var(--sample); }
|
||||
.project-row img { width: var(--list-icon-size, 32px); height: var(--list-icon-size, 32px); flex-shrink: 0; }
|
||||
.project-name { min-width: 0; overflow-wrap: anywhere; font-size: 15px; font-weight: 500; }
|
||||
.preview-options { display: flex; flex-direction: column; align-items: flex-end; gap: 12px; }
|
||||
.control { justify-content: flex-start; gap: 12px; font-size: 12px; color: var(--muted); }
|
||||
.swatches, .sizes, .filters { display: flex; gap: 6px; }
|
||||
.filters { flex-wrap: wrap; }
|
||||
.swatch { width: 23px; height: 23px; border-radius: 50%; border: 1px solid #dacdc6; padding: 0; }
|
||||
.swatch[aria-pressed="true"] { outline: 1.5px solid var(--accent); outline-offset: 3px; }
|
||||
.size, .filter, .clear { border: 1px solid var(--line); border-radius: 7px; color: var(--muted); background: transparent; padding: 3px 10px; font-size: 12px; white-space: nowrap; }
|
||||
.size[aria-pressed="true"], .filter[aria-pressed="true"] { background: var(--surface); border-color: var(--accent); color: var(--accent); }
|
||||
.clear { padding: 0; border: 0; color: var(--accent); }
|
||||
.controls { padding: 23px 1px 16px; flex-wrap: wrap; }
|
||||
.filter { padding: 5px 12px; }
|
||||
.samples { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
|
||||
.sample { overflow: hidden; border: 1px solid var(--line); border-radius: 17px; background: var(--surface); }
|
||||
.sample:has(.sample-stage[aria-pressed="true"]) { border-color: var(--accent); }
|
||||
.sample-stage { position: relative; display: grid; place-items: center; width: 100%; height: 144px; padding: 0; border: 0; background: var(--sample); color: var(--accent); }
|
||||
.sample-stage[aria-pressed="true"]:after { content: '✓'; position: absolute; top: 12px; right: 16px; font-size: 13px; }
|
||||
.sample-stage img { width: 112px; height: 112px; }
|
||||
.number { position: absolute; top: 13px; left: 15px; color: #9b8985; font-size: 10px; font-variant-numeric: tabular-nums; }
|
||||
.sample-body { padding: 12px 15px; }
|
||||
h2 { margin: 0; font-size: 14px; font-weight: 600; }
|
||||
.english { color: var(--muted); font-size: 11px; margin-top: 2px; min-height: 18px; }
|
||||
.sample-footer { margin-top: 10px; padding-top: 9px; border-top: 1px solid #f0e9e3; gap: 8px; }
|
||||
.scale { display: flex; align-items: flex-end; gap: 10px; }
|
||||
.scale span { display: flex; flex-direction: column; align-items: center; gap: 3px; }
|
||||
.scale small { color: #998b83; font-size: 9px; }
|
||||
.download { color: var(--accent); font-size: 11px; text-decoration: none; padding: 8px 0; }
|
||||
.download:hover { text-decoration: underline; }
|
||||
footer { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-top: 23px; padding-top: 16px; border-top: 1px solid var(--line); color: #998b83; font-size: 11px; }
|
||||
footer a { color: var(--muted); text-decoration: none; }
|
||||
@media (max-width: 850px) { .samples { grid-template-columns: repeat(3, minmax(0, 1fr)); } .scale { gap: 6px; } }
|
||||
@media (max-width: 600px) {
|
||||
main { width: calc(100% - 32px); padding-top: 24px; }
|
||||
h1 { font-size: 25px; }
|
||||
.samples { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
||||
.sample-stage { height: 132px; }
|
||||
.sample-stage img { width: 100px; height: 100px; }
|
||||
.sample-body { padding: 10px 12px; }
|
||||
.sample-footer { gap: 4px; }
|
||||
.scale span:last-child { display: none; }
|
||||
.scale { gap: 8px; }
|
||||
.download { font-size: 10px; }
|
||||
.preview-strip { flex-direction: column; align-items: stretch; padding: 13px; }
|
||||
.preview-options { flex-direction: column; align-items: stretch; gap: 10px; }
|
||||
.preview-options .control { justify-content: space-between; }
|
||||
.control { gap: 7px; }
|
||||
.size { padding: 3px 7px; }
|
||||
.controls { gap: 18px; padding-top: 20px; }
|
||||
.filter { padding: 4px 8px; }
|
||||
.palette span { width: 15px; height: 15px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="topline"><span class="brand">芭蕾岛</span><span>18 枚项目图标</span></div>
|
||||
<header class="header">
|
||||
<div><h1>项目图标库</h1><p class="muted">柔和线描 · 浅色填充</p></div>
|
||||
<div class="palette" aria-label="灰玫瑰、浅玫瑰、暖白配色"><span style="background:#9c5268"></span><span style="background:#ead1d7"></span><span style="background:#fff4ed"></span></div>
|
||||
</header>
|
||||
<section class="preview-strip" aria-label="项目图标预览">
|
||||
<div class="project-preview">
|
||||
<p class="preview-label"><span>项目列表预览</span><span>52 px 行高</span></p>
|
||||
<div class="project-row"><img id="chosen-project-icon" src="arabesque.svg" alt="阿拉贝斯克"><span class="project-name">周末基础课</span></div>
|
||||
</div>
|
||||
<div class="preview-options">
|
||||
<div class="control"><span>图标尺寸</span><div class="sizes" role="group" aria-label="列表图标大小"><button type="button" class="size" data-size="24" aria-pressed="false">24 px</button><button type="button" class="size" data-size="32" aria-pressed="true">32 px</button><button type="button" class="size" data-size="40" aria-pressed="false">40 px</button></div></div>
|
||||
<div class="control"><span id="chosen-label" aria-live="polite">阿拉贝斯克</span><button type="button" class="clear" id="clear-icon">无图标</button></div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="controls">
|
||||
<div class="filters" role="group" aria-label="图标分类"><button type="button" class="filter" data-filter="all" aria-pressed="true">全部 18</button><button type="button" class="filter" data-filter="motion" aria-pressed="false">专业动作 8</button><button type="button" class="filter" data-filter="training" aria-pressed="false">基础练习 6</button><button type="button" class="filter" data-filter="object" aria-pressed="false">物件 4</button></div>
|
||||
<div class="control"><span>预览底色</span><div class="swatches" role="group" aria-label="预览底色"><button type="button" class="swatch" data-background="#f4ece8" style="background:#f4ece8" aria-label="浅玫瑰" aria-pressed="true"></button><button type="button" class="swatch" data-background="#fffdfa" style="background:#fffdfa" aria-label="暖白" aria-pressed="false"></button><button type="button" class="swatch" data-background="#eef0e7" style="background:#eef0e7" aria-label="灰绿" aria-pressed="false"></button></div></div>
|
||||
</div>
|
||||
<section class="samples" aria-label="项目图标库,点击图案试选"></section>
|
||||
<footer><span>透明背景 · 可编辑 SVG · 点击图案试选</span><span>预览稿,尚未接入小程序</span><span>动作参考:<a href="https://australianballet.com.au/ballet-101/ballet-vocabulary" target="_blank" rel="noreferrer">澳大利亚芭蕾舞团</a> · <a href="https://www.atlantaballet.com/resources/ballet-terms-and-positions" target="_blank" rel="noreferrer">亚特兰大芭蕾舞团</a></span></footer>
|
||||
</main>
|
||||
<script>
|
||||
const icons = [
|
||||
{ id: 'plie', name: '蹲', term: 'Plié · 一位半蹲', group: 'motion' },
|
||||
{ id: 'tendu', name: '擦地', term: 'Tendu', group: 'motion' },
|
||||
{ id: 'releve', name: '立半脚尖', term: 'Relevé', group: 'motion' },
|
||||
{ id: 'port-de-bras', name: '手臂练习', term: 'Port de bras', group: 'motion' },
|
||||
{ id: 'arabesque', name: '阿拉贝斯克', term: 'Arabesque', group: 'motion' },
|
||||
{ id: 'attitude', name: '阿蒂蒂德', term: 'Attitude derrière', group: 'motion' },
|
||||
{ id: 'pirouette', name: '旋转', term: 'Pirouette', group: 'motion' },
|
||||
{ id: 'grand-jete', name: '大跳', term: 'Grand jeté', group: 'motion' },
|
||||
{ id: 'foot-hip', name: '足髋训练', term: 'Foot & hip', group: 'training' },
|
||||
{ id: 'flexibility', name: '软开素质', term: 'Flexibility', group: 'training' },
|
||||
{ id: 'ball-core', name: '小球核心', term: 'Small ball core', group: 'training' },
|
||||
{ id: 'swan-arms', name: '天鹅臂颈', term: 'Swan arms & neck', group: 'training' },
|
||||
{ id: 'breathing', name: '呼吸训练', term: 'Breathing', group: 'training' },
|
||||
{ id: 'core-glutes', name: '核心臀腿', term: 'Core, glutes & legs', group: 'training' },
|
||||
{ id: 'soft-shoes', name: '软底舞鞋', term: 'Ballet slippers', group: 'object' },
|
||||
{ id: 'pointe-shoes', name: '足尖鞋', term: 'Pointe shoes', group: 'object' },
|
||||
{ id: 'barre', name: '把杆', term: 'Barre', group: 'object' },
|
||||
{ id: 'piano', name: '钢琴', term: 'Piano', group: 'object' },
|
||||
];
|
||||
const gallery = document.querySelector('.samples');
|
||||
icons.forEach((icon, index) => {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'sample';
|
||||
card.dataset.group = icon.group;
|
||||
const stage = document.createElement('button');
|
||||
stage.type = 'button';
|
||||
stage.className = 'sample-stage';
|
||||
stage.dataset.icon = icon.id;
|
||||
stage.setAttribute('aria-label', '选择' + icon.name);
|
||||
stage.setAttribute('aria-pressed', String(icon.id === 'arabesque'));
|
||||
const number = document.createElement('span');
|
||||
number.className = 'number';
|
||||
number.textContent = String(index + 1).padStart(2, '0');
|
||||
const image = document.createElement('img');
|
||||
image.src = icon.id + '.svg';
|
||||
image.alt = icon.name;
|
||||
stage.append(number, image);
|
||||
const body = document.createElement('div');
|
||||
body.className = 'sample-body';
|
||||
const title = document.createElement('h2');
|
||||
title.textContent = icon.name;
|
||||
const term = document.createElement('p');
|
||||
term.className = 'english';
|
||||
term.textContent = icon.term;
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'sample-footer';
|
||||
const scale = document.createElement('div');
|
||||
scale.className = 'scale';
|
||||
[24, 32, 48].forEach(size => {
|
||||
const item = document.createElement('span');
|
||||
const preview = document.createElement('img');
|
||||
preview.src = icon.id + '.svg';
|
||||
preview.width = size;
|
||||
preview.height = size;
|
||||
preview.alt = icon.name + ' ' + size + ' 像素';
|
||||
const label = document.createElement('small');
|
||||
label.textContent = size;
|
||||
item.append(preview, label);
|
||||
scale.append(item);
|
||||
});
|
||||
const download = document.createElement('a');
|
||||
download.className = 'download';
|
||||
download.href = icon.id + '.svg';
|
||||
download.download = icon.id + '.svg';
|
||||
download.setAttribute('aria-label', '下载' + icon.name + ' SVG');
|
||||
download.textContent = 'SVG ↓';
|
||||
footer.append(scale, download);
|
||||
body.append(title, term, footer);
|
||||
card.append(stage, body);
|
||||
gallery.append(card);
|
||||
stage.addEventListener('click', () => selectIcon(icon));
|
||||
});
|
||||
function selectIcon(icon) {
|
||||
const preview = document.getElementById('chosen-project-icon');
|
||||
preview.hidden = !icon;
|
||||
if (icon) { preview.src = icon.id + '.svg'; preview.alt = icon.name; }
|
||||
document.getElementById('chosen-label').textContent = icon ? icon.name : '无图标';
|
||||
document.querySelectorAll('[data-icon]').forEach(button => button.setAttribute('aria-pressed', String(Boolean(icon) && button.dataset.icon === icon.id)));
|
||||
}
|
||||
document.getElementById('clear-icon').addEventListener('click', () => selectIcon(null));
|
||||
document.querySelectorAll('[data-background]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
document.documentElement.style.setProperty('--sample', button.dataset.background);
|
||||
document.querySelectorAll('[data-background]').forEach(item => item.setAttribute('aria-pressed', String(item === button)));
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('[data-size]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
document.documentElement.style.setProperty('--list-icon-size', button.dataset.size + 'px');
|
||||
document.querySelectorAll('[data-size]').forEach(item => item.setAttribute('aria-pressed', String(item === button)));
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('[data-filter]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
document.querySelectorAll('.sample').forEach(card => { card.hidden = button.dataset.filter !== 'all' && card.dataset.group !== button.dataset.filter; });
|
||||
document.querySelectorAll('[data-filter]').forEach(item => item.setAttribute('aria-pressed', String(item === button)));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user