Compare commits

..

1 Commits

Author SHA1 Message Date
5b19d9fe69 v1.0定版 2026-05-27 17:14:08 +08:00
32 changed files with 2074 additions and 2915 deletions

View File

@@ -8,7 +8,8 @@
"Bash(pip install *)",
"Bash(docker --version)",
"Bash(psql --version)",
"Bash(python -c ' *)"
"Bash(python -c ' *)",
"PowerShell(netstat -ano)"
]
}
}

319
README_STARTUP.md Normal file
View File

@@ -0,0 +1,319 @@
# 🚀 启动指南
## 快速开始
### 方式一:本地开发 (推荐)
**前置条件:**
- Python 3.9+
- Node.js 18+
- PostgreSQL 14+ (需要单独启动)
**Windows PowerShell:**
```powershell
.\start.ps1
```
**Windows CMD:**
```cmd
start.bat
```
**Mac/Linux:**
```bash
chmod +x start.sh
./start.sh
```
然后选择选项 `1` 进行本地开发。
**访问地址:**
- 前端: http://localhost:5173
- 后端: http://localhost:8000
- API文档: http://localhost:8000/docs
---
### 方式二Docker Compose (完全隔离)
**前置条件:**
- Docker Desktop (包含 Docker Compose)
**启动:**
```powershell
.\start-docker.ps1
```
或直接使用 docker-compose:
```bash
docker-compose up --build
```
**访问地址:**
- 前端: http://localhost:3000
- 后端: http://localhost:8000
- API文档: http://localhost:8000/docs
- 数据库: localhost:5432 (用户: pharma / 密码: pharma123)
---
## 详细说明
### 方式一:本地开发
#### Windows
1. **启动脚本:**
- PowerShell: `.\start.ps1`
- CMD: `start.bat`
2. **脚本会自动:**
- 检查 Python 和 Node.js
- 创建虚拟环境 (如需要)
- 安装依赖包
- 在新窗口启动后端和前端
3. **手动启动(可选):**
**后端:**
```powershell
cd backend
python -m venv venv
.\venv\Scripts\Activate.ps1
pip install -r requirements.txt
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
**前端(新终端):**
```powershell
cd frontend
npm install
npm run dev
```
4. **PostgreSQL 配置:**
- 确保 PostgreSQL 已启动
- 默认连接: `postgresql://pharma:pharma123@localhost/pharma_news`
- 如需修改,编辑 `.env` 文件
#### Mac/Linux
```bash
# 给脚本执行权限
chmod +x start.sh
# 运行脚本
./start.sh
```
---
### 方式二Docker Compose
#### 基本命令
```bash
# 启动所有服务(首次需要构建)
docker-compose up
# 重新构建并启动
docker-compose up --build
# 在后台运行
docker-compose up -d
# 查看日志
docker-compose logs -f
# 查看特定服务日志
docker-compose logs -f backend
docker-compose logs -f frontend
# 停止服务
docker-compose down
# 完全清理(包括数据)
docker-compose down -v
```
#### 常见问题
**问:如何修改端口?**
编辑 `docker-compose.yml`:
```yaml
ports:
- "8080:8000" # 后端: 外部:内部
- "3001:80" # 前端: 外部:内部
```
**问:如何在容器内执行命令?**
```bash
# 后端
docker-compose exec backend python -m alembic upgrade head
# 前端
docker-compose exec frontend npm run build
```
**问:如何清理 Docker 数据?**
```bash
# 停止并删除所有容器和卷
docker-compose down -v
# 重新启动会创建新的数据库
docker-compose up --build
```
---
## 环境变量配置
### 后端 (.env)
```env
# PostgreSQL
DATABASE_URL=postgresql://pharma:pharma123@localhost:5432/pharma_news
# LLM 服务
LLM_API_KEY=your_api_key
LLM_MODEL=gpt-4
# 其他配置
LOG_LEVEL=INFO
```
### 前端 (.env.local)
```env
VITE_API_BASE_URL=http://localhost:8000/api
```
---
## 故障排除
### 后端无法启动
```bash
# 1. 检查端口是否被占用
netstat -ano | findstr :8000 # Windows
lsof -i :8000 # Mac/Linux
# 2. 检查 Python 和依赖
python --version
pip list | grep fastapi
# 3. 检查数据库连接
# 确保 PostgreSQL 已启动,用户名和密码正确
```
### 前端无法启动
```bash
# 1. 清理依赖
rm -rf node_modules package-lock.json
npm install
# 2. 检查 Node 版本
node --version
npm --version
# 3. 清理 Vite 缓存
rm -rf dist .vite
```
### Docker 容器无法启动
```bash
# 查看详细错误日志
docker-compose logs
# 重建镜像
docker-compose build --no-cache
# 清理所有容器和镜像
docker system prune -a
```
---
## 开发建议
### 热重载
- **后端**: Uvicorn 自动热重载 (修改文件后自动重启)
- **前端**: Vite 自动热重载 (修改代码后自动更新浏览器)
### 调试
**后端:**
```python
# 在代码中添加断点
import pdb; pdb.set_trace()
# 或使用 IDE 的调试功能
```
**前端:**
- 使用浏览器 DevTools (F12)
- VS Code Debugger (需要配置 launch.json)
### 性能监测
```bash
# 后端性能
docker-compose stats backend
# 前端性能
npm run build # 检查构建体积
```
---
## 生产部署
### 使用 Docker
```bash
# 构建生产镜像
docker-compose -f docker-compose.yml -f docker-compose.prod.yml build
# 启动生产环境
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
```
### 本地生产预览
```bash
# 后端
uvicorn app.main:app --host 0.0.0.0 --port 8000
# 前端
npm run build
npm run preview
```
---
## 脚本文件说明
| 文件 | 说明 | 环境 |
|------|------|------|
| `start.ps1` | 本地开发启动脚本 | Windows PowerShell |
| `start.bat` | 本地开发启动脚本 | Windows CMD |
| `start.sh` | 本地开发启动脚本 | Mac/Linux |
| `start-docker.ps1` | Docker Compose 启动脚本 | Windows PowerShell |
---
## 更多帮助
- FastAPI 文档: https://fastapi.tiangolo.com/
- Vue.js 文档: https://vuejs.org/
- Vite 文档: https://vitejs.dev/
- PostgreSQL 文档: https://www.postgresql.org/docs/
- Docker 文档: https://docs.docker.com/

291
backend.log Normal file
View File

