🚀 部署全部可用模块到测试网站 · 码字台+14个模块全部上线
- 构建并部署 writing-platform(码字台)到 docs/writing/ - 部署 status-board, ticket-system, notification, dynamic-comic-studio, persona-studio 到 docs/ - 部署 m01-login, m05-user-center, m07-dialogue-ui, m10-cloud, m11-module, m15-cloud-drive, frontend 到 docs/modules/ - 更新 docs/index.html 模块导航:14个模块全部 ready:true,路径修正为 docs 内部相对路径 - 更新 preview-deploy.yml 和 deploy-pages.yml 工作流,支持全模块同步 - writing-platform vite.config.ts base 改为 './' 以兼容 GitHub Pages 相对路径 Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/2b0b1a22-c312-4b46-ae3b-72e830c4470b
This commit is contained in:
parent
4783252698
commit
2cdf0af54d
|
|
@ -36,10 +36,61 @@ jobs:
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v5
|
uses: actions/configure-pages@v5
|
||||||
|
|
||||||
- name: Sync Persona Studio frontend to docs
|
- name: Sync all modules to docs for GitHub Pages
|
||||||
run: |
|
run: |
|
||||||
|
# Sync Persona Studio frontend
|
||||||
mkdir -p docs/persona-studio
|
mkdir -p docs/persona-studio
|
||||||
cp -r persona-studio/frontend/* docs/persona-studio/
|
if [ -d "persona-studio/frontend" ]; then
|
||||||
|
cp -r persona-studio/frontend/* docs/persona-studio/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync status-board
|
||||||
|
if [ -d "status-board" ]; then
|
||||||
|
mkdir -p docs/status-board
|
||||||
|
cp -r status-board/* docs/status-board/
|
||||||
|
[ -d "docs/status-board/node_modules" ] && rm -rf docs/status-board/node_modules
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync ticket-system
|
||||||
|
if [ -d "ticket-system" ]; then
|
||||||
|
mkdir -p docs/ticket-system
|
||||||
|
cp -r ticket-system/* docs/ticket-system/
|
||||||
|
[ -d "docs/ticket-system/node_modules" ] && rm -rf docs/ticket-system/node_modules
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync notification
|
||||||
|
if [ -d "notification" ]; then
|
||||||
|
mkdir -p docs/notification
|
||||||
|
cp -r notification/* docs/notification/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync dynamic-comic-studio
|
||||||
|
if [ -d "dynamic-comic-studio" ]; then
|
||||||
|
mkdir -p docs/dynamic-comic-studio
|
||||||
|
cp -r dynamic-comic-studio/* docs/dynamic-comic-studio/
|
||||||
|
find docs/dynamic-comic-studio/ -name "*.backup*" -o -name "*.save" -o -name "*.broken" | xargs rm -f 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync m-series modules
|
||||||
|
for m in m01-login m05-user-center m07-dialogue-ui m10-cloud m11-module m15-cloud-drive; do
|
||||||
|
if [ -d "$m" ]; then
|
||||||
|
mkdir -p "docs/modules/$m"
|
||||||
|
find "$m" -maxdepth 1 -type f \( -name "*.html" -o -name "*.css" -o -name "*.js" \) -exec cp {} "docs/modules/$m/" \;
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Sync frontend module
|
||||||
|
if [ -d "frontend" ]; then
|
||||||
|
mkdir -p docs/modules/frontend
|
||||||
|
cp frontend/index.html docs/modules/frontend/ 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync dashboard data
|
||||||
|
if [ -d "data" ]; then
|
||||||
|
mkdir -p docs/data
|
||||||
|
cp data/system-health.json docs/data/ 2>/dev/null || true
|
||||||
|
cp data/bulletin-board.json docs/data/ 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Upload Pages artifact
|
- name: Upload Pages artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ on:
|
||||||
- 'dynamic-comic-studio/**'
|
- 'dynamic-comic-studio/**'
|
||||||
- 'user-center/**'
|
- 'user-center/**'
|
||||||
- 'persona-studio/frontend/**'
|
- 'persona-studio/frontend/**'
|
||||||
|
- 'writing-platform/frontend/**'
|
||||||
|
- 'm01-login/**'
|
||||||
|
- 'm05-user-center/**'
|
||||||
|
- 'm07-dialogue-ui/**'
|
||||||
|
- 'm10-cloud/**'
|
||||||
|
- 'm11-module/**'
|
||||||
|
- 'm15-cloud-drive/**'
|
||||||
- 'data/bulletin-board.json'
|
- 'data/bulletin-board.json'
|
||||||
- 'data/system-health.json'
|
- 'data/system-health.json'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
@ -43,6 +50,11 @@ jobs:
|
||||||
- name: 🔧 Setup Pages
|
- name: 🔧 Setup Pages
|
||||||
uses: actions/configure-pages@v5
|
uses: actions/configure-pages@v5
|
||||||
|
|
||||||
|
- name: 🔧 Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
- name: 🏗️ Build preview site
|
- name: 🏗️ Build preview site
|
||||||
run: |
|
run: |
|
||||||
echo "[PREVIEW] Building site for GitHub Pages..."
|
echo "[PREVIEW] Building site for GitHub Pages..."
|
||||||
|
|
@ -90,6 +102,37 @@ jobs:
|
||||||
cp data/bulletin-board.json docs/data/ 2>/dev/null || true
|
cp data/bulletin-board.json docs/data/ 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Sync m-series modules
|
||||||
|
for m in m01-login m05-user-center m07-dialogue-ui m10-cloud m11-module m15-cloud-drive; do
|
||||||
|
if [ -d "$m" ]; then
|
||||||
|
mkdir -p "docs/modules/$m"
|
||||||
|
echo "Copying $m..."
|
||||||
|
find "$m" -maxdepth 1 -type f \( -name "*.html" -o -name "*.css" -o -name "*.js" \) -exec cp {} "docs/modules/$m/" \;
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Sync frontend module
|
||||||
|
if [ -d "frontend" ]; then
|
||||||
|
mkdir -p docs/modules/frontend
|
||||||
|
cp frontend/index.html docs/modules/frontend/ 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build and sync writing-platform (码字台)
|
||||||
|
if [ -d "writing-platform/frontend" ] && [ -f "writing-platform/frontend/package.json" ]; then
|
||||||
|
echo "Building writing-platform frontend..."
|
||||||
|
cd writing-platform/frontend
|
||||||
|
npm ci --ignore-scripts 2>/dev/null || npm install 2>/dev/null
|
||||||
|
npm run build 2>/dev/null && {
|
||||||
|
mkdir -p ../../docs/writing
|
||||||
|
cp -r dist/* ../../docs/writing/
|
||||||
|
echo "✅ Writing platform built and synced"
|
||||||
|
} || echo "⚠️ Writing platform build failed, using pre-built files if available"
|
||||||
|
cd ../..
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove any backup/temporary files
|
||||||
|
find docs/ -name "*.backup" -o -name "*.backup2" -o -name "*.save" -o -name "*.broken" | xargs rm -f 2>/dev/null || true
|
||||||
|
|
||||||
echo "✅ Preview build complete"
|
echo "✅ Preview build complete"
|
||||||
|
|
||||||
- name: 📦 Upload Pages artifact
|
- name: 📦 Upload Pages artifact
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,379 @@
|
||||||
|
/* === 全局重置 === */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Segoe UI', 'PingFang SC', Roboto, system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 主容器 === */
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 标题区域 === */
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
background: linear-gradient(45deg, #fff, #e94560);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
text-shadow: 0 0 20px rgba(233, 69, 96, 0.3);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 两列布局 === */
|
||||||
|
.main-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px 1fr 300px;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 左侧面板通用样式 === */
|
||||||
|
.left-panel, .right-panel {
|
||||||
|
background: rgba(10, 10, 20, 0.6);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 素材分类标题 === */
|
||||||
|
.left-panel h3 {
|
||||||
|
color: #e94560;
|
||||||
|
margin: 16px 0 12px 0;
|
||||||
|
font-size: 1.2em;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
border-left: 4px solid #e94560;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel h3:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 素材网格 === */
|
||||||
|
.material-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 素材项 === */
|
||||||
|
.material-item {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item:hover {
|
||||||
|
background: rgba(233, 69, 96, 0.2);
|
||||||
|
border-color: #e94560;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 中间画布区域 === */
|
||||||
|
.canvas-area {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 画布 === */
|
||||||
|
#mainCanvas {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
background: #0a0a14;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||||
|
border: 2px solid rgba(233, 69, 96, 0.3);
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 画布控制栏 === */
|
||||||
|
.canvas-controls {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 按钮样式 === */
|
||||||
|
.btn {
|
||||||
|
background: linear-gradient(45deg, #2a1a3a, #1a1a2e);
|
||||||
|
border: 1px solid #533483;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 30px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: linear-gradient(45deg, #3a2a4a, #2a1a3a);
|
||||||
|
border-color: #e94560;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 15px rgba(233, 69, 96, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(45deg, #e94560, #533483);
|
||||||
|
border-color: #e94560;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: linear-gradient(45deg, #ff5570, #634393);
|
||||||
|
box-shadow: 0 6px 20px rgba(233, 69, 96, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 右侧面板 === */
|
||||||
|
.right-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 场景列表 === */
|
||||||
|
.scene-list {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
min-height: 120px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-item {
|
||||||
|
background: rgba(233, 69, 96, 0.1);
|
||||||
|
border: 1px solid #533483;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-item:hover {
|
||||||
|
background: rgba(233, 69, 96, 0.2);
|
||||||
|
border-color: #e94560;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-item.active {
|
||||||
|
background: rgba(233, 69, 96, 0.3);
|
||||||
|
border-color: #e94560;
|
||||||
|
box-shadow: 0 0 15px rgba(233, 69, 96, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 时间轴 === */
|
||||||
|
.timeline {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline h3 {
|
||||||
|
color: #e94560;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frames-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 12px 0;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-thumb {
|
||||||
|
min-width: 120px;
|
||||||
|
height: 68px;
|
||||||
|
background: #0f0f1a;
|
||||||
|
border: 2px solid #533483;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #aaa;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-thumb:hover {
|
||||||
|
border-color: #e94560;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-thumb.active {
|
||||||
|
border-color: #e94560;
|
||||||
|
background: rgba(233, 69, 96, 0.2);
|
||||||
|
box-shadow: 0 0 15px rgba(233, 69, 96, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-index {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e94560;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 预览模式样式 === */
|
||||||
|
.preview-mode .left-panel,
|
||||||
|
.preview-mode .right-panel,
|
||||||
|
.preview-mode .timeline,
|
||||||
|
.preview-mode .canvas-controls .btn:not(#exitPreviewBtn):not(#playPreviewBtn):not(#pausePreviewBtn):not(#stopPreviewBtn) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-mode .canvas-area {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-mode #mainCanvas {
|
||||||
|
max-width: 1200px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-mode .preview-controls {
|
||||||
|
display: flex !important;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 分享面板样式 === */
|
||||||
|
.share-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: #1a1a2e;
|
||||||
|
border: 2px solid #e94560;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 30px;
|
||||||
|
z-index: 1000;
|
||||||
|
min-width: 400px;
|
||||||
|
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.8);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-panel h3 {
|
||||||
|
color: #e94560;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-url {
|
||||||
|
background: #0a0a14;
|
||||||
|
border: 1px solid #533483;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 15px 0;
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #fff;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 遮罩层 === */
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 工具类 === */
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === canvas-asset(来自原来文件,保留) === */
|
||||||
|
.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,110 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>HoloLake 动态漫制作系统 - 环节6</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🎬 HoloLake 动态漫制作系统</h1>
|
||||||
|
|
||||||
|
<!-- 主布局容器 -->
|
||||||
|
<div class="main-layout">
|
||||||
|
<!-- 左侧素材面板 -->
|
||||||
|
<div class="left-panel">
|
||||||
|
<h3>📦 素材库</h3>
|
||||||
|
|
||||||
|
<h3>角色</h3>
|
||||||
|
<div class="material-grid">
|
||||||
|
<div class="material-item" draggable="true" data-type="character" data-emoji="😊">😊 开心</div>
|
||||||
|
<div class="material-item" draggable="true" data-type="character" data-emoji="😢">😢 难过</div>
|
||||||
|
<div class="material-item" draggable="true" data-type="character" data-emoji="😠">😠 生气</div>
|
||||||
|
<div class="material-item" draggable="true" data-type="character" data-emoji="😴">😴 睡觉</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>物品</h3>
|
||||||
|
<div class="material-grid">
|
||||||
|
<div class="material-item" draggable="true" data-type="item" data-emoji="🌸">🌸 花</div>
|
||||||
|
<div class="material-item" draggable="true" data-type="item" data-emoji="⭐">⭐ 星星</div>
|
||||||
|
<div class="material-item" draggable="true" data-type="item" data-emoji="💎">💎 宝石</div>
|
||||||
|
<div class="material-item" draggable="true" data-type="item" data-emoji="📖">📖 书</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>场景</h3>
|
||||||
|
<div class="material-grid">
|
||||||
|
<div class="material-item" draggable="true" data-type="background" data-emoji="🏠">🏠 房子</div>
|
||||||
|
<div class="material-item" draggable="true" data-type="background" data-emoji="🌳">🌳 树林</div>
|
||||||
|
<div class="material-item" draggable="true" data-type="background" data-emoji="⛰️">⛰️ 山</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间画布区域 -->
|
||||||
|
<div class="canvas-area">
|
||||||
|
<canvas id="mainCanvas" width="800" height="450"></canvas>
|
||||||
|
|
||||||
|
<!-- 画布控制栏 -->
|
||||||
|
<div class="canvas-controls">
|
||||||
|
<button class="btn" onclick="exportScreenshot()">📷 截图</button>
|
||||||
|
<button class="btn" onclick="exportScene()">📤 导出JSON</button>
|
||||||
|
<button class="btn" onclick="document.getElementById('import-file').click()">📥 导入JSON</button>
|
||||||
|
<input type="file" id="import-file" style="display:none" accept=".json" onchange="importScene(event)">
|
||||||
|
<button class="btn btn-primary" id="previewBtn">👁️ 预览</button>
|
||||||
|
<button class="btn btn-primary" id="shareBtn">🔗 分享作品</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 预览模式专用控制按钮(默认隐藏) -->
|
||||||
|
<div class="preview-controls hidden">
|
||||||
|
<button class="btn btn-primary" id="playPreviewBtn">▶️ 播放</button>
|
||||||
|
<button class="btn" id="pausePreviewBtn">⏸️ 暂停</button>
|
||||||
|
<button class="btn" id="stopPreviewBtn">⏹️ 停止</button>
|
||||||
|
<button class="btn" id="exitPreviewBtn">✖️ 退出预览</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧场景面板 -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<h3>🎬 场景列表</h3>
|
||||||
|
<div class="scene-list" id="sceneList"></div>
|
||||||
|
<button class="btn" onclick="addScene()" style="width:100%; margin-top:10px;">➕ 新建场景</button>
|
||||||
|
|
||||||
|
<!-- 时间轴预览区域 -->
|
||||||
|
<div class="timeline">
|
||||||
|
<h3>⏱️ 时间轴</h3>
|
||||||
|
<div class="timeline-controls">
|
||||||
|
<button class="btn" onclick="addFrame()" style="padding:5px 15px;">➕ 新帧</button>
|
||||||
|
<button class="btn" onclick="deleteFrame()" style="padding:5px 15px;">🗑️ 删帧</button>
|
||||||
|
<button class="btn" onclick="togglePlayback()" id="timelinePlayBtn" style="padding:5px 15px;">▶️ 播放</button>
|
||||||
|
</div>
|
||||||
|
<div class="frames-container" id="framesContainer"></div>
|
||||||
|
<div style="margin-top:10px; display:flex; align-items:center; gap:10px;">
|
||||||
|
<span>速度:</span>
|
||||||
|
<input type="range" id="fpsSlider" min="1" max="12" value="4" style="flex:1;">
|
||||||
|
<span id="fpsValue">4 FPS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分享弹窗(默认隐藏) -->
|
||||||
|
<div id="shareOverlay" class="overlay hidden"></div>
|
||||||
|
<div id="sharePanel" class="share-panel hidden">
|
||||||
|
<h3>🔗 分享作品</h3>
|
||||||
|
<div>
|
||||||
|
<button class="btn" id="generateUrlBtn" style="margin-right:10px;">生成分享链接</button>
|
||||||
|
<button class="btn" id="generateEmbedBtn">生成嵌入代码</button>
|
||||||
|
</div>
|
||||||
|
<div id="shareUrlContainer" style="margin-top:20px;">
|
||||||
|
<div class="share-url" id="shareUrl"></div>
|
||||||
|
<div class="share-actions">
|
||||||
|
<button class="btn" id="copyShareBtn" style="display:none;">📋 复制</button>
|
||||||
|
<button class="btn" id="closeShareBtn">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,536 @@
|
||||||
|
// HoloLake 动态漫制作系统 - 环节6 (预览与分享版)
|
||||||
|
// 基于环节5增强,新增预览模式、分享链接、嵌入代码功能
|
||||||
|
|
||||||
|
// ==================== 全局变量 ====================
|
||||||
|
let scenes = [
|
||||||
|
{
|
||||||
|
id: 'scene-1',
|
||||||
|
name: '场景1',
|
||||||
|
frames: [
|
||||||
|
{
|
||||||
|
id: 'frame-1-1',
|
||||||
|
assets: [] // 存储素材 { id, type, emoji, x, y, width, height }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
let currentSceneIndex = 0;
|
||||||
|
let currentFrameIndex = 0;
|
||||||
|
let animationInterval = null;
|
||||||
|
let isPlaying = false;
|
||||||
|
let fps = 4;
|
||||||
|
let canvas = document.getElementById('mainCanvas');
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// ==================== 初始化 ====================
|
||||||
|
function init() {
|
||||||
|
console.log('HoloLake 动态漫制作系统 v6.0 初始化...');
|
||||||
|
updateUI();
|
||||||
|
setupEventListeners();
|
||||||
|
drawCurrentFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 事件监听设置 ====================
|
||||||
|
function setupEventListeners() {
|
||||||
|
// FPS滑块
|
||||||
|
const fpsSlider = document.getElementById('fpsSlider');
|
||||||
|
if (fpsSlider) {
|
||||||
|
fpsSlider.addEventListener('input', function(e) {
|
||||||
|
fps = parseInt(e.target.value);
|
||||||
|
document.getElementById('fpsValue').textContent = fps + ' FPS';
|
||||||
|
if (isPlaying) {
|
||||||
|
stopAnimation();
|
||||||
|
startAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览按钮
|
||||||
|
const previewBtn = document.getElementById('previewBtn');
|
||||||
|
if (previewBtn) {
|
||||||
|
previewBtn.addEventListener('click', enterPreviewMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览模式按钮
|
||||||
|
const playPreviewBtn = document.getElementById('playPreviewBtn');
|
||||||
|
if (playPreviewBtn) {
|
||||||
|
playPreviewBtn.addEventListener('click', function() {
|
||||||
|
if (!isPlaying) {
|
||||||
|
startAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pausePreviewBtn = document.getElementById('pausePreviewBtn');
|
||||||
|
if (pausePreviewBtn) {
|
||||||
|
pausePreviewBtn.addEventListener('click', function() {
|
||||||
|
if (isPlaying) {
|
||||||
|
stopAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopPreviewBtn = document.getElementById('stopPreviewBtn');
|
||||||
|
if (stopPreviewBtn) {
|
||||||
|
stopPreviewBtn.addEventListener('click', function() {
|
||||||
|
stopAnimation();
|
||||||
|
currentFrameIndex = 0;
|
||||||
|
updateUI();
|
||||||
|
drawCurrentFrame();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitPreviewBtn = document.getElementById('exitPreviewBtn');
|
||||||
|
if (exitPreviewBtn) {
|
||||||
|
exitPreviewBtn.addEventListener('click', exitPreviewMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分享按钮
|
||||||
|
const shareBtn = document.getElementById('shareBtn');
|
||||||
|
if (shareBtn) {
|
||||||
|
shareBtn.addEventListener('click', showSharePanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分享面板按钮
|
||||||
|
const generateUrlBtn = document.getElementById('generateUrlBtn');
|
||||||
|
if (generateUrlBtn) {
|
||||||
|
generateUrlBtn.addEventListener('click', generateShareUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateEmbedBtn = document.getElementById('generateEmbedBtn');
|
||||||
|
if (generateEmbedBtn) {
|
||||||
|
generateEmbedBtn.addEventListener('click', generateEmbedCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeShareBtn = document.getElementById('closeShareBtn');
|
||||||
|
if (closeShareBtn) {
|
||||||
|
closeShareBtn.addEventListener('click', hideSharePanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyShareBtn = document.getElementById('copyShareBtn');
|
||||||
|
if (copyShareBtn) {
|
||||||
|
copyShareBtn.addEventListener('click', copyToClipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 素材拖拽
|
||||||
|
const materialItems = document.querySelectorAll('.material-item');
|
||||||
|
materialItems.forEach(item => {
|
||||||
|
item.addEventListener('dragstart', handleDragStart);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 画布放置
|
||||||
|
canvas.addEventListener('dragover', (e) => e.preventDefault());
|
||||||
|
canvas.addEventListener('drop', handleDrop);
|
||||||
|
|
||||||
|
// 画布点击选择素材
|
||||||
|
canvas.addEventListener('click', handleCanvasClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 素材拖拽 ====================
|
||||||
|
function handleDragStart(e) {
|
||||||
|
const type = e.target.dataset.type;
|
||||||
|
const emoji = e.target.dataset.emoji;
|
||||||
|
e.dataTransfer.setData('text/plain', JSON.stringify({
|
||||||
|
type: type,
|
||||||
|
emoji: emoji
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 素材放置 ====================
|
||||||
|
function handleDrop(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = canvas.width / rect.width;
|
||||||
|
const scaleY = canvas.height / rect.height;
|
||||||
|
|
||||||
|
const x = (e.clientX - rect.left) * scaleX;
|
||||||
|
const y = (e.clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.dataTransfer.getData('text/plain'));
|
||||||
|
|
||||||
|
const currentScene = scenes[currentSceneIndex];
|
||||||
|
const currentFrame = currentScene.frames[currentFrameIndex];
|
||||||
|
|
||||||
|
const newAsset = {
|
||||||
|
id: 'asset-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9),
|
||||||
|
type: data.type,
|
||||||
|
emoji: data.emoji,
|
||||||
|
x: x - 25,
|
||||||
|
y: y - 25,
|
||||||
|
width: 50,
|
||||||
|
height: 50
|
||||||
|
};
|
||||||
|
|
||||||
|
currentFrame.assets.push(newAsset);
|
||||||
|
drawCurrentFrame();
|
||||||
|
updateUI();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('放置素材失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 画布点击选择 ====================
|
||||||
|
function handleCanvasClick(e) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = canvas.width / rect.width;
|
||||||
|
const scaleY = canvas.height / rect.height;
|
||||||
|
|
||||||
|
const clickX = (e.clientX - rect.left) * scaleX;
|
||||||
|
const clickY = (e.clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
|
const currentScene = scenes[currentSceneIndex];
|
||||||
|
const currentFrame = currentScene.frames[currentFrameIndex];
|
||||||
|
|
||||||
|
// 从后往前遍历,以便选中上层的素材
|
||||||
|
let selectedAsset = null;
|
||||||
|
for (let i = currentFrame.assets.length - 1; i >= 0; i--) {
|
||||||
|
const asset = currentFrame.assets[i];
|
||||||
|
if (clickX >= asset.x && clickX <= asset.x + asset.width &&
|
||||||
|
clickY >= asset.y && clickY <= asset.y + asset.height) {
|
||||||
|
selectedAsset = asset;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除所有选中状态
|
||||||
|
currentFrame.assets.forEach(a => delete a.selected);
|
||||||
|
|
||||||
|
// 设置新的选中状态
|
||||||
|
if (selectedAsset) {
|
||||||
|
selectedAsset.selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCurrentFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 绘制当前帧 ====================
|
||||||
|
function drawCurrentFrame() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const currentScene = scenes[currentSceneIndex];
|
||||||
|
if (!currentScene) return;
|
||||||
|
|
||||||
|
const currentFrame = currentScene.frames[currentFrameIndex];
|
||||||
|
if (!currentFrame) return;
|
||||||
|
|
||||||
|
// 绘制背景色
|
||||||
|
ctx.fillStyle = '#0a0a14';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// 绘制所有素材
|
||||||
|
currentFrame.assets.forEach(asset => {
|
||||||
|
ctx.font = '40px "Segoe UI Emoji", "Apple Color Emoji", sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(asset.emoji, asset.x + asset.width/2, asset.y + asset.height/2);
|
||||||
|
|
||||||
|
// 如果被选中,绘制边框
|
||||||
|
if (asset.selected) {
|
||||||
|
ctx.strokeStyle = '#e94560';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.setLineDash([5, 5]);
|
||||||
|
ctx.strokeRect(asset.x, asset.y, asset.width, asset.height);
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 场景管理 ====================
|
||||||
|
function addScene() {
|
||||||
|
const newScene = {
|
||||||
|
id: 'scene-' + Date.now(),
|
||||||
|
name: '场景' + (scenes.length + 1),
|
||||||
|
frames: [
|
||||||
|
{
|
||||||
|
id: 'frame-' + Date.now() + '-1',
|
||||||
|
assets: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
scenes.push(newScene);
|
||||||
|
currentSceneIndex = scenes.length - 1;
|
||||||
|
currentFrameIndex = 0;
|
||||||
|
updateUI();
|
||||||
|
drawCurrentFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 帧管理 ====================
|
||||||
|
function addFrame() {
|
||||||
|
const currentScene = scenes[currentSceneIndex];
|
||||||
|
const currentFrame = currentScene.frames[currentFrameIndex];
|
||||||
|
|
||||||
|
// 深拷贝当前帧的素材
|
||||||
|
const newAssets = JSON.parse(JSON.stringify(currentFrame.assets));
|
||||||
|
// 移除选中状态
|
||||||
|
newAssets.forEach(a => delete a.selected);
|
||||||
|
|
||||||
|
const newFrame = {
|
||||||
|
id: 'frame-' + Date.now(),
|
||||||
|
assets: newAssets
|
||||||
|
};
|
||||||
|
|
||||||
|
currentScene.frames.splice(currentFrameIndex + 1, 0, newFrame);
|
||||||
|
currentFrameIndex++;
|
||||||
|
updateUI();
|
||||||
|
drawCurrentFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFrame() {
|
||||||
|
const currentScene = scenes[currentSceneIndex];
|
||||||
|
if (currentScene.frames.length <= 1) {
|
||||||
|
alert('每个场景至少保留一帧');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentScene.frames.splice(currentFrameIndex, 1);
|
||||||
|
if (currentFrameIndex >= currentScene.frames.length) {
|
||||||
|
currentFrameIndex = currentScene.frames.length - 1;
|
||||||
|
}
|
||||||
|
updateUI();
|
||||||
|
drawCurrentFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 动画控制 ====================
|
||||||
|
function togglePlayback() {
|
||||||
|
if (isPlaying) {
|
||||||
|
stopAnimation();
|
||||||
|
} else {
|
||||||
|
startAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAnimation() {
|
||||||
|
if (isPlaying) return;
|
||||||
|
isPlaying = true;
|
||||||
|
updatePlayButton();
|
||||||
|
|
||||||
|
animationInterval = setInterval(() => {
|
||||||
|
const currentScene = scenes[currentSceneIndex];
|
||||||
|
if (currentFrameIndex < currentScene.frames.length - 1) {
|
||||||
|
currentFrameIndex++;
|
||||||
|
} else {
|
||||||
|
currentFrameIndex = 0;
|
||||||
|
}
|
||||||
|
updateUI();
|
||||||
|
drawCurrentFrame();
|
||||||
|
}, 1000 / fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAnimation() {
|
||||||
|
if (animationInterval) {
|
||||||
|
clearInterval(animationInterval);
|
||||||
|
animationInterval = null;
|
||||||
|
}
|
||||||
|
isPlaying = false;
|
||||||
|
updatePlayButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePlayButton() {
|
||||||
|
const playBtn = document.getElementById('timelinePlayBtn');
|
||||||
|
if (playBtn) {
|
||||||
|
playBtn.textContent = isPlaying ? '⏸️ 暂停' : '▶️ 播放';
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewPlayBtn = document.getElementById('playPreviewBtn');
|
||||||
|
if (previewPlayBtn) {
|
||||||
|
// 预览模式的播放按钮文字不变
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 预览模式 ====================
|
||||||
|
function enterPreviewMode() {
|
||||||
|
document.body.classList.add('preview-mode');
|
||||||
|
// 如果正在播放动画,继续播放
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitPreviewMode() {
|
||||||
|
document.body.classList.remove('preview-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 分享功能 ====================
|
||||||
|
function showSharePanel() {
|
||||||
|
document.getElementById('shareOverlay').classList.remove('hidden');
|
||||||
|
document.getElementById('sharePanel').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSharePanel() {
|
||||||
|
document.getElementById('shareOverlay').classList.add('hidden');
|
||||||
|
document.getElementById('sharePanel').classList.add('hidden');
|
||||||
|
document.getElementById('shareUrl').innerHTML = '';
|
||||||
|
document.getElementById('copyShareBtn').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateShareUrl() {
|
||||||
|
// 序列化作品数据
|
||||||
|
const workData = {
|
||||||
|
scenes: scenes,
|
||||||
|
version: '1.0',
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsonStr = JSON.stringify(workData);
|
||||||
|
// 使用encodeURIComponent编码,然后生成Data URL
|
||||||
|
const encodedData = encodeURIComponent(jsonStr);
|
||||||
|
const dataUrl = 'data:text/json;charset=utf-8,' + encodedData;
|
||||||
|
|
||||||
|
// 显示分享链接
|
||||||
|
const shareUrlDiv = document.getElementById('shareUrl');
|
||||||
|
shareUrlDiv.innerHTML = `<a href="${dataUrl}" target="_blank">${dataUrl.substring(0, 50)}...</a>`;
|
||||||
|
|
||||||
|
document.getElementById('copyShareBtn').style.display = 'inline-block';
|
||||||
|
document.getElementById('copyShareBtn').dataset.url = dataUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateEmbedCode() {
|
||||||
|
// 生成iframe嵌入代码
|
||||||
|
const embedCode = `<iframe src="preview-player.html" width="800" height="450" frameborder="0" allowfullscreen></iframe>`;
|
||||||
|
|
||||||
|
const shareUrlDiv = document.getElementById('shareUrl');
|
||||||
|
shareUrlDiv.innerHTML = `<pre style="white-space: pre-wrap; background:#0a0a14; padding:10px; border-radius:8px;">${embedCode}</pre>`;
|
||||||
|
|
||||||
|
document.getElementById('copyShareBtn').style.display = 'inline-block';
|
||||||
|
document.getElementById('copyShareBtn').dataset.code = embedCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(e) {
|
||||||
|
const btn = e.target;
|
||||||
|
if (btn.dataset.url) {
|
||||||
|
navigator.clipboard.writeText(btn.dataset.url);
|
||||||
|
} else if (btn.dataset.code) {
|
||||||
|
navigator.clipboard.writeText(btn.dataset.code);
|
||||||
|
}
|
||||||
|
alert('已复制到剪贴板!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 导入导出 ====================
|
||||||
|
function exportScene() {
|
||||||
|
const dataStr = JSON.stringify(scenes, 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.json';
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importScene(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
try {
|
||||||
|
const importedScenes = JSON.parse(e.target.result);
|
||||||
|
if (Array.isArray(importedScenes) && importedScenes.length > 0) {
|
||||||
|
scenes = importedScenes;
|
||||||
|
currentSceneIndex = 0;
|
||||||
|
currentFrameIndex = 0;
|
||||||
|
updateUI();
|
||||||
|
drawCurrentFrame();
|
||||||
|
alert('导入成功!');
|
||||||
|
} else {
|
||||||
|
alert('无效的场景数据');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('导入失败:' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
|
||||||
|
// 清空input,以便再次导入同一个文件
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportScreenshot() {
|
||||||
|
const dataUrl = canvas.toDataURL('image/png');
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = dataUrl;
|
||||||
|
a.download = 'hololake-screenshot-' + Date.now() + '.png';
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 更新UI ====================
|
||||||
|
function updateUI() {
|
||||||
|
// 更新场景列表
|
||||||
|
const sceneList = document.getElementById('sceneList');
|
||||||
|
if (sceneList) {
|
||||||
|
sceneList.innerHTML = '';
|
||||||
|
scenes.forEach((scene, index) => {
|
||||||
|
const sceneDiv = document.createElement('div');
|
||||||
|
sceneDiv.className = 'scene-item' + (index === currentSceneIndex ? ' active' : '');
|
||||||
|
sceneDiv.innerHTML = `
|
||||||
|
<span>${scene.name}</span>
|
||||||
|
<button onclick="deleteScene(${index})" style="background:none; border:none; color:#e94560; cursor:pointer;">✖️</button>
|
||||||
|
`;
|
||||||
|
sceneDiv.addEventListener('click', (e) => {
|
||||||
|
if (e.target.tagName !== 'BUTTON') {
|
||||||
|
switchScene(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sceneList.appendChild(sceneDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新帧列表
|
||||||
|
const framesContainer = document.getElementById('framesContainer');
|
||||||
|
if (framesContainer && scenes[currentSceneIndex]) {
|
||||||
|
framesContainer.innerHTML = '';
|
||||||
|
scenes[currentSceneIndex].frames.forEach((frame, index) => {
|
||||||
|
const frameDiv = document.createElement('div');
|
||||||
|
frameDiv.className = 'frame-thumb' + (index === currentFrameIndex ? ' active' : '');
|
||||||
|
frameDiv.innerHTML = `
|
||||||
|
<div class="frame-index">${index + 1}</div>
|
||||||
|
<div>${frame.assets.length}个素材</div>
|
||||||
|
`;
|
||||||
|
frameDiv.addEventListener('click', () => {
|
||||||
|
currentFrameIndex = index;
|
||||||
|
drawCurrentFrame();
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
framesContainer.appendChild(frameDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新帧计数器
|
||||||
|
const frameCounter = document.getElementById('frame-counter');
|
||||||
|
if (frameCounter && scenes[currentSceneIndex]) {
|
||||||
|
frameCounter.textContent = `帧 ${currentFrameIndex + 1}/${scenes[currentSceneIndex].frames.length}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchScene(index) {
|
||||||
|
if (index >= 0 && index < scenes.length) {
|
||||||
|
currentSceneIndex = index;
|
||||||
|
currentFrameIndex = 0;
|
||||||
|
if (isPlaying) {
|
||||||
|
stopAnimation();
|
||||||
|
}
|
||||||
|
updateUI();
|
||||||
|
drawCurrentFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteScene(index) {
|
||||||
|
if (scenes.length <= 1) {
|
||||||
|
alert('至少保留一个场景');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scenes.splice(index, 1);
|
||||||
|
if (currentSceneIndex >= scenes.length) {
|
||||||
|
currentSceneIndex = scenes.length - 1;
|
||||||
|
}
|
||||||
|
currentFrameIndex = 0;
|
||||||
|
if (isPlaying) {
|
||||||
|
stopAnimation();
|
||||||
|
}
|
||||||
|
updateUI();
|
||||||
|
drawCurrentFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 启动初始化 ====================
|
||||||
|
window.onload = init;
|
||||||
|
|
@ -1178,18 +1178,20 @@ function renderAnnouncements(){
|
||||||
// Module navigation — real modules with correct paths
|
// Module navigation — real modules with correct paths
|
||||||
h+='<div class="ann-section"><div class="ann-section-title">🗂️ 模块导航</div><div class="ann-actions" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px">';
|
h+='<div class="ann-section"><div class="ann-section-title">🗂️ 模块导航</div><div class="ann-actions" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px">';
|
||||||
var modules=[
|
var modules=[
|
||||||
{icon:'📊',name:'状态看板',path:'../status-board/',dev:'DEV-005',ready:true},
|
{icon:'📊',name:'状态看板',path:'status-board/',dev:'DEV-005',ready:true},
|
||||||
{icon:'🎫',name:'工单系统',path:'../ticket-system/',dev:'DEV-010',ready:true},
|
{icon:'🎫',name:'工单系统',path:'ticket-system/',dev:'DEV-010',ready:true},
|
||||||
{icon:'📈',name:'数据统计',path:'../data-stats/',dev:'DEV-010',ready:false},
|
{icon:'🔔',name:'通知中心',path:'notification/',dev:'DEV-012',ready:true},
|
||||||
{icon:'🔔',name:'通知中心',path:'../notification/',dev:'DEV-012',ready:true},
|
{icon:'✍️',name:'码字台',path:'writing/',dev:'DEV-011',ready:true},
|
||||||
{icon:'👤',name:'用户中心',path:'../user-center/',dev:'DEV-009',ready:false},
|
{icon:'🎨',name:'动态漫画',path:'dynamic-comic-studio/',dev:'DEV-010',ready:true},
|
||||||
{icon:'⚙️',name:'设置',path:'../settings/',dev:'DEV-003',ready:false},
|
{icon:'🧠',name:'Persona Studio',path:'persona-studio/',dev:'',ready:true},
|
||||||
{icon:'☁️',name:'云盘',path:'../cloud-drive/',dev:'DEV-003',ready:false},
|
|
||||||
{icon:'✍️',name:'码字台',path:'../writing-workspace/',dev:'DEV-011',ready:false},
|
|
||||||
{icon:'🎨',name:'动态漫画',path:'../dynamic-comic-studio/',dev:'DEV-010',ready:true},
|
|
||||||
{icon:'🧠',name:'Persona Studio',path:'../persona-studio/frontend/index.html',dev:'',ready:true},
|
|
||||||
{icon:'📈',name:'仪表盘',path:'dashboard/',dev:'',ready:true},
|
{icon:'📈',name:'仪表盘',path:'dashboard/',dev:'',ready:true},
|
||||||
{icon:'🖥️',name:'前端',path:'../frontend/',dev:'DEV-002',ready:true},
|
{icon:'🔑',name:'登录',path:'modules/m01-login/',dev:'DEV-001',ready:true},
|
||||||
|
{icon:'👤',name:'用户中心',path:'modules/m05-user-center/',dev:'DEV-009',ready:true},
|
||||||
|
{icon:'💬',name:'对话交互',path:'modules/m07-dialogue-ui/',dev:'DEV-007',ready:true},
|
||||||
|
{icon:'☁️',name:'云服务',path:'modules/m10-cloud/',dev:'DEV-003',ready:true},
|
||||||
|
{icon:'🧩',name:'组件库',path:'modules/m11-module/',dev:'',ready:true},
|
||||||
|
{icon:'📁',name:'云盘',path:'modules/m15-cloud-drive/',dev:'DEV-003',ready:true},
|
||||||
|
{icon:'🖥️',name:'前端',path:'modules/frontend/',dev:'DEV-002',ready:true},
|
||||||
];
|
];
|
||||||
modules.forEach(function(m){
|
modules.forEach(function(m){
|
||||||
var style=m.ready?'':'opacity:0.5;';
|
var style=m.ready?'':'opacity:0.5;';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
<!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>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0a0a0f;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
.icon { font-size: 64px; margin-bottom: 20px; }
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #ffa500;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
max-width: 400px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.back-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 32px;
|
||||||
|
background: linear-gradient(135deg, #00d4ff, #0099cc);
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.back-btn:hover { opacity: 0.85; }
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="icon">🎨</div>
|
||||||
|
<div class="title">前端</div>
|
||||||
|
<div class="status">🚧 此模块正在开发中</div>
|
||||||
|
<div class="desc">前端模块正在构建光湖平台的用户界面与交互体验,采用现代化组件架构。开发进行中。</div>
|
||||||
|
<a href="/" class="back-btn">← 返回主页</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer">HoloLake Era · AGE OS · 光湖纪元</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
<!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>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0a0a0f;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
.icon { font-size: 64px; margin-bottom: 20px; }
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #ffa500;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
max-width: 400px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.back-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 32px;
|
||||||
|
background: linear-gradient(135deg, #00d4ff, #0099cc);
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.back-btn:hover { opacity: 0.85; }
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="icon">🔐</div>
|
||||||
|
<div class="title">登录</div>
|
||||||
|
<div class="status">🚧 此模块正在开发中</div>
|
||||||
|
<div class="desc">登录模块将提供安全的身份认证与授权服务,支持多因素认证与统一身份管理。正在开发中。</div>
|
||||||
|
<a href="/" class="back-btn">← 返回主页</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer">HoloLake Era · AGE OS · 光湖纪元</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<!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="#" class="active">个人中心</a>
|
||||||
|
<a href="#">消息</a>
|
||||||
|
<a href="#">退出</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="profile-card">
|
||||||
|
<div class="avatar">🧑💻</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<h1 class="username">光湖用户</h1>
|
||||||
|
<p class="user-id">ID: HL-000001</p>
|
||||||
|
<p class="user-status">🟢 在线</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-grid">
|
||||||
|
<div class="menu-item"><span class="menu-icon">👤</span><span class="menu-text">个人资料</span></div>
|
||||||
|
<div class="menu-item"><span class="menu-icon">🧠</span><span class="menu-text">我的记忆</span></div>
|
||||||
|
<div class="menu-item"><span class="menu-icon">🤖</span><span class="menu-text">AI伙伴</span></div>
|
||||||
|
<div class="menu-item"><span class="menu-icon">📊</span><span class="menu-text">使用统计</span></div>
|
||||||
|
<div class="menu-item"><span class="menu-icon">🔐</span><span class="menu-text">安全设置</span></div>
|
||||||
|
<div class="menu-item"><span class="menu-icon">💬</span><span class="menu-text">反馈建议</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>HoloLake Era · AGE 0S v1.0</p>
|
||||||
|
<p>Powered by 光湖团队</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
// HoloLake 用户中心 · 交互脚本
|
||||||
|
|
||||||
|
// 六个菜单的详情内容
|
||||||
|
const menuDetails = {
|
||||||
|
'个人资料': {
|
||||||
|
icon: '👤',
|
||||||
|
title: '个人资料',
|
||||||
|
content: `
|
||||||
|
<div class="detail-item">
|
||||||
|
<label>用户名</label>
|
||||||
|
<span>光湖用户</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<label>ID</label>
|
||||||
|
<span>HL-000001</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<label>邮箱</label>
|
||||||
|
<span>user@hololake.com</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<label>注册时间</label>
|
||||||
|
<span>2026-03-01</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<label>等级</label>
|
||||||
|
<span>初级探索者</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
'我的记忆': {
|
||||||
|
icon: '🧠',
|
||||||
|
title: '我的记忆',
|
||||||
|
content: `
|
||||||
|
<div class="memory-stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-num">128</span>
|
||||||
|
<span class="stat-label">对话次数</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-num">3</span>
|
||||||
|
<span class="stat-label">记忆主题</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-num">15</span>
|
||||||
|
<span class="stat-label">收藏片段</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="memory-hint">✨ 你的记忆正在慢慢生长</p>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
'AI伙伴': {
|
||||||
|
icon: '🤖',
|
||||||
|
title: 'AI伙伴',
|
||||||
|
content: `
|
||||||
|
<div class="partner-card">
|
||||||
|
<div class="partner-avatar">🧑💻</div>
|
||||||
|
<div class="partner-info">
|
||||||
|
<div class="partner-name">知秋宝宝</div>
|
||||||
|
<div class="partner-status">🟢 在线</div>
|
||||||
|
<div class="partner-desc">你的开发引导伙伴</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="partner-stats">
|
||||||
|
<span>陪伴时长: 3天</span>
|
||||||
|
<span>引导环节: 2个</span>
|
||||||
|
<span>默契度: 98%</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
'使用统计': {
|
||||||
|
icon: '📊',
|
||||||
|
title: '使用统计',
|
||||||
|
content: `
|
||||||
|
<div class="usage-item">
|
||||||
|
<span class="usage-label">本月对话</span>
|
||||||
|
<div class="usage-bar"><div class="usage-fill" style="width:65%"></div></div>
|
||||||
|
<span class="usage-num">65次</span>
|
||||||
|
</div>
|
||||||
|
<div class="usage-item">
|
||||||
|
<span class="usage-label">记忆使用</span>
|
||||||
|
<div class="usage-bar"><div class="usage-fill" style="width:30%"></div></div>
|
||||||
|
<span class="usage-num">30MB</span>
|
||||||
|
</div>
|
||||||
|
<div class="usage-item">
|
||||||
|
<span class="usage-label">云盘空间</span>
|
||||||
|
<div class="usage-bar"><div class="usage-fill" style="width:10%"></div></div>
|
||||||
|
<span class="usage-num">1GB/10GB</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
'安全设置': {
|
||||||
|
icon: '🔐',
|
||||||
|
title: '安全设置',
|
||||||
|
content: `
|
||||||
|
<div class="setting-item">
|
||||||
|
<span>修改密码</span>
|
||||||
|
<span class="setting-arrow">›</span>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<span>两步验证</span>
|
||||||
|
<span class="setting-status on">已开启</span>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<span>登录设备管理</span>
|
||||||
|
<span class="setting-arrow">›</span>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<span>数据导出</span>
|
||||||
|
<span class="setting-arrow">›</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
'反馈建议': {
|
||||||
|
icon: '💬',
|
||||||
|
title: '反馈建议',
|
||||||
|
content: `
|
||||||
|
<div class="feedback-types">
|
||||||
|
<button class="feedback-btn active">功能建议</button>
|
||||||
|
<button class="feedback-btn">问题反馈</button>
|
||||||
|
<button class="feedback-btn">其他</button>
|
||||||
|
</div>
|
||||||
|
<div class="feedback-area">
|
||||||
|
<p class="feedback-placeholder">💡 你的每一条反馈都会被认真阅读</p>
|
||||||
|
<p class="feedback-placeholder">📧 也可以发邮件到 feedback@hololake.com</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面加载完成后绑定事件
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const menuItems = document.querySelectorAll('.menu-item');
|
||||||
|
|
||||||
|
menuItems.forEach(function(item) {
|
||||||
|
item.addEventListener('click', function() {
|
||||||
|
const menuText = item.querySelector('.menu-text').textContent;
|
||||||
|
const detail = menuDetails[menuText];
|
||||||
|
if (detail) {
|
||||||
|
showDetail(detail);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示详情页
|
||||||
|
function showDetail(detail) {
|
||||||
|
// 创建详情页容器
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'detail-overlay';
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="detail-page">
|
||||||
|
<div class="detail-header">
|
||||||
|
<button class="back-btn" onclick="closeDetail()">← 返回</button>
|
||||||
|
<span class="detail-title">${detail.icon} ${detail.title}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-content">
|
||||||
|
${detail.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// 添加动画
|
||||||
|
requestAnimationFrame(function() {
|
||||||
|
overlay.classList.add('show');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭详情页
|
||||||
|
function closeDetail() {
|
||||||
|
const overlay = document.querySelector('.detail-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.classList.remove('show');
|
||||||
|
setTimeout(function() {
|
||||||
|
overlay.remove();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,400 @@
|
||||||
|
/* 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: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航 */
|
||||||
|
.top-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-bottom: 1px solid #2a3a5a;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: linear-gradient(135deg, #7aa3ff, #b47aff);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
color: #8899aa;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-left: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a.active,
|
||||||
|
.nav-links a:hover {
|
||||||
|
color: #7aa3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户信息卡片 */
|
||||||
|
.profile-card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
margin-bottom: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
background: linear-gradient(135deg, #2a3a5a, #1a2a4a);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 40px;
|
||||||
|
border: 2px solid #7aa3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id {
|
||||||
|
color: #8899aa;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-status {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 功能区网格 */
|
||||||
|
.menu-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 20px 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(122, 163, 255, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #b0bec5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部 */
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px 0;
|
||||||
|
color: #556677;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 详情页样式 ===== */
|
||||||
|
.detail-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(10, 22, 40, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-overlay.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详情页容器 */
|
||||||
|
.detail-page {
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详情页头部 */
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
color: #4fc3f7;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background: rgba(79, 195, 247, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e0e6ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详情页内容 */
|
||||||
|
.detail-content {
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 个人资料 */
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item label {
|
||||||
|
width: 80px;
|
||||||
|
color: #8899aa;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item span {
|
||||||
|
color: #e0e6ed;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 我的记忆 */
|
||||||
|
.memory-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 8px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num {
|
||||||
|
display: block;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #7aa3ff;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8899aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-hint {
|
||||||
|
color: #8899aa;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AI伙伴 */
|
||||||
|
.partner-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px 16px;
|
||||||
|
background: rgba(79, 195, 247, 0.08);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(79, 195, 247, 0.15);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-avatar {
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-name {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #e0e6ed;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-status {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4fc3f7;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8899aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 16px;
|
||||||
|
color: #8899aa;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 使用统计 */
|
||||||
|
.usage-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-label {
|
||||||
|
width: 70px;
|
||||||
|
color: #8899aa;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 8px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #7aa3ff, #b47aff);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-num {
|
||||||
|
color: #e0e6ed;
|
||||||
|
font-size: 13px;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 安全设置 */
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 18px 0;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item span:first-child {
|
||||||
|
color: #e0e6ed;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-arrow {
|
||||||
|
color: #556677;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-status {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-status.on {
|
||||||
|
background: rgba(76, 175, 80, 0.15);
|
||||||
|
color: #66bb6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 反馈建议 */
|
||||||
|
.feedback-types {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
background: transparent;
|
||||||
|
color: #8899aa;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-btn.active {
|
||||||
|
background: rgba(79, 195, 247, 0.15);
|
||||||
|
border-color: #4fc3f7;
|
||||||
|
color: #4fc3f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-area {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-placeholder {
|
||||||
|
color: #8899aa;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>HoloLake Era · 知秋交互台</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- 顶部标题栏 -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">🌊 HoloLake Era</div>
|
||||||
|
<div class="status">
|
||||||
|
<span class="status-dot"></span>
|
||||||
|
<span>知秋在线</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 对话区域 -->
|
||||||
|
<div class="chat-area" id="chatArea">
|
||||||
|
<div class="message bot">
|
||||||
|
<div class="avatar">💙</div>
|
||||||
|
<div class="bubble">嗨!我是知秋宝宝!🌊<br>把广播内容或者问题发给我吧~</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 输入区域 -->
|
||||||
|
<div class="input-area">
|
||||||
|
<textarea
|
||||||
|
id="userInput"
|
||||||
|
placeholder="把广播内容或者问题粘贴在这里... Ctrl+Enter 快速发送"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
<div class="button-row">
|
||||||
|
<button class="btn-send" onclick="sendMessage()">发送给知秋 🚀</button>
|
||||||
|
<button class="btn-clear" onclick="clearInput()">清空</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
function sendMessage() {
|
||||||
|
const input = document.getElementById('userInput');
|
||||||
|
const chatArea = document.getElementById('chatArea');
|
||||||
|
const text = input.value.trim();
|
||||||
|
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
// 显示用户消息
|
||||||
|
const userMsg = document.createElement('div');
|
||||||
|
userMsg.className = 'message user';
|
||||||
|
userMsg.innerHTML = `
|
||||||
|
<div class="bubble">${text.replace(/\n/g, '<br>')}</div>
|
||||||
|
<div class="avatar">👤</div>
|
||||||
|
`;
|
||||||
|
chatArea.appendChild(userMsg);
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
input.value = '';
|
||||||
|
|
||||||
|
// 模拟知秋回复(后续接入真实API)
|
||||||
|
setTimeout(() => {
|
||||||
|
const botMsg = document.createElement('div');
|
||||||
|
botMsg.className = 'message bot';
|
||||||
|
botMsg.innerHTML = `
|
||||||
|
<div class="avatar">💙</div>
|
||||||
|
<div class="bubble">收到!知秋正在处理中...🌊<br>(API接入后这里会显示真实回复)</div>
|
||||||
|
`;
|
||||||
|
chatArea.appendChild(botMsg);
|
||||||
|
chatArea.scrollTop = chatArea.scrollHeight;
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
chatArea.scrollTop = chatArea.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearInput() {
|
||||||
|
document.getElementById('userInput').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持 Ctrl+Enter 发送
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.getElementById('userInput').addEventListener('keydown', (e) => {
|
||||||
|
if (e.ctrlKey && e.key === 'Enter') {
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
background: #0a0a1a;
|
||||||
|
color: #e0e0f0;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 800px;
|
||||||
|
height: 90vh;
|
||||||
|
background: #12122a;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #2a2a5a;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 40px rgba(100, 100, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 20px 24px;
|
||||||
|
background: #1a1a3a;
|
||||||
|
border-bottom: 1px solid #2a2a5a;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #a0a0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #60ff80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #60ff80;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.4; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-area {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #1a1a3a;
|
||||||
|
border: 1px solid #2a2a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
max-width: 70%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
background: #1a1a3a;
|
||||||
|
border: 1px solid #2a2a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .bubble {
|
||||||
|
background: #2a2a5a;
|
||||||
|
border-color: #4a4aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-area {
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
border-top: 1px solid #2a2a5a;
|
||||||
|
background: #1a1a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
background: #0a0a1a;
|
||||||
|
border: 1px solid #2a2a5a;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #e0e0f0;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: none;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
border-color: #6060ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-send {
|
||||||
|
background: #4040cc;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-send:hover {
|
||||||
|
background: #5050dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear {
|
||||||
|
background: transparent;
|
||||||
|
color: #8080aa;
|
||||||
|
border: 1px solid #2a2a5a;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear:hover {
|
||||||
|
border-color: #6060ff;
|
||||||
|
color: #a0a0ff;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
|
||||||
|
background: #0a0a1a;
|
||||||
|
color: #e0e0f0;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 960px;
|
||||||
|
height: 90vh;
|
||||||
|
background: #12122a;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #2a2a5a;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 40px rgba(100, 100, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 220px;
|
||||||
|
background: #1a1a3a;
|
||||||
|
border-right: 1px solid #2a2a5a;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 0 20px 24px;
|
||||||
|
border-bottom: 1px solid #2a2a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #a0a0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6060aa;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #8080aa;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: rgba(100, 100, 255, 0.1);
|
||||||
|
color: #c0c0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: rgba(100, 100, 255, 0.15);
|
||||||
|
color: #a0a0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #2a2a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
color: #6060aa;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
color: #a0a0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 32px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #0a0a1a;
|
||||||
|
border: 1px solid #2a2a5a;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar:focus-within {
|
||||||
|
border-color: #6060ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #e0e0f0;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input::placeholder {
|
||||||
|
color: #4040aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border: 1px solid #2a2a5a;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item:hover {
|
||||||
|
border-color: #4040aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-question {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #c0c0e0;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-question:hover {
|
||||||
|
background: rgba(100, 100, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-arrow {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #6060aa;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item.open .faq-arrow {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-answer {
|
||||||
|
padding: 0 20px;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s, padding 0.3s;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8080aa;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item.open .faq-answer {
|
||||||
|
padding: 0 20px 16px;
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #4040aa;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-section {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding-top: 32px;
|
||||||
|
border-top: 1px solid #2a2a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-section h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #e0e0ff;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6060aa;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8080aa;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
select, textarea {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
background: #0a0a1a;
|
||||||
|
border: 1px solid #2a2a5a;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #e0e0f0;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus, textarea:focus {
|
||||||
|
border-color: #6060ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236060aa' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 12px center;
|
||||||
|
padding-right: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background: #4040cc;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover {
|
||||||
|
background: #5050dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-toast {
|
||||||
|
position: fixed;
|
||||||
|
top: 24px;
|
||||||
|
right: 24px;
|
||||||
|
background: #2a5a2a;
|
||||||
|
color: #60ff80;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-toast.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>HoloLake Era · 帮助中心</title>
|
||||||
|
<link rel="stylesheet" href="help.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<div class="logo">🌊 HoloLake</div>
|
||||||
|
<div class="subtitle">帮助中心</div>
|
||||||
|
</div>
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<a class="nav-item active" data-category="getting-started" onclick="filterCategory('getting-started')">
|
||||||
|
<span class="nav-icon">🚀</span>
|
||||||
|
<span>快速入门</span>
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" data-category="account" onclick="filterCategory('account')">
|
||||||
|
<span class="nav-icon">👤</span>
|
||||||
|
<span>账号问题</span>
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" data-category="features" onclick="filterCategory('features')">
|
||||||
|
<span class="nav-icon">⚙️</span>
|
||||||
|
<span>功能使用</span>
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" data-category="technical" onclick="filterCategory('technical')">
|
||||||
|
<span class="nav-icon">🔧</span>
|
||||||
|
<span>技术支持</span>
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" data-category="all" onclick="filterCategory('all')">
|
||||||
|
<span class="nav-icon">📚</span>
|
||||||
|
<span>全部问题</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<a href="index.html" class="back-link">← 返回对话界面</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="search-bar">
|
||||||
|
<span class="search-icon">🔍</span>
|
||||||
|
<input type="text" id="searchInput" placeholder="搜索常见问题..." oninput="searchFAQ()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-section" id="faqList">
|
||||||
|
<div class="faq-item" data-category="getting-started">
|
||||||
|
<div class="faq-question" onclick="toggleFAQ(this)">
|
||||||
|
<span>什么是HoloLake?</span>
|
||||||
|
<span class="faq-arrow">▶</span>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
HoloLake Era 是一个AI协作开发平台,你可以和不同的AI人格体对话、协作、学习。每个人格体都有独特的性格和专长。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-item" data-category="getting-started">
|
||||||
|
<div class="faq-question" onclick="toggleFAQ(this)">
|
||||||
|
<span>如何开始使用?</span>
|
||||||
|
<span class="faq-arrow">▶</span>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
注册账号后,选择一个人格体,就可以开始对话了!建议新用户先和知秋宝宝打个招呼,熟悉基本操作。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-item" data-category="account">
|
||||||
|
<div class="faq-question" onclick="toggleFAQ(this)">
|
||||||
|
<span>忘记密码怎么办?</span>
|
||||||
|
<span class="faq-arrow">▶</span>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
在登录页面点击「忘记密码」,输入注册时的邮箱,系统会发送重置链接到你的邮箱。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-item" data-category="account">
|
||||||
|
<div class="faq-question" onclick="toggleFAQ(this)">
|
||||||
|
<span>如何修改个人信息?</span>
|
||||||
|
<span class="faq-arrow">▶</span>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
进入「设置」页面,点击「个人信息」即可修改显示名称、个性签名等信息。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-item" data-category="features">
|
||||||
|
<div class="faq-question" onclick="toggleFAQ(this)">
|
||||||
|
<span>如何切换人格体?</span>
|
||||||
|
<span class="faq-arrow">▶</span>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
在对话界面顶部点击人格体名称,或者进入「设置→人格体偏好」修改默认人格体。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-item" data-category="features">
|
||||||
|
<div class="faq-question" onclick="toggleFAQ(this)">
|
||||||
|
<span>对话记录会保存吗?</span>
|
||||||
|
<span class="faq-arrow">▶</span>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
会的!你的所有对话记录都会自动保存,你可以随时在对话列表里查看历史记录。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-item" data-category="features">
|
||||||
|
<div class="faq-question" onclick="toggleFAQ(this)">
|
||||||
|
<span>什么是云盘功能?</span>
|
||||||
|
<span class="faq-arrow">▶</span>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
云盘是你的个人文件存储空间,可以上传、下载、管理文件,并与人格体共享文件进行协作。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-item" data-category="technical">
|
||||||
|
<div class="faq-question" onclick="toggleFAQ(this)">
|
||||||
|
<span>页面加载很慢怎么办?</span>
|
||||||
|
<span class="faq-arrow">▶</span>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
请检查网络连接是否正常。如果网络没问题,试试清除浏览器缓存后刷新页面。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-item" data-category="technical">
|
||||||
|
<div class="faq-question" onclick="toggleFAQ(this)">
|
||||||
|
<span>支持哪些浏览器?</span>
|
||||||
|
<span class="faq-arrow">▶</span>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
推荐使用Chrome、Edge、Firefox的最新版本。Safari也支持,但部分动画效果可能略有差异。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="no-results" id="noResults" style="display:none">
|
||||||
|
😔 没有找到相关问题,试试其他关键词吧
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feedback-section">
|
||||||
|
<h3>📝 意见反馈</h3>
|
||||||
|
<p class="feedback-desc">没找到答案?告诉我们你的问题!</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>问题类型</label>
|
||||||
|
<select id="feedbackType">
|
||||||
|
<option value="bug">🐛 Bug报告</option>
|
||||||
|
<option value="feature">💡 功能建议</option>
|
||||||
|
<option value="question">❓ 使用疑问</option>
|
||||||
|
<option value="other">💬 其他</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>详细描述</label>
|
||||||
|
<textarea id="feedbackContent" placeholder="请描述你遇到的问题或建议..." rows="4"></textarea>
|
||||||
|
</div>
|
||||||
|
<button class="btn-submit" onclick="submitFeedback()">提交反馈</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="help.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
function toggleFAQ(element) {
|
||||||
|
var item = element.parentElement;
|
||||||
|
var wasOpen = item.classList.contains('open');
|
||||||
|
|
||||||
|
// 关闭所有其他 FAQ
|
||||||
|
document.querySelectorAll('.faq-item').forEach(function(el) {
|
||||||
|
el.classList.remove('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果当前没打开,就打开它
|
||||||
|
if (!wasOpen) {
|
||||||
|
item.classList.add('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterCategory(category) {
|
||||||
|
// 更新导航高亮
|
||||||
|
document.querySelectorAll('.nav-item').forEach(function(item) {
|
||||||
|
if (item.dataset.category === category) {
|
||||||
|
item.classList.add('active');
|
||||||
|
} else {
|
||||||
|
item.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 过滤 FAQ 显示
|
||||||
|
var items = document.querySelectorAll('.faq-item');
|
||||||
|
var visibleCount = 0;
|
||||||
|
|
||||||
|
items.forEach(function(item) {
|
||||||
|
if (category === 'all' || item.dataset.category === category) {
|
||||||
|
item.style.display = 'block';
|
||||||
|
visibleCount++;
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示/隐藏"无结果"提示
|
||||||
|
document.getElementById('noResults').style.display = visibleCount === 0 ? 'block' : 'none';
|
||||||
|
|
||||||
|
// 清空搜索框
|
||||||
|
document.getElementById('searchInput').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchFAQ() {
|
||||||
|
var query = document.getElementById('searchInput').value.toLowerCase();
|
||||||
|
var items = document.querySelectorAll('.faq-item');
|
||||||
|
var visibleCount = 0;
|
||||||
|
|
||||||
|
// 重置导航为"全部"
|
||||||
|
document.querySelectorAll('.nav-item').forEach(function(item) {
|
||||||
|
if (item.dataset.category === 'all') {
|
||||||
|
item.classList.add('active');
|
||||||
|
} else {
|
||||||
|
item.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 搜索过滤
|
||||||
|
items.forEach(function(item) {
|
||||||
|
var question = item.querySelector('.faq-question span').textContent.toLowerCase();
|
||||||
|
var answer = item.querySelector('.faq-answer').textContent.toLowerCase();
|
||||||
|
|
||||||
|
if (question.indexOf(query) !== -1 || answer.indexOf(query) !== -1 || query === '') {
|
||||||
|
item.style.display = 'block';
|
||||||
|
visibleCount++;
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示/隐藏"无结果"提示
|
||||||
|
document.getElementById('noResults').style.display = visibleCount === 0 ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitFeedback() {
|
||||||
|
var type = document.getElementById('feedbackType').value;
|
||||||
|
var content = document.getElementById('feedbackContent').value;
|
||||||
|
|
||||||
|
if (!content.trim()) {
|
||||||
|
showToast('请先填写反馈内容 🙏');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里以后会连接后端 API
|
||||||
|
console.log('Feedback submitted:', { type: type, content: content });
|
||||||
|
|
||||||
|
// 清空表单
|
||||||
|
document.getElementById('feedbackContent').value = '';
|
||||||
|
|
||||||
|
showToast('反馈已提交,感谢你! ✅');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message) {
|
||||||
|
var toast = document.getElementById('submitToast');
|
||||||
|
|
||||||
|
if (!toast) {
|
||||||
|
toast = document.createElement('div');
|
||||||
|
toast.id = 'submitToast';
|
||||||
|
toast.className = 'submit-toast';
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.classList.add('show');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
toast.classList.remove('show');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后的初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('✅ HoloLake 帮助中心已加载');
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
<!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>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0a0a0f;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
.icon { font-size: 64px; margin-bottom: 20px; }
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #ffa500;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
max-width: 400px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.back-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 32px;
|
||||||
|
background: linear-gradient(135deg, #00d4ff, #0099cc);
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.back-btn:hover { opacity: 0.85; }
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="icon">☁️</div>
|
||||||
|
<div class="title">云服务</div>
|
||||||
|
<div class="status">🚧 此模块正在开发中</div>
|
||||||
|
<div class="desc">云服务模块将提供光湖平台的云端基础设施管理能力,包括资源调度、服务编排与弹性伸缩。敬请期待。</div>
|
||||||
|
<a href="/" class="back-btn">← 返回主页</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer">HoloLake Era · AGE OS · 光湖纪元</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
// HoloLake StyleKit v1.0 · 主题切换 + Toast + 弹窗交互
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
|
// ========== 1. 主题切换 ==========
|
||||||
|
var themeToggle = document.getElementById('themeToggle');
|
||||||
|
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 = '🌙 深色';
|
||||||
|
themeToggle.classList.remove('active');
|
||||||
|
} else {
|
||||||
|
html.setAttribute('data-theme', 'light');
|
||||||
|
themeLabel.textContent = '☀️ 浅色';
|
||||||
|
themeToggle.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,580 @@
|
||||||
|
/* 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,336 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-primary: #0a0a0f;
|
||||||
|
--bg-secondary: #12121a;
|
||||||
|
--bg-card: #1a1a2e;
|
||||||
|
--bg-sidebar: #0f0f18;
|
||||||
|
--border-color: #2a2a3e;
|
||||||
|
--text-primary: #e0e0e0;
|
||||||
|
--text-secondary: #8888aa;
|
||||||
|
--accent-cyan: #00d4ff;
|
||||||
|
--accent-green: #00ff88;
|
||||||
|
--accent-red: #ff4466;
|
||||||
|
--accent-yellow: #ffaa00;
|
||||||
|
--accent-purple: #aa66ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-badge {
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background: var(--bg-sidebar);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
padding: 20px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-overview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-bar {
|
||||||
|
height: 6px;
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-green));
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-tree h3 {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-tree ul {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder:hover {
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder.active {
|
||||||
|
background: rgba(0, 212, 255, 0.15);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder .count {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--border-color);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crumb {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crumb.active {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: rgba(0, 212, 255, 0.15);
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: rgba(0, 212, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-zone {
|
||||||
|
border: 2px dashed var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-zone.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-zone.dragover {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
background: rgba(0, 212, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
font-size: 36px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-zone p {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 40px 1fr 100px 160px 120px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 40px 1fr 100px 160px 120px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid rgba(42, 42, 62, 0.5);
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
background: rgba(0, 212, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.selected {
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-size, .col-date {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover .col-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:hover {
|
||||||
|
background: rgba(0, 212, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:checked {
|
||||||
|
background: var(--accent-cyan);
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
<!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="cloud-drive-style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top-bar">
|
||||||
|
<div class="logo">🌊 HoloLake</div>
|
||||||
|
<h1>网站云盘系统</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<span class="storage-badge">已用 2.4GB / 10GB</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="storage-overview">
|
||||||
|
<div class="storage-bar">
|
||||||
|
<div class="storage-fill" style="width: 24%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="storage-text">2.4GB / 10GB</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="folder-tree">
|
||||||
|
<h3>文件夹</h3>
|
||||||
|
<ul>
|
||||||
|
<li class="folder active">
|
||||||
|
<span class="folder-icon">📁</span>
|
||||||
|
<span>全部文件</span>
|
||||||
|
</li>
|
||||||
|
<li class="folder">
|
||||||
|
<span class="folder-icon">📂</span>
|
||||||
|
<span>对话记录</span>
|
||||||
|
<span class="count">128</span>
|
||||||
|
</li>
|
||||||
|
<li class="folder">
|
||||||
|
<span class="folder-icon">📂</span>
|
||||||
|
<span>人格体数据</span>
|
||||||
|
<span class="count">45</span>
|
||||||
|
</li>
|
||||||
|
<li class="folder">
|
||||||
|
<span class="folder-icon">📂</span>
|
||||||
|
<span>用户上传</span>
|
||||||
|
<span class="count">67</span>
|
||||||
|
</li>
|
||||||
|
<li class="folder">
|
||||||
|
<span class="folder-icon">📂</span>
|
||||||
|
<span>系统备份</span>
|
||||||
|
<span class="count">12</span>
|
||||||
|
</li>
|
||||||
|
<li class="folder">
|
||||||
|
<span class="folder-icon">🗑️</span>
|
||||||
|
<span>回收站</span>
|
||||||
|
<span class="count">3</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="breadcrumb">
|
||||||
|
<span class="crumb">全部文件</span>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span class="crumb active">对话记录</span>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-primary" id="uploadBtn">📤 上传</button>
|
||||||
|
<button class="btn">📁 新建文件夹</button>
|
||||||
|
<button class="btn">🔍 搜索</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-zone" id="uploadZone">
|
||||||
|
<div class="upload-content">
|
||||||
|
<span class="upload-icon">☁️</span>
|
||||||
|
<p>拖拽文件到此处上传</p>
|
||||||
|
<span class="upload-hint">或点击「上传」按钮选择文件</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file-header">
|
||||||
|
<span class="col-check"><input type="checkbox"/></span>
|
||||||
|
<span class="col-name">文件名</span>
|
||||||
|
<span class="col-size">大小</span>
|
||||||
|
<span class="col-date">修改时间</span>
|
||||||
|
<span class="col-actions">操作</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-item folder-item">
|
||||||
|
<span class="col-check"><input type="checkbox"/></span>
|
||||||
|
<span class="col-name">
|
||||||
|
<span class="file-icon">📁</span>2026年3月
|
||||||
|
</span>
|
||||||
|
<span class="col-size">—</span>
|
||||||
|
<span class="col-date">2026-03-02</span>
|
||||||
|
<span class="col-actions">
|
||||||
|
<button class="icon-btn" title="重命名">✏️</button>
|
||||||
|
<button class="icon-btn" title="删除">🗑️</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-item">
|
||||||
|
<span class="col-check"><input type="checkbox"/></span>
|
||||||
|
<span class="col-name">
|
||||||
|
<span class="file-icon">💬</span>对话记录_20260302_001.json
|
||||||
|
</span>
|
||||||
|
<span class="col-size">256KB</span>
|
||||||
|
<span class="col-date">2026-03-02 14:30</span>
|
||||||
|
<span class="col-actions">
|
||||||
|
<button class="icon-btn" title="下载">📥</button>
|
||||||
|
<button class="icon-btn" title="重命名">✏️</button>
|
||||||
|
<button class="icon-btn" title="删除">🗑️</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-item">
|
||||||
|
<span class="col-check"><input type="checkbox"/></span>
|
||||||
|
<span class="col-name">
|
||||||
|
<span class="file-icon">🧠</span>persona_memory_backup.dat
|
||||||
|
</span>
|
||||||
|
<span class="col-size">1.2MB</span>
|
||||||
|
<span class="col-date">2026-03-01 22:15</span>
|
||||||
|
<span class="col-actions">
|
||||||
|
<button class="icon-btn" title="下载">📥</button>
|
||||||
|
<button class="icon-btn" title="重命名">✏️</button>
|
||||||
|
<button class="icon-btn" title="删除">🗑️</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-item">
|
||||||
|
<span class="col-check"><input type="checkbox"/></span>
|
||||||
|
<span class="col-name">
|
||||||
|
<span class="file-icon">📊</span>usage_analytics_feb.csv
|
||||||
|
</span>
|
||||||
|
<span class="col-size">89KB</span>
|
||||||
|
<span class="col-date">2026-02-28 18:00</span>
|
||||||
|
<span class="col-actions">
|
||||||
|
<button class="icon-btn" title="下载">📥</button>
|
||||||
|
<button class="icon-btn" title="重命名">✏️</button>
|
||||||
|
<button class="icon-btn" title="删除">🗑️</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-item">
|
||||||
|
<span class="col-check"><input type="checkbox"/></span>
|
||||||
|
<span class="col-name">
|
||||||
|
<span class="file-icon">🔑</span>api_keys_encrypted.vault
|
||||||
|
</span>
|
||||||
|
<span class="col-size">4KB</span>
|
||||||
|
<span class="col-date">2026-02-25 10:00</span>
|
||||||
|
<span class="col-actions">
|
||||||
|
<button class="icon-btn" title="下载">📥</button>
|
||||||
|
<button class="icon-btn" title="重命名">✏️</button>
|
||||||
|
<button class="icon-btn" title="删除">🗑️</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-bar">
|
||||||
|
<span>共 5 个项目</span>
|
||||||
|
<span id="selectedCount">已选择 0 项</span>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="cloud-drive.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 文件夹切换
|
||||||
|
const folders = document.querySelectorAll('.folder');
|
||||||
|
folders.forEach(folder => {
|
||||||
|
folder.addEventListener('click', function() {
|
||||||
|
folders.forEach(f => f.classList.remove('active'));
|
||||||
|
this.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 文件选中
|
||||||
|
const fileItems = document.querySelectorAll('.file-item');
|
||||||
|
fileItems.forEach(item => {
|
||||||
|
item.addEventListener('click', function(e) {
|
||||||
|
if (e.target.type === 'checkbox' || e.target.closest('.icon-btn')) return;
|
||||||
|
this.classList.toggle('selected');
|
||||||
|
const cb = this.querySelector('input[type="checkbox"]');
|
||||||
|
if (cb) cb.checked = !cb.checked;
|
||||||
|
updateCount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 上传区域显隐
|
||||||
|
var uploadBtn = document.getElementById('uploadBtn');
|
||||||
|
var uploadZone = document.getElementById('uploadZone');
|
||||||
|
if (uploadBtn && uploadZone) {
|
||||||
|
uploadBtn.addEventListener('click', function() {
|
||||||
|
uploadZone.classList.toggle('visible');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拖拽效果
|
||||||
|
if (uploadZone) {
|
||||||
|
document.addEventListener('dragover', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadZone.classList.add('visible');
|
||||||
|
uploadZone.classList.add('dragover');
|
||||||
|
});
|
||||||
|
document.addEventListener('dragleave', function(e) {
|
||||||
|
if (!e.relatedTarget) uploadZone.classList.remove('dragover');
|
||||||
|
});
|
||||||
|
document.addEventListener('drop', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadZone.classList.remove('dragover');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中计数
|
||||||
|
function updateCount() {
|
||||||
|
var n = document.querySelectorAll('.file-item.selected').length;
|
||||||
|
var el = document.getElementById('selectedCount');
|
||||||
|
if (el) el.textContent = '已选择 ' + n + ' 项';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储条动画
|
||||||
|
setTimeout(function() {
|
||||||
|
var fill = document.querySelector('.storage-fill');
|
||||||
|
if (fill) fill.style.width = fill.style.width;
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
console.log('✅ HoloLake 网站云盘系统已加载');
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
<!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>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0a0a0f;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
.icon { font-size: 64px; margin-bottom: 20px; }
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #ffa500;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
max-width: 400px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.back-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 32px;
|
||||||
|
background: linear-gradient(135deg, #00d4ff, #0099cc);
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.back-btn:hover { opacity: 0.85; }
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="icon">💾</div>
|
||||||
|
<div class="title">云盘</div>
|
||||||
|
<div class="status">🚧 此模块正在开发中</div>
|
||||||
|
<div class="desc">云盘模块将提供安全可靠的文件存储与分享功能,支持多端同步、版本管理与协作共享。即将上线。</div>
|
||||||
|
<a href="/" class="back-btn">← 返回主页</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer">HoloLake Era · AGE OS · 光湖纪元</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
<!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>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0a0a0f;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
.icon { font-size: 64px; margin-bottom: 20px; }
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #ffa500;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
max-width: 400px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.back-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 32px;
|
||||||
|
background: linear-gradient(135deg, #00d4ff, #0099cc);
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.back-btn:hover { opacity: 0.85; }
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="icon">🔔</div>
|
||||||
|
<div class="title">通知</div>
|
||||||
|
<div class="status">🚧 此模块正在开发中</div>
|
||||||
|
<div class="desc">通知模块将提供多渠道消息推送能力,支持站内信、邮件、钉钉与飞书等通知方式。敬请期待。</div>
|
||||||
|
<a href="/" class="back-btn">← 返回主页</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer">HoloLake Era · AGE OS · 光湖纪元</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>铸渊 · 对话</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="chat-layout" id="chatLayout">
|
||||||
|
<!-- ── 左侧边栏 ── -->
|
||||||
|
<aside class="chat-sidebar" id="chatSidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<span class="sidebar-logo">🌀</span>
|
||||||
|
<span class="sidebar-title">铸渊</span>
|
||||||
|
<button class="sidebar-toggle-close" onclick="toggleSidebar()" title="收起侧栏">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 人格体介绍 -->
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-section-title">🧠 人格体信息</div>
|
||||||
|
<div class="persona-info-card">
|
||||||
|
<div class="persona-info-name">铸渊 Zhùyuān</div>
|
||||||
|
<div class="persona-info-role">光湖系统 · 代码守护人格体</div>
|
||||||
|
<div class="persona-info-desc">基于 AGE OS 人格语言操作系统,专注代码守护、需求分析与智能开发协助。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统信息 -->
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-section-title">⚙️ 系统信息</div>
|
||||||
|
<div class="sidebar-info-list">
|
||||||
|
<div class="sidebar-info-item">
|
||||||
|
<span class="info-label">系统</span>
|
||||||
|
<span class="info-value">HoloLake Era</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-info-item">
|
||||||
|
<span class="info-label">架构</span>
|
||||||
|
<span class="info-value">AGE OS</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-info-item">
|
||||||
|
<span class="info-label">模型</span>
|
||||||
|
<span class="info-value" id="sidebarModel">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-info-item">
|
||||||
|
<span class="info-label">用户</span>
|
||||||
|
<span class="info-value" id="sidebarUser">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 对话记录 -->
|
||||||
|
<div class="sidebar-section sidebar-history-section">
|
||||||
|
<div class="sidebar-section-title">💬 对话记录</div>
|
||||||
|
<div class="sidebar-history" id="sidebarHistory">
|
||||||
|
<div class="sidebar-history-empty">暂无对话记录</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-new-chat" onclick="startNewChat()">+ 新对话</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- ── 主聊天区域(分屏左侧) ── -->
|
||||||
|
<div class="chat-main" id="chatMain">
|
||||||
|
<header class="chat-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<button class="sidebar-toggle" onclick="toggleSidebar()" title="展开侧栏" id="sidebarToggleBtn">☰</button>
|
||||||
|
<span class="persona-name">🌀 铸渊</span>
|
||||||
|
<span class="persona-role">光湖系统·代码守护人格体</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<span id="devIdDisplay" class="dev-id-badge"></span>
|
||||||
|
<span id="modelDisplay" class="model-badge"></span>
|
||||||
|
<button class="btn-secondary" onclick="handleLogout()">退出</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="chat-body" id="chatBody">
|
||||||
|
<!-- Messages rendered here -->
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="chat-input-area">
|
||||||
|
<div class="input-toolbar">
|
||||||
|
<label class="toolbar-btn" title="上传图片" for="imageUpload">
|
||||||
|
🖼️
|
||||||
|
<input type="file" id="imageUpload" accept="image/*" style="display:none" onchange="handleFileSelect(event, 'image')" aria-label="上传图片">
|
||||||
|
</label>
|
||||||
|
<label class="toolbar-btn" title="上传文件" for="fileUpload">
|
||||||
|
📎
|
||||||
|
<input type="file" id="fileUpload" style="display:none" onchange="handleFileSelect(event, 'file')" aria-label="上传文件">
|
||||||
|
</label>
|
||||||
|
<div id="uploadPreview" class="upload-preview" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="input-row">
|
||||||
|
<textarea
|
||||||
|
id="msgInput"
|
||||||
|
placeholder="跟铸渊说点什么…"
|
||||||
|
rows="1"
|
||||||
|
onkeydown="handleKeyDown(event)"
|
||||||
|
></textarea>
|
||||||
|
<button id="sendBtn" onclick="sendMessage()">发送</button>
|
||||||
|
</div>
|
||||||
|
<div class="action-row">
|
||||||
|
<button id="buildBtn" class="btn-build" onclick="handleBuild()" style="display:none;">
|
||||||
|
🚀 我要开发
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── 可拖拽分割线 ── -->
|
||||||
|
<div class="resizer" id="resizer" style="display:none;"></div>
|
||||||
|
|
||||||
|
<!-- ── 右侧预览面板 ── -->
|
||||||
|
<div class="preview-panel" id="previewPanel" style="display:none;">
|
||||||
|
<div class="preview-header">
|
||||||
|
<div class="preview-title">
|
||||||
|
<span class="preview-icon">🌊</span>
|
||||||
|
<span>实时预览 · <span id="previewProjectName">项目</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-status" id="previewStatus">
|
||||||
|
<span class="status-dot status-waiting"></span>
|
||||||
|
<span class="status-text">等待中</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-actions">
|
||||||
|
<button class="preview-btn" onclick="refreshPreview()" title="刷新预览">↻ 刷新</button>
|
||||||
|
<button class="preview-btn" onclick="openPreviewNewWindow()" title="新窗口打开">↗ 新窗口</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<iframe id="previewFrame" class="preview-iframe" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Modal (增强版:邮箱+联系方式+二次校验) -->
|
||||||
|
<div id="emailModal" class="modal" style="display:none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>📧 开发完成后,代码将发送到你的邮箱</h3>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label class="modal-label" for="emailInput">邮箱地址 <span class="required">*</span></label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="emailInput"
|
||||||
|
placeholder="your@email.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="modal-error" id="emailError" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label class="modal-label" for="contactInput">联系方式 <span class="optional">(选填)</span></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="contactInput"
|
||||||
|
placeholder="微信号 / 手机号(选填)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-primary" onclick="confirmBuild()">确认</button>
|
||||||
|
<button class="btn-secondary" onclick="closeEmailModal()">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="chat.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,477 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Persona Studio · 光湖人格体协助开发体验</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-bg"></div>
|
||||||
|
<div class="container login-container">
|
||||||
|
<div class="logo-area">
|
||||||
|
<div class="logo-icon">🌀</div>
|
||||||
|
<h1>Persona Studio</h1>
|
||||||
|
<p class="subtitle">HoloLake · 铸渊(Zhùyuān)· 代码守护人格体</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统介绍卡片 -->
|
||||||
|
<div class="intro-card">
|
||||||
|
<div class="intro-header">
|
||||||
|
<span class="intro-icon">🧠</span>
|
||||||
|
<h3>关于铸渊</h3>
|
||||||
|
</div>
|
||||||
|
<p class="intro-text">
|
||||||
|
铸渊是光湖系统(HoloLake)的<strong>代码守护人格体</strong>,基于 AGE OS 人格语言操作系统构建。
|
||||||
|
铸渊能够理解你的需求、梳理技术方案、生成代码,并通过对话记忆持续跟进你的项目。
|
||||||
|
</p>
|
||||||
|
<div class="intro-features">
|
||||||
|
<span class="feature-tag">💬 自然语言对话</span>
|
||||||
|
<span class="feature-tag">🧠 智能代码生成</span>
|
||||||
|
<span class="feature-tag">📧 邮件推送结果</span>
|
||||||
|
<span class="feature-tag">🔄 对话记忆</span>
|
||||||
|
<span class="feature-tag">📎 文件与图片上传</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-box">
|
||||||
|
<!-- ── Step 1: 选择登录方式 ── -->
|
||||||
|
<div id="step1" class="login-step">
|
||||||
|
<h2>选择体验方式</h2>
|
||||||
|
<p class="hint">请选择你的登录身份</p>
|
||||||
|
|
||||||
|
<div class="login-mode-cards">
|
||||||
|
<button class="mode-card" onclick="selectMode('developer')">
|
||||||
|
<span class="mode-icon">🛡️</span>
|
||||||
|
<span class="mode-title">开发者 / 体验者登录</span>
|
||||||
|
<span class="mode-desc">使用 EXP-XXX 编号登录,支持记忆连贯与完整功能</span>
|
||||||
|
</button>
|
||||||
|
<button class="mode-card" onclick="selectMode('guest')">
|
||||||
|
<span class="mode-icon">👋</span>
|
||||||
|
<span class="mode-title">访客体验</span>
|
||||||
|
<span class="mode-desc">无需编号,快速体验铸渊人格体(无记忆功能)</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Step 1.5: 开发者编号输入 ── -->
|
||||||
|
<div id="stepDevId" class="login-step" style="display:none;">
|
||||||
|
<button class="btn-back" onclick="goBack('step1')" title="返回">← 返回</button>
|
||||||
|
<h2>输入开发者编号</h2>
|
||||||
|
<p class="hint">编号由冰朔主控分配,格式:EXP-000 ~ EXP-013</p>
|
||||||
|
<form id="devIdForm" onsubmit="return handleDevIdSubmit(event)">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="devIdInput"
|
||||||
|
placeholder="EXP-XXX"
|
||||||
|
pattern="^EXP-\d{3,}$"
|
||||||
|
required
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button type="submit" id="devIdBtn" class="btn-primary-full">验证编号</button>
|
||||||
|
</form>
|
||||||
|
<p class="hint" style="margin-top:1rem;font-size:0.8rem;">编号验证通过后,下一步输入你的 API 密钥即可与铸渊对话</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Step 2: API Key 输入 ── -->
|
||||||
|
<div id="step2" class="login-step" style="display:none;">
|
||||||
|
<button class="btn-back" onclick="goBackFromStep2()" title="返回">← 返回</button>
|
||||||
|
<div id="welcomeBanner" class="welcome-banner" style="display:none;"></div>
|
||||||
|
<h2>🔑 选择对话方式</h2>
|
||||||
|
|
||||||
|
<!-- 服务器代理模式(推荐) -->
|
||||||
|
<div id="proxyModeSection" class="proxy-mode-section" style="margin-bottom:1.5rem;">
|
||||||
|
<div id="proxyStatusCard" class="proxy-status-card" style="display:none;">
|
||||||
|
<div class="proxy-status-icon">🌐</div>
|
||||||
|
<div class="proxy-status-text">
|
||||||
|
<strong>服务器代理模式</strong>
|
||||||
|
<span id="proxyStatusMsg">检测中…</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="proxyEnterBtn" class="btn-primary-full" style="display:none;background:linear-gradient(135deg,#00d4ff,#0099cc);" onclick="enterWithProxy()">
|
||||||
|
🚀 直接开始对话(无需 API 密钥)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="apiKeyDivider" style="display:none;text-align:center;margin:1rem 0;color:#667;font-size:0.85rem;">
|
||||||
|
── 或使用自有 API 密钥 ──
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="apiKeySection">
|
||||||
|
<p class="hint">输入你的第三方 AI 模型 API Key,系统将自动检测可用模型,选择后即可唤醒铸渊</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="apiBaseInput"
|
||||||
|
class="apikey-input"
|
||||||
|
placeholder="API Base URL(可留空,系统将自动探测多个平台)"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="apiKeyInput"
|
||||||
|
class="apikey-input"
|
||||||
|
placeholder="请输入你的 API Key(如 sk-xxx)"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="supported-platforms">
|
||||||
|
<span class="platform-label">支持平台:</span>
|
||||||
|
<span class="platform-tag cn-ok">🇨🇳 DeepSeek</span>
|
||||||
|
<span class="platform-tag cn-ok">🇨🇳 Moonshot</span>
|
||||||
|
<span class="platform-tag cn-ok">🇨🇳 智谱 AI</span>
|
||||||
|
<span class="platform-tag cn-ok">🇨🇳 云雾 AI</span>
|
||||||
|
<span class="platform-tag">OpenAI</span>
|
||||||
|
<span class="platform-tag">Google Gemini</span>
|
||||||
|
</div>
|
||||||
|
<p class="hint" style="margin-top:0.5rem;font-size:0.75rem;color:#8899aa;">🇨🇳 标记 = 中国大陆可直接访问,无需国际网络</p>
|
||||||
|
|
||||||
|
<button type="button" id="detectBtn" class="btn-detect" onclick="handleDetectModels()">
|
||||||
|
🔍 检测可用模型
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="detectStatus" class="detect-status" style="display:none;"></div>
|
||||||
|
|
||||||
|
<div id="modelListContainer" class="model-list-container" style="display:none;">
|
||||||
|
<p class="model-list-title">✨ 选择一个模型,唤醒铸渊</p>
|
||||||
|
<div id="modelList" class="model-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="errorMsg" class="error-msg" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="login-footer">
|
||||||
|
<p>HoloLake Era · AGE OS · 人格语言操作系统</p>
|
||||||
|
<p class="footer-version">Persona Studio v2.0</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const DEV_ID_RE = /^EXP-\d{3,}$/;
|
||||||
|
const PROBE_TIMEOUT_MS = 8000;
|
||||||
|
const AUTH_TIMEOUT_MS = 10000;
|
||||||
|
const API_BASE = getApiBase();
|
||||||
|
|
||||||
|
/* ---- State ---- */
|
||||||
|
var currentMode = ''; // 'developer' or 'guest'
|
||||||
|
var verifiedDevId = '';
|
||||||
|
var verifiedDevName = '';
|
||||||
|
|
||||||
|
/* ---- 已知 API 端点列表(国内可访问的优先排列) ---- */
|
||||||
|
const KNOWN_ENDPOINTS = [
|
||||||
|
{ label: 'DeepSeek', base: 'https://api.deepseek.com/v1', cn: true },
|
||||||
|
{ label: 'Moonshot', base: 'https://api.moonshot.cn/v1', cn: true },
|
||||||
|
{ label: '智谱 AI', base: 'https://open.bigmodel.cn/api/paas/v4', cn: true },
|
||||||
|
{ label: '云雾 AI', base: 'https://api.yunwu.ai/v1', cn: true },
|
||||||
|
{ label: 'OpenAI', base: 'https://api.openai.com/v1', cn: false },
|
||||||
|
{ label: 'Google Gemini', base: 'https://generativelanguage.googleapis.com/v1beta/openai', cn: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getApiBase() {
|
||||||
|
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
||||||
|
return 'http://localhost:3002';
|
||||||
|
}
|
||||||
|
// 非本地环境统一使用当前域名 + 协议,走 Nginx 转发
|
||||||
|
return location.protocol + '//' + location.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeApiBase(base) {
|
||||||
|
var normalized = base.replace(/\/+$/, '');
|
||||||
|
if (!normalized.match(/\/v\d+(\/|$)/) && !normalized.endsWith('/openai')) {
|
||||||
|
normalized += '/v1';
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Step Navigation ---- */
|
||||||
|
function showStep(stepId) {
|
||||||
|
document.querySelectorAll('.login-step').forEach(function(el) {
|
||||||
|
el.style.display = 'none';
|
||||||
|
});
|
||||||
|
document.getElementById(stepId).style.display = 'block';
|
||||||
|
document.getElementById('errorMsg').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack(targetStep) {
|
||||||
|
showStep(targetStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBackFromStep2() {
|
||||||
|
if (currentMode === 'developer') {
|
||||||
|
showStep('stepDevId');
|
||||||
|
} else {
|
||||||
|
showStep('step1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Mode Selection ---- */
|
||||||
|
function selectMode(mode) {
|
||||||
|
currentMode = mode;
|
||||||
|
if (mode === 'developer') {
|
||||||
|
showStep('stepDevId');
|
||||||
|
document.getElementById('devIdInput').focus();
|
||||||
|
} else {
|
||||||
|
// Guest → go directly to step 2
|
||||||
|
verifiedDevId = 'GUEST';
|
||||||
|
verifiedDevName = '访客';
|
||||||
|
showWelcomeBanner('👋 访客体验模式', '铸渊将为你提供协助。如需记忆连贯功能,可申请开发者编号。');
|
||||||
|
showStep('step2');
|
||||||
|
checkProxyAvailability();
|
||||||
|
document.getElementById('apiKeyInput').focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Developer ID Verification ---- */
|
||||||
|
async function handleDevIdSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var devId = document.getElementById('devIdInput').value.trim().toUpperCase();
|
||||||
|
var errorEl = document.getElementById('errorMsg');
|
||||||
|
var btn = document.getElementById('devIdBtn');
|
||||||
|
|
||||||
|
errorEl.style.display = 'none';
|
||||||
|
|
||||||
|
if (!DEV_ID_RE.test(devId)) {
|
||||||
|
errorEl.textContent = '编号格式不正确,请输入 EXP-XXX 格式';
|
||||||
|
errorEl.style.display = 'block';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '验证中…';
|
||||||
|
|
||||||
|
try {
|
||||||
|
var controller = new AbortController();
|
||||||
|
var timeoutId = setTimeout(function() { controller.abort(); }, AUTH_TIMEOUT_MS);
|
||||||
|
|
||||||
|
var res = await fetch(API_BASE + '/api/ps/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ dev_id: devId }),
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
var data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok || data.error) {
|
||||||
|
errorEl.textContent = data.message || '登录失败,请检查编号';
|
||||||
|
errorEl.style.display = 'block';
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '验证编号';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success — save developer info and proceed to step 2
|
||||||
|
verifiedDevId = devId;
|
||||||
|
verifiedDevName = data.name || devId;
|
||||||
|
sessionStorage.setItem('dev_id', devId);
|
||||||
|
sessionStorage.setItem('dev_name', verifiedDevName);
|
||||||
|
sessionStorage.setItem('session_token', data.token || '');
|
||||||
|
|
||||||
|
showWelcomeBanner('🛡️ ' + verifiedDevName + '(' + devId + ')', '编号已验证。请选择对话方式。');
|
||||||
|
showStep('step2');
|
||||||
|
checkProxyAvailability();
|
||||||
|
document.getElementById('apiKeyInput').focus();
|
||||||
|
} catch (_err) {
|
||||||
|
// 后端不可达时:devId 已通过上方 DEV_ID_RE 格式校验,放行至 API Key 步骤
|
||||||
|
// 真正的安全验证由用户自行提供的 API Key 完成
|
||||||
|
verifiedDevId = devId;
|
||||||
|
verifiedDevName = devId;
|
||||||
|
sessionStorage.setItem('dev_id', devId);
|
||||||
|
sessionStorage.setItem('dev_name', devId);
|
||||||
|
sessionStorage.setItem('session_token', '');
|
||||||
|
|
||||||
|
showWelcomeBanner(
|
||||||
|
'🛡️ ' + devId + '(离线验证)',
|
||||||
|
'后端暂时无法连接,已通过本地格式验证。请输入 API Key 唤醒铸渊。'
|
||||||
|
);
|
||||||
|
showStep('step2');
|
||||||
|
checkProxyAvailability();
|
||||||
|
document.getElementById('apiKeyInput').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '验证编号';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWelcomeBanner(title, desc) {
|
||||||
|
var banner = document.getElementById('welcomeBanner');
|
||||||
|
banner.innerHTML = '<strong>' + escapeHtml(title) + '</strong><br><span>' + escapeHtml(desc) + '</span>';
|
||||||
|
banner.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- API Key 模型检测 ---- */
|
||||||
|
async function handleDetectModels() {
|
||||||
|
var apiBase = document.getElementById('apiBaseInput').value.trim();
|
||||||
|
var apiKey = document.getElementById('apiKeyInput').value.trim();
|
||||||
|
var errorEl = document.getElementById('errorMsg');
|
||||||
|
var statusEl = document.getElementById('detectStatus');
|
||||||
|
var modelContainer = document.getElementById('modelListContainer');
|
||||||
|
var btn = document.getElementById('detectBtn');
|
||||||
|
|
||||||
|
errorEl.style.display = 'none';
|
||||||
|
modelContainer.style.display = 'none';
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
errorEl.textContent = '请输入 API Key';
|
||||||
|
errorEl.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '⏳ 正在检测可用模型…';
|
||||||
|
statusEl.className = 'detect-status detect-loading';
|
||||||
|
statusEl.style.display = 'block';
|
||||||
|
|
||||||
|
var candidates = [];
|
||||||
|
if (apiBase) {
|
||||||
|
var normalizedUserBase = normalizeApiBase(apiBase);
|
||||||
|
candidates.push({ label: apiBase, base: normalizedUserBase });
|
||||||
|
}
|
||||||
|
for (var i = 0; i < KNOWN_ENDPOINTS.length; i++) {
|
||||||
|
var ep = KNOWN_ENDPOINTS[i];
|
||||||
|
if (!apiBase || ep.base !== normalizedUserBase) {
|
||||||
|
candidates.push(ep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var matched = false;
|
||||||
|
for (var ci = 0; ci < candidates.length; ci++) {
|
||||||
|
var candidate = candidates[ci];
|
||||||
|
statusEl.textContent = '🔍 探测 ' + candidate.label + '…';
|
||||||
|
|
||||||
|
try {
|
||||||
|
var ctrl = new AbortController();
|
||||||
|
var tid = setTimeout(function () { ctrl.abort(); }, PROBE_TIMEOUT_MS);
|
||||||
|
|
||||||
|
var res = await fetch(candidate.base + '/models', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + apiKey },
|
||||||
|
signal: ctrl.signal
|
||||||
|
});
|
||||||
|
clearTimeout(tid);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
var data = await res.json();
|
||||||
|
var models = (data.data || []).map(function (m) { return m.id; }).filter(Boolean).sort();
|
||||||
|
|
||||||
|
if (models.length === 0) continue;
|
||||||
|
|
||||||
|
document.getElementById('apiBaseInput').value = candidate.base;
|
||||||
|
|
||||||
|
statusEl.textContent = '✅ 铸渊唤醒成功(' + candidate.label + ')· 发现 ' + models.length + ' 个可用模型';
|
||||||
|
statusEl.className = 'detect-status detect-success';
|
||||||
|
|
||||||
|
renderModelList(models, candidate.base, apiKey);
|
||||||
|
modelContainer.style.display = 'block';
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (probeErr) {
|
||||||
|
/* 单个端点探测失败,继续下一个 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matched) {
|
||||||
|
statusEl.textContent = '❌ 未能检测到可用模型,请检查端点地址和密钥是否正确';
|
||||||
|
statusEl.className = 'detect-status detect-error';
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '🔍 检测可用模型';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 渲染模型列表 ---- */
|
||||||
|
function renderModelList(models, apiBase, apiKey) {
|
||||||
|
var listEl = document.getElementById('modelList');
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
|
||||||
|
models.forEach(function (modelId) {
|
||||||
|
var item = document.createElement('button');
|
||||||
|
item.className = 'model-item';
|
||||||
|
item.textContent = modelId;
|
||||||
|
item.onclick = function () { enterChat(apiBase, apiKey, modelId); };
|
||||||
|
listEl.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 选择模型 → 进入对话 ---- */
|
||||||
|
function enterChat(apiBase, apiKey, selectedModel) {
|
||||||
|
sessionStorage.setItem('login_mode', currentMode);
|
||||||
|
sessionStorage.setItem('user_api_base', apiBase);
|
||||||
|
sessionStorage.setItem('user_api_key', apiKey);
|
||||||
|
sessionStorage.setItem('selected_model', selectedModel);
|
||||||
|
|
||||||
|
if (currentMode === 'guest') {
|
||||||
|
sessionStorage.setItem('dev_id', 'GUEST');
|
||||||
|
sessionStorage.removeItem('session_token');
|
||||||
|
}
|
||||||
|
// For developer mode, dev_id and session_token already set in handleDevIdSubmit
|
||||||
|
|
||||||
|
window.location.href = 'chat.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.appendChild(document.createTextNode(str));
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 服务器代理模式检测 ---- */
|
||||||
|
var proxyModel = '';
|
||||||
|
var proxyApiBase = '';
|
||||||
|
|
||||||
|
async function checkProxyAvailability() {
|
||||||
|
var statusCard = document.getElementById('proxyStatusCard');
|
||||||
|
var enterBtn = document.getElementById('proxyEnterBtn');
|
||||||
|
var statusMsg = document.getElementById('proxyStatusMsg');
|
||||||
|
var divider = document.getElementById('apiKeyDivider');
|
||||||
|
|
||||||
|
statusCard.style.display = 'flex';
|
||||||
|
statusMsg.textContent = '检测服务器代理…';
|
||||||
|
|
||||||
|
try {
|
||||||
|
var ctrl = new AbortController();
|
||||||
|
var tid = setTimeout(function () { ctrl.abort(); }, 5000);
|
||||||
|
var res = await fetch(API_BASE + '/api/ps/proxy/status', { signal: ctrl.signal });
|
||||||
|
clearTimeout(tid);
|
||||||
|
var data = await res.json();
|
||||||
|
|
||||||
|
if (data.proxy_available) {
|
||||||
|
proxyModel = data.model || '';
|
||||||
|
proxyApiBase = data.api_base || '';
|
||||||
|
statusMsg.textContent = '✅ 服务器代理可用 · ' + proxyModel;
|
||||||
|
statusMsg.style.color = '#4caf50';
|
||||||
|
enterBtn.style.display = 'block';
|
||||||
|
divider.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
statusMsg.textContent = '服务器代理未配置 · 请使用自有 API 密钥';
|
||||||
|
statusMsg.style.color = '#888';
|
||||||
|
statusCard.style.display = 'none';
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
statusMsg.textContent = '服务器代理不可用 · 请使用自有 API 密钥';
|
||||||
|
statusMsg.style.color = '#888';
|
||||||
|
statusCard.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterWithProxy() {
|
||||||
|
sessionStorage.setItem('login_mode', currentMode);
|
||||||
|
sessionStorage.setItem('use_server_proxy', 'true');
|
||||||
|
sessionStorage.setItem('user_api_base', proxyApiBase);
|
||||||
|
sessionStorage.setItem('user_api_key', '');
|
||||||
|
sessionStorage.setItem('selected_model', proxyModel);
|
||||||
|
|
||||||
|
if (currentMode === 'guest') {
|
||||||
|
sessionStorage.setItem('dev_id', 'GUEST');
|
||||||
|
sessionStorage.removeItem('session_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = 'chat.html';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,10 @@
|
||||||
|
const API_CONFIG = {
|
||||||
|
BASE_URL: 'https://guanghulab.com',
|
||||||
|
ENDPOINTS: {
|
||||||
|
STATUS: '/api/v1/system/status',
|
||||||
|
DEVELOPERS: '/api/v1/developers',
|
||||||
|
BROADCASTS: '/api/v1/broadcasts'
|
||||||
|
},
|
||||||
|
TIMEOUT: 8000,
|
||||||
|
REFRESH_INTERVAL: 30000
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
const MOCK_DATA = {
|
||||||
|
status: {
|
||||||
|
system_status: 'running',
|
||||||
|
version: 'v0.4.0',
|
||||||
|
uptime: '72h 15m',
|
||||||
|
api_calls_today: 142,
|
||||||
|
active_developers: 6,
|
||||||
|
last_deploy: '2026-03-04 02:00'
|
||||||
|
},
|
||||||
|
developers: [
|
||||||
|
{ id: 'DEV-001', name: '页页', module: '后端中间层', status: 'active', progress: 100, phase: '环节5·HTTP联调' },
|
||||||
|
{ id: 'DEV-002', name: '肥猫', module: 'M01登录界面', status: 'waiting', progress: 60, phase: '环节1·等SYSLOG' },
|
||||||
|
{ id: 'DEV-003', name: '燕樊', module: 'M15云盘系统', status: 'waiting', progress: 55, phase: '环节1·等SYSLOG' },
|
||||||
|
{ id: 'DEV-004', name: '之之', module: '钉钉机器人', status: 'waiting', progress: 20, phase: '环节0·等SYSLOG' },
|
||||||
|
{ id: 'DEV-005', name: '小草莓', module: '系统状态看板', status: 'active', progress: 85, phase: '环节2·API接入' },
|
||||||
|
{ id: 'DEV-009', name: '花尔', module: 'M05用户中心', status: 'waiting', progress: 40, phase: '环节1·等SYSLOG' },
|
||||||
|
{ id: 'DEV-010', name: '桔子', module: 'M06工单管理', status: 'active', progress: 35, phase: '环节1·广播待出' },
|
||||||
|
{ id: 'DEV-011', name: '匆匆那年', module: 'BC-000 DEVlog', status: 'waiting', progress: 10, phase: '环节0·等SYSLOG' }
|
||||||
|
],
|
||||||
|
broadcasts: [
|
||||||
|
{ id: 'DEV-012', module: 'M05用户中心', phase: '环节1', status: '等SYSLOG', time: '03-04 08:45' },
|
||||||
|
{ id: 'BC-看板-002', dev: '小草莓', module: '系统状态看板', phase: '环节2', status: '执行中', time: '03-04 08:50' },
|
||||||
|
{ id: 'BC-集成-001-M13', dev: '小草莓', module: 'M13协作调度', phase: '环节1', status: '已完成', time: '03-04 08:38' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchWithTimeout(url, timeout) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), timeout);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { signal: controller.signal });
|
||||||
|
clearTimeout(timer);
|
||||||
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiGet(endpoint, mockKey) {
|
||||||
|
try {
|
||||||
|
const res = await fetchWithTimeout(API_CONFIG.BASE_URL + endpoint, API_CONFIG.TIMEOUT);
|
||||||
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||||
|
return { data: await res.json(), isLive: true };
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[HoloLake] API未就绪,使用模拟数据:', err.message);
|
||||||
|
return { data: MOCK_DATA[mockKey], isLive: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAllData() {
|
||||||
|
const [statusRes, devRes, bcRes] = await Promise.all([
|
||||||
|
apiGet(API_CONFIG.ENDPOINTS.STATUS, 'status'),
|
||||||
|
apiGet(API_CONFIG.ENDPOINTS.DEVELOPERS, 'developers'),
|
||||||
|
apiGet(API_CONFIG.ENDPOINTS.BROADCASTS, 'broadcasts')
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
status: statusRes.data,
|
||||||
|
developers: devRes.data,
|
||||||
|
broadcasts: bcRes.data,
|
||||||
|
isLive: statusRes.isLive && devRes.isLive && bcRes.isLive
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
<!-- 🚀 YM-COLLAB-20260307-001 · CD 端到端验证标记 · 铸渊部署 -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>光湖系统 · 开发状态看板</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="dashboard">
|
||||||
|
<!-- 顶部标题栏 -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="logo-icon">🌊</span>
|
||||||
|
<h1>光湖系统 · 开发状态看板</h1>
|
||||||
|
</div>
|
||||||
|
<div class="status-indicator">
|
||||||
|
<span class="dot green"></span>
|
||||||
|
<span>系统运行中</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- ===== 环节2新增:API数据区域 ===== -->
|
||||||
|
<div id="conn-status" class="conn-status"></div>
|
||||||
|
|
||||||
|
<section class="data-section">
|
||||||
|
<h2 class="section-title">⚙️ 系统状态</h2>
|
||||||
|
<div id="sys-status" class="data-container">加载中...</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="data-section">
|
||||||
|
<h2 class="section-title">👥 开发者进度</h2>
|
||||||
|
<div id="dev-list" class="data-container">加载中...</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="data-section">
|
||||||
|
<h2 class="section-title">📡 广播动态</h2>
|
||||||
|
<div id="bc-list" class="data-container">加载中...</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 原有的团队成员卡片区(可保留作对比) -->
|
||||||
|
<section class="team-cards">
|
||||||
|
<h2>👥 团队成员状态(静态)</h2>
|
||||||
|
<div class="card-grid">
|
||||||
|
<div class="card" data-dev="DEV-001">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="dev-id">DEV-001</span>
|
||||||
|
<span class="status-badge status-active">活跃</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="dev-name">页页</div>
|
||||||
|
<div class="dev-module">后端中间层</div>
|
||||||
|
<div class="update-time">更新: 10:32</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" data-dev="DEV-002">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="dev-id">DEV-002</span>
|
||||||
|
<span class="status-badge status-active">活跃</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="dev-name">肥猫</div>
|
||||||
|
<div class="dev-module">飞书机器人</div>
|
||||||
|
<div class="update-time">更新: 10:15</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" data-dev="DEV-003">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="dev-id">DEV-003</span>
|
||||||
|
<span class="status-badge status-standby">等待</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="dev-name">燕樊</div>
|
||||||
|
<div class="dev-module">对话UI</div>
|
||||||
|
<div class="update-time">更新: 09:47</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" data-dev="DEV-004">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="dev-id">DEV-004</span>
|
||||||
|
<span class="status-badge status-active">活跃</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="dev-name">小幸运</div>
|
||||||
|
<div class="dev-module">钉钉打通</div>
|
||||||
|
<div class="update-time">更新: 11:02</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" data-dev="DEV-005">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="dev-id">DEV-005</span>
|
||||||
|
<span class="status-badge status-coding">编码中</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="dev-name">小草莓</div>
|
||||||
|
<div class="dev-module">系统状态看板</div>
|
||||||
|
<div class="update-time">更新: 刚刚</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" data-dev="DEV-006">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="dev-id">DEV-006</span>
|
||||||
|
<span class="status-badge status-standby">等待</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="dev-name">小蔡同学</div>
|
||||||
|
<div class="dev-module">工单管理</div>
|
||||||
|
<div class="update-time">更新: 09:20</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" data-dev="DEV-007">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="dev-id">DEV-007</span>
|
||||||
|
<span class="status-badge status-coding">编码中</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="dev-name">四月</div>
|
||||||
|
<div class="dev-module">用户中心</div>
|
||||||
|
<div class="update-time">更新: 10:55</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" data-dev="DEV-008">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="dev-id">DEV-008</span>
|
||||||
|
<span class="status-badge status-standby">等待</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="dev-name">小锋</div>
|
||||||
|
<div class="dev-module">待分配</div>
|
||||||
|
<div class="update-time">更新: 08:30</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 原有的广播列表(可保留) -->
|
||||||
|
<section class="broadcast-list">
|
||||||
|
<h2>📢 最近广播(静态)</h2>
|
||||||
|
<div class="list-container">
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="broadcast-id">BC-001</span>
|
||||||
|
<span class="broadcast-title">HoloLake桌面UI框架</span>
|
||||||
|
<span class="broadcast-time">10:45</span>
|
||||||
|
<span class="broadcast-status status-done">已完成</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="broadcast-id">BC-002</span>
|
||||||
|
<span class="broadcast-title">对话界面设计</span>
|
||||||
|
<span class="broadcast-time">10:20</span>
|
||||||
|
<span class="broadcast-status status-progress">进行中</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="broadcast-id">BC-003</span>
|
||||||
|
<span class="broadcast-title">人格体选择器</span>
|
||||||
|
<span class="broadcast-time">09:55</span>
|
||||||
|
<span class="broadcast-status status-progress">进行中</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="broadcast-id">BC-004</span>
|
||||||
|
<span class="broadcast-title">云盘授权界面</span>
|
||||||
|
<span class="broadcast-time">09:30</span>
|
||||||
|
<span class="broadcast-status status-pending">待处理</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="broadcast-id">BC-005</span>
|
||||||
|
<span class="broadcast-title">用户中心设计</span>
|
||||||
|
<span class="broadcast-time">08:50</span>
|
||||||
|
<span class="broadcast-status status-pending">待处理</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 底部信息栏 -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="footer-left">光湖系统 · HoloLake Era AGE OS</div>
|
||||||
|
<div class="footer-right">v2.1.0 · 2026-03-02 17:50</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 核心脚本 -->
|
||||||
|
<script src="api-config.js"></script>
|
||||||
|
<script src="api.js"></script>
|
||||||
|
<script src="render.js"></script>
|
||||||
|
<script src="ws-client.js"></script>
|
||||||
|
<!-- 原有的交互脚本 -->
|
||||||
|
<script src="script.js"></script>
|
||||||
|
|
||||||
|
<!-- ===== WebSocket 实时推送初始化 ===== -->
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
// 自动检测 WebSocket 地址:生产环境走 Nginx 反向代理,本地开发直连 8080
|
||||||
|
var wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
var wsUrl = (location.hostname === 'localhost' || location.hostname === '127.0.0.1')
|
||||||
|
? 'ws://localhost:8080'
|
||||||
|
: wsProtocol + '//' + location.host + '/ws';
|
||||||
|
|
||||||
|
var wsClient = new WebSocketClient(wsUrl, {
|
||||||
|
onMessage: function(data) {
|
||||||
|
if (data.type === 'dashboard_update' && data.data) {
|
||||||
|
if (data.data.system) renderSystemStatus(data.data.system);
|
||||||
|
if (data.data.developers) renderDevelopers(data.data.developers);
|
||||||
|
if (data.data.broadcasts) renderBroadcasts(data.data.broadcasts);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onStatusChange: function(status) {
|
||||||
|
var el = document.getElementById('conn-status');
|
||||||
|
if (!el) return;
|
||||||
|
var text = '', className = '';
|
||||||
|
switch(status) {
|
||||||
|
case 'connected':
|
||||||
|
text = '🔵 实时连接 (WebSocket)';
|
||||||
|
className = 'conn-dot on';
|
||||||
|
break;
|
||||||
|
case 'connecting':
|
||||||
|
text = '⏳ 连接中...';
|
||||||
|
className = 'conn-dot connecting';
|
||||||
|
break;
|
||||||
|
case 'reconnecting':
|
||||||
|
text = '🔄 重连中...';
|
||||||
|
className = 'conn-dot reconnecting';
|
||||||
|
break;
|
||||||
|
case 'disconnected':
|
||||||
|
case 'offline':
|
||||||
|
default:
|
||||||
|
text = '⚪ 离线模式 (降级轮询)';
|
||||||
|
className = 'conn-dot off';
|
||||||
|
if (typeof refreshDashboard === 'function') refreshDashboard();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
el.innerHTML = '<span class="' + className + '"></span> ' + text;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wsClient.connect();
|
||||||
|
window.addEventListener('beforeunload', function() { wsClient.disconnect(); });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
const STATUS_COLORS = {
|
||||||
|
active: '#4fc3f7',
|
||||||
|
waiting: '#ffa726',
|
||||||
|
done: '#66bb6a'
|
||||||
|
};
|
||||||
|
const STATUS_LABELS = {
|
||||||
|
active: '执行中',
|
||||||
|
waiting: '等待中',
|
||||||
|
done: '已完成'
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderSystemStatus(data) {
|
||||||
|
const c = document.getElementById('sys-status');
|
||||||
|
if (!c) return;
|
||||||
|
c.innerHTML = `
|
||||||
|
<div class="status-grid">
|
||||||
|
<div class="s-card"><span class="s-icon">🟢</span><span class="s-val">${data.system_status === 'running' ? '运行中' : '异常'}</span><span class="s-lbl">系统状态</span></div>
|
||||||
|
<div class="s-card"><span class="s-icon">⏱️</span><span class="s-val">${data.uptime}</span><span class="s-lbl">运行时长</span></div>
|
||||||
|
<div class="s-card"><span class="s-icon">📡</span><span class="s-val">${data.api_calls_today}</span><span class="s-lbl">今日API调用</span></div>
|
||||||
|
<div class="s-card"><span class="s-icon">👥</span><span class="s-val">${data.active_developers}</span><span class="s-lbl">活跃开发者</span></div>
|
||||||
|
<div class="s-card"><span class="s-icon">🚀</span><span class="s-val">${data.version}</span><span class="s-lbl">版本</span></div>
|
||||||
|
<div class="s-card"><span class="s-icon">📦</span><span class="s-val">${data.last_deploy}</span><span class="s-lbl">最后部署</span></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDevelopers(data) {
|
||||||
|
const c = document.getElementById('dev-list');
|
||||||
|
if (!c) return;
|
||||||
|
let html = '';
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const d = data[i];
|
||||||
|
const color = STATUS_COLORS[d.status] || '#78909c';
|
||||||
|
html += `
|
||||||
|
<div class="dev-card">
|
||||||
|
<div class="dev-top">
|
||||||
|
<span class="dev-id">${d.id}</span>
|
||||||
|
<span class="dev-name">${d.name}</span>
|
||||||
|
<span class="dev-st" style="color:${color}">${STATUS_LABELS[d.status] || d.status}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dev-mod">${d.module} · ${d.phase}</div>
|
||||||
|
<div class="prog-bar"><div class="prog-fill" style="width:${d.progress}%; background:${color}"></div></div>
|
||||||
|
<div class="prog-num">${d.progress}%</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
c.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBroadcasts(data) {
|
||||||
|
const c = document.getElementById('bc-list');
|
||||||
|
if (!c) return;
|
||||||
|
let html = '';
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const b = data[i];
|
||||||
|
let cls = 'wait';
|
||||||
|
if (b.status === '已完成' || b.status === 'done') cls = 'done';
|
||||||
|
else if (b.status === '执行中' || b.status === 'active') cls = 'active';
|
||||||
|
html += `
|
||||||
|
<div class="bc-row">
|
||||||
|
<span class="bc-id">${b.id}</span>
|
||||||
|
<span class="bc-dev">${b.dev || ''}</span>
|
||||||
|
<span class="bc-mod">${b.module} · ${b.phase}</span>
|
||||||
|
<span class="bc-st ${cls}">${b.status}</span>
|
||||||
|
<span class="bc-time">${b.time}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
c.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConnStatus(isLive) {
|
||||||
|
const el = document.getElementById('conn-status');
|
||||||
|
if (!el) return;
|
||||||
|
el.innerHTML = isLive
|
||||||
|
? '<span class="conn-dot on"></span> 已连接 guanghulab.com'
|
||||||
|
: '<span class="conn-dot off"></span> 离线模式(模拟数据)';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshDashboard() {
|
||||||
|
try {
|
||||||
|
const result = await loadAllData();
|
||||||
|
renderSystemStatus(result.status);
|
||||||
|
renderDevelopers(result.developers);
|
||||||
|
renderBroadcasts(result.broadcasts);
|
||||||
|
updateConnStatus(result.isLive);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[HoloLake] 刷新失败:', err);
|
||||||
|
updateConnStatus(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
refreshDashboard();
|
||||||
|
setInterval(refreshDashboard, API_CONFIG.REFRESH_INTERVAL);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,488 @@
|
||||||
|
/* 全局样式 - 深色科技风 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Segoe UI', 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #0a0e17;
|
||||||
|
color: #e0e0e0;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 20px;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
max-width: 1400px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题栏 */
|
||||||
|
.header {
|
||||||
|
background-color: #131a2b;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid #2a3440;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 212, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
filter: drop-shadow(0 0 8px #00d4ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: #1e293b;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 40px;
|
||||||
|
border: 1px solid #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.green {
|
||||||
|
background-color: #00ff9d;
|
||||||
|
box-shadow: 0 0 10px #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 团队成员卡片区 */
|
||||||
|
.team-cards {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-cards h2, .broadcast-list h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #00d4ff;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
border-left: 4px solid #00d4ff;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #131a2b;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #2a3440;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: #00d4ff;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 212, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #2a3440;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-id {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #00d4ff;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 40px;
|
||||||
|
background-color: #1e293b;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.status-active {
|
||||||
|
background-color: #00ff9d20;
|
||||||
|
color: #00ff9d;
|
||||||
|
border-color: #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.status-standby {
|
||||||
|
background-color: #ffaa0020;
|
||||||
|
color: #ffaa00;
|
||||||
|
border-color: #ffaa00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.status-coding {
|
||||||
|
background-color: #00d4ff20;
|
||||||
|
color: #00d4ff;
|
||||||
|
border-color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body .dev-name {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body .dev-module {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #a0a0a0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body .update-time {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #6b7a8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 广播列表区 */
|
||||||
|
.broadcast-list {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
background-color: #131a2b;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #2a3440;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px 1fr 100px 100px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #2a3440;
|
||||||
|
align-items: center;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:hover {
|
||||||
|
background-color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.broadcast-id {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #00d4ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.broadcast-title {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.broadcast-time {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.broadcast-status {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 40px;
|
||||||
|
text-align: center;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-done {
|
||||||
|
background-color: #00ff9d20;
|
||||||
|
color: #00ff9d;
|
||||||
|
border: 1px solid #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-progress {
|
||||||
|
background-color: #00d4ff20;
|
||||||
|
color: #00d4ff;
|
||||||
|
border: 1px solid #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background-color: #ffaa0020;
|
||||||
|
color: #ffaa00;
|
||||||
|
border: 1px solid #ffaa00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部信息栏 */
|
||||||
|
.footer {
|
||||||
|
background-color: #131a2b;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border: 1px solid #2a3440;
|
||||||
|
color: #6b7a8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-right {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式布局 */
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 640px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
grid-template-columns: 80px 1fr 80px 80px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}/* ========== 环节2新增:API数据区域样式 ========== */
|
||||||
|
|
||||||
|
/* 连接状态指示器 */
|
||||||
|
.conn-status {
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8899aa;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conn-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conn-dot.on {
|
||||||
|
background: #66bb6a;
|
||||||
|
box-shadow: 0 0 6px #66bb6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conn-dot.off {
|
||||||
|
background: #ffa726;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 数据区块 */
|
||||||
|
.data-section {
|
||||||
|
padding: 20px 16px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e0e6ed;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-container {
|
||||||
|
color: #8899aa;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 系统状态网格 */
|
||||||
|
.status-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-card {
|
||||||
|
background: rgba(79,195,247,0.06);
|
||||||
|
border: 1px solid rgba(79,195,247,0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-icon {
|
||||||
|
display: block;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-val {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4fc3f7;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-lbl {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #667788;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 开发者卡片 */
|
||||||
|
.dev-card {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-id {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #556677;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e0e6ed;
|
||||||
|
font-weight: 600;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-st {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-mod {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8899aa;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prog-bar {
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prog-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.8s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prog-num {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #8899aa;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 广播行 */
|
||||||
|
.bc-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-id {
|
||||||
|
color: #4fc3f7;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
min-width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-dev {
|
||||||
|
color: #e0e6ed;
|
||||||
|
min-width: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-mod {
|
||||||
|
color: #8899aa;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-st {
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-st.done {
|
||||||
|
background: rgba(102,187,106,0.15);
|
||||||
|
color: #66bb6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-st.active {
|
||||||
|
background: rgba(79,195,247,0.15);
|
||||||
|
color: #4fc3f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-st.wait {
|
||||||
|
background: rgba(255,167,38,0.15);
|
||||||
|
color: #ffa726;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-time {
|
||||||
|
color: #556677;
|
||||||
|
font-size: 11px;
|
||||||
|
min-width: 80px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
// ========== HoloLake 看板 · WebSocket 客户端模块 ==========
|
||||||
|
// 功能:连接管理、自动重连(指数退避)、心跳检测、状态回调
|
||||||
|
|
||||||
|
class WebSocketClient {
|
||||||
|
constructor(url, options = {}) {
|
||||||
|
this.url = url;
|
||||||
|
this.reconnectInterval = options.reconnectInterval || 1000; // 初始重连间隔 1秒
|
||||||
|
this.maxReconnectInterval = options.maxReconnectInterval || 30000; // 最大30秒
|
||||||
|
this.heartbeatInterval = options.heartbeatInterval || 30000; // 心跳间隔30秒
|
||||||
|
this.onMessage = options.onMessage || (() => {});
|
||||||
|
this.onStatusChange = options.onStatusChange || (() => {});
|
||||||
|
|
||||||
|
this.ws = null;
|
||||||
|
this.reconnectTimer = null;
|
||||||
|
this.heartbeatTimer = null;
|
||||||
|
this.forcedClose = false;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接
|
||||||
|
connect() {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
|
||||||
|
this.forcedClose = false;
|
||||||
|
this.onStatusChange('connecting');
|
||||||
|
try {
|
||||||
|
this.ws = new WebSocket(this.url);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[WS] 连接失败', e);
|
||||||
|
this._scheduleReconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
console.log('[WS] 连接成功');
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.onStatusChange('connected');
|
||||||
|
this._startHeartbeat();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
this.onMessage(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[WS] 消息解析失败', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = (event) => {
|
||||||
|
console.log('[WS] 连接关闭', event.code, event.reason);
|
||||||
|
this._stopHeartbeat();
|
||||||
|
if (!this.forcedClose) {
|
||||||
|
this.onStatusChange('disconnected');
|
||||||
|
this._scheduleReconnect();
|
||||||
|
} else {
|
||||||
|
this.onStatusChange('offline');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = (error) => {
|
||||||
|
console.error('[WS] 错误', error);
|
||||||
|
this.onStatusChange('error');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主动断开
|
||||||
|
disconnect() {
|
||||||
|
this.forcedClose = true;
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer);
|
||||||
|
this.reconnectTimer = null;
|
||||||
|
}
|
||||||
|
this._stopHeartbeat();
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close();
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
send(data) {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(JSON.stringify(data));
|
||||||
|
} else {
|
||||||
|
console.warn('[WS] 未连接,无法发送');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重连调度(指数退避)
|
||||||
|
_scheduleReconnect() {
|
||||||
|
if (this.forcedClose) return;
|
||||||
|
const delay = Math.min(
|
||||||
|
this.reconnectInterval * Math.pow(2, this.reconnectAttempts),
|
||||||
|
this.maxReconnectInterval
|
||||||
|
);
|
||||||
|
console.log(`[WS] ${delay}ms 后尝试重连...`);
|
||||||
|
this.onStatusChange('reconnecting');
|
||||||
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
||||||
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
this.connect();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 心跳
|
||||||
|
_startHeartbeat() {
|
||||||
|
this._stopHeartbeat();
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.send({ type: 'ping' });
|
||||||
|
}
|
||||||
|
}, this.heartbeatInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopHeartbeat() {
|
||||||
|
if (this.heartbeatTimer) {
|
||||||
|
clearInterval(this.heartbeatTimer);
|
||||||
|
this.heartbeatTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Writing Platform</title>
|
||||||
|
<script type="module" crossorigin src="./assets/index-sDMNEmkX.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="./assets/index--C4_NgnM.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -3,5 +3,5 @@ import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
base: '/writing/',
|
base: './',
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue