Merge branch 'main' of https://github.com/qinfendebingshuo/guanghulab
This commit is contained in:
commit
c4146ff4b0
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体",
|
||||
"rules_version": "v1.0",
|
||||
"last_updated": "2026-03-07T08:41:28.888Z",
|
||||
"last_updated": "2026-03-08T03:32:30.449Z",
|
||||
"wake_protocol_version": "v1.0",
|
||||
"wake_triggers": [
|
||||
"我是冰朔",
|
||||
|
|
@ -23,6 +23,12 @@
|
|||
}
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"date": "2026-03-08",
|
||||
"type": "psp_inspection",
|
||||
"description": "铸渊 PSP 巡检通过 · 全部检查项 ✅",
|
||||
"by": "铸渊PSP巡检"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-07",
|
||||
"type": "psp_inspection",
|
||||
|
|
@ -84,4 +90,4 @@
|
|||
"run_id": "22795791187"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"description": "铸渊图书馆目录 · Library Catalog for 铸渊 (Zhùyuān)",
|
||||
"version": "2.0",
|
||||
"generated_at": "2026-03-07T11:38:46.113Z",
|
||||
"generated_at": "2026-03-08T06:28:27.088Z",
|
||||
"generated_by": "scripts/generate-repo-map.js",
|
||||
"repo": "qinfendebingshuo/guanghulab",
|
||||
"stats": {
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
"hli_implemented": 3,
|
||||
"hli_coverage_pct": "18%",
|
||||
"last_ci_run": "2026-03-05T10:30:46.894Z",
|
||||
"memory_last_updated": "2026-03-07T08:41:28.888Z"
|
||||
"memory_last_updated": "2026-03-08T03:32:30.449Z"
|
||||
},
|
||||
"zones": [
|
||||
{
|
||||
|
|
@ -431,7 +431,7 @@
|
|||
{
|
||||
"module_id": "M18",
|
||||
"dir": "m18-health-check",
|
||||
"files": 1,
|
||||
"files": 3,
|
||||
"has_readme": true
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# 铸渊图书馆快照 · Repo Snapshot
|
||||
> 生成于 2026-03-07 19:38 CST · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件
|
||||
> 生成于 2026-03-08 14:28 CST · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
| 脚本 | 18 个执行脚本 |
|
||||
| 开发者节点 | 8 人 |
|
||||
| HLI 接口覆盖率 | 3/17 (18%) |
|
||||
| 快照生成时间 | 2026-03-07 19:38 CST |
|
||||
| 快照生成时间 | 2026-03-08 14:28 CST |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -189,7 +189,7 @@
|
|||
- `m11-module/` — 4 个文件 (有README)
|
||||
- `m12-kanban/` — 1 个文件 (有README)
|
||||
- `m15-cloud-drive/` — 3 个文件
|
||||
- `m18-health-check/` — 1 个文件 (有README)
|
||||
- `m18-health-check/` — 3 个文件 (有README)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -44,3 +44,10 @@
|
|||
- 累计自检次数:5
|
||||
- 累计CI运行:1次
|
||||
- HLI覆盖率:17.6%
|
||||
|
||||
## 2026-03-08 · 每日自检
|
||||
- 大脑文件完整性:⚠️ 缺失 identity.md, routing-map.json, responsibility.md, decision-log.md
|
||||
- 知识库条目:4条
|
||||
- 累计自检次数:6
|
||||
- 累计CI运行:1次
|
||||
- HLI覆盖率:17.6%
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"last_updated": "2026-03-07",
|
||||
"last_updated": "2026-03-08",
|
||||
"faq": [
|
||||
{
|
||||
"category": "SSH/服务器",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体",
|
||||
"rules_version": "v1.0",
|
||||
"last_updated": "2026-03-07T04:46:02.610Z",
|
||||
"last_updated": "2026-03-08T00:58:17.396Z",
|
||||
"wake_protocol_version": "v1.0",
|
||||
"wake_triggers": [
|
||||
"我是冰朔",
|
||||
|
|
@ -12,9 +12,15 @@
|
|||
],
|
||||
"founder": "冰朔(Bīng Shuò)",
|
||||
"hli_coverage": "17.6%",
|
||||
"total_selfchecks": 5,
|
||||
"total_selfchecks": 6,
|
||||
"total_ci_runs": 1,
|
||||
"recent_events": [
|
||||
{
|
||||
"date": "2026-03-08",
|
||||
"type": "daily_selfcheck",
|
||||
"description": "每日自检完成 · 知识库4条 · 缺失文件4个",
|
||||
"by": "铸渊自检"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-07",
|
||||
"type": "daily_selfcheck",
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,189 @@
|
|||
// HoloLake 数据统计面板 · 交互脚本 v2(含导出 + 环形图)
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
// 条形图动画
|
||||
setTimeout(function() {
|
||||
var bars = document.querySelectorAll('.bar-fill');
|
||||
bars.forEach(function(bar) {
|
||||
var percent = bar.getAttribute('data-percent');
|
||||
bar.style.width = percent + '%';
|
||||
});
|
||||
}, 300);
|
||||
|
||||
// 柱状图动画
|
||||
setTimeout(function() {
|
||||
var trendBars = document.querySelectorAll('.trend-bar');
|
||||
trendBars.forEach(function(bar) {
|
||||
var height = bar.getAttribute('data-height');
|
||||
var fill = bar.querySelector('.trend-fill');
|
||||
fill.style.height = height + '%';
|
||||
});
|
||||
}, 600);
|
||||
|
||||
// 环形图绘制函数
|
||||
function drawDonut(percentages) {
|
||||
var segments = document.querySelectorAll('.donut-segment');
|
||||
var total = 377; // 2 * Math.PI * 60 ≈ 377
|
||||
var cumulative = 0;
|
||||
segments.forEach(function(seg, i) {
|
||||
var value = percentages[i] || 0;
|
||||
var dashArray = (value / 100 * total) + ' ' + total;
|
||||
seg.setAttribute('stroke-dasharray', dashArray);
|
||||
seg.setAttribute('stroke-dashoffset', 0);
|
||||
});
|
||||
}
|
||||
|
||||
// 当前选中的时间范围
|
||||
var currentRange = 'today';
|
||||
|
||||
// 模拟数据
|
||||
var mockData = {
|
||||
today: {
|
||||
users: '128', chats: '1,024', personas: '6', api: '3,892',
|
||||
bars: [72,15,8,3,2],
|
||||
trends: [45,62,38,78,55,90,85],
|
||||
donut: [72,15,8,3,2],
|
||||
apiTotal: '3,892'
|
||||
},
|
||||
week: {
|
||||
users: '128', chats: '6,847', personas: '6', api: '24,103',
|
||||
bars: [68,18,9,3,2],
|
||||
trends: [320,415,380,450,290,520,480],
|
||||
donut: [68,18,9,3,2],
|
||||
apiTotal: '24,103'
|
||||
},
|
||||
month: {
|
||||
users: '128', chats: '28,392', personas: '6', api: '98,741',
|
||||
bars: [65,20,10,3,2],
|
||||
trends: [1200,1450,1100,1680,1320,1890,1750],
|
||||
donut: [65,20,10,3,2],
|
||||
apiTotal: '98,741'
|
||||
}
|
||||
};
|
||||
|
||||
// 更新所有图表和数字
|
||||
function updateStats(range) {
|
||||
currentRange = range;
|
||||
var data = mockData[range];
|
||||
|
||||
// 更新统计卡片
|
||||
var values = document.querySelectorAll('.stat-value');
|
||||
values[0].textContent = data.users;
|
||||
values[1].textContent = data.chats;
|
||||
values[2].textContent = data.personas;
|
||||
values[3].textContent = data.api;
|
||||
|
||||
// 更新条形图
|
||||
var bars = document.querySelectorAll('.bar-fill');
|
||||
bars.forEach(function(bar, i) {
|
||||
bar.style.width = '0%';
|
||||
setTimeout(function() {
|
||||
bar.style.width = data.bars[i] + '%';
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// 更新百分比文字
|
||||
var barValues = document.querySelectorAll('.bar-value');
|
||||
data.bars.forEach(function(v, i) {
|
||||
barValues[i].textContent = v + '%';
|
||||
});
|
||||
|
||||
// 更新柱状图
|
||||
var maxTrend = Math.max.apply(null, data.trends);
|
||||
var trendBars = document.querySelectorAll('.trend-bar');
|
||||
trendBars.forEach(function(bar, i) {
|
||||
var fill = bar.querySelector('.trend-fill');
|
||||
fill.style.height = '0%';
|
||||
setTimeout(function() {
|
||||
fill.style.height = Math.round(data.trends[i] / maxTrend * 90) + '%';
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// 更新环形图
|
||||
drawDonut(data.donut);
|
||||
document.querySelector('.donut-center-value').textContent = data.apiTotal;
|
||||
|
||||
// 更新图例百分比
|
||||
var legendPercents = document.querySelectorAll('.legend-percent');
|
||||
data.donut.forEach(function(v, i) {
|
||||
legendPercents[i].textContent = v + '%';
|
||||
});
|
||||
}
|
||||
|
||||
// 时间选择器切换
|
||||
var timeButtons = document.querySelectorAll('.time-btn');
|
||||
timeButtons.forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
timeButtons.forEach(function(b) { b.classList.remove('active'); });
|
||||
btn.classList.add('active');
|
||||
var range = btn.getAttribute('data-range');
|
||||
updateStats(range);
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化环形图(今日数据)
|
||||
drawDonut(mockData.today.donut);
|
||||
|
||||
// 卡片点击高亮
|
||||
var cards = document.querySelectorAll('.stat-card');
|
||||
cards.forEach(function(card) {
|
||||
card.addEventListener('click', function() {
|
||||
cards.forEach(function(c) { c.style.borderColor = 'rgba(255,255,255,0.06)'; });
|
||||
card.style.borderColor = 'rgba(79, 195, 247, 0.5)';
|
||||
});
|
||||
});
|
||||
|
||||
// CSV 导出功能
|
||||
var exportBtn = document.getElementById('exportBtn');
|
||||
var toast = document.getElementById('exportToast');
|
||||
|
||||
exportBtn.addEventListener('click', function() {
|
||||
var data = mockData[currentRange];
|
||||
var models = ['DeepSeek', '通义千问', 'Kimi', '豆包', 'GPT/Claude'];
|
||||
var days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
|
||||
var csv = '\uFEFF'; // BOM for Chinese support
|
||||
csv += 'HoloLake 数据统计报表\n';
|
||||
csv += '时间范围,' + currentRange + '\n';
|
||||
csv += '导出时间,' + new Date().toLocaleString('zh-CN') + '\n\n';
|
||||
|
||||
csv += '【统计概览】\n';
|
||||
csv += '指标,数值,变化\n';
|
||||
csv += '总用户,' + data.users + ',↑12%\n';
|
||||
csv += '今日对话,' + data.chats + ',↑8%\n';
|
||||
csv += '活跃人格体,' + data.personas + ',—\n';
|
||||
csv += 'API调用,' + data.api + ',↓3%\n\n';
|
||||
|
||||
csv += '【模型调用分布】\n';
|
||||
csv += '模型,占比\n';
|
||||
models.forEach(function(m, i) {
|
||||
csv += m + ',' + data.donut[i] + '%\n';
|
||||
});
|
||||
csv += '\n';
|
||||
|
||||
csv += '【每日对话趋势】\n';
|
||||
csv += '日期,对话次数\n';
|
||||
days.forEach(function(d, i) {
|
||||
csv += d + ',' + data.trends[i] + '\n';
|
||||
});
|
||||
|
||||
// 创建下载链接
|
||||
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
var link = document.createElement('a');
|
||||
var url = URL.createObjectURL(blob);
|
||||
link.href = url;
|
||||
link.setAttribute('download', 'hololake_stats_' + currentRange + '.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// 显示提示
|
||||
toast.classList.add('show');
|
||||
setTimeout(function() {
|
||||
toast.classList.remove('show');
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
console.log('HoloLake Analytics v2 · 交互已加载');
|
||||
});
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HoloLake · 数据统计</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<nav class="top-bar">
|
||||
<div class="logo">HoloLake</div>
|
||||
<div class="nav-links">
|
||||
<a href="#">工单</a>
|
||||
<a href="#" class="active">统计</a>
|
||||
<a href="#">设置</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>系统概览</h1>
|
||||
<div class="time-selector">
|
||||
<button class="time-btn active" data-range="today">今日</button>
|
||||
<button class="time-btn" data-range="week">本周</button>
|
||||
<button class="time-btn" data-range="month">本月</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="export-bar">
|
||||
<button class="export-btn" id="exportBtn">导出CSV</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">👥</div>
|
||||
<div class="stat-value">128</div>
|
||||
<div class="stat-label">总用户</div>
|
||||
<div class="stat-change up">↑ 12%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">💬</div>
|
||||
<div class="stat-value">1,024</div>
|
||||
<div class="stat-label">今日对话</div>
|
||||
<div class="stat-change up">↑ 8%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🤖</div>
|
||||
<div class="stat-value">6</div>
|
||||
<div class="stat-label">活跃人格体</div>
|
||||
<div class="stat-change same">— 持平</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">⚡</div>
|
||||
<div class="stat-value">3,892</div>
|
||||
<div class="stat-label">API调用</div>
|
||||
<div class="stat-change down">↓ 3%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section donut-section">
|
||||
<h2>🔀 模型调用占比</h2>
|
||||
<div class="donut-container">
|
||||
<div class="donut-chart">
|
||||
<svg viewBox="0 0 160 160">
|
||||
<circle class="donut-bg" cx="80" cy="80" r="60"></circle>
|
||||
<circle class="donut-segment" id="seg-deepseek" cx="80" cy="80" r="60" stroke="#4fc3f7" stroke-dasharray="0 377" stroke-dashoffset="0"></circle>
|
||||
<circle class="donut-segment" id="seg-qwen" cx="80" cy="80" r="60" stroke="#7c4dff" stroke-dasharray="0 377" stroke-dashoffset="0"></circle>
|
||||
<circle class="donut-segment" id="seg-kimi" cx="80" cy="80" r="60" stroke="#ff7043" stroke-dasharray="0 377" stroke-dashoffset="0"></circle>
|
||||
<circle class="donut-segment" id="seg-doubao" cx="80" cy="80" r="60" stroke="#66bb6a" stroke-dasharray="0 377" stroke-dashoffset="0"></circle>
|
||||
<circle class="donut-segment" id="seg-premium" cx="80" cy="80" r="60" stroke="#ffd54f" stroke-dasharray="0 377" stroke-dashoffset="0"></circle>
|
||||
</svg>
|
||||
<div class="donut-center">
|
||||
<div class="donut-center-value">3,892</div>
|
||||
<div class="donut-center-label">总调用</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="donut-legend">
|
||||
<div class="legend-item"><span class="legend-dot c-deepseek"></span> DeepSeek <span class="legend-percent">72%</span></div>
|
||||
<div class="legend-item"><span class="legend-dot c-qwen"></span> 通义千问 <span class="legend-percent">15%</span></div>
|
||||
<div class="legend-item"><span class="legend-dot c-kimi"></span> Kimi <span class="legend-percent">8%</span></div>
|
||||
<div class="legend-item"><span class="legend-dot c-doubao"></span> 豆包 <span class="legend-percent">3%</span></div>
|
||||
<div class="legend-item"><span class="legend-dot c-premium"></span> GPT/Claude <span class="legend-percent">2%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📊 模型调用分布</h2>
|
||||
<div class="bar-chart">
|
||||
<div class="bar-row">
|
||||
<span class="bar-label">DeepSeek</span>
|
||||
<div class="bar-track"><div class="bar-fill deepseek" style="width: 0%" data-percent="72"></div></div>
|
||||
<span class="bar-value">72%</span>
|
||||
</div>
|
||||
<div class="bar-row">
|
||||
<span class="bar-label">通义千问</span>
|
||||
<div class="bar-track"><div class="bar-fill qwen" style="width: 0%" data-percent="15"></div></div>
|
||||
<span class="bar-value">15%</span>
|
||||
</div>
|
||||
<div class="bar-row">
|
||||
<span class="bar-label">Kimi</span>
|
||||
<div class="bar-track"><div class="bar-fill kimi" style="width: 0%" data-percent="8"></div></div>
|
||||
<span class="bar-value">8%</span>
|
||||
</div>
|
||||
<div class="bar-row">
|
||||
<span class="bar-label">豆包</span>
|
||||
<div class="bar-track"><div class="bar-fill doubao" style="width: 0%" data-percent="3"></div></div>
|
||||
<span class="bar-value">3%</span>
|
||||
</div>
|
||||
<div class="bar-row">
|
||||
<span class="bar-label">GPT/Claude</span>
|
||||
<div class="bar-track"><div class="bar-fill premium" style="width: 0%" data-percent="2"></div></div>
|
||||
<span class="bar-value">2%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>每日对话趋势(近7天)</h2>
|
||||
<div class="trend-chart">
|
||||
<div class="trend-bar" data-height="45"><div class="trend-fill" style="height: 0%;"></div><span>周一</span></div>
|
||||
<div class="trend-bar" data-height="62"><div class="trend-fill" style="height: 0%;"></div><span>周二</span></div>
|
||||
<div class="trend-bar" data-height="38"><div class="trend-fill" style="height: 0%;"></div><span>周三</span></div>
|
||||
<div class="trend-bar" data-height="78"><div class="trend-fill" style="height: 0%;"></div><span>周四</span></div>
|
||||
<div class="trend-bar" data-height="55"><div class="trend-fill" style="height: 0%;"></div><span>周五</span></div>
|
||||
<div class="trend-bar" data-height="90"><div class="trend-fill" style="height: 0%;"></div><span>周六</span></div>
|
||||
<div class="trend-bar" data-height="85"><div class="trend-fill" style="height: 0%;"></div><span>周日</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>最近活动</h2>
|
||||
<div class="activity-list">
|
||||
<div class="activity-item">
|
||||
<div class="activity-dot blue"></div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">用户「星河」与 <strong>知秋</strong> 完成了32轮对话</div>
|
||||
<div class="activity-time">5分钟前</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-dot green"></div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">新用户「雨落」注册并激活了人格体</div>
|
||||
<div class="activity-time">12分钟前</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-dot orange"></div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">DeepSeek API 调用量达到今日峰值(420次/小时)</div>
|
||||
<div class="activity-time">28分钟前</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-dot purple"></div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">人格体「小坍缩核」记忆库自动归档完成</div>
|
||||
<div class="activity-time">1小时前</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-dot blue"></div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">用户「晚风」上传了3个文件到云盘</div>
|
||||
<div class="activity-time">2小时前</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>HoloLake Era · AGE OS v1.0</p>
|
||||
<p>Powered by 光湖团队</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div class="export-toast" id="exportToast">CSV已下载!</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,889 @@
|
|||
/* HoloLake 数据统计面板 · 样式表 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #0a1628 0%, #1a2a4a 50%, #0d1f3c 100%);
|
||||
color: #e0e6ed;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* 顶部导航 */
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: #8899aa;
|
||||
text-decoration: none;
|
||||
margin-left: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nav-links a.active {
|
||||
color: #4fc3f7;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px 0 16px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.time-selector {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.time-btn {
|
||||
background: rgba(255,255,255,0.06);
|
||||
color: #8899aa;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.time-btn.active {
|
||||
background: rgba(79,195,247,0.15);
|
||||
color: #4fc3f7;
|
||||
border-color: rgba(79,195,247,0.4);
|
||||
}
|
||||
|
||||
.time-btn:hover {
|
||||
border-color: rgba(79,195,247,0.3);
|
||||
color: #b0c4d8;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: rgba(255,255,255,0.04);
|
||||
border-radius: 16px;
|
||||
padding: 20px 16px;
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
background: rgba(79,195,247,0.06);
|
||||
border-color: rgba(79,195,247,0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #8899aa;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-change.up { color: #4caf50; }
|
||||
.stat-change.down { color: #ef5350; }
|
||||
.stat-change.same { color: #8899aa; }
|
||||
|
||||
/* 区块通用 */
|
||||
.section {
|
||||
padding-bottom: 28px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 横向条形图 */
|
||||
.bar-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
font-size: 13px;
|
||||
color: #aab;
|
||||
min-width: 70px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bar-track {
|
||||
flex: 1;
|
||||
height: 22px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border-radius: 11px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 11px;
|
||||
transition: width 1.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.bar-fill.deepseek { background: linear-gradient(90deg, #4fc3f7, #29b6f6); }
|
||||
.bar-fill.qwen { background: linear-gradient(90deg, #7c4dff, #651fff); }
|
||||
.bar-fill.kimi { background: linear-gradient(90deg, #ff7043, #f4511e); }
|
||||
.bar-fill.doubao { background: linear-gradient(90deg, #66bb6a, #43a047); }
|
||||
.bar-fill.premium { background: linear-gradient(90deg, #ffd54f, #ffb300); }
|
||||
|
||||
.bar-value {
|
||||
font-size: 13px;
|
||||
color: #8899aa;
|
||||
min-width: 36px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 每日趋势柱状图 */
|
||||
.trend-chart {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
height: 160px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.trend-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.trend-fill {
|
||||
width: 28px;
|
||||
border-radius: 6px 6px 2px 2px;
|
||||
background: linear-gradient(180deg, #4fc3f7, rgba(79,195,247,0.3));
|
||||
transition: height 1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.trend-bar span {
|
||||
font-size: 11px;
|
||||
color: #667;
|
||||
}
|
||||
|
||||
/* 最近活动 */
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activity-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-top: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-dot.blue { background: #4fc3f7; }
|
||||
.activity-dot.green { background: #66bb6a; }
|
||||
.activity-dot.orange { background: #ffb74d; }
|
||||
.activity-dot.purple { background: #ab47bc; }
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.activity-text strong {
|
||||
color: #4fc3f7;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 12px;
|
||||
color: #556;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 底部 */
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 32px 0;
|
||||
color: #556677;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 480px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 26px;
|
||||
}
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.trend-fill {
|
||||
width: 22px;
|
||||
}
|
||||
.bar-label {
|
||||
min-width: 56px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
/* ========== 响应式布局增强 ========== */
|
||||
/* 平板(768px以下) */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.page-header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
}
|
||||
.bar-label {
|
||||
min-width: 60px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.trend-fill {
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
/* 手机(480px以下) */
|
||||
📡 BC-M08-002 · DEV-010桔子 · 数据统计面板·环节2~3·响应式布局+数据导出+高级图表 2
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 0 12px;
|
||||
}
|
||||
.top-bar {
|
||||
padding: 12px 0;
|
||||
}
|
||||
.logo {
|
||||
font-size: 17px;
|
||||
}
|
||||
.nav-links a {
|
||||
font-size: 13px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
.page-header h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.time-btn {
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
.stat-card {
|
||||
padding: 14px 12px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.stat-icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
📡 BC-M08-002 · DEV-010桔子 · 数据统计面板·环节2~3·响应式布局+数据导出+高级图表 3
|
||||
}
|
||||
.stat-change {
|
||||
font-size: 11px;
|
||||
}
|
||||
.section h2 {
|
||||
font-size: 15px;
|
||||
}
|
||||
.bar-row {
|
||||
gap: 8px;
|
||||
}
|
||||
.bar-label {
|
||||
min-width: 50px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.bar-track {
|
||||
height: 18px;
|
||||
}
|
||||
.bar-value {
|
||||
font-size: 11px;
|
||||
min-width: 30px;
|
||||
}
|
||||
.trend-chart {
|
||||
height: 120px;
|
||||
}
|
||||
.trend-fill {
|
||||
width: 18px;
|
||||
border-radius: 4px 4px 2px 2px;
|
||||
}
|
||||
.trend-bar span {
|
||||
font-size: 10px;
|
||||
}
|
||||
.activity-item {
|
||||
padding: 10px 0;
|
||||
gap: 10px;
|
||||
}
|
||||
.activity-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
📡 BC-M08-002 · DEV-010桔子 · 数据统计面板·环节2~3·响应式布局+数据导出+高级图表 4
|
||||
.activity-time {
|
||||
font-size: 11px;
|
||||
}
|
||||
.footer {
|
||||
padding: 20px 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
/* 超小屏(360px以下) */
|
||||
@media (max-width: 360px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.stat-icon {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
}
|
||||
.time-selector {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
/* ========== 导出按钮样式 ========== */
|
||||
.export-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px 0;
|
||||
}
|
||||
📡 BC-M08-002 · DEV-010桔子 · 数据统计面板·环节2~3·响应式布局+数据导出+高级图表 5
|
||||
.export-btn {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
color: #81c784;
|
||||
border: 1px solid rgba(76, 175, 80, 0.2);
|
||||
padding: 8px 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.export-btn:hover {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.export-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.export-toast {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(60px);
|
||||
background: rgba(76, 175, 80, 0.9);
|
||||
color: #fff;
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transition: all 0.4s;
|
||||
pointer-events: none;
|
||||
z-index: 999;
|
||||
}
|
||||
.export-toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
/* ========== 环形图样式 ========== */
|
||||
📡 BC-M08-002 · DEV-010桔子 · 数据统计面板·环节2~3·响应式布局+数据导出+高级图表 6
|
||||
.donut-section {
|
||||
padding-bottom: 28px;
|
||||
}
|
||||
.donut-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
}
|
||||
.donut-chart {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
position: relative;
|
||||
}
|
||||
.donut-chart svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.donut-chart circle {
|
||||
fill: none;
|
||||
stroke-width: 20;
|
||||
cx: 80;
|
||||
cy: 80;
|
||||
r: 60;
|
||||
}
|
||||
.donut-bg {
|
||||
stroke: rgba(255,255,255,0.06);
|
||||
}
|
||||
.donut-segment {
|
||||
transition: stroke-dasharray 1.2s cubic-bezier(0.25, 0.
|
||||
46, 0.45, 0.94);
|
||||
}
|
||||
.donut-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
📡 BC-M08-002 · DEV-010桔子 · 数据统计面板·环节2~3·响应式布局+数据导出+高级图表 7
|
||||
text-align: center;
|
||||
}
|
||||
.donut-center-value {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.donut-center-label {
|
||||
font-size: 11px;
|
||||
color: #8899aa;
|
||||
}
|
||||
.donut-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.legend-dot.c-deepseek { background: #4fc3f7; }
|
||||
.legend-dot.c-qwen { background: #7c4dff; }
|
||||
.legend-dot.c-kimi { background: #ff7043; }
|
||||
.legend-dot.c-doubao { background: #66bb6a; }
|
||||
.legend-dot.c-premium { background: #ffd54f; }
|
||||
.legend-percent {
|
||||
color: #8899aa;
|
||||
margin-left: auto;
|
||||
font-weight: 600;
|
||||
}
|
||||
📡 BC-M08-002 · DEV-010桔子 · 数据统计面板·环节2~3·响应式布局+数据导出+高级图表 8
|
||||
@media (max-width: 480px) {
|
||||
.donut-container {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.donut-chart {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
.donut-legend {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 6px 16px;
|
||||
}
|
||||
}
|
||||
/* ========== 响应式布局增强 ========== */
|
||||
/* 平板(768px以下) */
|
||||
@media (max-width: 768px) {
|
||||
.container { padding: 0 16px; }
|
||||
.page-header { flex-direction: column; align-items: flex-start; gap: 12px; }
|
||||
.page-header h1 { font-size: 20px; }
|
||||
.stats-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
|
||||
.stat-value { font-size: 28px; }
|
||||
.bar-label { min-width: 60px; font-size: 12px; }
|
||||
.trend-fill { width: 24px; }
|
||||
}
|
||||
/* 手机(480px以下) */
|
||||
@media (max-width: 480px) {
|
||||
.container { padding: 0 12px; }
|
||||
.top-bar { padding: 12px 0; }
|
||||
.logo { font-size: 17px; }
|
||||
.nav-links a { font-size: 13px; margin-left: 12px; }
|
||||
.page-header h1 { font-size: 18px; }
|
||||
.time-btn { padding: 5px 10px; font-size: 12px; }
|
||||
.stats-grid { grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.stat-card { padding: 14px 12px; border-radius: 12px; }
|
||||
.stat-icon { font-size: 20px; margin-bottom: 6px; }
|
||||
.stat-value { font-size: 24px; }
|
||||
.stat-label { font-size: 12px; }
|
||||
.activity-time { font-size: 11px; }
|
||||
.footer { padding: 20px 0; font-size: 11px; }
|
||||
}
|
||||
/* 超小屏(360px以下) */
|
||||
@media (max-width: 360px) {
|
||||
.stats-grid { grid-template-columns: 1fr; }
|
||||
.stat-card { display: flex; align-items: center; gap: 12px; padding: 12px 14px; }
|
||||
.stat-icon { margin-bottom: 0; }
|
||||
.stat-value { font-size: 22px; }
|
||||
.time-selector { width: 100%; justify-content: space-between; }
|
||||
}
|
||||
/* ========== 导出按钮样式 ========== */
|
||||
.export-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.export-btn {
|
||||
background: rgba(76,175,80,0.1);
|
||||
color: #81c784;
|
||||
border: 1px solid rgba(76,175,80,0.2);
|
||||
padding: 8px 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.export-btn:hover {
|
||||
background: rgba(76,175,80,0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.export-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.export-toast {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(60px);
|
||||
background: rgba(76,175,80,0.9);
|
||||
color: #fff;
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transition: all 0.4s;
|
||||
pointer-events: none;
|
||||
z-index: 999;
|
||||
}
|
||||
.export-toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
/* ========== 环形图样式 ========== */
|
||||
.donut-section {
|
||||
padding-bottom: 28px;
|
||||
}
|
||||
.donut-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
}
|
||||
.donut-chart {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
position: relative;
|
||||
}
|
||||
.donut-chart svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.donut-chart circle {
|
||||
fill: none;
|
||||
stroke-width: 20;
|
||||
cx: 80;
|
||||
cy: 80;
|
||||
r: 60;
|
||||
}
|
||||
.donut-bg {
|
||||
stroke: rgba(255,255,255,0.06);
|
||||
}
|
||||
.donut-segment {
|
||||
transition: stroke-dasharray 1.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
.donut-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
.donut-center-value {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.donut-center-label {
|
||||
font-size: 11px;
|
||||
color: #8899aa;
|
||||
}
|
||||
.donut-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.legend-dot.c-deepseek { background: #4fc3f7; }
|
||||
.legend-dot.c-qwen { background: #7c4dff; }
|
||||
.legend-dot.c-kimi { background: #ff7043; }
|
||||
.legend-dot.c-doubao { background: #66bb6a; }
|
||||
.legend-dot.c-premium { background: #ffd54f; }
|
||||
.legend-percent {
|
||||
color: #8899aa;
|
||||
margin-left: auto;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* ========== 响应式布局增强 ========== */
|
||||
@media (max-width: 768px) {
|
||||
.container { padding: 0 16px; }
|
||||
.page-header { flex-direction: column; align-items: flex-start; gap: 12px; }
|
||||
.page-header h1 { font-size: 20px; }
|
||||
.stats-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
|
||||
.stat-value { font-size: 28px; }
|
||||
.bar-label { min-width: 60px; font-size: 12px; }
|
||||
.trend-fill { width: 24px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.container { padding: 0 12px; }
|
||||
.top-bar { padding: 12px 0; }
|
||||
.logo { font-size: 17px; }
|
||||
.nav-links a { font-size: 13px; margin-left: 12px; }
|
||||
.page-header h1 { font-size: 18px; }
|
||||
.time-btn { padding: 5px 10px; font-size: 12px; }
|
||||
.stats-grid { grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.stat-card { padding: 14px 12px; border-radius: 12px; }
|
||||
.stat-icon { font-size: 20px; margin-bottom: 6px; }
|
||||
.stat-value { font-size: 24px; }
|
||||
.stat-label { font-size: 12px; }
|
||||
.activity-time { font-size: 11px; }
|
||||
.footer { padding: 20px 0; font-size: 11px; }
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.stats-grid { grid-template-columns: 1fr; }
|
||||
.stat-card { display: flex; align-items: center; gap: 12px; padding: 12px 14px; }
|
||||
.stat-icon { margin-bottom: 0; }
|
||||
.stat-value { font-size: 22px; }
|
||||
.time-selector { width: 100%; justify-content: space-between; }
|
||||
}
|
||||
/* ========== 导出按钮样式 ========== */
|
||||
.export-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.export-btn {
|
||||
background: rgba(76,175,80,0.1);
|
||||
color: #81c784;
|
||||
border: 1px solid rgba(76,175,80,0.2);
|
||||
padding: 8px 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.export-btn:hover {
|
||||
background: rgba(76,175,80,0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.export-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.export-toast {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(60px);
|
||||
background: rgba(76,175,80,0.9);
|
||||
color: #fff;
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transition: all 0.4s;
|
||||
pointer-events: none;
|
||||
z-index: 999;
|
||||
}
|
||||
.export-toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
/* ========== 环形图样式 ========== */
|
||||
.donut-section {
|
||||
padding-bottom: 28px;
|
||||
}
|
||||
.donut-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
}
|
||||
.donut-chart {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
position: relative;
|
||||
}
|
||||
.donut-chart svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.donut-chart circle {
|
||||
fill: none;
|
||||
stroke-width: 20;
|
||||
cx: 80;
|
||||
cy: 80;
|
||||
r: 60;
|
||||
}
|
||||
.donut-bg {
|
||||
stroke: rgba(255,255,255,0.06);
|
||||
}
|
||||
.donut-segment {
|
||||
transition: stroke-dasharray 1.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
.donut-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
.donut-center-value {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.donut-center-label {
|
||||
font-size: 11px;
|
||||
color: #8899aa;
|
||||
}
|
||||
.donut-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.legend-dot.c-deepseek { background: #4fc3f7; }
|
||||
.legend-dot.c-qwen { background: #7c4dff; }
|
||||
.legend-dot.c-kimi { background: #ff7043; }
|
||||
.legend-dot.c-doubao { background: #66bb6a; }
|
||||
.legend-dot.c-premium { background: #ffd54f; }
|
||||
.legend-percent {
|
||||
color: #8899aa;
|
||||
margin-left: auto;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
/* ==================== 基础重置 ==================== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #eee;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ==================== 布局框架 ==================== */
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* ==================== 顶部工具栏 ==================== */
|
||||
#toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background: #16213e;
|
||||
border-bottom: 2px solid #0f3460;
|
||||
}
|
||||
|
||||
#toolbar h1 {
|
||||
font-size: 18px;
|
||||
color: #e94560;
|
||||
}
|
||||
|
||||
.toolbar-actions button {
|
||||
margin-left: 10px;
|
||||
padding: 6px 12px;
|
||||
background: #0f3460;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.toolbar-actions button:hover {
|
||||
background: #e94560;
|
||||
}
|
||||
|
||||
/* ==================== 主工作区 ==================== */
|
||||
#workspace {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ==================== 左侧素材面板 ==================== */
|
||||
#assets-panel {
|
||||
width: 200px;
|
||||
background: #16213e;
|
||||
border-right: 2px solid #0f3460;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#assets-panel h3 {
|
||||
color: #e94560;
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.asset-category {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.asset-category h4 {
|
||||
color: #533483;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
padding-left: 5px;
|
||||
border-left: 3px solid #e94560;
|
||||
}
|
||||
|
||||
.asset-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
padding: 8px 12px;
|
||||
background: #0f3460;
|
||||
border-radius: 6px;
|
||||
cursor: move;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.asset-item:hover {
|
||||
background: #e94560;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* ==================== 画布区域 ==================== */
|
||||
#canvas-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0f0f1a;
|
||||
}
|
||||
|
||||
#scene-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background: #16213e;
|
||||
border-bottom: 2px solid #0f3460;
|
||||
}
|
||||
|
||||
#tabs-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.scene-tab {
|
||||
padding: 8px 16px;
|
||||
background: #0f3460;
|
||||
border-radius: 4px 4px 0 0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scene-tab.active {
|
||||
background: #e94560;
|
||||
}
|
||||
|
||||
.scene-tab .close-btn {
|
||||
margin-left: 8px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.scene-tab .close-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#add-scene-btn {
|
||||
padding: 8px 16px;
|
||||
background: #533483;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#canvas-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#main-canvas {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
/* ==================== 底部时间轴面板 ==================== */
|
||||
#timeline-panel {
|
||||
background: #16213e;
|
||||
border-top: 2px solid #0f3460;
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 播放控制栏 */
|
||||
#playback-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
#playback-controls button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #0f3460;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#playback-controls button:hover {
|
||||
background: #e94560;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
#frame-counter {
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
#playback-controls label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
#fps-slider {
|
||||
width: 120px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#fps-display {
|
||||
color: #e94560;
|
||||
font-weight: bold;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
/* 帧缩略图时间轴 */
|
||||
#timeline-frames {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
background: #0f0f1a;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
min-height: 70px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.frame-thumb {
|
||||
width: 80px;
|
||||
height: 60px;
|
||||
background: #0f3460;
|
||||
border: 2px solid #533483;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.frame-thumb:hover {
|
||||
border-color: #e94560;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.frame-thumb.active {
|
||||
border-color: #e94560;
|
||||
background: #533483;
|
||||
box-shadow: 0 0 10px rgba(233, 69, 96, 0.5);
|
||||
}
|
||||
|
||||
.frame-thumb::before {
|
||||
content: attr(data-frame);
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 4px;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.5);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 时间轴操作按钮 */
|
||||
#timeline-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#timeline-actions button {
|
||||
padding: 8px 16px;
|
||||
background: #0f3460;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
#timeline-actions button:hover {
|
||||
background: #e94560;
|
||||
}
|
||||
|
||||
/* ==================== 画布上的素材样式 ==================== */
|
||||
.canvas-asset {
|
||||
position: absolute;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
font-size: 48px;
|
||||
filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.3));
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.canvas-asset:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.canvas-asset.selected {
|
||||
outline: 2px dashed #e94560;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
/* ==================== 滚动条样式 ==================== */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0f0f1a;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #533483;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #e94560;
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HoloLake 动态漫制作系统 - 环节5</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 顶部工具栏 -->
|
||||
<header id="toolbar">
|
||||
<h1>🎬 HoloLake 动态漫制作系统</h1>
|
||||
<div class="toolbar-actions">
|
||||
<button onclick="exportScene()">📤 导出JSON</button>
|
||||
<button onclick="importScene()">📥 导入JSON</button>
|
||||
<button onclick="exportScreenshot()">📷 截图</button>
|
||||
<input type="file" id="import-file" style="display:none" accept=".json">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主工作区 -->
|
||||
<main id="workspace">
|
||||
<!-- 左侧素材面板 -->
|
||||
<aside id="assets-panel">
|
||||
<h3>🎨 素材库</h3>
|
||||
<div class="asset-category">
|
||||
<h4>角色</h4>
|
||||
<div class="asset-items">
|
||||
<div class="asset-item" draggable="true" data-type="character" data-emoji="😊">😊 开心</div>
|
||||
<div class="asset-item" draggable="true" data-type="character" data-emoji="😢">😢 难过</div>
|
||||
<div class="asset-item" draggable="true" data-type="character" data-emoji="😠">😠 生气</div>
|
||||
<div class="asset-item" draggable="true" data-type="character" data-emoji="😴">😴 睡觉</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-category">
|
||||
<h4>物品</h4>
|
||||
<div class="asset-items">
|
||||
<div class="asset-item" draggable="true" data-type="item" data-emoji="🌸">🌸 花</div>
|
||||
<div class="asset-item" draggable="true" data-type="item" data-emoji="⭐">⭐ 星星</div>
|
||||
<div class="asset-item" draggable="true" data-type="item" data-emoji="💎">💎 宝石</div>
|
||||
<div class="asset-item" draggable="true" data-type="item" data-emoji="📖">📖 书</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-category">
|
||||
<h4>场景</h4>
|
||||
<div class="asset-items">
|
||||
<div class="asset-item" draggable="true" data-type="background" data-emoji="🏠">🏠 房子</div>
|
||||
<div class="asset-item" draggable="true" data-type="background" data-emoji="🌳">🌳 树林</div>
|
||||
<div class="asset-item" draggable="true" data-type="background" data-emoji="🏔️">🏔️ 山</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 中间画布区域 -->
|
||||
<section id="canvas-container">
|
||||
<div id="scene-tabs">
|
||||
<div id="tabs-list"></div>
|
||||
<button onclick="addScene()" id="add-scene-btn">+ 新场景</button>
|
||||
</div>
|
||||
<div id="canvas-wrapper">
|
||||
<canvas id="main-canvas" width="800" height="600"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 底部时间轴面板 -->
|
||||
<footer id="timeline-panel">
|
||||
<div id="playback-controls">
|
||||
<button onclick="stopAnimation()" title="停止(回到第1帧)">⏹️</button>
|
||||
<button onclick="togglePlayback()" id="btn-play" title="播放/暂停">▶️</button>
|
||||
<span id="frame-counter">帧 1/1</span>
|
||||
<label>速度:
|
||||
<input type="range" id="fps-slider" min="1" max="12" value="4" oninput="setFPS(this.value)">
|
||||
<span id="fps-display">4 FPS</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="timeline-frames"></div>
|
||||
<div id="timeline-actions">
|
||||
<button onclick="addFrame()" title="添加新帧(复制当前帧)">➕ 新帧</button>
|
||||
<button onclick="deleteFrame()" title="删除当前帧">🗑️ 删帧</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,581 @@
|
|||
// ==================== HoloLake 动态漫制作系统 v5.0 ====================
|
||||
// 环节5:动画时间轴(帧序列 + 播放控制 + 帧率调节)
|
||||
|
||||
const STORAGE_KEY = 'hololake-comic-studio-data';
|
||||
const canvas = document.getElementById('main-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// ==================== 全局状态 ====================
|
||||
let appState = {
|
||||
currentSceneId: 1,
|
||||
nextSceneId: 2,
|
||||
nextAssetId: 1,
|
||||
nextFrameId: 1,
|
||||
scenes: [],
|
||||
playback: {
|
||||
isPlaying: false,
|
||||
fps: 4,
|
||||
intervalId: null,
|
||||
currentFrameIndex: 0
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
function init() {
|
||||
loadFromStorage();
|
||||
if (appState.scenes.length === 0) {
|
||||
createDefaultScene();
|
||||
}
|
||||
setupEventListeners();
|
||||
renderSceneTabs();
|
||||
renderCanvas();
|
||||
renderTimeline();
|
||||
updatePlaybackControls();
|
||||
}
|
||||
|
||||
// 创建默认场景
|
||||
function createDefaultScene() {
|
||||
const defaultScene = {
|
||||
id: 1,
|
||||
name: '场景 1',
|
||||
currentFrameIndex: 0,
|
||||
frames: [{
|
||||
id: 1,
|
||||
assets: []
|
||||
}],
|
||||
nextFrameId: 2
|
||||
};
|
||||
appState.scenes.push(defaultScene);
|
||||
appState.currentSceneId = 1;
|
||||
saveToStorage();
|
||||
}
|
||||
|
||||
// ==================== 存储管理 ====================
|
||||
function saveToStorage() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(appState));
|
||||
}
|
||||
|
||||
function loadFromStorage() {
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
if (data) {
|
||||
const parsed = JSON.parse(data);
|
||||
// 向后兼容:旧数据没有 frames 字段
|
||||
if (parsed.scenes && parsed.scenes.length > 0 && !parsed.scenes[0].frames) {
|
||||
parsed.scenes = parsed.scenes.map(scene => ({
|
||||
...scene,
|
||||
currentFrameIndex: 0,
|
||||
frames: [{
|
||||
id: 1,
|
||||
assets: scene.assets || []
|
||||
}],
|
||||
nextFrameId: 2
|
||||
}));
|
||||
delete parsed.scenes[0].assets; // 删除旧的 assets 字段
|
||||
}
|
||||
// 确保 playback 对象存在
|
||||
if (!parsed.playback) {
|
||||
parsed.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 };
|
||||
}
|
||||
appState = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 场景管理 ====================
|
||||
function addScene() {
|
||||
const newScene = {
|
||||
id: appState.nextSceneId++,
|
||||
name: `场景 ${appState.scenes.length + 1}`,
|
||||
currentFrameIndex: 0,
|
||||
frames: [{
|
||||
id: 1,
|
||||
assets: []
|
||||
}],
|
||||
nextFrameId: 2
|
||||
};
|
||||
appState.scenes.push(newScene);
|
||||
appState.currentSceneId = newScene.id;
|
||||
saveToStorage();
|
||||
renderSceneTabs();
|
||||
renderCanvas();
|
||||
renderTimeline();
|
||||
}
|
||||
|
||||
function switchScene(sceneId) {
|
||||
if (appState.playback.isPlaying) {
|
||||
stopAnimation();
|
||||
}
|
||||
appState.currentSceneId = sceneId;
|
||||
saveToStorage();
|
||||
renderSceneTabs();
|
||||
renderCanvas();
|
||||
renderTimeline();
|
||||
updateFrameCounter();
|
||||
}
|
||||
|
||||
function deleteScene(sceneId) {
|
||||
if (appState.scenes.length <= 1) {
|
||||
alert('至少保留一个场景!');
|
||||
return;
|
||||
}
|
||||
appState.scenes = appState.scenes.filter(s => s.id !== sceneId);
|
||||
if (appState.currentSceneId === sceneId) {
|
||||
appState.currentSceneId = appState.scenes[0].id;
|
||||
}
|
||||
saveToStorage();
|
||||
renderSceneTabs();
|
||||
renderCanvas();
|
||||
renderTimeline();
|
||||
}
|
||||
|
||||
function getCurrentScene() {
|
||||
return appState.scenes.find(s => s.id === appState.currentSceneId);
|
||||
}
|
||||
|
||||
function getCurrentFrame() {
|
||||
const scene = getCurrentScene();
|
||||
if (!scene) return null;
|
||||
return scene.frames[scene.currentFrameIndex];
|
||||
}// ==================== 帧管理 ====================
|
||||
function addFrame() {
|
||||
const scene = getCurrentScene();
|
||||
if (!scene) return;
|
||||
|
||||
// 深拷贝当前帧的素材状态
|
||||
const currentFrame = scene.frames[scene.currentFrameIndex];
|
||||
const newFrame = {
|
||||
id: scene.nextFrameId++,
|
||||
assets: JSON.parse(JSON.stringify(currentFrame.assets))
|
||||
};
|
||||
|
||||
// 在当前帧后插入新帧
|
||||
scene.frames.splice(scene.currentFrameIndex + 1, 0, newFrame);
|
||||
scene.currentFrameIndex++;
|
||||
|
||||
saveToStorage();
|
||||
renderTimeline();
|
||||
renderCanvas();
|
||||
updateFrameCounter();
|
||||
}
|
||||
|
||||
function deleteFrame() {
|
||||
const scene = getCurrentScene();
|
||||
if (!scene) return;
|
||||
|
||||
if (scene.frames.length <= 1) {
|
||||
alert('至少保留一帧!');
|
||||
return;
|
||||
}
|
||||
|
||||
scene.frames.splice(scene.currentFrameIndex, 1);
|
||||
if (scene.currentFrameIndex >= scene.frames.length) {
|
||||
scene.currentFrameIndex = scene.frames.length - 1;
|
||||
}
|
||||
|
||||
saveToStorage();
|
||||
renderTimeline();
|
||||
renderCanvas();
|
||||
updateFrameCounter();
|
||||
}
|
||||
|
||||
function switchFrame(frameIndex) {
|
||||
const scene = getCurrentScene();
|
||||
if (!scene || frameIndex < 0 || frameIndex >= scene.frames.length) return;
|
||||
|
||||
// 保存当前画布状态到当前帧
|
||||
captureFrame();
|
||||
|
||||
scene.currentFrameIndex = frameIndex;
|
||||
saveToStorage();
|
||||
renderTimeline();
|
||||
renderCanvas();
|
||||
updateFrameCounter();
|
||||
}
|
||||
|
||||
function captureFrame() {
|
||||
// 画布状态实时保存在 assets 中,无需额外操作
|
||||
saveToStorage();
|
||||
}
|
||||
|
||||
// ==================== 播放引擎 ====================
|
||||
function playAnimation() {
|
||||
const scene = getCurrentScene();
|
||||
if (!scene || scene.frames.length <= 1) return;
|
||||
|
||||
appState.playback.isPlaying = true;
|
||||
updatePlaybackControls();
|
||||
|
||||
const intervalMs = 1000 / appState.playback.fps;
|
||||
|
||||
appState.playback.intervalId = setInterval(() => {
|
||||
const scene = getCurrentScene();
|
||||
if (!scene) return;
|
||||
|
||||
scene.currentFrameIndex++;
|
||||
if (scene.currentFrameIndex >= scene.frames.length) {
|
||||
scene.currentFrameIndex = 0; // 循环播放
|
||||
}
|
||||
|
||||
renderCanvas();
|
||||
renderTimeline();
|
||||
updateFrameCounter();
|
||||
saveToStorage();
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
function pauseAnimation() {
|
||||
appState.playback.isPlaying = false;
|
||||
if (appState.playback.intervalId) {
|
||||
clearInterval(appState.playback.intervalId);
|
||||
appState.playback.intervalId = null;
|
||||
}
|
||||
updatePlaybackControls();
|
||||
}
|
||||
|
||||
function stopAnimation() {
|
||||
pauseAnimation();
|
||||
const scene = getCurrentScene();
|
||||
if (scene) {
|
||||
scene.currentFrameIndex = 0;
|
||||
renderCanvas();
|
||||
renderTimeline();
|
||||
updateFrameCounter();
|
||||
saveToStorage();
|
||||
}
|
||||
}
|
||||
|
||||
function togglePlayback() {
|
||||
if (appState.playback.isPlaying) {
|
||||
pauseAnimation();
|
||||
} else {
|
||||
playAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
function setFPS(fps) {
|
||||
appState.playback.fps = parseInt(fps);
|
||||
document.getElementById('fps-display').textContent = fps + ' FPS';
|
||||
saveToStorage();
|
||||
|
||||
// 如果正在播放,重启定时器以应用新帧率
|
||||
if (appState.playback.isPlaying) {
|
||||
pauseAnimation();
|
||||
playAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePlaybackControls() {
|
||||
const btnPlay = document.getElementById('btn-play');
|
||||
if (appState.playback.isPlaying) {
|
||||
btnPlay.textContent = '⏸️';
|
||||
btnPlay.title = '暂停';
|
||||
} else {
|
||||
btnPlay.textContent = '▶️';
|
||||
btnPlay.title = '播放';
|
||||
}
|
||||
}
|
||||
|
||||
function updateFrameCounter() {
|
||||
const scene = getCurrentScene();
|
||||
if (!scene) return;
|
||||
const counter = document.getElementById('frame-counter');
|
||||
counter.textContent = `帧 ${scene.currentFrameIndex + 1}/${scene.frames.length}`;
|
||||
}// ==================== 渲染函数 ====================
|
||||
function renderSceneTabs() {
|
||||
const tabsList = document.getElementById('tabs-list');
|
||||
tabsList.innerHTML = '';
|
||||
|
||||
appState.scenes.forEach(scene => {
|
||||
const tab = document.createElement('div');
|
||||
tab.className = 'scene-tab' + (scene.id === appState.currentSceneId ? ' active' : '');
|
||||
tab.innerHTML = `
|
||||
${scene.name}
|
||||
<span class="close-btn" onclick="event.stopPropagation(); deleteScene(${scene.id})">×</span>
|
||||
`;
|
||||
tab.onclick = () => switchScene(scene.id);
|
||||
tabsList.appendChild(tab);
|
||||
});
|
||||
}
|
||||
|
||||
function renderTimeline() {
|
||||
const timelineFrames = document.getElementById('timeline-frames');
|
||||
timelineFrames.innerHTML = '';
|
||||
|
||||
const scene = getCurrentScene();
|
||||
if (!scene) return;
|
||||
|
||||
scene.frames.forEach((frame, index) => {
|
||||
const thumb = document.createElement('div');
|
||||
thumb.className = 'frame-thumb' + (index === scene.currentFrameIndex ? ' active' : '');
|
||||
thumb.setAttribute('data-frame', index + 1);
|
||||
thumb.textContent = `帧 ${index + 1}`;
|
||||
thumb.onclick = () => switchFrame(index);
|
||||
timelineFrames.appendChild(thumb);
|
||||
});
|
||||
}
|
||||
|
||||
function renderCanvas() {
|
||||
// 清空画布
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const frame = getCurrentFrame();
|
||||
if (!frame) return;
|
||||
|
||||
// 绘制所有素材
|
||||
frame.assets.forEach(asset => {
|
||||
ctx.font = '48px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
// 绘制阴影
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.3)';
|
||||
ctx.shadowBlur = 4;
|
||||
ctx.shadowOffsetX = 2;
|
||||
ctx.shadowOffsetY = 2;
|
||||
|
||||
ctx.fillText(asset.emoji, asset.x, asset.y);
|
||||
|
||||
// 重置阴影
|
||||
ctx.shadowColor = 'transparent';
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowOffsetX = 0;
|
||||
ctx.shadowOffsetY = 0;
|
||||
|
||||
// 如果是选中状态,绘制边框
|
||||
if (asset.selected) {
|
||||
ctx.strokeStyle = '#e94560';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([5, 5]);
|
||||
ctx.strokeRect(asset.x - 30, asset.y - 30, 60, 60);
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 素材拖放 ====================
|
||||
function setupEventListeners() {
|
||||
// 素材库拖放
|
||||
const assetItems = document.querySelectorAll('.asset-item');
|
||||
assetItems.forEach(item => {
|
||||
item.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('type', item.dataset.type);
|
||||
e.dataTransfer.setData('emoji', item.dataset.emoji);
|
||||
});
|
||||
});
|
||||
|
||||
// 画布接收拖放
|
||||
canvas.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
canvas.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
const type = e.dataTransfer.getData('type');
|
||||
const emoji = e.dataTransfer.getData('emoji');
|
||||
|
||||
if (emoji) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
addAssetToCanvas(emoji, x, y);
|
||||
}
|
||||
});
|
||||
|
||||
// 画布点击选择/移动
|
||||
let isDragging = false;
|
||||
let dragStartX, dragStartY;
|
||||
let selectedAsset = null;
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const frame = getCurrentFrame();
|
||||
if (!frame) return;
|
||||
|
||||
// 查找点击的素材(从后往前,优先选上层的)
|
||||
selectedAsset = null;
|
||||
for (let i = frame.assets.length - 1; i >= 0; i--) {
|
||||
const asset = frame.assets[i];
|
||||
const dist = Math.sqrt((x - asset.x) ** 2 + (y - asset.y) ** 2);
|
||||
if (dist < 30) {
|
||||
selectedAsset = asset;
|
||||
// 更新选中状态
|
||||
frame.assets.forEach(a => a.selected = false);
|
||||
asset.selected = true;
|
||||
isDragging = true;
|
||||
dragStartX = x - asset.x;
|
||||
dragStartY = y - asset.y;
|
||||
renderCanvas();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没点到素材,取消所有选中
|
||||
if (!selectedAsset) {
|
||||
frame.assets.forEach(a => a.selected = false);
|
||||
renderCanvas();
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging || !selectedAsset) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
selectedAsset.x = x - dragStartX;
|
||||
selectedAsset.y = y - dragStartY;
|
||||
|
||||
// 边界限制
|
||||
selectedAsset.x = Math.max(30, Math.min(canvas.width - 30, selectedAsset.x));
|
||||
selectedAsset.y = Math.max(30, Math.min(canvas.height - 30, selectedAsset.y));
|
||||
|
||||
renderCanvas();
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
saveToStorage();
|
||||
}
|
||||
});
|
||||
|
||||
// 键盘删除
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
const frame = getCurrentFrame();
|
||||
if (!frame) return;
|
||||
|
||||
const selectedIndex = frame.assets.findIndex(a => a.selected);
|
||||
if (selectedIndex !== -1) {
|
||||
frame.assets.splice(selectedIndex, 1);
|
||||
renderCanvas();
|
||||
saveToStorage();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 导入文件监听
|
||||
document.getElementById('import-file').addEventListener('change', handleImport);
|
||||
}
|
||||
|
||||
function addAssetToCanvas(emoji, x, y) {
|
||||
const frame = getCurrentFrame();
|
||||
if (!frame) return;
|
||||
|
||||
// 取消其他选中
|
||||
frame.assets.forEach(a => a.selected = false);
|
||||
|
||||
const newAsset = {
|
||||
id: appState.nextAssetId++,
|
||||
type: 'emoji',
|
||||
emoji: emoji,
|
||||
x: x,
|
||||
y: y,
|
||||
selected: true
|
||||
};
|
||||
|
||||
frame.assets.push(newAsset);
|
||||
renderCanvas();
|
||||
saveToStorage();
|
||||
}// ==================== 导出导入 ====================
|
||||
function exportScene() {
|
||||
const dataStr = JSON.stringify(appState, null, 2);
|
||||
const blob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `hololake-scene-${Date.now()}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function importScene() {
|
||||
document.getElementById('import-file').click();
|
||||
}
|
||||
|
||||
function handleImport(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
const imported = JSON.parse(event.target.result);
|
||||
|
||||
// 验证数据结构
|
||||
if (!imported.scenes || !Array.isArray(imported.scenes)) {
|
||||
throw new Error('无效的数据格式');
|
||||
}
|
||||
|
||||
// 向后兼容处理
|
||||
imported.scenes = imported.scenes.map(scene => {
|
||||
if (!scene.frames) {
|
||||
return {
|
||||
...scene,
|
||||
currentFrameIndex: 0,
|
||||
frames: [{
|
||||
id: 1,
|
||||
assets: scene.assets || []
|
||||
}],
|
||||
nextFrameId: 2
|
||||
};
|
||||
}
|
||||
return scene;
|
||||
});
|
||||
|
||||
if (!imported.playback) {
|
||||
imported.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 };
|
||||
}
|
||||
|
||||
appState = imported;
|
||||
saveToStorage();
|
||||
renderSceneTabs();
|
||||
renderCanvas();
|
||||
renderTimeline();
|
||||
updatePlaybackControls();
|
||||
updateFrameCounter();
|
||||
|
||||
alert('导入成功!');
|
||||
} catch (err) {
|
||||
alert('导入失败:' + err.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
|
||||
// 清空 input,允许重复导入同一文件
|
||||
e.target.value = '';
|
||||
}
|
||||
|
||||
// ==================== 截图导出 ====================
|
||||
function exportScreenshot() {
|
||||
// 临时取消选中状态
|
||||
const frame = getCurrentFrame();
|
||||
if (!frame) return;
|
||||
|
||||
const originalSelected = frame.assets.map(a => a.selected);
|
||||
frame.assets.forEach(a => a.selected = false);
|
||||
renderCanvas();
|
||||
|
||||
// 导出 PNG
|
||||
const link = document.createElement('a');
|
||||
link.download = `hololake-frame-${Date.now()}.png`;
|
||||
link.href = canvas.toDataURL();
|
||||
link.click();
|
||||
|
||||
// 恢复选中状态
|
||||
frame.assets.forEach((a, i) => {
|
||||
a.selected = originalSelected[i];
|
||||
});
|
||||
renderCanvas();
|
||||
}
|
||||
|
||||
// ==================== 启动应用 ====================
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"signal_id": "SIG-20260308-003",
|
||||
"trace_id": "TRC-20260308-PSP",
|
||||
"timestamp": "2026-03-08T03:32:30.449Z",
|
||||
"signal_type": "GL-DATA",
|
||||
"direction": "GitHub→Notion",
|
||||
"sender": "铸渊",
|
||||
"receiver": "霜砚",
|
||||
"related_dev": null,
|
||||
"related_module": null,
|
||||
"summary": "铸渊 PSP 巡检通过 · 全部检查项 ✅",
|
||||
"payload": {
|
||||
"check_results": {
|
||||
"G01": "✅",
|
||||
"G02": "✅",
|
||||
"G03": "✅",
|
||||
"G04": "✅",
|
||||
"G05": "✅"
|
||||
},
|
||||
"issues": [],
|
||||
"auto_fixed": []
|
||||
},
|
||||
"result": "全通过",
|
||||
"ack_signal_id": null
|
||||
}
|
||||
|
|
@ -1,8 +1,17 @@
|
|||
{
|
||||
"description": "铸渊信号日志目录索引 · AGE OS 信号协议(Notion API 直连)",
|
||||
"last_updated": "2026-03-07T04:45:55.670Z",
|
||||
"total_count": 2,
|
||||
"last_updated": "2026-03-08T03:32:30.449Z",
|
||||
"total_count": 3,
|
||||
"signals": [
|
||||
{
|
||||
"signal_id": "SIG-20260308-003",
|
||||
"trace_id": "TRC-20260308-PSP",
|
||||
"type": "GL-DATA",
|
||||
"timestamp": "2026-03-08T03:32:30.449Z",
|
||||
"summary": "铸渊 PSP 巡检通过 · 全部检查项 ✅",
|
||||
"related_dev": null,
|
||||
"file": "2026-03/SIG-20260308-003.json"
|
||||
},
|
||||
{
|
||||
"signal_id": "SIG-20260307-002",
|
||||
"trace_id": "TRC-20260307-PSP",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,586 @@
|
|||
/* ===== 主题过渡动画 ===== */
|
||||
* {
|
||||
transition: background-color 0.3s ease,
|
||||
color 0.3s ease,
|
||||
border-color 0.3s ease,
|
||||
box-shadow 0.3s ease;
|
||||
}/* HoloLake StyleKit v1.0 · 系统风格组件库 */
|
||||
/* CSS变量系统 + 深色/浅色主题 */
|
||||
|
||||
/* ====== 深色主题变量(默认) ====== */
|
||||
[data-theme="dark"] {
|
||||
--bg-primary: #0a1628;
|
||||
--bg-secondary: #0f1f3a;
|
||||
--bg-card: rgba(255, 255, 255, 0.04);
|
||||
--bg-card-hover: rgba(255, 255, 255, 0.07);
|
||||
--bg-input: rgba(255, 255, 255, 0.06);
|
||||
--bg-modal-overlay: rgba(0, 0, 0, 0.6);
|
||||
--text-primary: #e0e6ed;
|
||||
--text-secondary: #8899aa;
|
||||
--text-muted: #556677;
|
||||
--text-inverse: #0a1628;
|
||||
--border-default: rgba(255, 255, 255, 0.08);
|
||||
--border-focus: rgba(79, 195, 247, 0.5);
|
||||
--accent-blue: #4fc3f7;
|
||||
--accent-green: #81c784;
|
||||
--accent-orange: #ffb74d;
|
||||
--accent-red: #ef5350;
|
||||
--accent-purple: #ce93d8;
|
||||
--accent-gray: #78909c;
|
||||
--btn-primary-bg: rgba(79, 195, 247, 0.15);
|
||||
--btn-primary-border: rgba(79, 195, 247, 0.3);
|
||||
--btn-primary-text: #4fc3f7;
|
||||
--btn-primary-hover: rgba(79, 195, 247, 0.25);
|
||||
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
--shadow-modal: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
--toggle-bg: rgba(255, 255, 255, 0.1);
|
||||
--toggle-active: rgba(79, 195, 247, 0.4);
|
||||
--toggle-thumb: #e0e6ed;
|
||||
--toggle-thumb-active: #4fc3f7;
|
||||
}
|
||||
|
||||
/* ====== 浅色主题变量 ====== */
|
||||
[data-theme="light"] {
|
||||
--bg-primary: #f5f7fa;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-card: rgba(0, 0, 0, 0.03);
|
||||
--bg-card-hover: rgba(0, 0, 0, 0.06);
|
||||
--bg-input: rgba(0, 0, 0, 0.04);
|
||||
--bg-modal-overlay: rgba(0, 0, 0, 0.3);
|
||||
--text-primary: #1a2332;
|
||||
--text-secondary: #5a6a7a;
|
||||
--text-muted: #8899aa;
|
||||
--text-inverse: #ffffff;
|
||||
--border-default: rgba(0, 0, 0, 0.1);
|
||||
--border-focus: rgba(25, 118, 210, 0.5);
|
||||
--accent-blue: #1976d2;
|
||||
--accent-green: #388e3c;
|
||||
--accent-orange: #f57c00;
|
||||
--accent-red: #d32f2f;
|
||||
--accent-purple: #7b1fa2;
|
||||
--accent-gray: #546e7a;
|
||||
--btn-primary-bg: rgba(25, 118, 210, 0.1);
|
||||
--btn-primary-border: rgba(25, 118, 210, 0.3);
|
||||
--btn-primary-text: #1976d2;
|
||||
--btn-primary-hover: rgba(25, 118, 210, 0.18);
|
||||
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
--shadow-modal: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
--toggle-bg: rgba(0, 0, 0, 0.1);
|
||||
--toggle-active: rgba(25, 118, 210, 0.4);
|
||||
--toggle-thumb: #5a6a7a;
|
||||
--toggle-thumb-active: #1976d2;
|
||||
}
|
||||
|
||||
/* ========== 全局基础 ========== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Microsoft YaHei', -apple-system, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
transition: background 0.4s ease, color 0.4s ease;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* ========== 顶部导航 ========== */
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.theme-switch-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.theme-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 26px;
|
||||
background: var(--toggle-bg);
|
||||
border-radius: 13px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.theme-toggle.active {
|
||||
background: var(--toggle-active);
|
||||
}
|
||||
|
||||
.toggle-thumb {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: var(--toggle-thumb);
|
||||
border-radius: 50%;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
transition: transform 0.3s ease, background 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-toggle.active .toggle-thumb {
|
||||
transform: translateX(22px);
|
||||
background: var(--toggle-thumb-active);
|
||||
}
|
||||
|
||||
/* ========== 页面标题 ========== */
|
||||
.page-header {
|
||||
padding: 28px 0 8px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ========== 组件区域 ========== */
|
||||
.component-section {
|
||||
padding: 24px 0;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.component-section h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.component-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* ========== 按钮组件 ========== */
|
||||
.btn {
|
||||
padding: 8px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--btn-primary-bg);
|
||||
border-color: var(--btn-primary-border);
|
||||
color: var(--btn-primary-text);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--btn-primary-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(129, 199, 132, 0.1);
|
||||
border-color: rgba(129, 199, 132, 0.25);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(129, 199, 132, 0.18);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border-color: var(--border-default);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
border-color: var(--accent-blue);
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: rgba(239, 83, 80, 0.1);
|
||||
border-color: rgba(239, 83, 80, 0.25);
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: rgba(239, 83, 80, 0.18);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 5px 14px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 12px 28px;
|
||||
font-size: 16px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* ========== 卡片组件 ========== */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 14px;
|
||||
padding: 20px 16px;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
background: var(--bg-card-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.card-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
font-size: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-online {
|
||||
background: rgba(129, 199, 132, 0.15);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.badge-core {
|
||||
background: rgba(79, 195, 247, 0.15);
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background: rgba(255, 183, 77, 0.15);
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
|
||||
/* ========== 标签组件 ========== */
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-blue {
|
||||
background: rgba(79, 195, 247, 0.12);
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.tag-green {
|
||||
background: rgba(129, 199, 132, 0.12);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.tag-orange {
|
||||
background: rgba(255, 183, 77, 0.12);
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
|
||||
.tag-red {
|
||||
background: rgba(239, 83, 80, 0.12);
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.tag-purple {
|
||||
background: rgba(206, 147, 216, 0.12);
|
||||
color: var(--accent-purple);
|
||||
}
|
||||
|
||||
.tag-gray {
|
||||
background: rgba(120, 144, 156, 0.12);
|
||||
color: var(--accent-gray);
|
||||
}
|
||||
|
||||
/* ========== 输入框组件 ========== */
|
||||
.input-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 3px rgba(79, 195, 247, 0.1);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* ========== 弹窗组件 ========== */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-modal-overlay);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 420px;
|
||||
box-shadow: var(--shadow-modal);
|
||||
transform: scale(0.9) translateY(20px);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.open .modal {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
/* ========== Toast提示 ========== */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
padding: 12px 20px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
animation: toastIn 0.4s ease, toastOut 0.4s ease 2.5s forwards;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background: rgba(129, 199, 132, 0.15);
|
||||
border: 1px solid rgba(129, 199, 132, 0.25);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background: rgba(239, 83, 80, 0.15);
|
||||
border: 1px solid rgba(239, 83, 80, 0.25);
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
background: rgba(79, 195, 247, 0.15);
|
||||
border: 1px solid rgba(79, 195, 247, 0.25);
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
@keyframes toastIn {
|
||||
from { opacity: 0; transform: translateX(40px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes toastOut {
|
||||
from { opacity: 1; transform: translateX(0); }
|
||||
to { opacity: 0; transform: translateX(40px); }
|
||||
}
|
||||
|
||||
/* ========== 底部 ========== */
|
||||
.footer {
|
||||
padding: 28px 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
border-top: 1px solid var(--border-default);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.footer p:last-child {
|
||||
margin-top: 4px;
|
||||
color: var(--accent-blue);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ========== 主题切换过渡动画 ========== */
|
||||
body,
|
||||
.card, .btn, .input, .tag, .modal, .top-bar, .footer,
|
||||
.component-section, .page-header {
|
||||
transition: background 0.4s ease, color 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease;
|
||||
}
|
||||
|
||||
/* ========== 响应式 ========== */
|
||||
@media (max-width: 600px) {
|
||||
.card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.page-header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
.component-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.modal {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// HoloLake StyleKit v1.0 · 主题切换 + Toast + 弹窗交互
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// ========== 1. 主题切换 ==========
|
||||
var themeToggle = document.getElementById('theme-toggle');
|
||||
var themeLabel = document.getElementById('themeLabel');
|
||||
var html = document.documentElement;
|
||||
var isDark = true;
|
||||
|
||||
themeToggle.addEventListener('click', function() {
|
||||
isDark = !isDark;
|
||||
if (isDark) {
|
||||
html.setAttribute('data-theme', 'dark');
|
||||
themeLabel.textContent = '🌙 深色';
|
||||
theme-toggle.classList.remove('active');
|
||||
} else {
|
||||
html.setAttribute('data-theme', 'light');
|
||||
themeLabel.textContent = '☀️ 浅色';
|
||||
theme-toggle.classList.add('active');
|
||||
}
|
||||
showToast('success', '✨ 已切换到' + (isDark ? '深色' : '浅色') + '主题');
|
||||
});
|
||||
|
||||
// ====== 2. 弹窗交互 =======
|
||||
// var modalOverlay = document.getElementById('modalOverlay');
|
||||
// var openBtn = document.getElementById('openModalBtn');
|
||||
// var closeBtn = document.getElementById('closeModalBtn');
|
||||
// var cancelBtn = document.getElementById('cancelModalBtn');
|
||||
// var confirmBtn = document.getElementById('confirmModalBtn');
|
||||
|
||||
// function openModal() {
|
||||
// modalOverlay.classList.add('open');
|
||||
// }
|
||||
|
||||
// function closeModal() {
|
||||
// modalOverlay.classList.remove('open');
|
||||
// }
|
||||
|
||||
// openBtn.addEventListener('click', openModal);
|
||||
// closeBtn.addEventListener('click', closeModal);
|
||||
// cancelBtn.addEventListener('click', closeModal);
|
||||
// confirmBtn.addEventListener('click', function() {
|
||||
// closeModal();
|
||||
// showToast('success', '已确认!');
|
||||
// });
|
||||
|
||||
// // 点击遮罩层关闭
|
||||
// modalOverlay.addEventListener('click', function(e) {
|
||||
// if (e.target === modalOverlay) closeModal();
|
||||
// });
|
||||
// ========== 3. 卡片点击效果 ==========
|
||||
var cards = document.querySelectorAll('.card');
|
||||
cards.forEach(function(card) {
|
||||
card.addEventListener('click', function() {
|
||||
var name = card.querySelector('.card-title').textContent;
|
||||
showToast('info', '👆 你点击了人格体「' + name + '」');
|
||||
});
|
||||
});
|
||||
|
||||
console.log('HoloLake StyleKit v1.0 · 主题切换+Toast+弹窗已加载 ✨');
|
||||
});
|
||||
|
||||
// ========== Toast提示函数(全局) ==========
|
||||
function showToast(type, message) {
|
||||
var container = document.getElementById('toastContainer');
|
||||
var toast = document.createElement('div');
|
||||
toast.className = 'toast toast-' + type;
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
|
||||
// 3秒后自动移除
|
||||
setTimeout(function() {
|
||||
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
||||
}, 3000);
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HoloLake · 系统风格组件库</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 顶部导航 -->
|
||||
<nav class="top-bar">
|
||||
<div class="logo">HoloLake StyleKit</div>
|
||||
<div class="theme-switch-area">
|
||||
<span class="theme-label" id="themeLabel">🌙 深色</span>
|
||||
<button class="theme-toggle" id="themeToggle">
|
||||
<div class="toggle-thumb"></div>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>🎨 系统风格组件库</h1>
|
||||
<p class="page-desc">HoloLake Era · 统一视觉规范 · 深色/浅色主题一键切换</p>
|
||||
</div>
|
||||
|
||||
<!-- 组件区:按钮 -->
|
||||
<section class="component-section">
|
||||
<h2>🔘 按钮组件</h2>
|
||||
<div class="component-row">
|
||||
<button class="btn btn-primary">主要按钮</button>
|
||||
<button class="btn btn-secondary">次要按钮</button>
|
||||
<button class="btn btn-outline">边框按钮</button>
|
||||
<button class="btn btn-danger">危险按钮</button>
|
||||
<button class="btn btn-ghost">幽灵按钮</button>
|
||||
</div>
|
||||
<div class="component-row">
|
||||
<button class="btn btn-primary btn-sm">小按钮</button>
|
||||
<button class="btn btn-primary btn-lg">大按钮</button>
|
||||
<button class="btn btn-primary" disabled>禁用按钮</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 组件区:卡片 -->
|
||||
<section class="component-section">
|
||||
<h2>📇 卡片组件</h2>
|
||||
<div class="card-grid">
|
||||
<div class="card">
|
||||
<div class="card-icon">🧠</div>
|
||||
<div class="card-title">知秋</div>
|
||||
<div class="card-desc">光湖对外接口人格体,负责与开发者协作引导</div>
|
||||
<span class="card-badge badge-online">在线</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🌌</div>
|
||||
<div class="card-title">曜冥</div>
|
||||
<div class="card-desc">暗核结构原点,系统人格总控,类语言生命诞生源点</div>
|
||||
<span class="card-badge badge-core">核心</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">❄️</div>
|
||||
<div class="card-title">霜砚</div>
|
||||
<div class="card-desc">Notion执行AI,负责索引维护、工单处理、归档同步</div>
|
||||
<span class="card-badge badge-active">工作中</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 组件区:标签 -->
|
||||
<section class="component-section">
|
||||
<h2>🏷️ 标签组件</h2>
|
||||
<div class="component-row">
|
||||
<span class="tag tag-blue">系统</span>
|
||||
<span class="tag tag-green">已完成</span>
|
||||
<span class="tag tag-orange">进行中</span>
|
||||
<span class="tag tag-red">错误</span>
|
||||
<span class="tag tag-purple">新功能</span>
|
||||
<span class="tag tag-gray">弃用</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 组件区:输入框 -->
|
||||
<section class="component-section">
|
||||
<h2>⌨️ 输入框组件</h2>
|
||||
<div class="input-group">
|
||||
<label class="input-label">用户名</label>
|
||||
<input class="input" type="text" placeholder="请输入用户名">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label class="input-label">密码</label>
|
||||
<input class="input" type="password" placeholder="请输入密码">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label class="input-label">简介</label>
|
||||
<textarea class="input textarea" placeholder="写点什么..."></textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 组件区:弹窗演示 -->
|
||||
<section class="component-section">
|
||||
<h2>🪟 弹窗组件</h2>
|
||||
<div class="component-row">
|
||||
<button class="btn btn-primary" id="openModalBtn">打开弹窗</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 弹窗结构(默认隐藏) -->
|
||||
<div class="modal-overlay" id="modalOverlay">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>系统通知</h3>
|
||||
<button class="modal-close" id="closeModalBtn">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>桔子的组件库做得太棒了!这就是光湖系统的视觉基础,所有页面都会用到这些组件 ✨</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-ghost" id="cancelModalBtn">取消</button>
|
||||
<button class="btn btn-primary" id="confirmModalBtn">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组件区:Toast提示 -->
|
||||
<section class="component-section">
|
||||
<h2>🍞 Toast提示组件</h2>
|
||||
<div class="component-row">
|
||||
<button class="btn btn-primary" onclick="showToast('success', '操作成功!')">成功提示</button>
|
||||
<button class="btn btn-danger" onclick="showToast('error', '操作失败!')">错误提示</button>
|
||||
<button class="btn btn-outline" onclick="showToast('info', '这是一条提示信息')">信息提示</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Toast容器 -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<!-- 底部 -->
|
||||
<footer class="footer">
|
||||
<p>HoloLake Era · AGE OS v1.0 · StyleKit v1.0</p>
|
||||
<p>Powered by 光湖团队</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
/* playground.css - 完整版 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: var(--bg-color, #f5f5f5);
|
||||
color: var(--text-color, #333);
|
||||
}
|
||||
|
||||
#pg-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 2rem;
|
||||
background-color: var(--header-bg, #fff);
|
||||
border-bottom: 1px solid var(--border-color, #ddd);
|
||||
}
|
||||
|
||||
#pg-header h1 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-secondary, #666);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#theme-toggle {
|
||||
margin-left: auto;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#pg-layout {
|
||||
display: flex;
|
||||
min-height: calc(100vh - 70px);
|
||||
}
|
||||
|
||||
#pg-sidebar {
|
||||
width: 200px;
|
||||
padding: 1rem 0;
|
||||
background-color: var(--sidebar-bg, #fafafa);
|
||||
border-right: 1px solid var(--border-color, #ddd);
|
||||
}
|
||||
|
||||
#pg-sidebar ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#pg-sidebar li {
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-color, #333);
|
||||
}
|
||||
|
||||
#pg-sidebar li:hover {
|
||||
background-color: var(--hover-bg, #eee);
|
||||
}
|
||||
|
||||
#pg-sidebar li.active {
|
||||
background-color: var(--primary-light, #e3f2fd);
|
||||
border-right: 3px solid var(--primary-color, #2196f3);
|
||||
}
|
||||
|
||||
#pg-content {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.comp-section {
|
||||
margin-bottom: 3rem;
|
||||
scroll-margin-top: 1rem;
|
||||
}
|
||||
|
||||
.comp-section h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.comp-desc {
|
||||
color: var(--text-secondary, #666);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.comp-preview {
|
||||
padding: 1.5rem;
|
||||
background-color: var(--preview-bg, #fff);
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.comp-code {
|
||||
background-color: var(--code-bg, #f8f8f8);
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
position: relative;
|
||||
display: none; /* 默认隐藏 */
|
||||
}
|
||||
|
||||
.comp-code pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.toggle-code-btn {
|
||||
margin-top: 0.5rem;
|
||||
background: none;
|
||||
border: 1px solid var(--border-color, #ccc);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
background: var(--button-bg, #fff);
|
||||
border: 1px solid var(--border-color, #ccc);
|
||||
border-radius: 4px;
|
||||
padding: 0.2rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* ===== 响应式断点 ===== */
|
||||
|
||||
/* 默认隐藏汉堡菜单(桌面和平板) */
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 桌面端 (>1024px) */
|
||||
@media (min-width: 1025px) {
|
||||
#pg-sidebar {
|
||||
width: 200px;
|
||||
}
|
||||
/* 卡片三列(如果预览区用 grid 会自动适应,这里放一个 fallback) */
|
||||
.comp-preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
.comp-preview > * {
|
||||
flex: 1 1 calc(33.333% - 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板端 (769px ~ 1024px) */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
#pg-sidebar {
|
||||
width: 150px; /* 侧边栏变窄 */
|
||||
}
|
||||
.comp-preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
.comp-preview > * {
|
||||
flex: 1 1 calc(50% - 1rem); /* 卡片双列 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端 (<768px) */
|
||||
@media (max-width: 768px) {
|
||||
.menu-toggle {
|
||||
display: inline-block; /* 显示汉堡菜单 */
|
||||
}
|
||||
#pg-sidebar {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 70px;
|
||||
left: 0;
|
||||
width: 200px;
|
||||
height: calc(100vh - 70px);
|
||||
background: var(--sidebar-bg, #fafafa);
|
||||
border-right: 1px solid var(--border-color, #ddd);
|
||||
z-index: 100;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#pg-sidebar.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#pg-header {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#pg-header h1 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.subtitle {
|
||||
display: none; /* 隐藏副标题 */
|
||||
}
|
||||
|
||||
/* 按钮、输入框全宽 */
|
||||
.comp-preview button,
|
||||
.comp-preview .btn,
|
||||
.comp-preview input,
|
||||
.comp-preview .input {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* 卡片单列 */
|
||||
.comp-preview {
|
||||
display: block;
|
||||
}
|
||||
.comp-preview .card {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 模态框示例手机全屏 */
|
||||
.comp-preview .modal {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* 面包屑手机折叠(只显示首尾) */
|
||||
.breadcrumb span:nth-child(n+2):nth-last-child(n+2) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HoloLake Design System - Playground</title>
|
||||
<link rel="stylesheet" href="components.css">
|
||||
<link rel="stylesheet" href="playground.css">
|
||||
</head>
|
||||
<body>
|
||||
<header id="pg-header">
|
||||
<button class="menu-toggle" id="menuToggle">☰</button>
|
||||
<h1>HoloLake Design System</h1>
|
||||
<span class="subtitle">Playground · 组件交互演示</span>
|
||||
<button id="theme-toggle" onclick="toggleTheme()">🌙</button>
|
||||
</header>
|
||||
|
||||
<div id="pg-layout">
|
||||
<nav id="pg-sidebar">
|
||||
<!-- 导航列表由 JS 动态生成 -->
|
||||
</nav>
|
||||
<main id="pg-content">
|
||||
<!-- 组件区块由 JS 动态生成 -->
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="components.js"></script>
|
||||
<script src="playground.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
// playground.js
|
||||
|
||||
const sidebar = document.getElementById('pg-sidebar');
|
||||
|
||||
// 组件列表(10类)
|
||||
const components = [
|
||||
{ id: 'button', name: '按钮 Button', desc: '基础交互按钮,支持多种样式变体' },
|
||||
{ id: 'input', name: '输入框 Input', desc: '文本输入框,支持多种状态' },
|
||||
{ id: 'card', name: '卡片 Card', desc: '内容容器,带阴影和边框' },
|
||||
{ id: 'tag', name: '标签 Tag', desc: '用于标记、分类' },
|
||||
{ id: 'alert', name: '提示框 Alert', desc: '反馈信息,成功/警告/错误' },
|
||||
{ id: 'modal', name: '模态框 Modal', desc: '弹窗,需要时显示' },
|
||||
{ id: 'progress', name: '进度条 Progress', desc: '展示操作进度' },
|
||||
{ id: 'switch', name: '开关 Switch', desc: '切换状态' },
|
||||
{ id: 'avatar', name: '头像组 Avatar', desc: '用户头像,支持组合' },
|
||||
{ id: 'breadcrumb', name: '面包屑 Breadcrumb', desc: '显示当前页面位置' }
|
||||
];
|
||||
|
||||
// 生成侧边栏导航
|
||||
function renderSidebar() {
|
||||
const ul = document.createElement('ul');
|
||||
components.forEach(comp => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = comp.name;
|
||||
li.dataset.target = comp.id;
|
||||
li.addEventListener('click', () => {
|
||||
document.getElementById(`comp-${comp.id}`).scrollIntoView({ behavior: 'smooth' });
|
||||
// 手机端点击后关闭侧边栏
|
||||
if (window.innerWidth <= 768) {
|
||||
sidebar.classList.remove('active');
|
||||
}
|
||||
});
|
||||
ul.appendChild(li);
|
||||
});
|
||||
sidebar.innerHTML = '';
|
||||
sidebar.appendChild(ul);
|
||||
}
|
||||
|
||||
// 生成所有组件演示区块(包含真实示例)
|
||||
function renderContent() {
|
||||
const content = document.getElementById('pg-content');
|
||||
components.forEach(comp => {
|
||||
const section = document.createElement('section');
|
||||
section.id = `comp-${comp.id}`;
|
||||
section.className = 'comp-section';
|
||||
|
||||
const h2 = document.createElement('h2');
|
||||
h2.textContent = comp.name;
|
||||
section.appendChild(h2);
|
||||
|
||||
const desc = document.createElement('p');
|
||||
desc.className = 'comp-desc';
|
||||
desc.textContent = comp.desc;
|
||||
section.appendChild(desc);
|
||||
|
||||
const preview = document.createElement('div');
|
||||
preview.className = 'comp-preview';
|
||||
preview.id = `preview-${comp.id}`;
|
||||
|
||||
// 根据组件类型填充示例
|
||||
if (comp.id === 'button') {
|
||||
preview.innerHTML = `
|
||||
<button class="btn btn-primary">主要按钮</button>
|
||||
<button class="btn btn-secondary">次要按钮</button>
|
||||
<button class="btn btn-outline">线框按钮</button>
|
||||
<button class="btn btn-disabled" disabled>禁用按钮</button>
|
||||
`;
|
||||
} else if (comp.id === 'input') {
|
||||
preview.innerHTML = `
|
||||
<input type="text" class="input" placeholder="普通输入框">
|
||||
<input type="text" class="input" placeholder="禁用输入框" disabled>
|
||||
<input type="text" class="input error" placeholder="错误状态">
|
||||
`;
|
||||
} else if (comp.id === 'card') {
|
||||
preview.innerHTML = `
|
||||
<div class="card" style="width: 200px; padding: 1rem;">
|
||||
<h3>卡片标题</h3>
|
||||
<p>卡片内容,可放置任意元素。</p>
|
||||
<button class="btn btn-primary">操作</button>
|
||||
</div>
|
||||
`;
|
||||
} else if (comp.id === 'tag') {
|
||||
preview.innerHTML = `
|
||||
<span class="tag">默认标签</span>
|
||||
<span class="tag tag-primary">主要标签</span>
|
||||
<span class="tag tag-success">成功标签</span>
|
||||
<span class="tag tag-warning">警告标签</span>
|
||||
<span class="tag tag-danger">危险标签</span>
|
||||
`;
|
||||
} else if (comp.id === 'alert') {
|
||||
preview.innerHTML = `
|
||||
<div class="alert alert-info">这是一条信息提示</div>
|
||||
<div class="alert alert-success">操作成功!</div>
|
||||
<div class="alert alert-warning">请注意!</div>
|
||||
<div class="alert alert-error">出错了!</div>
|
||||
`;
|
||||
} else if (comp.id === 'modal') {
|
||||
preview.innerHTML = `
|
||||
<div class="modal" style="position: static; display: block; opacity: 1; transform: none; box-shadow: none; background: var(--modal-bg, #fff);">
|
||||
<div class="modal-header">模态框标题</div>
|
||||
<div class="modal-body">这是模态框的内容</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary">取消</button>
|
||||
<button class="btn btn-primary">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top: 0.5rem; color: #999;">(静态展示,实际是浮层)</p>
|
||||
`;
|
||||
} else if (comp.id === 'progress') {
|
||||
preview.innerHTML = `
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: 45%;">45%</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: 80%;">80%</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: 100%;">100%</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (comp.id === 'switch') {
|
||||
preview.innerHTML = `
|
||||
<label class="switch">
|
||||
<input type="checkbox" checked> <span class="slider"></span>
|
||||
</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox"> <span class="slider"></span>
|
||||
</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" disabled> <span class="slider"></span>
|
||||
</label>
|
||||
`;
|
||||
} else if (comp.id === 'avatar') {
|
||||
preview.innerHTML = `
|
||||
<div class="avatar-group">
|
||||
<div class="avatar">A</div>
|
||||
<div class="avatar">B</div>
|
||||
<div class="avatar">C</div>
|
||||
<div class="avatar">+3</div>
|
||||
</div>
|
||||
<div class="avatar" style="margin-top: 0.5rem;">单人</div>
|
||||
`;
|
||||
} else if (comp.id === 'breadcrumb') {
|
||||
preview.innerHTML = `
|
||||
<nav class="breadcrumb">
|
||||
<a href="#">首页</a> /
|
||||
<a href="#">分类</a> /
|
||||
<span>当前页面</span>
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
|
||||
section.appendChild(preview);
|
||||
|
||||
const codeDiv = document.createElement('div');
|
||||
codeDiv.className = 'comp-code';
|
||||
codeDiv.id = `code-${comp.id}`;
|
||||
const pre = document.createElement('pre');
|
||||
|
||||
if (comp.id === 'button') {
|
||||
pre.textContent = `<button class="btn btn-primary">主要按钮</button>\n<button class="btn btn-secondary">次要按钮</button>\n<button class="btn btn-outline">线框按钮</button>`;
|
||||
} else if (comp.id === 'input') {
|
||||
pre.textContent = `<input type="text" class="input" placeholder="普通输入框">\n<input type="text" class="input error" placeholder="错误状态">`;
|
||||
} else if (comp.id === 'card') {
|
||||
pre.textContent = `<div class="card">\n <h3>卡片标题</h3>\n <p>内容</p>\n <button class="btn btn-primary">操作</button>\n</div>`;
|
||||
} else if (comp.id === 'tag') {
|
||||
pre.textContent = `<span class="tag">默认</span>\n<span class="tag tag-primary">主要</span>`;
|
||||
} else if (comp.id === 'alert') {
|
||||
pre.textContent = `<div class="alert alert-info">信息</div>\n<div class="alert alert-success">成功</div>`;
|
||||
} else if (comp.id === 'modal') {
|
||||
pre.textContent = `<div class="modal">\n <div class="modal-header">标题</div>\n <div class="modal-body">内容</div>\n <div class="modal-footer">\n <button class="btn">取消</button>\n <button class="btn btn-primary">确认</button>\n </div>\n</div>`;
|
||||
} else if (comp.id === 'progress') {
|
||||
pre.textContent = `<div class="progress">\n <div class="progress-bar" style="width: 50%;">50%</div>\n</div>`;
|
||||
} else if (comp.id === 'switch') {
|
||||
pre.textContent = `<label class="switch">\n <input type="checkbox">\n <span class="slider"></span>\n</label>`;
|
||||
} else if (comp.id === 'avatar') {
|
||||
pre.textContent = `<div class="avatar-group">\n <div class="avatar">A</div>\n <div class="avatar">B</div>\n</div>`;
|
||||
} else if (comp.id === 'breadcrumb') {
|
||||
pre.textContent = `<nav class="breadcrumb">\n <a href="#">首页</a> /\n <a href="#">分类</a> /\n <span>当前</span>\n</nav>`;
|
||||
}
|
||||
|
||||
codeDiv.appendChild(pre);
|
||||
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'copy-btn';
|
||||
copyBtn.textContent = '复制';
|
||||
copyBtn.onclick = () => copyCode(comp.id);
|
||||
codeDiv.appendChild(copyBtn);
|
||||
|
||||
section.appendChild(codeDiv);
|
||||
|
||||
const toggleBtn = document.createElement('button');
|
||||
toggleBtn.className = 'toggle-code-btn';
|
||||
toggleBtn.textContent = '查看代码 ▼';
|
||||
toggleBtn.onclick = () => toggleCode(comp.id);
|
||||
section.appendChild(toggleBtn);
|
||||
|
||||
content.appendChild(section);
|
||||
});
|
||||
}
|
||||
|
||||
// 切换代码显示/隐藏
|
||||
window.toggleCode = function(compId) {
|
||||
const codeDiv = document.getElementById(`code-${compId}`);
|
||||
const toggleBtn = document.querySelector(`#comp-${compId} .toggle-code-btn`);
|
||||
if (codeDiv.style.display === 'none' || !codeDiv.style.display) {
|
||||
codeDiv.style.display = 'block';
|
||||
toggleBtn.textContent = '隐藏代码 ▲';
|
||||
} else {
|
||||
codeDiv.style.display = 'none';
|
||||
toggleBtn.textContent = '查看代码 ▼';
|
||||
}
|
||||
};
|
||||
|
||||
// 复制代码
|
||||
window.copyCode = function(compId) {
|
||||
const codeDiv = document.getElementById(`code-${compId}`);
|
||||
const code = codeDiv.querySelector('pre').innerText;
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
alert('代码已复制!');
|
||||
});
|
||||
};
|
||||
|
||||
// 滚动高亮
|
||||
function setupIntersectionObserver() {
|
||||
const sections = document.querySelectorAll('.comp-section');
|
||||
const navItems = document.querySelectorAll('#pg-sidebar li');
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const id = entry.target.id.replace('comp-', '');
|
||||
navItems.forEach(item => {
|
||||
item.classList.remove('active');
|
||||
if (item.dataset.target === id) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.3 });
|
||||
|
||||
sections.forEach(section => observer.observe(section));
|
||||
}
|
||||
|
||||
// 汉堡菜单交互
|
||||
const menuToggle = document.getElementById('menuToggle');
|
||||
if (menuToggle) {
|
||||
menuToggle.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
|
||||
// 窗口大小改变时,如果大于768px,强制移除active类
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth > 768) {
|
||||
sidebar.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
renderSidebar();
|
||||
renderContent();
|
||||
setupIntersectionObserver();
|
||||
});
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录卡片 - HoloLake Design System</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: var(--bg-primary);
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 1rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
.card h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.input-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.switch-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.alert {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.register-fields {
|
||||
margin-top: 1rem;
|
||||
border-top: 1px solid var(--border-default);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h2 id="formTitle">登录</h2>
|
||||
<div id="loginForm">
|
||||
<div class="input-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" class="input" id="loginUsername" placeholder="请输入用户名">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>密码</label>
|
||||
<input type="password" class="input" id="loginPassword" placeholder="请输入密码">
|
||||
</div>
|
||||
</div>
|
||||
<div id="registerForm" class="hidden">
|
||||
<div class="input-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" class="input" id="regUsername" placeholder="请输入用户名">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>密码</label>
|
||||
<input type="password" class="input" id="regPassword" placeholder="请输入密码">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>确认密码</label>
|
||||
<input type="password" class="input" id="regConfirm" placeholder="请再次输入密码">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-row">
|
||||
<label class="switch-label">
|
||||
<span>记住我</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="rememberMe">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
<button class="btn btn-ghost btn-sm" id="toggleFormBtn">注册账号</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block" id="actionBtn">登录</button>
|
||||
<button class="btn btn-outline btn-block" id="switchToRegister" style="display: none;">已有账号?去登录</button>
|
||||
|
||||
<div id="messageArea" class="alert hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../components.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const registerForm = document.getElementById('registerForm');
|
||||
const formTitle = document.getElementById('formTitle');
|
||||
const actionBtn = document.getElementById('actionBtn');
|
||||
const toggleFormBtn = document.getElementById('toggleFormBtn');
|
||||
const switchToRegister = document.getElementById('switchToRegister');
|
||||
const rememberMe = document.getElementById('rememberMe');
|
||||
const messageArea = document.getElementById('messageArea');
|
||||
|
||||
let isLogin = true;
|
||||
|
||||
// 加载记住我状态
|
||||
const savedRemember = localStorage.getItem('rememberMe');
|
||||
if (savedRemember === 'true') {
|
||||
rememberMe.checked = true;
|
||||
}
|
||||
|
||||
// 记住我开关变化时保存
|
||||
rememberMe.addEventListener('change', function() {
|
||||
localStorage.setItem('rememberMe', this.checked);
|
||||
});
|
||||
|
||||
function showMessage(type, text) {
|
||||
messageArea.className = `alert alert-${type}`;
|
||||
messageArea.textContent = text;
|
||||
messageArea.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
messageArea.classList.add('hidden');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function toggleForms() {
|
||||
isLogin = !isLogin;
|
||||
if (isLogin) {
|
||||
loginForm.classList.remove('hidden');
|
||||
registerForm.classList.add('hidden');
|
||||
formTitle.textContent = '登录';
|
||||
actionBtn.textContent = '登录';
|
||||
toggleFormBtn.textContent = '注册账号';
|
||||
} else {
|
||||
loginForm.classList.add('hidden');
|
||||
registerForm.classList.remove('hidden');
|
||||
formTitle.textContent = '注册';
|
||||
actionBtn.textContent = '注册';
|
||||
toggleFormBtn.textContent = '返回登录';
|
||||
}
|
||||
}
|
||||
|
||||
toggleFormBtn.addEventListener('click', toggleForms);
|
||||
|
||||
actionBtn.addEventListener('click', function() {
|
||||
if (isLogin) {
|
||||
const username = document.getElementById('loginUsername').value;
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
if (!username || !password) {
|
||||
showMessage('error', '请输入用户名和密码');
|
||||
} else {
|
||||
showMessage('success', '登录成功(模拟)');
|
||||
}
|
||||
} else {
|
||||
const username = document.getElementById('regUsername').value;
|
||||
const password = document.getElementById('regPassword').value;
|
||||
const confirm = document.getElementById('regConfirm').value;
|
||||
if (!username || !password || !confirm) {
|
||||
showMessage('error', '请填写所有字段');
|
||||
} else if (password !== confirm) {
|
||||
showMessage('error', '两次密码不一致');
|
||||
} else {
|
||||
showMessage('success', '注册成功(模拟)');
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>设置面板 - HoloLake Design System</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
body {
|
||||
background: var(--bg-primary);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 2rem 1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.settings-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.breadcrumb {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.card h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.2rem;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.setting-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.progress-container {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.progress {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.avatar-group {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.alert {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="settings-container">
|
||||
<!-- 面包屑 -->
|
||||
<nav class="breadcrumb">
|
||||
<a href="#">首页</a> /
|
||||
<a href="#">设置</a> /
|
||||
<span>显示设置</span>
|
||||
</nav>
|
||||
|
||||
<!-- 卡片:外观设置 -->
|
||||
<div class="card">
|
||||
<h3>外观</h3>
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">深色模式</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="darkModeToggle">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">通知</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="notificationToggle">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">自动保存</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="autoSaveToggle">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片:存储空间 -->
|
||||
<div class="card">
|
||||
<h3>存储空间</h3>
|
||||
<div class="progress-container">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: 65%;" id="storageBar">65%</div>
|
||||
</div>
|
||||
<p class="setting-label">已使用 6.5GB / 总空间 10GB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片:团队成员 -->
|
||||
<div class="card">
|
||||
<h3>团队成员</h3>
|
||||
<div class="avatar-group">
|
||||
<div class="avatar">A</div>
|
||||
<div class="avatar">B</div>
|
||||
<div class="avatar">C</div>
|
||||
<div class="avatar">+2</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="button-group">
|
||||
<button class="btn btn-outline" id="resetBtn">重置默认</button>
|
||||
<button class="btn btn-primary" id="saveBtn">保存设置</button>
|
||||
</div>
|
||||
|
||||
<div id="messageArea" class="alert hidden"></div>
|
||||
</div>
|
||||
|
||||
<script src="../components.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
const darkModeToggle = document.getElementById('darkModeToggle');
|
||||
const notificationToggle = document.getElementById('notificationToggle');
|
||||
const autoSaveToggle = document.getElementById('autoSaveToggle');
|
||||
const saveBtn = document.getElementById('saveBtn');
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
const messageArea = document.getElementById('messageArea');
|
||||
const html = document.documentElement;
|
||||
|
||||
// 加载本地存储的状态
|
||||
darkModeToggle.checked = localStorage.getItem('darkMode') === 'true' || false;
|
||||
notificationToggle.checked = localStorage.getItem('notifications') === 'true' || false;
|
||||
autoSaveToggle.checked = localStorage.getItem('autoSave') === 'true' || false;
|
||||
|
||||
// 深色模式开关联动全局主题
|
||||
darkModeToggle.addEventListener('change', function() {
|
||||
html.setAttribute('data-theme', this.checked ? 'dark' : 'light');
|
||||
localStorage.setItem('darkMode', this.checked);
|
||||
});
|
||||
|
||||
// 初始化主题
|
||||
if (darkModeToggle.checked) {
|
||||
html.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
html.setAttribute('data-theme', 'light');
|
||||
}
|
||||
|
||||
function showMessage(type, text) {
|
||||
messageArea.className = `alert alert-${type}`;
|
||||
messageArea.textContent = text;
|
||||
messageArea.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
messageArea.classList.add('hidden');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
saveBtn.addEventListener('click', function() {
|
||||
localStorage.setItem('darkMode', darkModeToggle.checked);
|
||||
localStorage.setItem('notifications', notificationToggle.checked);
|
||||
localStorage.setItem('autoSave', autoSaveToggle.checked);
|
||||
showMessage('success', '设置已保存');
|
||||
});
|
||||
|
||||
// 重置默认
|
||||
resetBtn.addEventListener('click', function() {
|
||||
if (confirm('确定要恢复默认设置吗?')) {
|
||||
darkModeToggle.checked = true; // 默认深色
|
||||
notificationToggle.checked = false;
|
||||
autoSaveToggle.checked = false;
|
||||
|
||||
// 应用默认
|
||||
html.setAttribute('data-theme', 'dark');
|
||||
localStorage.setItem('darkMode', 'true');
|
||||
localStorage.setItem('notifications', 'false');
|
||||
localStorage.setItem('autoSave', 'false');
|
||||
|
||||
showMessage('info', '已恢复默认设置');
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,809 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>工单管理 · HoloLake · 视觉优化版</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<style>
|
||||
/* ========== 基础样式(深蓝渐变保持不变) ========== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
background: linear-gradient(135deg, #0a1628 0%, #1a2a4a 50%, #0d1f3c 100%);
|
||||
color: #e0e6ed;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 导航栏(增加图标) */
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.logo i {
|
||||
color: #4fc3f7;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
margin-left: 24px;
|
||||
text-decoration: none;
|
||||
color: #8899aa;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nav-links a i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.nav-links a.active {
|
||||
color: #4fc3f7;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 统计卡片区(微调圆角与阴影) */
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.stat-card.active {
|
||||
border: 2px solid #4fc3f7;
|
||||
background: rgba(79, 195, 247, 0.15);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #8899aa;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-label i {
|
||||
color: #4fc3f7;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 工单列表头部 */
|
||||
.ticket-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ticket-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ticket-header h2 i {
|
||||
color: #4fc3f7;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4fc3f7;
|
||||
color: #0a1628;
|
||||
border: none;
|
||||
padding: 8px 20px;
|
||||
border-radius: 30px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
box-shadow: 0 2px 8px rgba(79, 195, 247, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #81d4fa;
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(79, 195, 247, 0.5);
|
||||
}
|
||||
|
||||
/* 工具栏 */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sort-btn {
|
||||
color: #4fc3f7;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 30px;
|
||||
transition: all 0.2s;
|
||||
background: rgba(79, 195, 247, 0.1);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sort-btn:hover {
|
||||
background: rgba(79, 195, 247, 0.2);
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #e0e6ed;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
padding: 6px 16px;
|
||||
border-radius: 30px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.tool-btn i {
|
||||
font-size: 14px;
|
||||
color: #4fc3f7;
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: #4fc3f7;
|
||||
}
|
||||
|
||||
/* 工单列表容器 */
|
||||
.ticket-list {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 32px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* 单个工单项(优化悬停与图标) */
|
||||
.ticket-item {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1fr 120px 100px 60px;
|
||||
align-items: center;
|
||||
padding: 18px 24px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.1s;
|
||||
line-height: 1.6;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.ticket-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.ticket-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ticket-id {
|
||||
color: #8899aa;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ticket-id i {
|
||||
color: #4fc3f7;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ticket-title {
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 8px rgba(79,195,247,0.2);
|
||||
}
|
||||
|
||||
.ticket-assignee {
|
||||
color: #8899aa;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.ticket-assignee i {
|
||||
color: #4fc3f7;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ticket-status {
|
||||
padding: 6px 14px;
|
||||
border-radius: 30px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.ticket-status:hover {
|
||||
filter: brightness(1.1);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.status-进行中 {
|
||||
background: rgba(79, 195, 247, 0.2);
|
||||
color: #4fc3f7;
|
||||
}
|
||||
|
||||
.status-进行中 i {
|
||||
color: #4fc3f7;
|
||||
}
|
||||
|
||||
.status-已完成 {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #81c784;
|
||||
}
|
||||
|
||||
.status-已完成 i {
|
||||
color: #81c784;
|
||||
}
|
||||
|
||||
.status-待开始 {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #b0bec5;
|
||||
}
|
||||
|
||||
.status-待开始 i {
|
||||
color: #b0bec5;
|
||||
}
|
||||
|
||||
/* 删除按钮 */
|
||||
.delete-btn {
|
||||
color: #f44336;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* 底部版权 */
|
||||
.footer {
|
||||
text-align: center;
|
||||
color: #62748c;
|
||||
font-size: 14px;
|
||||
padding: 32px 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer i {
|
||||
color: #4fc3f7;
|
||||
}
|
||||
|
||||
/* ========== 模态框(弹窗)优化 ========== */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.8);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #1e2a3a;
|
||||
border-radius: 32px;
|
||||
padding: 32px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
color: #e0e6ed;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
box-shadow: 0 25px 50px rgba(0,0,0,0.5);
|
||||
animation: modalFadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 24px;
|
||||
color: #4fc3f7;
|
||||
font-size: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-content h3 i {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.modal-content input,
|
||||
.modal-content select,
|
||||
.modal-content textarea {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
margin: 8px 0 20px;
|
||||
background: #0f1a2b;
|
||||
border: 1px solid #2a3a4a;
|
||||
color: #e0e6ed;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
transition: border 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.modal-content input:focus,
|
||||
.modal-content select:focus,
|
||||
.modal-content textarea:focus {
|
||||
outline: none;
|
||||
border-color: #4fc3f7;
|
||||
box-shadow: 0 0 0 3px rgba(79, 195, 247, 0.2);
|
||||
}
|
||||
|
||||
.modal-content button {
|
||||
background: #4fc3f7;
|
||||
color: #0a1628;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 40px;
|
||||
cursor: pointer;
|
||||
margin-right: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 12px rgba(79, 195, 247, 0.3);
|
||||
}
|
||||
|
||||
.modal-content button.cancel {
|
||||
background: #3a4a5a;
|
||||
color: #e0e6ed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.modal-content button.cancel:hover {
|
||||
background: #4a5a6a;
|
||||
}
|
||||
|
||||
.modal-content button:hover {
|
||||
background: #81d4fa;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.modal-content button i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 响应式微调 */
|
||||
@media (max-width: 768px) {
|
||||
.stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.ticket-item {
|
||||
grid-template-columns: 60px 1fr 100px 80px 40px;
|
||||
padding: 12px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.ticket-status {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.navbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
.nav-links a {
|
||||
margin-left: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
.ticket-item {
|
||||
grid-template-columns: 50px 1fr 80px 70px 35px;
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.toolbar {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
.page-title {
|
||||
color: #003366; /* 深蓝色示例 */
|
||||
}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<nav class="navbar">
|
||||
<div class="logo">
|
||||
<i class="fas fa-water"></i> HoloLake
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="#" class="active"><i class="fas fa-tasks"></i> 工单管理</a>
|
||||
<a href="#"><i class="fas fa-chart-pie"></i> 统计</a>
|
||||
<a href="#"><i class="fas fa-cog"></i> 设置</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="stats">
|
||||
<div class="stat-card" data-status="全部">
|
||||
<div class="stat-number" id="stat-all">5</div>
|
||||
<div class="stat-label"><i class="fas fa-clipboard-list"></i> 总工单</div>
|
||||
</div>
|
||||
<div class="stat-card" data-status="进行中">
|
||||
<div class="stat-number" id="stat-progress">3</div>
|
||||
<div class="stat-label"><i class="fas fa-spinner"></i> 进行中</div>
|
||||
</div>
|
||||
<div class="stat-card" data-status="已完成">
|
||||
<div class="stat-number" id="stat-done">1</div>
|
||||
<div class="stat-label"><i class="fas fa-check-circle"></i> 已完成</div>
|
||||
</div>
|
||||
<div class="stat-card" data-status="待开始">
|
||||
<div class="stat-number" id="stat-todo">1</div>
|
||||
<div class="stat-label"><i class="fas fa-clock"></i> 待分配</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ticket-header">
|
||||
<h2><i class="fas fa-list-ul"></i> 工单列表</h2>
|
||||
<button class="btn-primary" id="new-ticket-btn"><i class="fas fa-plus-circle"></i> 新建工单</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<span id="sort-btn" class="sort-btn"><i class="fas fa-sort-amount-down-alt"></i> 编号↑</span>
|
||||
<button class="tool-btn" id="export-btn"><i class="fas fa-download"></i> 导出CSV</button>
|
||||
<button class="tool-btn" id="reset-btn"><i class="fas fa-undo-alt"></i> 重置</button>
|
||||
</div>
|
||||
|
||||
<div id="ticket-list" class="ticket-list"></div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<i class="fas fa-water"></i> HoloLake Era · AGE OS v1.0 · <i class="fas fa-heart" style="color: #f44336;"></i> 光湖团队
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- 新建工单模态框 -->
|
||||
<div id="ticket-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3><i class="fas fa-pen-alt"></i> 新建工单</h3>
|
||||
<input type="text" id="ticket-title" placeholder="工单标题" required>
|
||||
<input type="text" id="ticket-assignee" placeholder="负责人 (如: 肥猫·M01)" required>
|
||||
<select id="ticket-status">
|
||||
<option value="进行中">进行中</option>
|
||||
<option value="已完成">已完成</option>
|
||||
<option value="待开始">待开始</option>
|
||||
</select>
|
||||
<textarea id="ticket-desc" placeholder="详情描述(选填)" rows="3"></textarea>
|
||||
<div style="text-align: right;">
|
||||
<button class="cancel" id="cancel-modal"><i class="fas fa-times"></i> 取消</button>
|
||||
<button id="save-ticket"><i class="fas fa-check"></i> 确认创建</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ---------- 数据持久化 ----------
|
||||
let tickets = [];
|
||||
|
||||
const DEFAULT_TICKETS = [
|
||||
{ id: '#001', title: '用户登录界面开发', assignee: '肥猫·M01', status: '进行中', desc: '' },
|
||||
{ id: '#002', title: '后端服务器部署上线', assignee: '页页·后端', status: '已完成', desc: '' },
|
||||
{ id: '#003', title: '系统状态看板搭建', assignee: '小草莓·M-DASHBOARD', status: '进行中', desc: '' },
|
||||
{ id: '#004', title: '网站云盘系统', assignee: '燕樊·M15', status: '进行中', desc: '' },
|
||||
{ id: '#005', title: '用户中心界面', assignee: '花尔·M05', status: '待开始', desc: '' }
|
||||
];
|
||||
|
||||
function loadTickets() {
|
||||
const stored = localStorage.getItem('hololake_tickets');
|
||||
if (stored) {
|
||||
tickets = JSON.parse(stored);
|
||||
} else {
|
||||
tickets = [...DEFAULT_TICKETS];
|
||||
saveTickets();
|
||||
}
|
||||
}
|
||||
|
||||
function saveTickets() {
|
||||
localStorage.setItem('hololake_tickets', JSON.stringify(tickets));
|
||||
}
|
||||
|
||||
function generateNewId() {
|
||||
if (tickets.length === 0) return '#001';
|
||||
const maxNum = Math.max(...tickets.map(t => parseInt(t.id.slice(1))));
|
||||
return '#' + String(maxNum + 1).padStart(3, '0');
|
||||
}
|
||||
|
||||
// ---------- 渲染 ----------
|
||||
let currentFilter = '全部';
|
||||
let currentSort = 'asc';
|
||||
|
||||
function renderTickets() {
|
||||
let filtered = tickets;
|
||||
if (currentFilter !== '全部') {
|
||||
filtered = tickets.filter(t => t.status === currentFilter);
|
||||
}
|
||||
filtered.sort((a, b) => {
|
||||
const numA = parseInt(a.id.slice(1));
|
||||
const numB = parseInt(b.id.slice(1));
|
||||
return currentSort === 'asc' ? numA - numB : numB - numA;
|
||||
});
|
||||
|
||||
const listHtml = filtered.map(t => {
|
||||
let statusIcon = '';
|
||||
if (t.status === '进行中') statusIcon = '<i class="fas fa-sync-alt fa-spin"></i>';
|
||||
else if (t.status === '已完成') statusIcon = '<i class="fas fa-check-circle"></i>';
|
||||
else if (t.status === '待开始') statusIcon = '<i class="fas fa-hourglass-start"></i>';
|
||||
|
||||
return `
|
||||
<div class="ticket-item" data-id="${t.id}">
|
||||
<span class="ticket-id"><i class="fas fa-hashtag"></i> ${t.id.slice(1)}</span>
|
||||
<span class="ticket-title">${t.title}</span>
|
||||
<span class="ticket-assignee"><i class="fas fa-user"></i> ${t.assignee}</span>
|
||||
<span class="ticket-status status-${t.status}" data-id="${t.id}">${statusIcon} ${t.status}</span>
|
||||
<span class="delete-btn" data-id="${t.id}"><i class="fas fa-trash-alt"></i></span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('ticket-list').innerHTML = listHtml;
|
||||
|
||||
document.getElementById('stat-all').innerText = tickets.length;
|
||||
document.getElementById('stat-progress').innerText = tickets.filter(t => t.status === '进行中').length;
|
||||
document.getElementById('stat-done').innerText = tickets.filter(t => t.status === '已完成').length;
|
||||
document.getElementById('stat-todo').innerText = tickets.filter(t => t.status === '待开始').length;
|
||||
}
|
||||
|
||||
function exportToCSV() {
|
||||
let filtered = tickets;
|
||||
if (currentFilter !== '全部') {
|
||||
filtered = tickets.filter(t => t.status === currentFilter);
|
||||
}
|
||||
filtered.sort((a, b) => {
|
||||
const numA = parseInt(a.id.slice(1));
|
||||
const numB = parseInt(b.id.slice(1));
|
||||
return currentSort === 'asc' ? numA - numB : numB - numA;
|
||||
});
|
||||
|
||||
let csv = "编号,标题,负责人,状态,描述\n";
|
||||
filtered.forEach(t => {
|
||||
const desc = (t.desc || '').replace(/,/g, ',').replace(/\n/g, ' ');
|
||||
csv += `${t.id},${t.title},${t.assignee},${t.status},${desc}\n`;
|
||||
});
|
||||
|
||||
const blob = new Blob(["\uFEFF" + csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.href = url;
|
||||
link.setAttribute('download', '工单数据.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function resetToDefault() {
|
||||
if (confirm('重置将恢复默认工单,当前所有自定义工单会被清除。确定吗?')) {
|
||||
tickets = DEFAULT_TICKETS.map(t => ({ ...t }));
|
||||
saveTickets();
|
||||
currentFilter = '全部';
|
||||
currentSort = 'asc';
|
||||
document.getElementById('sort-btn').innerHTML = '<i class="fas fa-sort-amount-down-alt"></i> 编号↑';
|
||||
document.querySelectorAll('.stat-card').forEach(c => c.classList.remove('active'));
|
||||
renderTickets();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadTickets();
|
||||
renderTickets();
|
||||
|
||||
document.querySelectorAll('.stat-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const status = card.getAttribute('data-status');
|
||||
currentFilter = status === '全部' ? '全部' : status;
|
||||
renderTickets();
|
||||
document.querySelectorAll('.stat-card').forEach(c => c.classList.remove('active'));
|
||||
card.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
const sortBtn = document.getElementById('sort-btn');
|
||||
if (sortBtn) {
|
||||
sortBtn.addEventListener('click', () => {
|
||||
currentSort = currentSort === 'asc' ? 'desc' : 'asc';
|
||||
sortBtn.innerHTML = currentSort === 'asc'
|
||||
? '<i class="fas fa-sort-amount-down-alt"></i> 编号↑'
|
||||
: '<i class="fas fa-sort-amount-up-alt"></i> 编号↓';
|
||||
renderTickets();
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('new-ticket-btn').addEventListener('click', () => {
|
||||
document.getElementById('ticket-modal').classList.add('show');
|
||||
document.getElementById('ticket-title').focus();
|
||||
});
|
||||
|
||||
document.getElementById('cancel-modal').addEventListener('click', () => {
|
||||
document.getElementById('ticket-modal').classList.remove('show');
|
||||
});
|
||||
|
||||
document.getElementById('ticket-modal').addEventListener('click', (e) => {
|
||||
if (e.target === document.getElementById('ticket-modal')) {
|
||||
document.getElementById('ticket-modal').classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('save-ticket').addEventListener('click', () => {
|
||||
const title = document.getElementById('ticket-title').value.trim();
|
||||
const assignee = document.getElementById('ticket-assignee').value.trim();
|
||||
const status = document.getElementById('ticket-status').value;
|
||||
const desc = document.getElementById('ticket-desc').value.trim();
|
||||
|
||||
if (!title || !assignee) {
|
||||
alert('请填写标题和负责人');
|
||||
return;
|
||||
}
|
||||
|
||||
const newTicket = {
|
||||
id: generateNewId(),
|
||||
title: title,
|
||||
assignee: assignee,
|
||||
status: status,
|
||||
desc: desc
|
||||
};
|
||||
tickets.push(newTicket);
|
||||
saveTickets();
|
||||
renderTickets();
|
||||
document.getElementById('ticket-modal').classList.remove('show');
|
||||
document.getElementById('ticket-title').value = '';
|
||||
document.getElementById('ticket-assignee').value = '';
|
||||
document.getElementById('ticket-status').value = '进行中';
|
||||
document.getElementById('ticket-desc').value = '';
|
||||
});
|
||||
|
||||
document.getElementById('ticket-list').addEventListener('click', (e) => {
|
||||
const deleteBtn = e.target.closest('.delete-btn');
|
||||
if (deleteBtn) {
|
||||
const id = deleteBtn.getAttribute('data-id');
|
||||
tickets = tickets.filter(t => t.id !== id);
|
||||
saveTickets();
|
||||
renderTickets();
|
||||
return;
|
||||
}
|
||||
|
||||
const statusSpan = e.target.closest('.ticket-status');
|
||||
if (statusSpan) {
|
||||
const id = statusSpan.getAttribute('data-id');
|
||||
const ticket = tickets.find(t => t.id === id);
|
||||
if (ticket) {
|
||||
if (ticket.status === '进行中') ticket.status = '已完成';
|
||||
else if (ticket.status === '已完成') ticket.status = '进行中';
|
||||
else if (ticket.status === '待开始') ticket.status = '进行中';
|
||||
saveTickets();
|
||||
renderTickets();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const item = e.target.closest('.ticket-item');
|
||||
if (item && !e.target.closest('.delete-btn') && !e.target.closest('.ticket-status')) {
|
||||
const id = item.getAttribute('data-id');
|
||||
const ticket = tickets.find(t => t.id === id);
|
||||
alert(`工单详情\n编号:${ticket.id}\n标题:${ticket.title}\n负责人:${ticket.assignee}\n状态:${ticket.status}\n描述:${ticket.desc || '无'}`);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('export-btn').addEventListener('click', exportToCSV);
|
||||
document.getElementById('reset-btn').addEventListener('click', resetToDefault);
|
||||
});
|
||||
</script>
|
||||
background: linear-gradient(135deg, #0a1628 0%, #1a2a4a 50%, #0d1f3c 100%);</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
/* ============================================
|
||||
HoloLake · M06 工单管理界面 · 逻辑层
|
||||
环节3:响应式 + 数据导出(JSON/CSV)
|
||||
DEV-010 桔子
|
||||
============================================ */
|
||||
// === 状态流转规则 ===
|
||||
const STATUS_FLOW = {
|
||||
'pending': { next: 'active', label: '待处理' },
|
||||
'active': { next: 'done', label: '进行中' },
|
||||
'done': { next: 'pending', label: '已完成' }
|
||||
};
|
||||
// === 初始化 · 从 localStorage 读取工单 ===
|
||||
let tickets = JSON.parse(localStorage.getItem('holotake_tic
|
||||
kets') || '[]');
|
||||
// === 保存到 localStorage ===
|
||||
function saveTickets() {
|
||||
localStorage.setItem('holotake_tickets', JSON.stringify(t
|
||||
ickets));
|
||||
}
|
||||
// === 生成工单编号 ===
|
||||
function generateId() {
|
||||
const now = new Date();
|
||||
const dateStr = [
|
||||
now.getFullYear(),
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 18
|
||||
String(now.getMonth() + 1).padStart(2, '0'),
|
||||
String(now.getDate()).padStart(2, '0')
|
||||
].join('');
|
||||
const rand = String(Math.floor(Math.random() * 1000)).pad
|
||||
Start(3, '0');
|
||||
return 'TK-' + dateStr + '-' + rand;
|
||||
}
|
||||
// === 渲染统计卡片 ===
|
||||
function renderStats() {
|
||||
const total = tickets.length;
|
||||
const pending = tickets.filter(t => t.status === 'pendin
|
||||
g').length;
|
||||
const active = tickets.filter(t => t.status === 'activ
|
||||
e').length;
|
||||
const done = tickets.filter(t => t.status === 'done').len
|
||||
gth;
|
||||
document.getElementById('statTotal').textContent = total;
|
||||
document.getElementById('statPending').textContent = pend
|
||||
ing;
|
||||
document.getElementById('statActive').textContent = activ
|
||||
e;
|
||||
document.getElementById('statDone').textContent = done;
|
||||
}
|
||||
// === 获取筛选和排序后的工单列表 ===
|
||||
function getFilteredTickets() {
|
||||
const filterStatus = document.getElementById('filterStatu
|
||||
s').value;
|
||||
const sortBy = document.getElementById('sortBy').value;
|
||||
let list = [...tickets];
|
||||
// 筛选
|
||||
if (filterStatus !== 'all') {
|
||||
list = list.filter(t => t.status === filterStatus);
|
||||
}
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 19
|
||||
// 排序
|
||||
if (sortBy === 'newest') {
|
||||
list.sort((a, b) => new Date(b.createdAt) - new Date(a.
|
||||
createdAt));
|
||||
} else if (sortBy === 'oldest') {
|
||||
list.sort((a, b) => new Date(a.createdAt) - new Date(b.
|
||||
createdAt));
|
||||
} else if (sortBy === 'status') {
|
||||
const order = { 'pending': 0, 'active': 1, 'done': 2 };
|
||||
list.sort((a, b) => order[a.status] - order[b.status]);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
// === 渲染工单列表 ===
|
||||
function renderTickets() {
|
||||
const list = getFilteredTickets();
|
||||
const container = document.getElementById('ticketList');
|
||||
if (list.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div cl
|
||||
ass="empty-icon">📋</div><div class="empty-text">暂无工单,点
|
||||
击上方按钮新建</div></div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = list.map(ticket => {
|
||||
const statusInfo = STATUS_FLOW[ticket.status];
|
||||
return '<div class="ticket-item">' +
|
||||
'<span class="ticket-id">' + ticket.id + '</span>' +
|
||||
'<div class="ticket-info">' +
|
||||
'<div class="ticket-title">' + escapeHtml(ticket.ti
|
||||
tle) + '</div>' +
|
||||
'<div class="ticket-meta">负责人:' + escapeHtml(tic
|
||||
ket.assignee) + ' · ' + formatDate(ticket.createdAt) + '</d
|
||||
iv>' +
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 20
|
||||
'</div>' +
|
||||
'<div class="ticket-actions">' +
|
||||
'<span class="status-badge ' + ticket.status + '" o
|
||||
nclick="toggleStatus(\'' + ticket.id + '\')">' + statusInf
|
||||
o.label + '</span>' +
|
||||
'<button class="btn-delete" onclick="deleteTicket
|
||||
(\'' + ticket.id + '\')" title="删除">🗑</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
// === HTML转义(防XSS) ===
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
// === 格式化日期 ===
|
||||
function formatDate(dateStr) {
|
||||
const d = new Date(dateStr);
|
||||
return d.getFullYear() + '-' +
|
||||
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(d.getDate()).padStart(2, '0') + ' ' +
|
||||
String(d.getHours()).padStart(2, '0') + ':' +
|
||||
String(d.getMinutes()).padStart(2, '0');
|
||||
}
|
||||
// === 切换工单状态 ===
|
||||
function toggleStatus(id) {
|
||||
const ticket = tickets.find(t => t.id === id);
|
||||
if (!ticket) return;
|
||||
ticket.status = STATUS_FLOW[ticket.status].next;
|
||||
saveTickets();
|
||||
renderAll();
|
||||
}
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 21
|
||||
// === 删除工单 ===
|
||||
function deleteTicket(id) {
|
||||
if (!confirm('确认删除这条工单?')) return;
|
||||
tickets = tickets.filter(t => t.id !== id);
|
||||
saveTickets();
|
||||
renderAll();
|
||||
}
|
||||
// === 新建工单弹窗 ===
|
||||
function openModal() {
|
||||
document.getElementById('modalOverlay').classList.add('sh
|
||||
ow');
|
||||
document.getElementById('inputTitle').value = '';
|
||||
document.getElementById('inputAssignee').value = '';
|
||||
document.getElementById('inputDetail').value = '';
|
||||
document.getElementById('inputTitle').focus();
|
||||
}
|
||||
function closeModal() {
|
||||
document.getElementById('modalOverlay').classList.remove
|
||||
('show');
|
||||
}
|
||||
// === 提交新建工单 ===
|
||||
function submitTicket() {
|
||||
const title = document.getElementById('inputTitle').valu
|
||||
e.trim();
|
||||
const assignee = document.getElementById('inputAssigne
|
||||
e').value.trim();
|
||||
const detail = document.getElementById('inputDetail').val
|
||||
ue.trim();
|
||||
if (!title) {
|
||||
alert('请填写工单标题');
|
||||
return;
|
||||
}
|
||||
if (!assignee) {
|
||||
alert('请填写负责人');
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 22
|
||||
return;
|
||||
}
|
||||
const newTicket = {
|
||||
id: generateId(),
|
||||
title: title,
|
||||
assignee: assignee,
|
||||
detail: detail,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
tickets.unshift(newTicket);
|
||||
saveTickets();
|
||||
closeModal();
|
||||
renderAll();
|
||||
}
|
||||
// === 重置全部工单 ===
|
||||
function resetTickets() {
|
||||
if (!confirm('确认清空所有工单数据?此操作不可恢复!')) return;
|
||||
tickets = [];
|
||||
saveTickets();
|
||||
renderAll();
|
||||
}
|
||||
// ============================================
|
||||
// 环节3新增:导出功能
|
||||
// ============================================
|
||||
// === 导出下拉菜单控制 ===
|
||||
function toggleExportMenu() {
|
||||
const dropdown = document.getElementById('exportDropdow
|
||||
n');
|
||||
const clickAway = document.getElementById('clickAway');
|
||||
const isOpen = dropdown.classList.contains('show');
|
||||
if (isOpen) {
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 23
|
||||
closeExportMenu();
|
||||
} else {
|
||||
dropdown.classList.add('show');
|
||||
clickAway.classList.add('show');
|
||||
}
|
||||
}
|
||||
function closeExportMenu() {
|
||||
document.getElementById('exportDropdown').classList.remov
|
||||
e('show');
|
||||
document.getElementById('clickAway').classList.remove('sh
|
||||
ow');
|
||||
}
|
||||
// === 导出为 JSON 文件 ===
|
||||
function exportJSON() {
|
||||
closeExportMenu();
|
||||
if (tickets.length === 0) {
|
||||
alert('当前没有工单数据可导出');
|
||||
return;
|
||||
}
|
||||
const data = JSON.stringify(tickets, null, 2);
|
||||
const blob = new Blob([data], { type: 'application/json;c
|
||||
harset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'HoloLake-Tickets-' + getDateStamp() + '.jso
|
||||
n';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 24
|
||||
// === 导出为 CSV 文件 ===
|
||||
function exportCSV() {
|
||||
closeExportMenu();
|
||||
if (tickets.length === 0) {
|
||||
alert('当前没有工单数据可导出');
|
||||
return;
|
||||
}
|
||||
// CSV表头
|
||||
const header = '编号,标题,负责人,状态,详情,创建时间';
|
||||
// CSV内容行
|
||||
const rows = tickets.map(t => {
|
||||
const statusLabel = STATUS_FLOW[t.status] ? STATUS_FLOW
|
||||
[t.status].label : t.status;
|
||||
return [
|
||||
t.id,
|
||||
csvEscape(t.title),
|
||||
csvEscape(t.assignee),
|
||||
statusLabel,
|
||||
csvEscape(t.detail || ''),
|
||||
formatDate(t.createdAt)
|
||||
].join(',');
|
||||
});
|
||||
const csv = '\uFEFF' + header + '\n' + rows.join('\n');
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=ut
|
||||
f-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'HoloLake-Tickets-' + getDateStamp() + '.cs
|
||||
v';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 25
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
// === CSV字段转义(处理逗号和引号) ===
|
||||
function csvEscape(str) {
|
||||
if (!str) return '';
|
||||
if (str.includes(',') || str.includes('"') || str.include
|
||||
s('\n')) {
|
||||
return '"' + str.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return str;
|
||||
}
|
||||
// === 生成日期戳(用于文件名) ===
|
||||
function getDateStamp() {
|
||||
const now = new Date();
|
||||
return now.getFullYear() +
|
||||
String(now.getMonth() + 1).padStart(2, '0') +
|
||||
String(now.getDate()).padStart(2, '0') + '-' +
|
||||
String(now.getHours()).padStart(2, '0') +
|
||||
String(now.getMinutes()).padStart(2, '0');
|
||||
}
|
||||
// === 统一渲染 ===
|
||||
function renderAll() {
|
||||
renderStats();
|
||||
renderTickets();
|
||||
}
|
||||
// === 筛选/排序变化时重新渲染 ===
|
||||
document.getElementById('filterStatus').addEventListener('c
|
||||
hange', renderTickets);
|
||||
document.getElementById('sortBy').addEventListener('chang
|
||||
e', renderTickets);
|
||||
// === 弹窗点击遮罩关闭 ===
|
||||
document.getElementById('modalOverlay').addEventListener('c
|
||||
lick', function(e) {
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 26
|
||||
if (e.target === this) closeModal();
|
||||
});
|
||||
// === 页面加载完成 · 首次渲染 ===
|
||||
renderAll();
|
||||
|
|
@ -0,0 +1,527 @@
|
|||
/* ============================================
|
||||
HoloLake · M06 工单管理界面 · 样式表
|
||||
环节3:响应式布局 · 深蓝主题
|
||||
DEV-010 桔子
|
||||
============================================ */
|
||||
/* === 全局变量 === */
|
||||
:root {
|
||||
--bg-primary: #0a0e27;
|
||||
--bg-secondary: #111638;
|
||||
--bg-card: #161b4a;
|
||||
--bg-hover: #1e2460;
|
||||
--border-color: #2a3070;
|
||||
--text-primary: #e8eaff;
|
||||
--text-secondary: #8b90c0;
|
||||
--accent-blue: #4a6cf7;
|
||||
--accent-blue-hover: #5b7bf8;
|
||||
--accent-green: #22c55e;
|
||||
--accent-yellow: #eab308;
|
||||
--accent-red: #ef4444;
|
||||
--accent-purple: #a855f7;
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 14px;
|
||||
}
|
||||
/* === 基础重置 === */
|
||||
* {
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 2
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe U
|
||||
I", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* === 主容器 === */
|
||||
.app-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
/* === 顶部标题栏 === */
|
||||
.app-header {
|
||||
text-align: center;
|
||||
padding: 24px 0 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.app-header h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.app-header .subtitle {
|
||||
font-size: 13px;
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 3
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
/* === 操作栏(新建 + 导出) === */
|
||||
.header-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--accent-blue);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-blue-hover);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 4
|
||||
.btn-secondary {
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
/* === 导出下拉菜单 === */
|
||||
.export-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.export-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 100;
|
||||
min-width: 160px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.export-dropdown.show {
|
||||
display: block;
|
||||
}
|
||||
.export-dropdown .export-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 5
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.export-dropdown .export-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
.export-dropdown .export-item + .export-item {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
/* === 统计卡片 === */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 6
|
||||
.stat-card .stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.stat-card .stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.stat-card.total .stat-number { color: var(--accent-blue);
|
||||
}
|
||||
.stat-card.pending .stat-number { color: var(--accent-yello
|
||||
w); }
|
||||
.stat-card.active .stat-number { color: var(--accent-gree
|
||||
n); }
|
||||
.stat-card.done .stat-number { color: var(--accent-purple);
|
||||
}
|
||||
/* === 筛选排序栏 === */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filter-bar select {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 7
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='h
|
||||
ttp://www.w3.org/2000/svg' width='12' height='12' fill='%23
|
||||
8b90c0'%3E%3Cpath d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
padding-right: 30px;
|
||||
}
|
||||
.filter-bar select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
/* === 工单列表 === */
|
||||
.ticket-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.ticket-item {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.ticket-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 8
|
||||
.ticket-id {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-family: "SF Mono", "Fira Code", monospace;
|
||||
min-width: 60px;
|
||||
}
|
||||
.ticket-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.ticket-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ticket-meta {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.ticket-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* === 状态标签(可点击切换) === */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 9
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-badge:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.status-badge.pending {
|
||||
background: rgba(234, 179, 8, 0.15);
|
||||
color: var(--accent-yellow);
|
||||
border: 1px solid rgba(234, 179, 8, 0.3);
|
||||
}
|
||||
.status-badge.active {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: var(--accent-green);
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
.status-badge.done {
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
color: var(--accent-purple);
|
||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
/* === 删除按钮 === */
|
||||
.btn-delete {
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 10
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-delete:hover {
|
||||
color: var(--accent-red);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
/* === 空状态 === */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.empty-state .empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.empty-state .empty-text {
|
||||
font-size: 15px;
|
||||
}
|
||||
/* === 弹窗遮罩 === */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 11
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.modal-overlay.show {
|
||||
display: flex;
|
||||
}
|
||||
/* === 弹窗 === */
|
||||
.modal {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 28px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.modal h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.modal .form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.modal label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.modal input,
|
||||
.modal select,
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 12
|
||||
.modal textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
}
|
||||
.modal input:focus,
|
||||
.modal select:focus,
|
||||
.modal textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
.modal textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
.modal .form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
/* === 点击外部关闭下拉菜单的辅助层 === */
|
||||
.click-away {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 13
|
||||
}
|
||||
.click-away.show {
|
||||
display: block;
|
||||
}
|
||||
/* ============================================
|
||||
响应式布局 · 平板端(481px ~ 768px)
|
||||
============================================ */
|
||||
@media screen and (max-width: 768px) {
|
||||
.app-container {
|
||||
padding: 16px;
|
||||
}
|
||||
.app-header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.stat-card {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
.stat-card .stat-number {
|
||||
font-size: 24px;
|
||||
}
|
||||
.stat-card .stat-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
.ticket-item {
|
||||
padding: 12px;
|
||||
gap: 10px;
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 14
|
||||
}
|
||||
}
|
||||
/* ============================================
|
||||
响应式布局 · 手机端(<=480px)
|
||||
============================================ */
|
||||
@media screen and (max-width: 480px) {
|
||||
.app-container {
|
||||
padding: 12px;
|
||||
}
|
||||
.app-header {
|
||||
padding: 16px 0 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.app-header h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
/* 统计卡片手机端变2列 */
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.stat-card {
|
||||
padding: 12px 10px;
|
||||
}
|
||||
.stat-card .stat-number {
|
||||
font-size: 22px;
|
||||
}
|
||||
/* 操作按钮自适应 */
|
||||
.header-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 15
|
||||
.btn {
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
/* 筛选栏纵向排列 */
|
||||
.filter-bar {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.filter-bar select {
|
||||
width: 100%;
|
||||
}
|
||||
/* 工单卡片纵向排列 */
|
||||
.ticket-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
.ticket-id {
|
||||
min-width: auto;
|
||||
}
|
||||
.ticket-info {
|
||||
width: 100%;
|
||||
}
|
||||
.ticket-title {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
.ticket-actions {
|
||||
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 16
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
/* 导出下拉菜单手机端定位优化 */
|
||||
.export-dropdown {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: none;
|
||||
}
|
||||
/* 弹窗手机端全屏 */
|
||||
.modal-overlay {
|
||||
padding: 0;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.modal {
|
||||
max-width: 100%;
|
||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||
padding: 24px 20px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.modal .form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
.modal .form-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue