feat: frontend skeleton done

This commit is contained in:
simonkoson
2026-05-15 09:46:42 +08:00
parent 2bee0ce8c8
commit d29b1bb394
36 changed files with 5008 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
.dashboard {
max-width: 1200px;
}
/* Banner */
.dashboard-banner {
background: #fff;
border-radius: var(--radius-card);
padding: 24px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.banner-text h2 {
margin: 0 0 8px;
font-size: 20px;
color: var(--color-text-primary);
}
.banner-text p {
margin: 0;
color: var(--color-text-secondary);
}
.banner-image-placeholder {
width: 200px;
height: 100px;
background: var(--color-accent-green);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-primary-green);
font-size: 12px;
}
/* KPI Cards */
.kpi-row {
margin-bottom: 24px;
}
.kpi-card {
border-radius: var(--radius-card);
display: flex;
align-items: center;
gap: 16px;
}
.kpi-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.kpi-info {
display: flex;
flex-direction: column;
}
.kpi-value {
font-size: 24px;
font-weight: 600;
color: var(--color-text-primary);
}
.kpi-label {
font-size: 12px;
color: var(--color-text-secondary);
}
/* Content Cards */
.cards-row {
margin-bottom: 24px;
}
.content-card {
border-radius: var(--radius-card);
height: 100%;
}
.content-card .ant-card-head-title {
font-weight: 600;
}
.placeholder-text {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 16px;
text-align: center;
color: var(--color-text-secondary);
}
.placeholder-text p {
margin: 8px 0 4px;
font-weight: 500;
}
.placeholder-text small {
color: #999;
}
/* Chart Container */
.chart-container {
padding: 8px 0;
}
.bars-wrapper {
display: flex;
justify-content: space-between;
align-items: flex-end;
height: 200px;
padding: 0 8px;
}
.bar-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
max-width: 40px;
}
.bar {
width: 24px;
border-radius: 4px 4px 0 0;
transition: all 0.3s;
cursor: pointer;
}
.bar-label-top {
font-size: 9px;
color: var(--color-text-secondary);
text-align: center;
margin-top: 4px;
max-width: 40px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.bar-label-bottom {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
margin-top: 4px;
}
.bar-label-bottom span {
font-size: 9px;
color: var(--color-text-secondary);
}
/* Chart Legend */
.chart-legend {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--color-text-secondary);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
+152
View File
@@ -0,0 +1,152 @@
import { Row, Col, Card, Avatar, Tooltip } from 'antd'
import {
BarChartOutlined,
EyeOutlined,
CalendarOutlined,
FireOutlined,
} from '@ant-design/icons'
import './Dashboard.css'
// 假数据:近 9 期收视(基础目标 0.6,摸高目标 0.9)
const fakeEpisodes = [
{ id: 1, title: '第 1247 期 现代防空反导大对决', editor: 'simonkoson', share: 0.72 },
{ id: 2, title: '第 1246 期 深海暗战', editor: 'simonkoson', share: 0.85 },
{ id: 3, title: '第 1245 期 隐形战场', editor: 'simonkoson', share: 0.94 },
{ id: 4, title: '第 1244 期 无人机革命', editor: 'simonkoson', share: 0.65 },
{ id: 5, title: '第 1243 期 核潜艇传奇', editor: 'simonkoson', share: 0.58 },
{ id: 6, title: '第 1242 期 电子战风暴', editor: 'simonkoson', share: 0.88 },
{ id: 7, title: '第 1241 期 太空博弈', editor: 'simonkoson', share: 0.91 },
{ id: 8, title: '第 1240 期 装甲对决', editor: 'simonkoson', share: 0.67 },
{ id: 9, title: '第 1239 期 隐身战机', editor: 'simonkoson', share: 0.55 },
]
const BASE_TARGET = 0.6
const STRETCH_TARGET = 0.9
function getBarColor(share) {
if (share > STRETCH_TARGET) return '#cf1322' // 红=优秀
if (share >= BASE_TARGET) return '#6b8e6b' // 蓝=达标
return '#d4a017' // 绿=待提升
}
function getBarHeight(share) {
return Math.round((share / 1.0) * 180)
}
function getShortTitle(title) {
return title.length > 12 ? title.slice(0, 12) + '...' : title
}
function Dashboard() {
return (
<div className="dashboard">
{/* 顶部 Banner */}
<div className="dashboard-banner">
<div className="banner-text">
<h2>本月收视最佳</h2>
<p> 1245 隐形战场 · 收视份额 0.94</p>
</div>
<div className="banner-image-placeholder">
<span>题图位 · Phase 2 实装</span>
</div>
</div>
{/* KPI Cards */}
<Row gutter={16} className="kpi-row">
<Col span={8}>
<Card className="kpi-card">
<div className="kpi-icon" style={{ background: '#e8f5e9' }}>
<EyeOutlined style={{ color: '#6b8e6b', fontSize: 24 }} />
</div>
<div className="kpi-info">
<span className="kpi-value">0.82</span>
<span className="kpi-label">本期平均收视</span>
</div>
</Card>
</Col>
<Col span={8}>
<Card className="kpi-card">
<div className="kpi-icon" style={{ background: '#fff3e0' }}>
<FireOutlined style={{ color: '#ff9800', fontSize: 24 }} />
</div>
<div className="kpi-info">
<span className="kpi-value">98.6%</span>
<span className="kpi-label">完成率</span>
</div>
</Card>
</Col>
<Col span={8}>
<Card className="kpi-card">
<div className="kpi-icon" style={{ background: '#e3f2fd' }}>
<CalendarOutlined style={{ color: '#2196f3', fontSize: 24 }} />
</div>
<div className="kpi-info">
<span className="kpi-value">6</span>
<span className="kpi-label">未来待排期</span>
</div>
</Card>
</Col>
</Row>
{/* 三卡片区域 */}
<Row gutter={16} className="cards-row">
{/* 热点雷达 */}
<Col span={8}>
<Card className="content-card" title="热点雷达">
<div className="placeholder-text">
<BarChartOutlined style={{ fontSize: 32, color: '#ccc', marginBottom: 8 }} />
<p>模块 H · Phase 4c 实施</p>
<small>热点雷达将在 Phase 4c 开发作为编导个人首页的核心功能</small>
</div>
</Card>
</Col>
{/* 近 9 期收视柱图 */}
<Col span={8}>
<Card className="content-card" title="近 9 期收视">
<div className="chart-container">
<div className="bars-wrapper">
{fakeEpisodes.map((ep) => (
<div key={ep.id} className="bar-item">
<Tooltip title={`${ep.title} · ${ep.share}`}>
<div
className="bar"
style={{
height: getBarHeight(ep.share),
backgroundColor: getBarColor(ep.share),
}}
/>
</Tooltip>
<div className="bar-label-top">{getShortTitle(ep.title)}</div>
<div className="bar-label-bottom">
<Avatar size={20} style={{ fontSize: 10 }}>{ep.editor[0]}</Avatar>
<span>{ep.editor}</span>
</div>
</div>
))}
</div>
<div className="chart-legend">
<span className="legend-item"><span className="dot" style={{ background: '#cf1322' }}></span>=优秀</span>
<span className="legend-item"><span className="dot" style={{ background: '#6b8e6b' }}></span>=达标</span>
<span className="legend-item"><span className="dot" style={{ background: '#d4a017' }}></span>绿=待提升</span>
</div>
</div>
</Card>
</Col>
{/* 排播计划 */}
<Col span={8}>
<Card className="content-card" title="排播计划">
<div className="placeholder-text">
<CalendarOutlined style={{ fontSize: 32, color: '#ccc', marginBottom: 8 }} />
<p>模块 F · Phase 4b 实施</p>
<small>甘特图排期将在 Phase 4b 开发使用 frappe-gantt 实现拖拽排期</small>
</div>
</Card>
</Col>
</Row>
</div>
)
}
export default Dashboard
+13
View File
@@ -0,0 +1,13 @@
import { Result } from 'antd'
function Doco() {
return (
<Result
status="info"
title="文稿对齐模块"
subTitle="该模块计划在 1.0 主体上线后作为独立 Phase 开发,详见 doco_project_design.md"
/>
)
}
export default Doco
@@ -0,0 +1,17 @@
import { Card } from 'antd'
import { UserOutlined } from '@ant-design/icons'
function EditorHome() {
return (
<Card style={{ borderRadius: 16 }}>
<div style={{ textAlign: 'center', padding: '48px 0', color: '#999' }}>
<UserOutlined style={{ fontSize: 48, marginBottom: 16 }} />
<h3 style={{ color: '#666' }}>个人首页</h3>
<p>模块 H · Phase 4c 实施</p>
<small>今日热点雷达 · RSS + AI 摘要</small>
</div>
</Card>
)
}
export default EditorHome
@@ -0,0 +1,17 @@
import { Card } from 'antd'
import { BookOutlined } from '@ant-design/icons'
function KnowledgeBase() {
return (
<Card style={{ borderRadius: 16 }}>
<div style={{ textAlign: 'center', padding: '48px 0', color: '#999' }}>
<BookOutlined style={{ fontSize: 48, marginBottom: 16 }} />
<h3 style={{ color: '#666' }}>知识库</h3>
<p>模块 C · Phase 3 实施</p>
<small>往期报题单 / 文稿 / 军报语义检索</small>
</div>
</Card>
)
}
export default KnowledgeBase
+29
View File
@@ -0,0 +1,29 @@
.login-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: var(--color-bg-cream);
}
.login-card {
width: 400px;
border-radius: var(--radius-card);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
}
.login-header {
text-align: center;
margin-bottom: 32px;
}
.login-header h1 {
font-size: 28px;
color: var(--color-primary-green);
margin: 0 0 8px;
}
.login-header p {
color: var(--color-text-secondary);
margin: 0;
}
+78
View File
@@ -0,0 +1,78 @@
import { useState } from 'react'
import { Form, Input, Button, Card, message } from 'antd'
import { UserOutlined, LockOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
import useAuthStore from '../../stores/authStore'
import './Login.css'
function Login() {
const [loading, setLoading] = useState(false)
const navigate = useNavigate()
const { login } = useAuthStore()
const onFinish = async (values) => {
setLoading(true)
const result = await login(values.username, values.password)
setLoading(false)
if (result.success) {
navigate('/dashboard')
} else {
message.error(result.error || '登录失败')
}
}
return (
<div className="login-page">
<Card className="login-card" bordered={false}>
<div className="login-header">
<h1>军事科技</h1>
<p>TPS 工作台</p>
</div>
<Form
name="login"
onFinish={onFinish}
autoComplete="off"
layout="vertical"
requiredMark={false}
>
<Form.Item
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input
prefix={<UserOutlined />}
placeholder="用户名"
size="large"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="密码"
size="large"
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
size="large"
block
loading={loading}
>
登录
</Button>
</Form.Item>
</Form>
</Card>
</div>
)
}
export default Login
+17
View File
@@ -0,0 +1,17 @@
import { Card } from 'antd'
import { FileTextOutlined } from '@ant-design/icons'
function TPS() {
return (
<Card style={{ borderRadius: 16 }}>
<div style={{ textAlign: 'center', padding: '48px 0', color: '#999' }}>
<FileTextOutlined style={{ fontSize: 48, marginBottom: 16 }} />
<h3 style={{ color: '#666' }}>TPS 选题策划</h3>
<p>模块 A / B / C / D / G · Phase 4a 实施</p>
<small>查重 + 知识库参考 + 报题单生成</small>
</div>
</Card>
)
}
export default TPS
@@ -0,0 +1,17 @@
import { Card } from 'antd'
import { TeamOutlined } from '@ant-design/icons'
function UserManage() {
return (
<Card style={{ borderRadius: 16 }}>
<div style={{ textAlign: 'center', padding: '48px 0', color: '#999' }}>
<TeamOutlined style={{ fontSize: 48, marginBottom: 16 }} />
<h3 style={{ color: '#666' }}>用户管理</h3>
<p>Phase 2 实施</p>
<small>管理员创建账号 / 角色分配 zhipianren 可见</small>
</div>
</Card>
)
}
export default UserManage