@@ -0,0 +1,291 @@
INFO: Will watch for changes in these directories: ['C:\\Users\\Chenwu\\OneDrive - AZCollaboration\\Desktop\\tmp\\ai_news_v1\\backend']
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [45476] using WatchFiles
INFO: Started server process [55920]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: 127.0.0.1:62512 - "GET /api/news?page_size=1&date=2026-05-26 HTTP/1.1" 200 OK
INFO: 127.0.0.1:51410 - "GET /api/news?page_size=20&date=2026-05-26 HTTP/1.1" 200 OK
INFO: 127.0.0.1:57871 - "GET /api/news?page_size=20&date=2026-05-26 HTTP/1.1" 200 OK
INFO: 127.0.0.1:50345 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 127.0.0.1:62949 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 127.0.0.1:50346 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 127.0.0.1:63742 - "GET /api/news?page_size=20&date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
Daily pipeline failed: (sqlalchemy.dialects.postgresql.asyncpg.InterfaceError) <class 'asyncpg.exceptions._base.InterfaceError'>: connection is closed
[SQL: INSERT INTO system_logs (event_type, message, level, created_at) VALUES ($1::VARCHAR, $2::VARCHAR, $3::VARCHAR, $4::TIMESTAMP WITHOUT TIME ZONE) RETURNING system_logs.id]
[parameters: ('pipeline_start', '\u6bcf\u65e5\u6d41\u6c34\u7ebf\u542f\u52a8', 'INFO', datetime.datetime(2026, 5, 26, 22, 0, 0, 12142))]
(Background on this error at: https://sqlalche.me/e/20/rvf5)
Traceback (most recent call last):
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 843, in _start_transaction
self._transaction = self._connection.transaction(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
isolation=self.isolation_level,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
readonly=self.readonly,
^^^^^^^^^^^^^^^^^^^^^^^
deferrable=self.deferrable,
^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\asyncpg\connection.py", line 302, in transaction
self._check_open()
~~~~~~~~~~~~~~~~^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\asyncpg\connection.py", line 1605, in _check_open
raise exceptions.InterfaceError('connection is closed')
asyncpg.exceptions._base.InterfaceError: connection is closed
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\engine\base.py", line 1969, in _exec_single_context
self.dialect.do_execute(
~~~~~~~~~~~~~~~~~~~~~~~^
cursor, str_statement, effective_parameters, context
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\engine\default.py", line 952, in do_execute
cursor.execute(statement, parameters)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 585, in execute
self._adapt_connection.await_(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
self._prepare_and_execute(operation, parameters)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\util\_concurrency_py3k.py", line 132, in await_only
return current.parent.switch(awaitable) # type: ignore[no-any-return,attr-defined] # noqa: E501
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\util\_concurrency_py3k.py", line 196, in greenlet_spawn
value = await result
^^^^^^^^^^^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 520, in _prepare_and_execute
await adapt_connection._start_transaction()
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 850, in _start_transaction
self._handle_exception(error)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 797, in _handle_exception
raise translated_error from error
sqlalchemy.dialects.postgresql.asyncpg.AsyncAdapt_asyncpg_dbapi.InterfaceError: <class 'asyncpg.exceptions._base.InterfaceError'>: connection is closed
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "C:\Users\Chenwu\OneDrive - AZCollaboration\Desktop\tmp\ai_news_v1\backend\app\scheduler.py", line 14, in daily_pipeline_job
await run_daily_pipeline(db)
File "C:\Users\Chenwu\OneDrive - AZCollaboration\Desktop\tmp\ai_news_v1\backend\app\ai\processor.py", line 120, in run_daily_pipeline
await _log(db, "INFO", "pipeline_start", "\u6bcf\u65e5\u6d41\u6c34\u7ebf\u542f\u52a8")
File "C:\Users\Chenwu\OneDrive - AZCollaboration\Desktop\tmp\ai_news_v1\backend\app\ai\processor.py", line 47, in _log
await db.commit()
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\ext\asyncio\session.py", line 999, in commit
await greenlet_spawn(self.sync_session.commit)
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\util\_concurrency_py3k.py", line 203, in greenlet_spawn
result = context.switch(value)
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\orm\session.py", line 2034, in commit
trans.commit(_to_root=True)
~~~~~~~~~~~~^^^^^^^^^^^^^^^
File "<string>", line 2, in commit
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\orm\state_changes.py", line 137, in _go
ret_value = fn(self, *arg, **kw)
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\orm\session.py", line 1315, in commit
self._prepare_impl()
~~~~~~~~~~~~~~~~~~^^
File "<string>", line 2, in _prepare_impl
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\orm\state_changes.py", line 137, in _go
ret_value = fn(self, *arg, **kw)
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\orm\session.py", line 1290, in _prepare_impl
self.session.flush()
~~~~~~~~~~~~~~~~~~^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\orm\session.py", line 4352, in flush
self._flush(objects)
~~~~~~~~~~~^^^^^^^^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\orm\session.py", line 4487, in _flush
with util.safe_reraise():
~~~~~~~~~~~~~~~~~^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\util\langhelpers.py", line 122, in __exit__
raise exc_value.with_traceback(exc_tb)
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\orm\session.py", line 4448, in _flush
flush_context.execute()
~~~~~~~~~~~~~~~~~~~~~^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\orm\unitofwork.py", line 465, in execute
rec.execute(self)
~~~~~~~~~~~^^^^^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\orm\unitofwork.py", line 641, in execute
util.preloaded.orm_persistence.save_obj(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
self.mapper,
^^^^^^^^^^^^
uow.states_for_mapper_hierarchy(self.mapper, False, False),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
uow,
^^^^
)
^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\orm\persistence.py", line 94, in save_obj
_emit_insert_statements(
~~~~~~~~~~~~~~~~~~~~~~~^
base_mapper,
^^^^^^^^^^^^
...<3 lines>...
insert,
^^^^^^^
)
^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\orm\persistence.py", line 1234, in _emit_insert_statements
result = connection.execute(
statement,
params,
execution_options=execution_options,
)
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\engine\base.py", line 1421, in execute
return meth(
self,
distilled_parameters,
execution_options or NO_OPTIONS,
)
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\sql\elements.py", line 526, in _execute_on_connection
return connection._execute_clauseelement(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
self, distilled_params, execution_options
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\engine\base.py", line 1643, in _execute_clauseelement
ret = self._execute_context(
dialect,
...<8 lines>...
cache_hit=cache_hit,
)
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\engine\base.py", line 1848, in _execute_context
return self._exec_single_context(
~~~~~~~~~~~~~~~~~~~~~~~~~^
dialect, context, statement, parameters
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\engine\base.py", line 1988, in _exec_single_context
self._handle_dbapi_exception(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
e, str_statement, effective_parameters, cursor, context
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\engine\base.py", line 2365, in _handle_dbapi_exception
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\engine\base.py", line 1969, in _exec_single_context
self.dialect.do_execute(
~~~~~~~~~~~~~~~~~~~~~~~^
cursor, str_statement, effective_parameters, context
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\engine\default.py", line 952, in do_execute
cursor.execute(statement, parameters)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 585, in execute
self._adapt_connection.await_(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
self._prepare_and_execute(operation, parameters)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\util\_concurrency_py3k.py", line 132, in await_only
return current.parent.switch(awaitable) # type: ignore[no-any-return,attr-defined] # noqa: E501
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\util\_concurrency_py3k.py", line 196, in greenlet_spawn
value = await result
^^^^^^^^^^^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 520, in _prepare_and_execute
await adapt_connection._start_transaction()
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 850, in _start_transaction
self._handle_exception(error)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
File "C:\Users\Chenwu\AppData\Roaming\Python\Python314\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 797, in _handle_exception
raise translated_error from error
sqlalchemy.exc.InterfaceError: (sqlalchemy.dialects.postgresql.asyncpg.InterfaceError) <class 'asyncpg.exceptions._base.InterfaceError'>: connection is closed
[SQL: INSERT INTO system_logs (event_type, message, level, created_at) VALUES ($1::VARCHAR, $2::VARCHAR, $3::VARCHAR, $4::TIMESTAMP WITHOUT TIME ZONE) RETURNING system_logs.id]
[parameters: ('pipeline_start', '\u6bcf\u65e5\u6d41\u6c34\u7ebf\u542f\u52a8', 'INFO', datetime.datetime(2026, 5, 26, 22, 0, 0, 12142))]
(Background on this error at: https://sqlalche.me/e/20/rvf5)
INFO: 58.37.56.82:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/featured?date=2026-05-27 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news?date=2026-05-27&page=1 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/featured?date=2026-05-27 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news?date=2026-05-27&page=1 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/featured?date=2026-05-27 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news?date=2026-05-27&page=1 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61914 - "GET /api/news?page_size=1&date=2026-05-26 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/featured?date=2026-05-27 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news?date=2026-05-27&page=1 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/featured?date=2026-05-27 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news?date=2026-05-27&page=1 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/featured?date=2026-05-27 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news?date=2026-05-27&page=1 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/featured?date=2026-05-27 HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 58.37.56.82:0 - "GET /api/news?date=2026-05-27&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-27 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-27&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-27 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-27&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-26 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-26&page=1 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/featured?date=2026-05-27 HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 149.112.116.74:0 - "GET /api/news?date=2026-05-27&page=1 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61458 - "GET /api/news/featured?date=2026-05-27 HTTP/1.1" 200 OK
INFO: 127.0.0.1:65226 - "GET /api/news/dates HTTP/1.1" 200 OK
INFO: 127.0.0.1:61459 - "GET /api/news?date=2026-05-27&page=1 HTTP/1.1" 200 OK

Binary file not shown.

View File

@@ -0,0 +1,26 @@
"""add image_url to raw_news and processed_news
Revision ID: 0002
Revises: 0001
Create Date: 2026-05-27
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "0002"
down_revision: Union[str, None] = "0001"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column("raw_news", sa.Column("image_url", sa.String(2000), nullable=True))
op.add_column("processed_news", sa.Column("image_url", sa.String(2000), nullable=True))
def downgrade() -> None:
op.drop_column("processed_news", "image_url")
op.drop_column("raw_news", "image_url")

View File

@@ -25,18 +25,20 @@ ANALYSIS_PROMPT = """分析以下新闻,返回严格的 JSON 格式结果,
"summary": "中文摘要100-150字客观陈述核心内容",
"opinion": "核心观点或行业影响50-100字分析性语言点明实际意义",
"keywords": ["关键词1", "关键词2", "关键词3", "关键词4", "关键词5"],
"importance_score": 8.5,
"importance_score": 85,
"importance_reason": "评分理由30字内",
"category": "药品监管"
}}
category 只能是以下四个之一:药品监管 / 临床研究 / 行业动态 / 政策法规
importance_score 评分标准1-10
9-10重大监管决定 / 突破性研究 / 影响整个行业的政策
7-8 :行业重要动态,有明显商业或学术价值
5-6 :常规行业新闻,有一定参考价值
1-4 :普通资讯,信息价值有限
importance_score 评分标准1-100整数
90-100:重大监管决定 / 突破性研究 / 影响整个行业的政策
70-89 :行业重要动态,有明显商业或学术价值
50-69 :常规行业新闻,有一定参考价值
1-49 :普通资讯,信息价值有限
注意:只有 85 分及以上的新闻才有资格进入每日精选,请严格区分。
"""
@@ -70,7 +72,8 @@ async def _analyze_article(client: LLMClient, title: str, content: str, language
async def _select_top_10(db: AsyncSession, target: date):
"""Reset featured flags and elect TOP 10 with category diversity."""
"""Reset featured flags and elect TOP 10 with category diversity.
Only news with importance_score >= 85 is eligible for 精选."""
result = await db.execute(
select(ProcessedNews)
.where(func.date(ProcessedNews.processed_at) == target)
@@ -78,25 +81,28 @@ async def _select_top_10(db: AsyncSession, target: date):
)
all_news = result.scalars().all()
# Reset
# Reset all
for n in all_news:
n.is_featured = False
n.featured_rank = None
# Only candidates with score >= 85
candidates = [n for n in all_news if n.importance_score >= 85]
categories = ["药品监管", "临床研究", "行业动态", "政策法规"]
selected: list[ProcessedNews] = []
seen_cats: set[str] = set()
# First pass: one guaranteed per category
# First pass: one guaranteed per category (from high-score candidates)
for cat in categories:
for n in all_news:
for n in candidates:
if n.category == cat and cat not in seen_cats and n not in selected:
selected.append(n)
seen_cats.add(cat)
break
# Second pass: fill up to 10 by score
for n in all_news:
# Second pass: fill up to 10 by score (still from candidates only)
for n in candidates:
if len(selected) >= 10:
break
if n not in selected:
@@ -141,6 +147,7 @@ async def run_daily_pipeline(db: AsyncSession):
title=item["title"],
url=item["url"],
raw_content=item["content"],
image_url=item.get("image_url"),
published_at=item["published_at"],
))
raw_added += 1
@@ -170,11 +177,12 @@ async def run_daily_pipeline(db: AsyncSession):
summary=analysis.get("summary", ""),
opinion=analysis.get("opinion"),
keywords=analysis.get("keywords", []),
importance_score=float(analysis.get("importance_score", 5.0)),
importance_score=float(analysis.get("importance_score", 50.0)),
importance_reason=analysis.get("importance_reason"),
category=analysis.get("category", "行业动态"),
source_name=raw.source.name if raw.source else "",
source_url=raw.url,
image_url=raw.image_url,
published_at=raw.published_at,
))
raw.status = "processed"

View File

@@ -24,6 +24,7 @@ def _serialize(n: ProcessedNews) -> dict:
"featured_rank": n.featured_rank,
"source_name": n.source_name or "",
"source_url": n.source_url or "",
"image_url": n.image_url or None,
"published_at": n.published_at.isoformat() if n.published_at else None,
"processed_at": n.processed_at.isoformat() if n.processed_at else None,
}

View File

@@ -25,6 +25,31 @@ def _parse_date(raw: str) -> Optional[datetime]:
return None
def _extract_image(entry) -> Optional[str]:
"""Try to pull an image URL from common RSS media extensions."""
# <media:thumbnail>
thumbnails = getattr(entry, "media_thumbnail", [])
if thumbnails:
url = thumbnails[0].get("url", "").strip()
if url:
return url
# <media:content medium="image">
for mc in getattr(entry, "media_content", []):
mc_type = mc.get("type", "")
mc_medium = mc.get("medium", "")
if mc_medium == "image" or mc_type.startswith("image/"):
url = mc.get("url", "").strip()
if url:
return url
# <enclosure type="image/...">
for enc in getattr(entry, "enclosures", []):
if enc.get("type", "").startswith("image/"):
url = (enc.get("href") or enc.get("url") or "").strip()
if url:
return url
return None
async def fetch_rss(url: str, max_items: int = 30) -> list[dict]:
try:
async with httpx.AsyncClient(headers=HEADERS, timeout=30, follow_redirects=True) as client:
@@ -54,6 +79,7 @@ async def fetch_rss(url: str, max_items: int = 30) -> list[dict]:
"url": link,
"content": content[:3000],
"published_at": _parse_date(published_raw),
"image_url": _extract_image(entry),
})
logger.info(f"RSS {url}: got {len(items)} items")

View File

@@ -28,6 +28,7 @@ class RawNews(Base):
title: Mapped[str] = mapped_column(String(500))
url: Mapped[str] = mapped_column(String(1000), unique=True)
raw_content: Mapped[Optional[str]] = mapped_column(Text)
image_url: Mapped[Optional[str]] = mapped_column(String(2000))
published_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
crawled_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
status: Mapped[str] = mapped_column(String(20), default="pending") # pending|processed|skipped|error
@@ -52,6 +53,7 @@ class ProcessedNews(Base):
featured_rank: Mapped[Optional[int]] = mapped_column(Integer)
source_name: Mapped[Optional[str]] = mapped_column(String(200))
source_url: Mapped[Optional[str]] = mapped_column(String(1000))
image_url: Mapped[Optional[str]] = mapped_column(String(2000))
published_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
processed_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

87
frontend/dist/assets/index-DzAkVrZs.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -7,8 +7,8 @@
<!-- 国内可将 Google Fonts 替换为 fonts.loli.net 镜像 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-CEnIijqK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-fkMVII4Y.css">
<script type="module" crossorigin src="/assets/index-DzAkVrZs.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DuApBvfc.css">
</head>
<body>
<div id="app"></div>

View File

View File

@@ -0,0 +1,63 @@
> pharma-intel-frontend@1.0.0 dev
> vite --host 0.0.0.0
Port 5173 is in use, trying another one...
VITE v6.4.2 ready in 360 ms
➜ Local: http://localhost:5174/
➜ Network: http://198.18.0.1:5174/
➜ Network: http://100.106.241.41:5174/
➜ Network: http://172.19.112.1:5174/
➜ Network: http://10.119.18.252:5174/
6:31:24 PM [vite] (client) hmr update /src/views/NewsReader.vue
6:31:31 PM [vite] (client) hmr update /src/views/NewsReader.vue
6:31:37 PM [vite] (client) hmr update /src/views/NewsReader.vue
6:32:13 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
6:32:21 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
6:42:38 PM [vite] (client) hmr update /src/views/NewsReader.vue
6:42:50 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
6:42:54 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
6:55:03 PM [vite] (client) hmr update /src/views/NewsReader.vue
6:55:23 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
6:55:33 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
8:15:11 PM [vite] (client) page reload index.html
8:16:10 PM [vite] (client) hmr update /src/styles/theme.css
8:16:21 PM [vite] (client) hmr update /src/components/ThemeControls.vue, /src/components/ThemeControls.vue?vue&type=style&index=0&scoped=c6bc2c52&lang.css
8:16:42 PM [vite] (client) hmr update /src/components/NewsCard.vue, /src/components/NewsCard.vue?vue&type=style&index=0&scoped=1d5294f0&lang.css
8:17:18 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
8:17:59 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
8:18:14 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
8:18:47 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
8:18:59 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
8:19:18 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
8:19:43 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
8:20:10 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
8:20:31 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
8:40:02 PM [vite] (client) hmr update /src/views/NewsReader.vue
8:40:53 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
8:41:13 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
8:59:15 PM [vite] (client) hmr update /src/views/NewsReader.vue
8:59:28 PM [vite] (client) hmr update /src/views/NewsReader.vue
9:00:23 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
9:00:35 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
10:33:14 PM [vite] (client) hmr update /src/styles/theme.css
10:33:20 PM [vite] (client) hmr update /src/styles/theme.css
10:33:26 PM [vite] (client) hmr update /src/styles/theme.css
10:33:31 PM [vite] (client) hmr update /src/styles/theme.css
10:33:36 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
10:33:43 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
10:33:48 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
10:33:54 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
10:34:00 PM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
10:34:08 PM [vite] (client) hmr update /src/components/NewsCard.vue?vue&type=style&index=0&scoped=1d5294f0&lang.css
12:42:07 AM [vite] (client) hmr update /src/components/NewsCard.vue
12:42:13 AM [vite] (client) hmr update /src/components/NewsCard.vue
12:42:19 AM [vite] (client) hmr update /src/components/NewsCard.vue?vue&type=style&index=0&scoped=1d5294f0&lang.css
12:42:25 AM [vite] (client) hmr update /src/views/NewsReader.vue
12:42:31 AM [vite] (client) hmr update /src/views/NewsReader.vue
12:42:39 AM [vite] (client) hmr update /src/views/NewsReader.vue
12:42:46 AM [vite] (client) hmr update /src/views/NewsReader.vue
12:42:54 AM [vite] (client) hmr update /src/views/NewsReader.vue?vue&type=style&index=0&scoped=94478ee1&lang.css
12:43:13 AM [vite] (client) hmr update /src/views/NewsReader.vue

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,19 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- Light blue background diamond -->
<polygon points="100,20 180,100 100,180 20,100" fill="#D0E8F8"/>
<!-- Top triangle (light cyan) -->
<polygon points="100,50 140,85 60,85" fill="#A0D8F0"/>
<!-- Right triangle (cyan) -->
<polygon points="140,85 150,130 100,110" fill="#3DD9C6"/>
<!-- Bottom triangle (blue) -->
<polygon points="60,85 100,110 70,150" fill="#1E5F8F"/>
<!-- Left triangle (purple) -->
<polygon points="60,85 55,130 100,110" fill="#7B3FF2"/>
<!-- Center diamond (magenta) -->
<polygon points="100,85 115,100 100,115 85,100" fill="#D946EF"/>
</svg>

After

Width:  |  Height:  |  Size: 666 B

View File

@@ -1,7 +1,11 @@
<template>
<div class="news-card" :class="{ featured: news.is_featured }" @click="$emit('open', news)">
<!-- cover image (shown only when available) -->
<div v-if="news.image_url" class="card-cover">
<img :src="news.image_url" :alt="news.title_zh" class="cover-img" @error="onImgError" />
</div>
<div class="card-top">
<span class="score-badge" :class="badgeClass">{{ news.importance_score?.toFixed(1) }}</span>
<span class="score-badge" :class="badgeClass">{{ Math.round(news.importance_score) }}</span>
<span class="cat-label">{{ news.category }}</span>
<span class="spacer"></span>
<span class="source-meta">{{ news.source_name }} · {{ timeAgo }}</span>
@@ -28,12 +32,18 @@ defineEmits(['open'])
const badgeClass = computed(() => {
const s = props.news.importance_score
if (s >= 9) return 'badge-red'
if (s >= 7) return 'badge-amber'
if (s >= 5) return 'badge-blue'
if (s >= 90) return 'badge-red'
if (s >= 70) return 'badge-amber'
if (s >= 50) return 'badge-blue'
return 'badge-gray'
})
function onImgError(e) {
// hide the whole cover wrapper on broken image
const el = e.target?.closest('.card-cover')
if (el) el.style.display = 'none'
}
const timeAgo = computed(() => {
if (!props.news.published_at) return ''
const diff = Date.now() - new Date(props.news.published_at).getTime()
@@ -48,9 +58,13 @@ const timeAgo = computed(() => {
.news-card {
background: var(--bg-card);
border: 0.5px solid var(--rule2);
border-top: 0.5px solid var(--glass-border);
border-left: 0.5px solid var(--glass-border);
border-radius: var(--r);
padding: 16px 18px;
cursor: pointer;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: border-color 0.15s, background 0.15s;
}
.news-card:hover {
@@ -61,6 +75,22 @@ const timeAgo = computed(() => {
border-left: 2px solid var(--blue);
}
/* cover image */
.card-cover {
margin: -16px -18px 12px;
border-radius: var(--r) var(--r) 0 0;
overflow: hidden;
height: 140px;
}
.cover-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.3s ease;
}
.news-card:hover .cover-img { transform: scale(1.03); }
/* top row */
.card-top {
display: flex;

View File

@@ -16,7 +16,7 @@
>
<span class="rank">{{ String(i + 1).padStart(2, '0') }}</span>
<div class="item-body">
<span class="item-score" :class="badgeClass(n.importance_score)">{{ n.importance_score?.toFixed(1) }}</span>
<span class="item-score" :class="badgeClass(n.importance_score)">{{ Math.round(n.importance_score) }}</span>
<span class="item-title">{{ n.title_zh }}</span>
</div>
</div>
@@ -47,8 +47,8 @@ defineProps({
defineEmits(['select', 'date-change'])
function badgeClass(score) {
if (score >= 9) return 'badge-red'
if (score >= 7) return 'badge-amber'
if (score >= 90) return 'badge-red'
if (score >= 70) return 'badge-amber'
return 'badge-blue'
}
</script>

View File

@@ -5,12 +5,15 @@
/* ── Light ────────────────────────────────────────────────────── */
:root {
--bg: #F0F6FA;
--bg-card: #FFFFFF;
--bg-hover: rgba(13,155,142,0.05);
--bg-1: #FFFFFF;
--bg-2: #F5F9FC;
--bg-hi: rgba(13,155,142,0.06);
--bg: #EDF3F8;
--bg-grad-a: #DAE9F5;
--bg-grad-b: #E3F2EC;
--bg-card: rgba(255, 255, 255, 0.68);
--bg-hover: rgba(13,155,142,0.06);
--bg-1: rgba(240, 246, 250, 0.82);
--bg-2: #F5F9FC;
--bg-hi: rgba(13,155,142,0.06);
--glass-border: rgba(255, 255, 255, 0.52);
/* primary accent — clinical teal */
--blue: #0D9B8E;
@@ -63,12 +66,15 @@
/* ── Dark ─────────────────────────────────────────────────────── */
:root[data-theme="dark"] {
--bg: #050D12;
--bg-card: #0B1820;
--bg-hover: rgba(61,217,198,0.06);
--bg-1: #0B1820;
--bg-2: #071018;
--bg-hi: rgba(61,217,198,0.06);
--bg: #050D12;
--bg-grad-a: #01080E;
--bg-grad-b: #010E09;
--bg-card: rgba(11, 24, 32, 0.52);
--bg-hover: rgba(61,217,198,0.07);
--bg-1: rgba(5, 13, 18, 0.78);
--bg-2: #071018;
--bg-hi: rgba(61,217,198,0.06);
--glass-border: rgba(61, 217, 198, 0.14);
--blue: #3DD9C6;
--blue-2: #3DD9C6;
@@ -113,7 +119,8 @@ html { font-size: 16px; }
body {
font-family: var(--sans);
background: var(--bg);
background: linear-gradient(135deg, var(--bg-grad-a) 0%, var(--bg) 45%, var(--bg-grad-b) 100%) fixed;
background-attachment: fixed;
color: var(--t1);
line-height: 1.5;
font-size: 14px;
@@ -128,7 +135,7 @@ input, select, textarea { font: inherit; }
/* Element Plus overrides */
.el-input__wrapper { background: var(--bg-card) !important; box-shadow: 0 0 0 1px var(--rule2) !important; }
.el-input__inner { color: var(--t1) !important; }
.el-select-dropdown { background: var(--bg-card); border-color: var(--rule2); }
.el-select-dropdown { background: var(--bg-2); border-color: var(--rule2); }
.el-select-dropdown__item { color: var(--t2); }
.el-select-dropdown__item.hover,
.el-select-dropdown__item:hover { background: var(--bg-hover); }

View File

@@ -6,7 +6,7 @@
<div class="header-inner">
<div class="hd-left">
<div class="logo">
<div class="logo-icon">PI</div>
<img src="@/assets/log_picture.png" alt="PI Logo" class="logo-icon" />
<span class="logo-text">医药情报</span>
</div>
<div class="hd-sep"></div>
@@ -69,7 +69,7 @@
<span class="tl-source">{{ n.source_name }}</span>
<span class="tl-rank">#{{ String(i + 1).padStart(2, '0') }}</span>
<span class="score-badge" :class="badgeClass(n.importance_score)">
{{ n.importance_score?.toFixed(1) }}
{{ Math.round(n.importance_score) }}
</span>
<span class="tl-featured">精选</span>
</div>
@@ -189,7 +189,7 @@
<span class="dig-chip">{{ n.source_name }}</span>
<span class="dig-chip dig-chip-time">{{ formatTime(n.published_at) }}</span>
<span class="dig-chip" :class="badgeClass(n.importance_score)">
{{ n.importance_score?.toFixed(1) }}
{{ Math.round(n.importance_score) }}
</span>
</div>
<p v-if="n.summary" class="dig-card-sum">{{ n.summary }}</p>
@@ -224,19 +224,25 @@
<div class="sh-tags">
<span class="cat-chip">{{ selectedNews.category }}</span>
<span class="score-badge" :class="badgeClass(selectedNews.importance_score)">
{{ selectedNews.importance_score?.toFixed(1) }}
{{ Math.round(selectedNews.importance_score) }}
</span>
</div>
<h2 class="sh-title">{{ selectedNews.title_zh }}</h2>
<p class="sh-meta">{{ selectedNews.source_name }} · {{ selectedNews.published_at?.slice(0, 10) }}</p>
</div>
<!-- cover image in detail sheet -->
<div v-if="selectedNews.image_url" class="sh-cover">
<img :src="selectedNews.image_url" :alt="selectedNews.title_zh" class="sh-cover-img"
@error="e => e.target.closest('.sh-cover').style.display='none'" />
</div>
<div class="sheet-bd">
<p class="sh-summary">{{ selectedNews.summary }}</p>
<div v-if="selectedNews.opinion" class="sh-block sh-blue">
<b>核心观点</b>{{ selectedNews.opinion }}
</div>
<div v-if="selectedNews.importance_reason" class="sh-block sh-green">
<b>评分依据 · {{ selectedNews.importance_score?.toFixed(1) }} </b>
<b>评分依据 · {{ Math.round(selectedNews.importance_score) }} </b>
{{ selectedNews.importance_reason }}
</div>
<div v-if="selectedNews.keywords?.length" class="sh-kws">
@@ -319,9 +325,9 @@ const digestCategoryCount = computed(() =>
)
function badgeClass(score) {
if (score >= 9) return 'badge-red'
if (score >= 7) return 'badge-amber'
if (score >= 5) return 'badge-blue'
if (score >= 90) return 'badge-red'
if (score >= 70) return 'badge-amber'
if (score >= 50) return 'badge-blue'
return 'badge-gray'
}
@@ -466,6 +472,8 @@ onUnmounted(() => window.removeEventListener('keydown', onKeyDown))
z-index: 100;
background: var(--bg-1);
border-bottom: 1px solid var(--rule2);
backdrop-filter: blur(18px) saturate(1.5);
-webkit-backdrop-filter: blur(18px) saturate(1.5);
}
.header-inner {
max-width: 1280px;
@@ -483,11 +491,8 @@ onUnmounted(() => window.removeEventListener('keydown', onKeyDown))
.logo { display: flex; align-items: center; gap: 9px; margin-right: 20px; flex-shrink: 0; }
.logo-icon {
width: 28px; height: 28px;
background: var(--blue);
border-radius: 5px;
display: flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 700; color: var(--bg);
font-family: var(--mono); letter-spacing: -0.02em;
flex-shrink: 0;
}
.logo-text {
font-size: 13px; font-weight: 700; color: var(--t1);
@@ -694,8 +699,12 @@ onUnmounted(() => window.removeEventListener('keydown', onKeyDown))
.tl-card {
padding: 14px 16px;
border: 0.5px solid var(--rule2);
border-top: 0.5px solid var(--glass-border);
border-left: 0.5px solid var(--glass-border);
border-radius: var(--r);
background: var(--bg-card);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
@@ -817,6 +826,9 @@ onUnmounted(() => window.removeEventListener('keydown', onKeyDown))
flex-direction: column;
border-right: 1px solid var(--rule2);
padding-right: 14px;
background: var(--bg-1);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.dig-arc-label {
font-size: 9px;
@@ -989,9 +1001,13 @@ onUnmounted(() => window.removeEventListener('keydown', onKeyDown))
display: block;
background: var(--bg-card);
border: 0.5px solid var(--rule2);
border-top: 0.5px solid var(--glass-border);
border-left: 0.5px solid var(--glass-border);
border-radius: 12px;
padding: 16px 18px;
text-decoration: none;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: border-color 0.15s, background 0.15s;
}
.dig-card:hover {
@@ -1072,9 +1088,11 @@ onUnmounted(() => window.removeEventListener('keydown', onKeyDown))
.sheet {
width: 100%; max-width: 660px;
background: var(--bg-1);
border-top: 1px solid var(--rule2);
border-top: 1px solid var(--glass-border);
border-radius: 12px 12px 0 0;
max-height: 88vh; overflow-y: auto; scrollbar-width: none;
backdrop-filter: blur(24px) saturate(1.4);
-webkit-backdrop-filter: blur(24px) saturate(1.4);
}
.sheet::-webkit-scrollbar { display: none; }
.sheet-handle {
@@ -1101,6 +1119,18 @@ onUnmounted(() => window.removeEventListener('keydown', onKeyDown))
letter-spacing: 0.06em; text-transform: uppercase;
}
.sh-cover {
margin: 0;
overflow: hidden;
max-height: 220px;
}
.sh-cover-img {
width: 100%;
height: 220px;
object-fit: cover;
display: block;
}
.sheet-bd { padding: 16px 24px 0; }
.sh-summary { font-size: 13.5px; color: var(--t2); line-height: 1.75; margin-bottom: 14px; }
.sh-block {

View File

@@ -1,257 +0,0 @@
# Design System: MiniMax
## 1. Visual Theme & Atmosphere
MiniMax's website is a clean, product-showcase platform for a Chinese AI technology company that bridges consumer-friendly appeal with technical credibility. The design language is predominantly white-space-driven with a light, airy feel — pure white backgrounds (`#ffffff`) dominate, letting colorful product cards and AI model illustrations serve as the visual anchors. The overall aesthetic sits at the intersection of Apple's product marketing clarity and a playful, rounded design language that makes AI technology feel approachable.
The typography system is notably multi-font: DM Sans serves as the primary UI workhorse, Outfit handles display headings with geometric elegance, Poppins appears for mid-tier headings, and Roboto handles data-heavy contexts. This variety reflects a brand in rapid growth — each font serves a distinct communicative purpose rather than competing for attention. The hero heading at 80px weight 500 in both DM Sans and Outfit with a tight 1.10 line-height creates a bold but not aggressive opening statement.
What makes MiniMax distinctive is its pill-button geometry (9999px radius) for navigation and primary actions, combined with softer 8px24px radiused cards for product showcases. The product cards themselves are richly colorful — vibrant gradients in pink, purple, orange, and blue — creating a "gallery of AI capabilities" feel. Against the white canvas, these colorful cards pop like app icons on a phone home screen, making each AI model/product feel like a self-contained creative tool.
**Key Characteristics:**
- White-dominant layout with colorful product card accents
- Multi-font system: DM Sans (UI), Outfit (display), Poppins (mid-tier), Roboto (data)
- Pill buttons (9999px radius) for primary navigation and CTAs
- Generous rounded cards (20px24px radius) for product showcases
- Brand blue spectrum: from `#1456f0` (brand-6) through `#3b82f6` (primary-500) to `#60a5fa` (light)
- Brand pink (`#ea5ec1`) as secondary accent
- Near-black text (`#222222`, `#18181b`) on white backgrounds
- Purple-tinted shadows (`rgba(44, 30, 116, 0.16)`) creating subtle brand-colored depth
- Dark footer section (`#181e25`) with product/company links
## 2. Color Palette & Roles
### Brand Primary
- **Brand Blue** (`#1456f0`): `--brand-6`, primary brand identity color
- **Sky Blue** (`#3daeff`): `--col-brand00`, lighter brand variant for accents
- **Brand Pink** (`#ea5ec1`): `--col-brand02`, secondary brand accent
### Blue Scale (Primary)
- **Primary 200** (`#bfdbfe`): `--color-primary-200`, light blue backgrounds
- **Primary Light** (`#60a5fa`): `--color-primary-light`, active states, highlights
- **Primary 500** (`#3b82f6`): `--color-primary-500`, standard blue actions
- **Primary 600** (`#2563eb`): `--color-primary-600`, hover states
- **Primary 700** (`#1d4ed8`): `--color-primary-700`, pressed/active states
- **Brand Deep** (`#17437d`): `--brand-3`, deep blue for emphasis
### Text Colors
- **Near Black** (`#222222`): `--col-text00`, primary text
- **Dark** (`#18181b`): Button text, headings
- **Charcoal** (`#181e25`): Dark surface text, footer background
- **Dark Gray** (`#45515e`): `--col-text04`, secondary text
- **Mid Gray** (`#8e8e93`): Tertiary text, muted labels
- **Light Gray** (`#5f5f5f`): `--brand-2`, helper text
### Surface & Background
- **Pure White** (`#ffffff`): `--col-bg13`, primary background
- **Light Gray** (`#f0f0f0`): Secondary button backgrounds
- **Glass White** (`hsla(0, 0%, 100%, 0.4)`): `--fill-bg-white`, frosted glass overlay
- **Border Light** (`#f2f3f5`): Subtle section dividers
- **Border Gray** (`#e5e7eb`): Component borders
### Semantic
- **Success Background** (`#e8ffea`): `--success-bg`, positive state backgrounds
### Shadows
- **Standard** (`rgba(0, 0, 0, 0.08) 0px 4px 6px`): Default card shadow
- **Soft Glow** (`rgba(0, 0, 0, 0.08) 0px 0px 22.576px`): Ambient soft shadow
- **Brand Purple** (`rgba(44, 30, 116, 0.16) 0px 0px 15px`): Brand-tinted glow
- **Brand Purple Offset** (`rgba(44, 30, 116, 0.11) 6.5px 2px 17.5px`): Directional brand glow
- **Card Elevation** (`rgba(36, 36, 36, 0.08) 0px 12px 16px -4px`): Lifted card shadow
## 3. Typography Rules
### Font Families
- **Primary UI**: `DM Sans`, with fallbacks: `Helvetica Neue, Helvetica, Arial`
- **Display**: `Outfit`, with fallbacks: `Helvetica Neue, Helvetica, Arial`
- **Mid-tier**: `Poppins`
- **Data/Technical**: `Roboto`, with fallbacks: `Helvetica Neue, Helvetica, Arial`
### Hierarchy
| Role | Font | Size | Weight | Line Height | Notes |
|------|------|------|--------|-------------|-------|
| Display Hero | DM Sans / Outfit | 80px (5.00rem) | 500 | 1.10 (tight) | Hero headlines |
| Section Heading | Outfit | 31px (1.94rem) | 600 | 1.50 | Feature section titles |
| Section Heading Alt | Roboto / DM Sans | 32px (2.00rem) | 600 | 0.88 (tight) | Compact headers |
| Card Title | Outfit | 28px (1.75rem) | 500600 | 1.71 (relaxed) | Product card headings |
| Sub-heading | Poppins | 24px (1.50rem) | 500 | 1.50 | Mid-tier headings |
| Feature Label | Poppins | 18px (1.13rem) | 500 | 1.50 | Feature names |
| Body Large | DM Sans | 20px (1.25rem) | 500 | 1.50 | Emphasized body |
| Body | DM Sans | 16px (1.00rem) | 400500 | 1.50 | Standard body text |
| Body Bold | DM Sans | 16px (1.00rem) | 700 | 1.50 | Strong emphasis |
| Nav/Link | DM Sans | 14px (0.88rem) | 400500 | 1.50 | Navigation, links |
| Button Small | DM Sans | 13px (0.81rem) | 600 | 1.50 | Compact buttons |
| Caption | DM Sans / Poppins | 13px (0.81rem) | 400 | 1.70 (relaxed) | Metadata |
| Small Label | DM Sans | 12px (0.75rem) | 500600 | 1.251.50 | Tags, badges |
| Micro | DM Sans / Outfit | 10px (0.63rem) | 400500 | 1.501.80 | Tiny annotations |
### Principles
- **Multi-font purpose**: DM Sans = UI workhorse (body, nav, buttons); Outfit = geometric display (headings, product names); Poppins = friendly mid-tier (sub-headings, features); Roboto = technical/data contexts.
- **Universal 1.50 line-height**: The overwhelming majority of text uses 1.50 line-height, creating a consistent reading rhythm regardless of font or size. Exceptions: display (1.10 tight) and some captions (1.70 relaxed).
- **Weight 500 as default emphasis**: Most headings use 500 (medium) rather than bold, creating a modern, approachable tone. 600 for section titles, 700 reserved for strong emphasis.
- **Compact hierarchy**: The size scale jumps from 80px display straight to 2832px section, then 1620px body — a deliberate compression that keeps the visual hierarchy feeling efficient.
## 4. Component Stylings
### Buttons
**Pill Primary Dark**
- Background: `#181e25`
- Text: `#ffffff`
- Padding: 11px 20px
- Radius: 8px
- Use: Primary CTA ("Get Started", "Learn More")
**Pill Nav**
- Background: `rgba(0, 0, 0, 0.05)` (subtle tint)
- Text: `#18181b`
- Radius: 9999px (full pill)
- Use: Navigation tabs, filter toggles
**Pill White**
- Background: `#ffffff`
- Text: `rgba(24, 30, 37, 0.8)`
- Radius: 9999px
- Opacity: 0.5 (default state)
- Use: Secondary nav, inactive tabs
**Secondary Light**
- Background: `#f0f0f0`
- Text: `#333333`
- Padding: 11px 20px
- Radius: 8px
- Use: Secondary actions
### Product Cards
- Background: Vibrant gradients (pink/purple/orange/blue)
- Radius: 20px24px (generous rounding)
- Shadow: `rgba(44, 30, 116, 0.16) 0px 0px 15px` (brand purple glow)
- Content: Product name, model version, descriptive text
- Each card has its own color palette matching the product identity
### AI Product Cards (Matrix)
- Background: white with subtle shadow
- Radius: 13px16px
- Shadow: `rgba(0, 0, 0, 0.08) 0px 4px 6px`
- Icon/illustration centered above product name
- Product name in DM Sans 1416px weight 500
### Links
- **Primary**: `#18181b` or `#181e25`, underline on dark text
- **Secondary**: `#8e8e93`, muted for less emphasis
- **On Dark**: `rgba(255, 255, 255, 0.8)` for footer and dark sections
### Navigation
- Clean horizontal nav on white background
- MiniMax logo left-aligned (red accent in logo)
- DM Sans 14px weight 500 for nav items
- Pill-shaped active indicators (9999px radius)
- "Login" text link, minimal right-side actions
- Sticky header behavior
## 5. Layout Principles
### Spacing System
- Base unit: 8px
- Scale: 1px, 2px, 4px, 6px, 8px, 10px, 11px, 14px, 16px, 24px, 32px, 40px, 50px, 64px, 80px
### Grid & Container
- Max content width centered on page
- Product card grids: horizontal scroll or 34 column layout
- Full-width white sections with contained content
- Dark footer at full-width
### Breakpoints
| Name | Width | Key Changes |
|------|-------|-------------|
| Mobile | <768px | Single column, stacked cards |
| Tablet | 7681024px | 2-column grids |
| Desktop | >1024px | Full layout, horizontal card scrolls |
### Whitespace Philosophy
- **Gallery spacing**: Products are presented like gallery items with generous white space between cards, letting each AI model breathe as its own showcase.
- **Section rhythm**: Large vertical gaps (64px80px) between major sections create distinct "chapters" of content.
- **Card breathing**: Product cards use internal padding of 16px24px with ample whitespace around text.
### Border Radius Scale
- Minimal (4px): Small tags, micro badges
- Standard (8px): Buttons, small cards
- Comfortable (11px13px): Medium cards, panels
- Generous (16px20px): Large product cards
- Large (22px24px): Hero product cards, major containers
- Pill (30px32px): Badge pills, rounded panels
- Full (9999px): Buttons, nav tabs
## 6. Depth & Elevation
| Level | Treatment | Use |
|-------|-----------|-----|
| Flat (Level 0) | No shadow | White background, text blocks |
| Subtle (Level 1) | `rgba(0, 0, 0, 0.08) 0px 4px 6px` | Standard cards, containers |
| Ambient (Level 2) | `rgba(0, 0, 0, 0.08) 0px 0px 22.576px` | Soft glow around elements |
| Brand Glow (Level 3) | `rgba(44, 30, 116, 0.16) 0px 0px 15px` | Featured product cards |
| Elevated (Level 4) | `rgba(36, 36, 36, 0.08) 0px 12px 16px -4px` | Lifted cards, hover states |
**Shadow Philosophy**: MiniMax uses a distinctive purple-tinted shadow (`rgba(44, 30, 116, ...)`) for featured elements, creating a subtle brand-color glow that connects the shadow system to the blue brand identity. Standard shadows use neutral black but at low opacity (0.08), keeping everything feeling light and airy. The directional shadow variant (6.5px offset) adds dimensional interest to hero product cards.
## 7. Do's and Don'ts
### Do
- Use white as the dominant background — let product cards provide the color
- Apply pill radius (9999px) for navigation tabs and toggle buttons
- Use generous border radius (20px24px) for product showcase cards
- Employ the purple-tinted shadow for featured/hero product cards
- Keep body text at DM Sans weight 400500 — heavier weights for buttons only
- Use Outfit for display headings, DM Sans for everything functional
- Maintain the universal 1.50 line-height across body text
- Let colorful product illustrations/gradients serve as the primary visual interest
### Don't
- Don't add colored backgrounds to main content sections — white is structural
- Don't use sharp corners (04px radius) on product cards — the rounded aesthetic is core
- Don't apply the brand pink (`#ea5ec1`) to text or buttons — it's for logo and decorative accents only
- Don't mix more than one display font per section (Outfit OR Poppins, not both)
- Don't use weight 700 for headings — 500600 is the range, 700 is reserved for strong emphasis in body text
- Don't darken shadows beyond 0.16 opacity — the light, airy feel requires restraint
- Don't use Roboto for headings — it's the data/technical context font only
## 8. Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|------|-------|-------------|
| Mobile | <768px | Single column, stacked product cards, hamburger nav |
| Tablet | 7681024px | 2-column product grids, condensed spacing |
| Desktop | >1024px | Full horizontal card layouts, expanded spacing |
### Collapsing Strategy
- Hero: 80px → responsive scaling to ~40px on mobile
- Product card grid: horizontal scroll → 2-column → single column stacked
- Navigation: horizontal → hamburger menu
- Footer: multi-column → stacked sections
- Spacing: 6480px gaps → 3240px on mobile
## 9. Agent Prompt Guide
### Quick Color Reference
- Background: `#ffffff` (primary), `#181e25` (dark/footer)
- Text: `#222222` (primary), `#45515e` (secondary), `#8e8e93` (muted)
- Brand Blue: `#1456f0` (brand), `#3b82f6` (primary-500), `#2563eb` (hover)
- Brand Pink: `#ea5ec1` (accent only)
- Borders: `#e5e7eb`, `#f2f3f5`
### Example Component Prompts
- "Create a hero section on white background. Headline at 80px Outfit weight 500, line-height 1.10, near-black (#222222) text. Sub-text at 16px DM Sans weight 400, line-height 1.50, #45515e. Dark CTA button (#181e25, 8px radius, 11px 20px padding, white text)."
- "Design a product card grid: white cards with 20px border-radius, shadow rgba(44,30,116,0.16) 0px 0px 15px. Product name at 28px Outfit weight 600. Internal gradient background for the product illustration area."
- "Build navigation bar: white background, DM Sans 14px weight 500 for links, #18181b text. Pill-shaped active tab (9999px radius, rgba(0,0,0,0.05) background). MiniMax logo left-aligned."
- "Create an AI product matrix: 4-column grid of cards with 13px radius, subtle shadow rgba(0,0,0,0.08) 0px 4px 6px. Centered icon above product name in DM Sans 16px weight 500."
- "Design footer on dark (#181e25) background. Product links in DM Sans 14px, rgba(255,255,255,0.8). Multi-column layout."
### Iteration Guide
1. Start with white — color comes from product cards and illustrations only
2. Pill buttons (9999px) for nav/tabs, standard radius (8px) for CTA buttons
3. Purple-tinted shadows for featured cards, neutral shadows for everything else
4. DM Sans handles 70% of text — Outfit is display-only, Poppins is mid-tier only
5. Keep weights moderate (500600 for headings) — the brand tone is confident but approachable
6. Large radius cards (2024px) for products, smaller radius (813px) for UI elements

View File

@@ -0,0 +1,410 @@
<!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 href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600&family=Noto+Sans+SC:wght@300;400;500&display=swap" rel="stylesheet">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
:root {
--glass: rgba(255,255,255,0.55);
--glass-hv: rgba(255,255,255,0.72);
--glass-bd: rgba(255,255,255,0.85);
--border: rgba(255,255,255,0.7);
--shadow: rgba(100,120,180,0.12);
--blur: 20px;
--t1: #1a1f3a;
--t2: #4a5480;
--t3: #8a93b8;
--accent: #4361ee;
--accent2: #e85d88;
--accent3: #06b6a4;
}
body {
min-height: 100vh;
font-family: 'Noto Sans SC', sans-serif;
font-weight: 300;
background: #e8eef8;
overflow-x: hidden;
color: var(--t1);
}
.bg {
position: fixed; inset: 0; z-index: 0;
background:
radial-gradient(ellipse 70% 55% at 15% 20%, rgba(180,200,255,0.7) 0%, transparent 55%),
radial-gradient(ellipse 55% 45% at 85% 75%, rgba(255,180,220,0.55) 0%, transparent 55%),
radial-gradient(ellipse 50% 40% at 60% 5%, rgba(160,240,220,0.5) 0%, transparent 50%),
radial-gradient(ellipse 45% 40% at 90% 20%, rgba(255,220,160,0.45) 0%, transparent 50%),
radial-gradient(ellipse 40% 35% at 5% 85%, rgba(200,180,255,0.45) 0%, transparent 50%),
linear-gradient(145deg, #dce8f8 0%, #eef3fc 40%, #f0eaf8 70%, #fce8ef 100%);
}
.orb { position:fixed; border-radius:50%; filter:blur(70px); pointer-events:none; z-index:0; animation:drift 14s ease-in-out infinite; }
.orb-1 { width:480px;height:480px; top:-120px;left:-80px; background:rgba(150,180,255,0.35); animation-delay:0s; }
.orb-2 { width:380px;height:380px; bottom:-80px;right:-60px; background:rgba(255,150,200,0.3); animation-delay:-5s; }
.orb-3 { width:320px;height:320px; top:35%;left:55%; background:rgba(100,220,200,0.25); animation-delay:-9s; }
.orb-4 { width:260px;height:260px; top:10%;right:5%; background:rgba(255,220,130,0.3); animation-delay:-3s; }
@keyframes drift {
0%,100%{ transform:translate(0,0) scale(1); }
33%{ transform:translate(25px,-18px) scale(1.04); }
66%{ transform:translate(-18px,12px) scale(0.97); }
}
.glass {
background: var(--glass);
backdrop-filter: blur(var(--blur));
-webkit-backdrop-filter: blur(var(--blur));
border: 1px solid var(--border);
box-shadow: 0 8px 32px var(--shadow), inset 0 1px 0 rgba(255,255,255,0.9);
}
.glass-deep {
background: rgba(255,255,255,0.45);
backdrop-filter: blur(28px);
-webkit-backdrop-filter: blur(28px);
border: 1px solid rgba(255,255,255,0.75);
box-shadow: 0 12px 40px rgba(80,100,160,0.1), inset 0 1px 0 rgba(255,255,255,0.95);
}
.page { position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:60px 32px; }
.nav {
position:fixed; top:0; left:0; right:0; z-index:100;
display:flex; align-items:center; justify-content:space-between;
padding: 0 48px; height:64px;
background: rgba(255,255,255,0.6);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-bottom: 1px solid rgba(255,255,255,0.7);
box-shadow: 0 1px 20px rgba(100,120,200,0.08);
}
.nav-logo {
font-family:'Noto Serif SC',serif; font-size:18px; font-weight:600;
color:var(--accent); letter-spacing:0.05em;
}
.nav-links { display:flex; gap:36px; }
.nav-links a { font-size:13px; color:var(--t2); text-decoration:none; letter-spacing:0.06em; transition:color 0.2s; cursor:pointer; }
.nav-links a:hover { color:var(--accent); }
.nav-btn {
font-size:12px; color:#fff;
background: var(--accent);
border:none; border-radius:20px;
padding:8px 22px; cursor:pointer;
letter-spacing:0.06em;
box-shadow: 0 4px 14px rgba(67,97,238,0.3);
transition: box-shadow 0.2s, transform 0.15s;
}
.nav-btn:hover { box-shadow:0 6px 20px rgba(67,97,238,0.4); transform:translateY(-1px); }
.hero { padding:140px 0 70px; display:grid; grid-template-columns:1fr 1fr; gap:60px; align-items:center; }
.eyebrow { font-size:11px; letter-spacing:0.2em; text-transform:uppercase; color:var(--accent); margin-bottom:18px; }
.hero h1 { font-family:'Noto Serif SC',serif; font-size:50px; font-weight:300; line-height:1.2; color:var(--t1); margin-bottom:18px; }
.hero h1 em { font-style:normal; color:var(--accent); font-weight:600; }
.hero p { font-size:15px; line-height:1.85; color:var(--t2); margin-bottom:34px; max-width:420px; }
.hero-actions { display:flex; gap:16px; align-items:center; }
.btn-primary {
padding:12px 30px;
background:var(--accent); color:#fff;
border:none; border-radius:28px; font-size:13px; letter-spacing:0.06em; cursor:pointer;
box-shadow:0 6px 20px rgba(67,97,238,0.3);
transition:all 0.22s;
}
.btn-primary:hover { box-shadow:0 8px 28px rgba(67,97,238,0.4); transform:translateY(-2px); }
.btn-ghost { padding:12px 22px; color:var(--t2); font-size:13px; letter-spacing:0.06em; cursor:pointer; transition:color 0.2s; }
.btn-ghost:hover { color:var(--accent); }
.hero-card { border-radius:24px; padding:32px; position:relative; overflow:hidden; }
.hero-card::before {
content:''; position:absolute; top:-1px;left:-1px;right:-1px; height:1px;
background:linear-gradient(90deg,transparent,rgba(255,255,255,1),transparent);
}
.card-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:22px; }
.card-label { font-size:11px; letter-spacing:0.15em; text-transform:uppercase; color:var(--t3); }
.card-badge {
font-size:11px; padding:4px 12px; border-radius:12px;
background:rgba(6,182,164,0.1); border:1px solid rgba(6,182,164,0.25);
color:#0a8c7a; letter-spacing:0.05em;
}
.big-number { font-family:'Noto Serif SC',serif; font-size:52px; font-weight:300; color:var(--t1); line-height:1; margin-bottom:6px; }
.big-number span { font-size:18px; color:var(--t2); font-family:'Noto Sans SC',sans-serif; }
.mini-chart { display:flex; align-items:flex-end; gap:4px; height:48px; margin:18px 0; }
.bar { flex:1; border-radius:3px 3px 0 0; background:rgba(67,97,238,0.18); transition:background 0.2s; animation:barGrow 0.7s ease-out both; }
.bar:hover { background:rgba(67,97,238,0.4); }
.bar.hi { background:rgba(67,97,238,0.5); }
@keyframes barGrow { from{transform:scaleY(0);transform-origin:bottom} to{transform:scaleY(1);transform-origin:bottom} }
.bar:nth-child(1){height:45%;animation-delay:.05s} .bar:nth-child(2){height:62%;animation-delay:.1s}
.bar:nth-child(3){height:50%;animation-delay:.15s} .bar:nth-child(4){height:78%;animation-delay:.2s}
.bar:nth-child(5){height:58%;animation-delay:.25s} .bar:nth-child(6){height:88%;animation-delay:.3s}
.bar:nth-child(7){height:68%;animation-delay:.35s} .bar:nth-child(8){height:55%;animation-delay:.4s}
.bar:nth-child(9){height:82%;animation-delay:.45s} .bar:nth-child(10){height:72%;animation-delay:.5s}
.card-row { display:flex; justify-content:space-between; font-size:12px; color:var(--t3); margin-top:4px; }
.stats-row { display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:60px; }
.stat-card { border-radius:18px; padding:24px; text-align:center; transition:transform 0.25s; cursor:default; }
.stat-card:hover { transform:translateY(-4px); }
.stat-icon { width:40px;height:40px; border-radius:12px; display:flex;align-items:center;justify-content:center; font-size:20px; margin:0 auto 14px; }
.stat-value { font-size:28px; font-weight:500; color:var(--t1); line-height:1; margin-bottom:6px; }
.stat-label { font-size:12px; color:var(--t3); letter-spacing:0.06em; }
.section-title { font-family:'Noto Serif SC',serif; font-size:30px; font-weight:300; color:var(--t1); margin-bottom:10px; }
.section-sub { font-size:14px; color:var(--t2); margin-bottom:36px; line-height:1.75; }
.features-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:18px; margin-bottom:60px; }
.feature-card { border-radius:20px; padding:26px; transition:transform 0.25s; cursor:default; }
.feature-card:hover { transform:translateY(-3px); }
.feature-icon {
width:44px;height:44px; border-radius:14px;
background:rgba(255,255,255,0.6); border:1px solid rgba(255,255,255,0.8);
display:flex;align-items:center;justify-content:center; font-size:22px; margin-bottom:16px;
}
.feature-title { font-size:15px; font-weight:500; color:var(--t1); margin-bottom:9px; }
.feature-desc { font-size:13px; color:var(--t2); line-height:1.75; }
.bottom-row { display:grid; grid-template-columns:1fr 1.2fr; gap:22px; margin-bottom:60px; }
.user-list { display:flex; flex-direction:column; gap:10px; margin-top:18px; }
.user-item {
display:flex; align-items:center; gap:14px; padding:12px 16px; border-radius:14px;
background:rgba(255,255,255,0.4); border:1px solid rgba(255,255,255,0.65);
cursor:default; transition:background 0.2s;
}
.user-item:hover { background:rgba(255,255,255,0.65); }
.avatar { width:36px;height:36px; border-radius:50%; display:flex;align-items:center;justify-content:center; font-size:13px;font-weight:500; flex-shrink:0; }
.user-name { font-size:13px; color:var(--t1); font-weight:400; }
.user-role { font-size:11px; color:var(--t3); margin-top:2px; }
.user-status { margin-left:auto; width:7px;height:7px; border-radius:50%; }
.online { background:#10b981; box-shadow:0 0 6px rgba(16,185,129,0.5); }
.away { background:#f59e0b; }
.offline { background:#cbd5e1; }
.activity-feed { display:flex; flex-direction:column; margin-top:18px; }
.activity-item { display:flex; gap:14px; padding:14px 0; border-bottom:1px solid rgba(100,120,200,0.08); }
.activity-item:last-child { border-bottom:none; }
.activity-dot { width:8px;height:8px; border-radius:50%; margin-top:6px; flex-shrink:0; }
.activity-text { font-size:13px; color:var(--t2); line-height:1.65; }
.activity-text strong { color:var(--t1); font-weight:500; }
.activity-time { font-size:11px; color:var(--t3); margin-top:3px; }
.tag {
display:inline-block; font-size:10px; letter-spacing:0.08em; text-transform:uppercase;
padding:3px 10px; border-radius:8px;
background:rgba(255,255,255,0.6); border:1px solid rgba(255,255,255,0.75);
color:var(--t2); margin-right:6px; margin-bottom:8px;
}
.footer { border-top:1px solid rgba(100,120,200,0.1); padding:30px 0; display:flex; justify-content:space-between; align-items:center; }
.footer-copy { font-size:12px; color:var(--t3); }
.footer-links { display:flex; gap:28px; }
.footer-links a { font-size:12px; color:var(--t3); text-decoration:none; transition:color 0.2s; }
.footer-links a:hover { color:var(--accent); }
.pill {
display:inline-flex; align-items:center; gap:6px;
font-size:11px; padding:5px 14px; border-radius:20px; font-weight:400;
background:rgba(255,255,255,0.55); border:1px solid rgba(255,255,255,0.75); color:var(--t2);
}
</style>
</head>
<body>
<div class="bg"></div>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="orb orb-3"></div>
<div class="orb orb-4"></div>
<nav class="nav">
<div class="nav-logo">琉光</div>
<div class="nav-links">
<a>产品</a><a>设计</a><a>文档</a><a>关于</a>
</div>
<button class="nav-btn">开始体验</button>
</nav>
<div class="page">
<section class="hero">
<div>
<p class="eyebrow">下一代设计语言</p>
<h1>光与<em>玻璃</em><br>的艺术</h1>
<p>毛玻璃设计将透明、光泽与柔和色彩融为一体,在明亮的底色上营造出如薄冰般的清透层次感。</p>
<div class="hero-actions">
<button class="btn-primary">探索设计</button>
<button class="btn-ghost">查看文档 →</button>
</div>
</div>
<div class="hero-card glass-deep">
<div class="card-header">
<span class="card-label">本月收益</span>
<span class="card-badge">↑ 12.4%</span>
</div>
<div class="big-number">¥84,231 <span></span></div>
<div class="mini-chart">
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
<div class="bar"></div><div class="bar"></div><div class="bar hi"></div>
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
<div class="bar"></div>
</div>
<div class="card-row"><span>3月</span><span>4月</span><span>5月</span></div>
<div style="margin-top:18px">
<span class="tag">实时数据</span>
<span class="tag">自动同步</span>
<span class="tag">加密存储</span>
</div>
</div>
</section>
<div class="stats-row">
<div class="stat-card glass">
<div class="stat-icon" style="background:rgba(67,97,238,0.1);border:1px solid rgba(67,97,238,0.18)">🌊</div>
<div class="stat-value">2.4M</div>
<div class="stat-label">活跃用户</div>
</div>
<div class="stat-card glass">
<div class="stat-icon" style="background:rgba(232,93,136,0.1);border:1px solid rgba(232,93,136,0.18)"></div>
<div class="stat-value">98.7%</div>
<div class="stat-label">满意度</div>
</div>
<div class="stat-card glass">
<div class="stat-icon" style="background:rgba(6,182,164,0.1);border:1px solid rgba(6,182,164,0.18)"></div>
<div class="stat-value">0.3s</div>
<div class="stat-label">平均响应</div>
</div>
<div class="stat-card glass">
<div class="stat-icon" style="background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.18)"></div>
<div class="stat-value">340+</div>
<div class="stat-label">设计组件</div>
</div>
</div>
<h2 class="section-title">核心特性</h2>
<p class="section-sub">每一处细节都经过精心打磨,在亮色系中实现毛玻璃的极致通透感。</p>
<div class="features-grid">
<div class="feature-card glass">
<div class="feature-icon"></div>
<div class="feature-title">动态模糊层</div>
<div class="feature-desc">基于背景内容自适应调整模糊强度,在任何亮色背景下都保持完美可读性与透亮感。</div>
</div>
<div class="feature-card glass">
<div class="feature-icon"></div>
<div class="feature-title">多层透明度</div>
<div class="feature-desc">精确控制每一层的透明度与折射效果,亮色系下呈现薄冰般通透的空间层次。</div>
</div>
<div class="feature-card glass">
<div class="feature-icon"></div>
<div class="feature-title">光泽描边</div>
<div class="feature-desc">内嵌高光描边以高饱和白色模拟玻璃光学特性,亮色系中尤为细腻自然。</div>
</div>
<div class="feature-card glass">
<div class="feature-icon"></div>
<div class="feature-title">柔和阴影</div>
<div class="feature-desc">以带色相的低饱和阴影代替黑色投影,在亮色背景中更显轻盈与立体感。</div>
</div>
<div class="feature-card glass">
<div class="feature-icon"></div>
<div class="feature-title">流体动画</div>
<div class="feature-desc">精心设计的弹性过渡动画,使组件在亮色系中如丝绸般顺滑地浮动与响应。</div>
</div>
<div class="feature-card glass">
<div class="feature-icon"></div>
<div class="feature-title">色彩渐层背景</div>
<div class="feature-desc">多色彩虹渐层光球系统,让每一区域的毛玻璃呈现出独特的背景色调折射效果。</div>
</div>
</div>
<div class="bottom-row">
<div class="glass-deep" style="border-radius:20px;padding:26px">
<div style="font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--t3);margin-bottom:4px">团队成员</div>
<div style="font-size:21px;font-weight:400;color:var(--t1)">在线状态</div>
<div class="user-list">
<div class="user-item">
<div class="avatar" style="background:rgba(67,97,238,0.12);color:#3451c7"></div>
<div><div class="user-name">陈雨薇</div><div class="user-role">产品设计师</div></div>
<div class="user-status online"></div>
</div>
<div class="user-item">
<div class="avatar" style="background:rgba(232,93,136,0.12);color:#b84070"></div>
<div><div class="user-name">林俊杰</div><div class="user-role">前端工程师</div></div>
<div class="user-status online"></div>
</div>
<div class="user-item">
<div class="avatar" style="background:rgba(6,182,164,0.12);color:#0a7a6e"></div>
<div><div class="user-name">苏晓明</div><div class="user-role">视觉设计师</div></div>
<div class="user-status away"></div>
</div>
<div class="user-item">
<div class="avatar" style="background:rgba(245,158,11,0.12);color:#b45309"></div>
<div><div class="user-name">张晨曦</div><div class="user-role">项目经理</div></div>
<div class="user-status offline"></div>
</div>
</div>
</div>
<div class="glass-deep" style="border-radius:20px;padding:26px">
<div style="font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--t3);margin-bottom:4px">实时动态</div>
<div style="font-size:21px;font-weight:400;color:var(--t1)">最新活动</div>
<div class="activity-feed">
<div class="activity-item">
<div class="activity-dot" style="background:#4361ee"></div>
<div>
<div class="activity-text"><strong>陈雨薇</strong> 上传了新的设计稿「首页重构 v3.2」</div>
<div class="activity-time">2 分钟前</div>
</div>
</div>
<div class="activity-item">
<div class="activity-dot" style="background:#06b6a4"></div>
<div>
<div class="activity-text"><strong>林俊杰</strong> 完成了亮色毛玻璃组件库的开发与测试</div>
<div class="activity-time">18 分钟前</div>
</div>
</div>
<div class="activity-item">
<div class="activity-dot" style="background:#e85d88"></div>
<div>
<div class="activity-text"><strong>苏晓明</strong> 提交了新的亮色系色彩规范文档</div>
<div class="activity-time">1 小时前</div>
</div>
</div>
<div class="activity-item">
<div class="activity-dot" style="background:#f59e0b"></div>
<div>
<div class="activity-text"><strong>张晨曦</strong> 创建了里程碑「Q2 发布」并分配任务</div>
<div class="activity-time">3 小时前</div>
</div>
</div>
<div class="activity-item">
<div class="activity-dot" style="background:#a78bfa"></div>
<div>
<div class="activity-text">系统自动完成了本周数据备份</div>
<div class="activity-time">昨天 23:00</div>
</div>
</div>
</div>
</div>
</div>
<footer class="footer">
<div class="footer-copy">© 2026 琉光设计系统 · 保留所有权利</div>
<div class="footer-links">
<a href="#">隐私政策</a>
<a href="#">服务条款</a>
<a href="#">设计资源</a>
<a href="#">联系我们</a>
</div>
</footer>
</div>
</body>
</html>

703
refrence/glassmorphism.html Normal file
View File

@@ -0,0 +1,703 @@
<!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 href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600&family=Noto+Sans+SC:wght@300;400;500&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--glass-bg: rgba(255,255,255,0.12);
--glass-border: rgba(255,255,255,0.25);
--glass-shadow: rgba(0,0,0,0.15);
--blur: 18px;
--text-primary: rgba(255,255,255,0.95);
--text-secondary: rgba(255,255,255,0.65);
--text-muted: rgba(255,255,255,0.4);
--accent: rgba(163,210,255,0.85);
--accent-warm: rgba(255,195,140,0.85);
}
body {
min-height: 100vh;
font-family: 'Noto Sans SC', sans-serif;
font-weight: 300;
background: #0a0e1a;
overflow-x: hidden;
}
/* ── 背景 ── */
.bg {
position: fixed;
inset: 0;
z-index: 0;
background:
radial-gradient(ellipse 80% 60% at 20% 30%, rgba(67,97,238,0.45) 0%, transparent 60%),
radial-gradient(ellipse 60% 50% at 80% 70%, rgba(138,43,226,0.35) 0%, transparent 55%),
radial-gradient(ellipse 50% 40% at 55% 10%, rgba(0,180,216,0.3) 0%, transparent 50%),
radial-gradient(ellipse 40% 35% at 10% 80%, rgba(255,100,100,0.2) 0%, transparent 50%),
linear-gradient(135deg, #0a0e1a 0%, #0f1629 50%, #0d1120 100%);
}
.bg::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.025'%3E%3Ccircle cx='30' cy='30' r='1'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
/* 漂浮光球 */
.orb { position: fixed; border-radius: 50%; filter: blur(80px); pointer-events: none; z-index: 0; animation: drift 12s ease-in-out infinite; }
.orb-1 { width: 500px; height: 500px; top: -100px; left: -100px; background: rgba(67,97,238,0.25); animation-delay: 0s; }
.orb-2 { width: 400px; height: 400px; bottom: -80px; right: -80px; background: rgba(138,43,226,0.2); animation-delay: -4s; }
.orb-3 { width: 300px; height: 300px; top: 40%; left: 60%; background: rgba(0,180,216,0.18); animation-delay: -8s; }
@keyframes drift {
0%, 100% { transform: translate(0,0) scale(1); }
33% { transform: translate(30px, -20px) scale(1.05); }
66% { transform: translate(-20px, 15px) scale(0.97); }
}
/* ── 毛玻璃基础 ── */
.glass {
background: var(--glass-bg);
backdrop-filter: blur(var(--blur));
-webkit-backdrop-filter: blur(var(--blur));
border: 1px solid var(--glass-border);
box-shadow: 0 8px 32px var(--glass-shadow), inset 0 1px 0 rgba(255,255,255,0.15);
}
.glass-deep {
background: rgba(255,255,255,0.07);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(255,255,255,0.12);
box-shadow: 0 16px 48px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.1);
}
/* ── 布局 ── */
.page {
position: relative;
z-index: 1;
max-width: 1200px;
margin: 0 auto;
padding: 60px 32px;
}
/* ── 导航 ── */
.nav {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 48px;
height: 64px;
background: rgba(10,14,26,0.6);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.nav-logo {
font-family: 'Noto Serif SC', serif;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
letter-spacing: 0.05em;
}
.nav-links { display: flex; gap: 36px; }
.nav-links a {
font-size: 13px;
color: var(--text-secondary);
text-decoration: none;
letter-spacing: 0.08em;
transition: color 0.2s;
cursor: pointer;
}
.nav-links a:hover { color: var(--text-primary); }
.nav-btn {
font-size: 12px;
color: var(--text-primary);
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 20px;
padding: 7px 20px;
cursor: pointer;
letter-spacing: 0.06em;
transition: background 0.2s;
}
.nav-btn:hover { background: rgba(255,255,255,0.18); }
/* ── 英雄区 ── */
.hero {
padding: 160px 0 80px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
align-items: center;
}
.hero-text .eyebrow {
font-size: 11px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 20px;
}
.hero-text h1 {
font-family: 'Noto Serif SC', serif;
font-size: 52px;
font-weight: 300;
line-height: 1.2;
color: var(--text-primary);
margin-bottom: 20px;
}
.hero-text h1 em {
font-style: normal;
color: var(--accent);
font-weight: 600;
}
.hero-text p {
font-size: 15px;
line-height: 1.8;
color: var(--text-secondary);
margin-bottom: 36px;
max-width: 420px;
}
.hero-actions { display: flex; gap: 16px; align-items: center; }
.btn-primary {
padding: 13px 32px;
background: rgba(163,210,255,0.18);
border: 1px solid rgba(163,210,255,0.4);
border-radius: 30px;
color: var(--accent);
font-size: 13px;
letter-spacing: 0.06em;
cursor: pointer;
transition: all 0.25s;
backdrop-filter: blur(8px);
}
.btn-primary:hover {
background: rgba(163,210,255,0.28);
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(163,210,255,0.15);
}
.btn-ghost {
padding: 13px 24px;
color: var(--text-secondary);
font-size: 13px;
letter-spacing: 0.06em;
cursor: pointer;
transition: color 0.2s;
}
.btn-ghost:hover { color: var(--text-primary); }
/* ── 英雄卡片 ── */
.hero-card {
border-radius: 24px;
padding: 32px;
position: relative;
overflow: hidden;
}
.hero-card::before {
content: '';
position: absolute;
top: -1px; left: -1px; right: -1px;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.card-label {
font-size: 11px;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-muted);
}
.card-badge {
font-size: 11px;
padding: 4px 12px;
border-radius: 12px;
background: rgba(0,220,130,0.12);
border: 1px solid rgba(0,220,130,0.25);
color: rgba(0,220,130,0.9);
letter-spacing: 0.05em;
}
.big-number {
font-family: 'Noto Serif SC', serif;
font-size: 56px;
font-weight: 300;
color: var(--text-primary);
line-height: 1;
margin-bottom: 6px;
}
.big-number span {
font-size: 20px;
color: var(--text-secondary);
font-family: 'Noto Sans SC', sans-serif;
}
.mini-chart {
display: flex;
align-items: flex-end;
gap: 4px;
height: 48px;
margin: 20px 0;
}
.bar {
flex: 1;
border-radius: 3px 3px 0 0;
background: rgba(163,210,255,0.25);
transition: background 0.2s;
animation: barGrow 0.8s ease-out both;
}
.bar:hover { background: rgba(163,210,255,0.5); }
.bar.active { background: rgba(163,210,255,0.65); }
@keyframes barGrow {
from { transform: scaleY(0); transform-origin: bottom; }
to { transform: scaleY(1); transform-origin: bottom; }
}
.bar:nth-child(1) { height: 45%; animation-delay:0.05s }
.bar:nth-child(2) { height: 65%; animation-delay:0.1s }
.bar:nth-child(3) { height: 50%; animation-delay:0.15s }
.bar:nth-child(4) { height: 80%; animation-delay:0.2s }
.bar:nth-child(5) { height: 60%; animation-delay:0.25s }
.bar:nth-child(6) { height: 90%; animation-delay:0.3s; }
.bar:nth-child(6).active { animation-delay:0.3s }
.bar:nth-child(7) { height: 70%; animation-delay:0.35s }
.bar:nth-child(8) { height: 55%; animation-delay:0.4s }
.bar:nth-child(9) { height: 85%; animation-delay:0.45s }
.bar:nth-child(10){ height: 75%; animation-delay:0.5s }
.card-row {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
}
/* ── 统计行 ── */
.stats-row {
display: grid;
grid-template-columns: repeat(4,1fr);
gap: 16px;
margin-bottom: 64px;
}
.stat-card {
border-radius: 16px;
padding: 24px;
text-align: center;
transition: transform 0.25s, box-shadow 0.25s;
cursor: default;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 20px 48px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.2);
}
.stat-icon {
width: 40px; height: 40px;
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 20px;
margin: 0 auto 14px;
}
.stat-value {
font-size: 28px;
font-weight: 500;
color: var(--text-primary);
line-height: 1;
margin-bottom: 6px;
}
.stat-label {
font-size: 12px;
color: var(--text-muted);
letter-spacing: 0.06em;
}
/* ── 特性卡片 ── */
.section-title {
font-family: 'Noto Serif SC', serif;
font-size: 32px;
font-weight: 300;
color: var(--text-primary);
margin-bottom: 12px;
}
.section-sub {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 40px;
line-height: 1.7;
}
.features-grid {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: 20px;
margin-bottom: 64px;
}
.feature-card {
border-radius: 20px;
padding: 28px;
transition: transform 0.25s;
cursor: default;
}
.feature-card:hover { transform: translateY(-3px); }
.feature-icon {
width: 44px; height: 44px;
border-radius: 14px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.12);
display: flex; align-items: center; justify-content: center;
font-size: 22px;
margin-bottom: 18px;
}
.feature-title {
font-size: 15px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 10px;
letter-spacing: 0.02em;
}
.feature-desc {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.75;
}
/* ── 底部行 ── */
.bottom-row {
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 24px;
margin-bottom: 64px;
}
/* 用户列表 */
.user-list { display: flex; flex-direction: column; gap: 12px; margin-top: 20px; }
.user-item {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 18px;
border-radius: 14px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.07);
cursor: default;
transition: background 0.2s;
}
.user-item:hover { background: rgba(255,255,255,0.09); }
.avatar {
width: 36px; height: 36px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
flex-shrink: 0;
}
.user-name { font-size: 13px; color: var(--text-primary); font-weight: 400; }
.user-role { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.user-status {
margin-left: auto;
width: 7px; height: 7px;
border-radius: 50%;
}
.online { background: rgba(0,220,130,0.9); box-shadow: 0 0 6px rgba(0,220,130,0.5); }
.away { background: rgba(255,190,50,0.9); }
.offline{ background: rgba(150,150,150,0.5); }
/* 活动流 */
.activity-feed { display: flex; flex-direction: column; gap: 0; margin-top: 20px; }
.activity-item {
display: flex; gap: 16px; padding: 16px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.activity-item:last-child { border-bottom: none; }
.activity-dot {
width: 8px; height: 8px;
border-radius: 50%;
margin-top: 6px;
flex-shrink: 0;
}
.activity-text { font-size: 13px; color: var(--text-secondary); line-height: 1.6; }
.activity-text strong { color: var(--text-primary); font-weight: 500; }
.activity-time { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
/* ── 页脚 ── */
.footer {
border-top: 1px solid rgba(255,255,255,0.07);
padding: 32px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-copy { font-size: 12px; color: var(--text-muted); }
.footer-links { display: flex; gap: 28px; }
.footer-links a { font-size: 12px; color: var(--text-muted); text-decoration: none; transition: color 0.2s; }
.footer-links a:hover { color: var(--text-secondary); }
/* 标签 */
.tag {
display: inline-block;
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 3px 10px;
border-radius: 8px;
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.1);
color: var(--text-secondary);
margin-right: 6px;
margin-bottom: 8px;
}
</style>
</head>
<body>
<div class="bg"></div>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="orb orb-3"></div>
<!-- 导航 -->
<nav class="nav">
<div class="nav-logo">琉光</div>
<div class="nav-links">
<a>产品</a>
<a>设计</a>
<a>文档</a>
<a>关于</a>
</div>
<button class="nav-btn">开始体验</button>
</nav>
<!-- 主内容 -->
<div class="page">
<!-- 英雄 -->
<section class="hero">
<div class="hero-text">
<p class="eyebrow">下一代设计语言</p>
<h1>光与<em>玻璃</em><br>的艺术</h1>
<p>毛玻璃设计语言将透明、模糊与光泽融为一体,在深邃背景中营造出如水晶般的层次感与深度。</p>
<div class="hero-actions">
<button class="btn-primary">探索设计</button>
<button class="btn-ghost">查看文档 →</button>
</div>
</div>
<div class="hero-card glass-deep">
<div class="card-header">
<span class="card-label">本月收益</span>
<span class="card-badge">↑ 12.4%</span>
</div>
<div class="big-number">¥84,231 <span></span></div>
<div class="mini-chart">
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
<div class="bar"></div><div class="bar"></div><div class="bar active"></div>
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
<div class="bar"></div>
</div>
<div class="card-row">
<span>3月</span><span>4月</span><span>5月</span>
</div>
<div style="margin-top:20px; display:flex; gap:10px; flex-wrap:wrap;">
<span class="tag">实时数据</span>
<span class="tag">自动同步</span>
<span class="tag">加密存储</span>
</div>
</div>
</section>
<!-- 统计 -->
<div class="stats-row">
<div class="stat-card glass">
<div class="stat-icon" style="background:rgba(163,210,255,0.12); border:1px solid rgba(163,210,255,0.2);">🌊</div>
<div class="stat-value">2.4M</div>
<div class="stat-label">活跃用户</div>
</div>
<div class="stat-card glass">
<div class="stat-icon" style="background:rgba(200,160,255,0.12); border:1px solid rgba(200,160,255,0.2);"></div>
<div class="stat-value">98.7%</div>
<div class="stat-label">满意度</div>
</div>
<div class="stat-card glass">
<div class="stat-icon" style="background:rgba(100,230,180,0.12); border:1px solid rgba(100,230,180,0.2);"></div>
<div class="stat-value">0.3s</div>
<div class="stat-label">平均响应</div>
</div>
<div class="stat-card glass">
<div class="stat-icon" style="background:rgba(255,195,140,0.12); border:1px solid rgba(255,195,140,0.2);"></div>
<div class="stat-value">340+</div>
<div class="stat-label">设计组件</div>
</div>
</div>
<!-- 特性 -->
<h2 class="section-title">核心特性</h2>
<p class="section-sub">每一处细节都经过精心打磨,为您带来沉浸式的视觉体验与流畅的交互感受。</p>
<div class="features-grid">
<div class="feature-card glass">
<div class="feature-icon"></div>
<div class="feature-title">动态模糊层</div>
<div class="feature-desc">基于背景内容自适应调整模糊强度,在任何背景下都能保持完美的可读性与美观度。</div>
</div>
<div class="feature-card glass">
<div class="feature-icon"></div>
<div class="feature-title">多层透明度</div>
<div class="feature-desc">精确控制每一层的透明度与折射效果,创造如水晶般的深度感与空间层次。</div>
</div>
<div class="feature-card glass">
<div class="feature-icon"></div>
<div class="feature-title">光泽描边</div>
<div class="feature-desc">内嵌高光描边模拟真实玻璃的光学特性,在不同光线角度呈现自然的光泽变化。</div>
</div>
<div class="feature-card glass">
<div class="feature-icon"></div>
<div class="feature-title">深度阴影</div>
<div class="feature-desc">多层叠加的阴影系统赋予组件真实的立体感,强化视觉层级与空间关系。</div>
</div>
<div class="feature-card glass">
<div class="feature-icon"></div>
<div class="feature-title">流体动画</div>
<div class="feature-desc">精心设计的过渡动画遵循物理规律,所有交互均有丝滑的弹性反馈响应。</div>
</div>
<div class="feature-card glass">
<div class="feature-icon"></div>
<div class="feature-title">暗色适配</div>
<div class="feature-desc">专为深色背景优化的色彩体系,冷暖色调精妙平衡,营造高级氛围感。</div>
</div>
</div>
<!-- 底部双列 -->
<div class="bottom-row">
<!-- 用户面板 -->
<div class="glass-deep" style="border-radius:20px; padding:28px;">
<div style="font-size:13px; color:var(--text-muted); letter-spacing:0.1em; text-transform:uppercase; margin-bottom:4px;">团队成员</div>
<div style="font-size:22px; font-weight:400; color:var(--text-primary); margin-bottom:2px;">在线状态</div>
<div class="user-list">
<div class="user-item">
<div class="avatar" style="background:rgba(163,210,255,0.2);"></div>
<div>
<div class="user-name">陈雨薇</div>
<div class="user-role">产品设计师</div>
</div>
<div class="user-status online"></div>
</div>
<div class="user-item">
<div class="avatar" style="background:rgba(200,160,255,0.2);"></div>
<div>
<div class="user-name">林俊杰</div>
<div class="user-role">前端工程师</div>
</div>
<div class="user-status online"></div>
</div>
<div class="user-item">
<div class="avatar" style="background:rgba(100,230,180,0.2);"></div>
<div>
<div class="user-name">苏晓明</div>
<div class="user-role">视觉设计师</div>
</div>
<div class="user-status away"></div>
</div>
<div class="user-item">
<div class="avatar" style="background:rgba(255,195,140,0.2);"></div>
<div>
<div class="user-name">张晨曦</div>
<div class="user-role">项目经理</div>
</div>
<div class="user-status offline"></div>
</div>
</div>
</div>
<!-- 动态流 -->
<div class="glass-deep" style="border-radius:20px; padding:28px;">
<div style="font-size:13px; color:var(--text-muted); letter-spacing:0.1em; text-transform:uppercase; margin-bottom:4px;">实时动态</div>
<div style="font-size:22px; font-weight:400; color:var(--text-primary); margin-bottom:2px;">最新活动</div>
<div class="activity-feed">
<div class="activity-item">
<div class="activity-dot" style="background:rgba(163,210,255,0.9);"></div>
<div>
<div class="activity-text"><strong>陈雨薇</strong> 上传了新的设计稿「首页重构 v3.2」</div>
<div class="activity-time">2 分钟前</div>
</div>
</div>
<div class="activity-item">
<div class="activity-dot" style="background:rgba(100,230,180,0.9);"></div>
<div>
<div class="activity-text"><strong>林俊杰</strong> 完成了毛玻璃组件库的开发与测试</div>
<div class="activity-time">18 分钟前</div>
</div>
</div>
<div class="activity-item">
<div class="activity-dot" style="background:rgba(200,160,255,0.9);"></div>
<div>
<div class="activity-text"><strong>苏晓明</strong> 提交了新的色彩规范文档</div>
<div class="activity-time">1 小时前</div>
</div>
</div>
<div class="activity-item">
<div class="activity-dot" style="background:rgba(255,195,140,0.9);"></div>
<div>
<div class="activity-text"><strong>张晨曦</strong> 创建了里程碑「Q2 发布」并分配任务</div>
<div class="activity-time">3 小时前</div>
</div>
</div>
<div class="activity-item">
<div class="activity-dot" style="background:rgba(255,100,130,0.9);"></div>
<div>
<div class="activity-text">系统自动完成了本周数据备份</div>
<div class="activity-time">昨天 23:00</div>
</div>
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="footer">
<div class="footer-copy">© 2026 琉光设计系统 · 保留所有权利</div>
<div class="footer-links">
<a href="#">隐私政策</a>
<a href="#">服务条款</a>
<a href="#">设计资源</a>
<a href="#">联系我们</a>
</div>
</footer>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff