feat:初始化项目
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# 设计文档目录
|
||||
|
||||
本目录包含字词学习平台的产品设计和数据库设计文档。
|
||||
|
||||
## 文档列表
|
||||
|
||||
### 1. requirements.md
|
||||
**产品需求文档**
|
||||
- 项目概述和目标
|
||||
- 核心功能需求详细说明
|
||||
- 后台管理功能需求
|
||||
- 图片处理功能需求
|
||||
- 用户权限管理需求
|
||||
- 非功能需求和技术约束
|
||||
|
||||
### 2. database-design.md
|
||||
**数据库设计文档**
|
||||
- 数据库选型分析(推荐 PostgreSQL)
|
||||
- 数据表详细设计
|
||||
- 字段说明和索引优化
|
||||
- 数据关系图
|
||||
- 性能优化建议
|
||||
|
||||
### 3. ER-diagram.sql
|
||||
**数据库建表 SQL 脚本**
|
||||
- 完整的 PostgreSQL 建表语句
|
||||
- 包含所有索引和约束
|
||||
- 触发器定义
|
||||
- 可直接执行创建数据库结构
|
||||
|
||||
## 设计要点
|
||||
|
||||
### 核心设计决策
|
||||
|
||||
1. **统一表设计**
|
||||
- 汉字、词语、英语单词、字母使用统一的 `words` 表
|
||||
- 通过 `type` 字段区分不同类型
|
||||
- 优点:结构统一、查询便利、维护简单
|
||||
|
||||
2. **图片存储策略**
|
||||
- 原图和标准图分别存储
|
||||
- 支持多图关联
|
||||
- 主图标记机制
|
||||
|
||||
3. **关联关系设计**
|
||||
- 字词关联:支持多对多关系
|
||||
- 句子关联:支持多对多关系
|
||||
- 使用中间表管理关联关系
|
||||
|
||||
4. **用户权限设计**
|
||||
- 基于角色的访问控制(RBAC)
|
||||
- 超级管理员和一般管理员两级权限
|
||||
- 操作日志记录
|
||||
|
||||
## 数据库选型
|
||||
|
||||
**推荐:PostgreSQL**
|
||||
|
||||
**理由:**
|
||||
- 关系型数据优势,适合强关联关系
|
||||
- 原生 JSON/JSONB 支持,灵活存储 SVG 数据
|
||||
- 全文搜索功能,适合字词搜索
|
||||
- 数组类型支持,适合存储多个拼音
|
||||
- 企业级性能,开源免费
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 创建数据库
|
||||
|
||||
```bash
|
||||
# 使用 PostgreSQL
|
||||
createdb doodle_platform
|
||||
|
||||
# 执行建表脚本
|
||||
psql -d doodle_platform -f ER-diagram.sql
|
||||
```
|
||||
|
||||
### 2. 数据表结构
|
||||
|
||||
- `words` - 字词主表
|
||||
- `word_images` - 图片表
|
||||
- `word_relations` - 字词关联表
|
||||
- `sentences` - 句子表
|
||||
- `word_sentences` - 字词-句子关联表
|
||||
- `users` - 用户表
|
||||
- `operation_logs` - 操作日志表
|
||||
|
||||
## 后续工作
|
||||
|
||||
1. **API 接口设计**
|
||||
- RESTful API 规范
|
||||
- 接口文档(Swagger/OpenAPI)
|
||||
|
||||
2. **前端页面设计**
|
||||
- 字词列表/卡片页面
|
||||
- 字词编辑页面
|
||||
- 图片上传页面
|
||||
- 用户管理页面
|
||||
|
||||
3. **图片处理服务**
|
||||
- 集成 node-tools 的图片处理功能
|
||||
- 批量处理接口
|
||||
- 异步处理队列
|
||||
|
||||
4. **权限系统实现**
|
||||
- JWT 认证
|
||||
- 角色权限中间件
|
||||
- 操作日志记录
|
||||
@@ -0,0 +1,298 @@
|
||||
# 字词学习平台 - 数据库设计文档
|
||||
|
||||
## 1. 数据库选型
|
||||
|
||||
### 1.1 推荐方案:PostgreSQL
|
||||
|
||||
**选择理由:**
|
||||
- ✅ **关系型数据优势**:字词、关联关系、用户等数据具有强关联性,适合关系型数据库
|
||||
- ✅ **JSON 支持**:PostgreSQL 原生支持 JSON/JSONB,可以灵活存储 SVG 数据
|
||||
- ✅ **全文搜索**:内置全文搜索功能,适合字词搜索场景
|
||||
- ✅ **扩展性强**:支持数组类型,适合存储多个拼音、多个读音等
|
||||
- ✅ **成熟稳定**:企业级数据库,性能优秀
|
||||
- ✅ **开源免费**:成本可控
|
||||
|
||||
### 1.2 备选方案
|
||||
|
||||
**MySQL 8.0+**
|
||||
- 优势:生态成熟,使用广泛
|
||||
- 劣势:JSON 支持不如 PostgreSQL 完善
|
||||
|
||||
**MongoDB**
|
||||
- 优势:文档型数据库,灵活性强
|
||||
- 劣势:关联查询复杂,不适合强关联关系场景
|
||||
|
||||
## 2. 数据表设计
|
||||
|
||||
### 2.1 核心设计思路
|
||||
|
||||
**是否分离英语和汉字?**
|
||||
|
||||
**建议:统一表设计,通过类型字段区分**
|
||||
|
||||
**理由:**
|
||||
1. ✅ **数据结构相似**:汉字、词语、英语单词、字母的基础字段基本相同
|
||||
2. ✅ **关联关系统一**:都需要关联图片、句子、关联字词等
|
||||
3. ✅ **查询便利**:统一表结构便于统一查询和筛选
|
||||
4. ✅ **维护简单**:减少表数量,降低维护成本
|
||||
5. ✅ **扩展性好**:后续如需新增类型,只需扩展类型枚举
|
||||
|
||||
**如果数据量特别大,可以考虑:**
|
||||
- 按类型分表(水平分表)
|
||||
- 使用数据库分区功能
|
||||
|
||||
## 3. 数据表详细设计
|
||||
|
||||
### 3.1 字词主表 (words)
|
||||
|
||||
存储所有字词的基础信息。
|
||||
|
||||
```sql
|
||||
CREATE TABLE words (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
content VARCHAR(50) NOT NULL COMMENT '字词内容',
|
||||
type VARCHAR(20) NOT NULL COMMENT '类型: chinese_char(汉字), chinese_word(词语), english_word(英语单词), english_letter(英语字母)',
|
||||
grade INT COMMENT '年级: 1-9,NULL表示未设置',
|
||||
pinyins TEXT[] COMMENT '拼音数组,支持多音字',
|
||||
pronunciations TEXT[] COMMENT '读音数组,存储音频文件路径或读音标注',
|
||||
svg_data JSONB COMMENT 'SVG笔画数据,JSON格式',
|
||||
description TEXT COMMENT '描述信息',
|
||||
status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active(启用), inactive(禁用)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by BIGINT COMMENT '创建人ID',
|
||||
updated_by BIGINT COMMENT '更新人ID',
|
||||
|
||||
-- 索引
|
||||
INDEX idx_content (content),
|
||||
INDEX idx_type (type),
|
||||
INDEX idx_grade (grade),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
);
|
||||
|
||||
-- 全文搜索索引
|
||||
CREATE INDEX idx_content_fulltext ON words USING GIN (to_tsvector('simple', content));
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
- `content`: 字词内容,如"中"、"中国"、"hello"
|
||||
- `type`: 类型枚举,便于后续扩展
|
||||
- `grade`: 年级,1-9年级,NULL 表示未设置
|
||||
- `pinyins`: PostgreSQL 数组类型,存储多个拼音
|
||||
- `pronunciations`: 读音数组,可以是音频文件路径或文本标注
|
||||
- `svg_data`: JSONB 类型,存储 SVG 配置数据
|
||||
- `status`: 启用/禁用状态,支持软删除
|
||||
|
||||
### 3.2 图片表 (word_images)
|
||||
|
||||
存储字词关联的图片信息。
|
||||
|
||||
```sql
|
||||
CREATE TABLE word_images (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
word_id BIGINT NOT NULL COMMENT '关联的字词ID',
|
||||
image_type VARCHAR(20) NOT NULL COMMENT '图片类型: original(原图), standard(标准图)',
|
||||
file_path VARCHAR(500) NOT NULL COMMENT '图片文件路径',
|
||||
file_name VARCHAR(200) NOT NULL COMMENT '原始文件名',
|
||||
file_size BIGINT COMMENT '文件大小(字节)',
|
||||
width INT COMMENT '图片宽度',
|
||||
height INT COMMENT '图片高度',
|
||||
mime_type VARCHAR(50) COMMENT 'MIME类型',
|
||||
is_primary BOOLEAN DEFAULT FALSE COMMENT '是否为主图',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序顺序',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (word_id) REFERENCES words(id) ON DELETE CASCADE,
|
||||
INDEX idx_word_id (word_id),
|
||||
INDEX idx_image_type (image_type),
|
||||
INDEX idx_is_primary (is_primary)
|
||||
);
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
- `word_id`: 关联的字词ID
|
||||
- `image_type`: 区分原图和标准图
|
||||
- `file_path`: 图片存储路径
|
||||
- `is_primary`: 标记主图,用于列表展示
|
||||
- `sort_order`: 排序字段,支持多图排序
|
||||
|
||||
### 3.3 字词关联表 (word_relations)
|
||||
|
||||
存储字词之间的关联关系。
|
||||
|
||||
```sql
|
||||
CREATE TABLE word_relations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
source_word_id BIGINT NOT NULL COMMENT '源字词ID',
|
||||
target_word_id BIGINT NOT NULL COMMENT '目标字词ID',
|
||||
relation_type VARCHAR(20) NOT NULL COMMENT '关联类型: word_to_char(词包含字), char_to_word(字的组词), synonym(同义词), antonym(反义词)',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序顺序',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (source_word_id) REFERENCES words(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (target_word_id) REFERENCES words(id) ON DELETE CASCADE,
|
||||
UNIQUE (source_word_id, target_word_id, relation_type),
|
||||
INDEX idx_source_word (source_word_id),
|
||||
INDEX idx_target_word (target_word_id),
|
||||
INDEX idx_relation_type (relation_type)
|
||||
);
|
||||
```
|
||||
|
||||
**关联类型说明:**
|
||||
- `word_to_char`: 词包含字,如"中国"关联"中"和"国"
|
||||
- `char_to_word`: 字的组词,如"中"关联"中国"、"中间"等
|
||||
- `synonym`: 同义词(后续扩展)
|
||||
- `antonym`: 反义词(后续扩展)
|
||||
|
||||
### 3.4 句子表 (sentences)
|
||||
|
||||
存储字词关联的例句。
|
||||
|
||||
```sql
|
||||
CREATE TABLE sentences (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
content TEXT NOT NULL COMMENT '句子内容',
|
||||
translation TEXT COMMENT '翻译(英语句子需要)',
|
||||
audio_path VARCHAR(500) COMMENT '音频文件路径',
|
||||
source VARCHAR(100) COMMENT '来源,如教材名称',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_content_fulltext (to_tsvector('simple', content))
|
||||
);
|
||||
```
|
||||
|
||||
### 3.5 字词-句子关联表 (word_sentences)
|
||||
|
||||
存储字词和句子的关联关系。
|
||||
|
||||
```sql
|
||||
CREATE TABLE word_sentences (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
word_id BIGINT NOT NULL COMMENT '字词ID',
|
||||
sentence_id BIGINT NOT NULL COMMENT '句子ID',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序顺序',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (word_id) REFERENCES words(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (sentence_id) REFERENCES sentences(id) ON DELETE CASCADE,
|
||||
UNIQUE (word_id, sentence_id),
|
||||
INDEX idx_word_id (word_id),
|
||||
INDEX idx_sentence_id (sentence_id)
|
||||
);
|
||||
```
|
||||
|
||||
### 3.6 用户表 (users)
|
||||
|
||||
存储管理员用户信息。
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
|
||||
email VARCHAR(100) UNIQUE COMMENT '邮箱',
|
||||
password_hash VARCHAR(255) NOT NULL COMMENT '密码哈希',
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'admin' COMMENT '角色: super_admin(超级管理员), admin(一般管理员)',
|
||||
real_name VARCHAR(50) COMMENT '真实姓名',
|
||||
avatar VARCHAR(500) COMMENT '头像URL',
|
||||
status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active(启用), inactive(禁用)',
|
||||
last_login_at TIMESTAMP COMMENT '最后登录时间',
|
||||
last_login_ip VARCHAR(50) COMMENT '最后登录IP',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_username (username),
|
||||
INDEX idx_email (email),
|
||||
INDEX idx_role (role),
|
||||
INDEX idx_status (status)
|
||||
);
|
||||
```
|
||||
|
||||
### 3.7 操作日志表 (operation_logs)
|
||||
|
||||
记录用户操作日志,用于审计。
|
||||
|
||||
```sql
|
||||
CREATE TABLE operation_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT COMMENT '操作用户ID',
|
||||
action VARCHAR(50) NOT NULL COMMENT '操作类型: create, update, delete, upload等',
|
||||
resource_type VARCHAR(50) NOT NULL COMMENT '资源类型: word, image, sentence等',
|
||||
resource_id BIGINT COMMENT '资源ID',
|
||||
description TEXT COMMENT '操作描述',
|
||||
ip_address VARCHAR(50) COMMENT 'IP地址',
|
||||
user_agent TEXT COMMENT '用户代理',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_action (action),
|
||||
INDEX idx_resource (resource_type, resource_id),
|
||||
INDEX idx_created_at (created_at)
|
||||
);
|
||||
```
|
||||
|
||||
## 4. 数据库关系图
|
||||
|
||||
```
|
||||
words (字词主表)
|
||||
├── word_images (图片表) - 一对多
|
||||
├── word_relations (字词关联) - 自关联(多对多)
|
||||
├── word_sentences (字词-句子关联) - 多对多
|
||||
└── operation_logs (操作日志) - 关联资源
|
||||
|
||||
sentences (句子表)
|
||||
└── word_sentences (字词-句子关联) - 多对多
|
||||
|
||||
users (用户表)
|
||||
└── operation_logs (操作日志) - 一对多
|
||||
```
|
||||
|
||||
## 5. 索引优化建议
|
||||
|
||||
### 5.1 查询优化索引
|
||||
- 字词内容索引:支持快速查找
|
||||
- 类型+年级组合索引:支持类型和年级筛选
|
||||
- 全文搜索索引:支持模糊搜索
|
||||
|
||||
### 5.2 关联查询优化
|
||||
- 外键索引:所有外键字段建立索引
|
||||
- 关联表索引:word_relations、word_sentences 的关联字段索引
|
||||
|
||||
## 6. 数据迁移策略
|
||||
|
||||
### 6.1 初始化数据
|
||||
- 创建默认超级管理员账户
|
||||
- 初始化年级数据字典(可选)
|
||||
|
||||
### 6.2 数据导入
|
||||
- 支持批量导入字词数据
|
||||
- 支持 CSV/Excel 格式导入
|
||||
|
||||
## 7. 数据备份策略
|
||||
|
||||
### 7.1 备份方案
|
||||
- 每日全量备份
|
||||
- 实时增量备份(可选)
|
||||
|
||||
### 7.2 恢复方案
|
||||
- 支持时间点恢复
|
||||
- 定期恢复演练
|
||||
|
||||
## 8. 性能优化建议
|
||||
|
||||
### 8.1 查询优化
|
||||
- 使用连接池管理数据库连接
|
||||
- 复杂查询使用视图或物化视图
|
||||
- 合理使用缓存(Redis)
|
||||
|
||||
### 8.2 数据量预估
|
||||
- 字词数据:预计 10,000+ 条
|
||||
- 图片数据:预计 50,000+ 条
|
||||
- 关联关系:预计 100,000+ 条
|
||||
|
||||
### 8.3 分表策略(如需要)
|
||||
- 当 words 表数据量超过 100 万时,考虑按类型分表
|
||||
- 使用 PostgreSQL 分区功能
|
||||
@@ -0,0 +1,205 @@
|
||||
# 字词学习平台 - 产品需求文档
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 项目背景
|
||||
为 Doodle 小程序提供字词学习数据支撑,包括汉字、词语、英语单词和字母的完整学习资源管理。
|
||||
|
||||
### 1.2 项目目标
|
||||
- 建立完整的字词学习资源库
|
||||
- 提供高效的后台管理系统
|
||||
- 支持多维度数据关联和查询
|
||||
- 实现图片标准化处理流程
|
||||
|
||||
## 2. 核心功能需求
|
||||
|
||||
### 2.1 字词数据管理
|
||||
|
||||
#### 2.1.1 基础信息
|
||||
- **字词内容**:汉字、词语、英语单词、英语字母
|
||||
- **类型标识**:汉字、词语、英语单词、英语字母
|
||||
- **年级信息**:根据人教版语文书判断学习年级(1-9年级)
|
||||
- **拼音信息**:支持多个拼音(多音字场景)
|
||||
- **读音信息**:音频文件或读音标注
|
||||
|
||||
#### 2.1.2 SVG 笔画数据
|
||||
- 存储 SVG 格式的笔画数据
|
||||
- 支持编辑和修改 SVG 配置
|
||||
- 用于小程序中的笔画展示和练习
|
||||
|
||||
#### 2.1.3 图片资源
|
||||
- **多图支持**:一个字词可以关联多张图片
|
||||
- **图片类型**:
|
||||
- 原图:用户上传的原始图片
|
||||
- 标准图:经过标准化处理的图片(背景半透明、统一像素大小)
|
||||
- **图片用途**:示例图、练习图、卡片图等
|
||||
|
||||
#### 2.1.4 关联关系
|
||||
- **字词关联**:
|
||||
- 一个字可以关联多个常用组词
|
||||
- 一个词可以关联多个组成字
|
||||
- **句子关联**:一个字词可以关联多条例句
|
||||
|
||||
### 2.2 后台管理功能
|
||||
|
||||
#### 2.2.1 汉字管理(第一阶段)
|
||||
|
||||
**2.2.1.1 汉字卡片列表**
|
||||
- **展示形式**:卡片列表形式展示汉字
|
||||
- **分页规则**:
|
||||
- 每页展示 9 个卡片
|
||||
- 每次请求返回一页数据(9 个汉字)
|
||||
- 支持分页导航
|
||||
- **搜索功能**:
|
||||
- 仅支持汉字内容搜索
|
||||
- 实时搜索,支持模糊匹配
|
||||
- **排序规则**:
|
||||
- 按年级排序:低年级排在前面,高年级排在后面
|
||||
- 年级相同时按创建时间排序
|
||||
- **卡片内容**:
|
||||
- **左上角**:汉字内容(大号字体)
|
||||
- **右上角上方**:拼音信息(多音字用 "|" 分割,如:zhōng|zhòng)
|
||||
- **右上角下方**:SVG 笔画标识
|
||||
- 有 SVG 数据:显示绿色标识
|
||||
- 无 SVG 数据:显示灰色标识
|
||||
- **下方**:关联图片(仅展示第一张图片,如果有多张图片)
|
||||
|
||||
**2.2.1.2 汉字详情弹窗**
|
||||
- **触发方式**:点击卡片后,右侧滑出 Popup 弹窗
|
||||
- **弹窗布局**:
|
||||
- **第一行**:
|
||||
- **左侧**:田字格 + Canvas 渲染
|
||||
- 有 SVG 数据:使用 Canvas 渲染笔画,颜色为黑色
|
||||
- 无 SVG 数据:在田字格中直接展示文字,颜色为灰色
|
||||
- **右侧上方**:拼音信息(多音字用 "|" 分割)
|
||||
- **右侧下方**:按笔画顺序展示多个小田字格和笔画
|
||||
- **第二行**:汉字对应的图片
|
||||
- 最多支持上传 4 张图片
|
||||
- 支持图片预览、删除、排序
|
||||
- **第三行**:汉字对应的多个词语
|
||||
- 展示关联的词语列表
|
||||
- 支持添加、删除、编辑词语
|
||||
- **第四行**:汉字对应的多个句子
|
||||
- 展示关联的句子列表
|
||||
- 支持添加、删除、编辑句子
|
||||
|
||||
**2.2.1.3 编辑功能**
|
||||
- **查看模式**:默认模式,仅展示数据,不可编辑
|
||||
- **编辑模式**:
|
||||
- 点击编辑按钮进入编辑模式
|
||||
- 汉字、拼音、SVG 数据不可编辑(通过其他方式导入)
|
||||
- 图片可编辑:支持上传、删除、排序(最多 4 张)
|
||||
- 词语可编辑:支持添加、删除、编辑
|
||||
- 句子可编辑:支持添加、删除、编辑
|
||||
- **无新建功能**:汉字、拼音、SVG 数据通过其他方式导入,不支持在后台新建
|
||||
|
||||
#### 2.2.2 字词列表管理(后续功能)
|
||||
- **列表视图**:表格形式展示字词列表
|
||||
- **卡片视图**:卡片形式分页展示
|
||||
- **分页功能**:支持分页浏览
|
||||
- **筛选功能**:
|
||||
- 按类型筛选(汉字/词语/英语单词/英语字母)
|
||||
- 按年级筛选
|
||||
- 按拼音筛选
|
||||
- 关键词搜索
|
||||
|
||||
#### 2.2.3 字词编辑功能(后续功能)
|
||||
- **基础信息编辑**:
|
||||
- 字词内容
|
||||
- 类型选择
|
||||
- 年级设置
|
||||
- 拼音添加/删除
|
||||
- 读音上传/编辑
|
||||
- **SVG 配置编辑**:
|
||||
- 上传/编辑 SVG 笔画数据
|
||||
- SVG 预览功能
|
||||
- **图片管理**:
|
||||
- 上传多张图片
|
||||
- 图片预览
|
||||
- 删除图片
|
||||
- 设置主图
|
||||
- **关联管理**:
|
||||
- 添加/删除关联词语
|
||||
- 添加/删除关联句子
|
||||
- 关联关系可视化展示
|
||||
|
||||
#### 2.2.4 词语查询功能(后续功能)
|
||||
- 独立的词语列表页面
|
||||
- 支持词语搜索和筛选
|
||||
- 查看词语关联的字和句子
|
||||
|
||||
### 2.3 图片处理功能
|
||||
|
||||
#### 2.3.1 图片标准化处理
|
||||
- **背景处理**:将白色背景变为半透明
|
||||
- **尺寸统一**:统一像素大小(默认 300x300)
|
||||
- **格式转换**:统一输出为 PNG 格式
|
||||
- **水印去除**:自动检测并去除底部水印
|
||||
|
||||
#### 2.3.2 图片存储策略
|
||||
- **原图保存**:保留用户上传的原始图片
|
||||
- **标准图生成**:自动生成标准化处理后的图片
|
||||
- **存储路径管理**:清晰的目录结构
|
||||
|
||||
### 2.4 用户权限管理
|
||||
|
||||
#### 2.4.1 用户角色
|
||||
- **超级管理员**:
|
||||
- 所有数据管理权限
|
||||
- 用户管理权限(添加/删除管理员)
|
||||
- 系统配置权限
|
||||
- **一般管理员**:
|
||||
- 字词数据管理权限
|
||||
- 图片上传和编辑权限
|
||||
- 无用户管理权限
|
||||
|
||||
#### 2.4.2 权限控制
|
||||
- 基于角色的访问控制(RBAC)
|
||||
- 操作日志记录
|
||||
- 数据修改审核(可选)
|
||||
|
||||
## 3. 非功能需求
|
||||
|
||||
### 3.1 性能要求
|
||||
- 列表查询响应时间 < 500ms
|
||||
- 图片上传处理时间 < 5s
|
||||
- 支持并发用户数 > 50
|
||||
|
||||
### 3.2 数据要求
|
||||
- 数据完整性:保证关联关系的一致性
|
||||
- 数据准确性:年级信息需准确对应人教版教材
|
||||
- 数据可扩展性:支持后续功能扩展
|
||||
|
||||
### 3.3 安全要求
|
||||
- 用户认证和授权
|
||||
- 数据操作审计
|
||||
- 图片上传安全校验
|
||||
|
||||
## 4. 技术约束
|
||||
|
||||
### 4.1 后端技术
|
||||
- NestJS 框架
|
||||
- RESTful API
|
||||
|
||||
### 4.2 前端技术
|
||||
- React 框架
|
||||
- 响应式设计
|
||||
|
||||
### 4.3 图片处理
|
||||
- Node.js + Jimp 库
|
||||
- 支持批量处理
|
||||
|
||||
## 5. 后续扩展需求
|
||||
|
||||
### 5.1 数据导入导出
|
||||
- 批量导入字词数据
|
||||
- 数据导出功能
|
||||
|
||||
### 5.2 统计分析
|
||||
- 字词使用统计
|
||||
- 图片使用统计
|
||||
- 用户操作统计
|
||||
|
||||
### 5.3 API 接口
|
||||
- 为小程序提供数据接口
|
||||
- 支持分页、筛选、搜索
|
||||
@@ -0,0 +1,329 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>字词学习平台 - 管理后台</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 侧边栏导航 -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1 class="logo">📚 字词平台</h1>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<a href="#words" class="nav-item active" data-page="words">
|
||||
<span class="nav-icon">📝</span>
|
||||
<span class="nav-text">字词管理</span>
|
||||
</a>
|
||||
<a href="#words-list" class="nav-item" data-page="words-list">
|
||||
<span class="nav-icon">🔍</span>
|
||||
<span class="nav-text">词语查询</span>
|
||||
</a>
|
||||
<a href="#users" class="nav-item" data-page="users">
|
||||
<span class="nav-icon">👥</span>
|
||||
<span class="nav-text">用户管理</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-info">
|
||||
<div class="user-avatar">A</div>
|
||||
<div class="user-details">
|
||||
<div class="user-name">管理员</div>
|
||||
<div class="user-role">超级管理员</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="main-content">
|
||||
<!-- 字词管理页面 -->
|
||||
<div id="words-page" class="page active">
|
||||
<div class="page-header">
|
||||
<h2>字词管理</h2>
|
||||
<button class="btn btn-primary" onclick="openEditModal(null)">
|
||||
<span>+</span> 新增字词
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="filter-bar">
|
||||
<div class="filter-group">
|
||||
<input type="text" class="search-input" placeholder="搜索字词内容..." id="search-input">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<select class="filter-select" id="type-filter">
|
||||
<option value="">全部类型</option>
|
||||
<option value="chinese_char">汉字</option>
|
||||
<option value="chinese_word">词语</option>
|
||||
<option value="english_word">英语单词</option>
|
||||
<option value="english_letter">英语字母</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<select class="filter-select" id="grade-filter">
|
||||
<option value="">全部年级</option>
|
||||
<option value="1">一年级</option>
|
||||
<option value="2">二年级</option>
|
||||
<option value="3">三年级</option>
|
||||
<option value="4">四年级</option>
|
||||
<option value="5">五年级</option>
|
||||
<option value="6">六年级</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" data-view="list" title="列表视图">
|
||||
<span>☰</span>
|
||||
</button>
|
||||
<button class="view-btn" data-view="card" title="卡片视图">
|
||||
<span>⊞</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表视图 -->
|
||||
<div id="list-view" class="view-container active">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>内容</th>
|
||||
<th>类型</th>
|
||||
<th>年级</th>
|
||||
<th>拼音</th>
|
||||
<th>图片数</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="words-table-body">
|
||||
<!-- 数据通过 JS 动态加载 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 卡片视图 -->
|
||||
<div id="card-view" class="view-container">
|
||||
<div class="card-grid" id="words-card-grid">
|
||||
<!-- 卡片通过 JS 动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<button class="page-btn" id="prev-page" disabled>上一页</button>
|
||||
<div class="page-info">
|
||||
<span id="current-page">1</span> / <span id="total-pages">1</span>
|
||||
</div>
|
||||
<button class="page-btn" id="next-page">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 词语查询页面 -->
|
||||
<div id="words-list-page" class="page">
|
||||
<div class="page-header">
|
||||
<h2>词语查询</h2>
|
||||
</div>
|
||||
<div class="filter-bar">
|
||||
<input type="text" class="search-input" placeholder="搜索词语..." id="word-search-input">
|
||||
</div>
|
||||
<div class="word-list-container">
|
||||
<div id="word-list-content">
|
||||
<!-- 词语列表通过 JS 动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户管理页面 -->
|
||||
<div id="users-page" class="page">
|
||||
<div class="page-header">
|
||||
<h2>用户管理</h2>
|
||||
<button class="btn btn-primary" onclick="openUserModal(null)">
|
||||
<span>+</span> 添加管理员
|
||||
</button>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th>角色</th>
|
||||
<th>创建时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-table-body">
|
||||
<!-- 数据通过 JS 动态加载 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 字词编辑模态框 -->
|
||||
<div id="edit-modal" class="modal">
|
||||
<div class="modal-content modal-large">
|
||||
<div class="modal-header">
|
||||
<h3 id="modal-title">编辑字词</h3>
|
||||
<button class="modal-close" onclick="closeEditModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-tabs">
|
||||
<button class="tab-btn active" data-tab="basic">基础信息</button>
|
||||
<button class="tab-btn" data-tab="svg">SVG配置</button>
|
||||
<button class="tab-btn" data-tab="images">图片管理</button>
|
||||
<button class="tab-btn" data-tab="relations">关联管理</button>
|
||||
</div>
|
||||
|
||||
<!-- 基础信息标签页 -->
|
||||
<div class="tab-content active" id="basic-tab">
|
||||
<div class="form-group">
|
||||
<label>字词内容 *</label>
|
||||
<input type="text" class="form-input" id="edit-content" placeholder="输入字词内容">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>类型 *</label>
|
||||
<select class="form-select" id="edit-type">
|
||||
<option value="chinese_char">汉字</option>
|
||||
<option value="chinese_word">词语</option>
|
||||
<option value="english_word">英语单词</option>
|
||||
<option value="english_letter">英语字母</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>年级</label>
|
||||
<select class="form-select" id="edit-grade">
|
||||
<option value="">未设置</option>
|
||||
<option value="1">一年级</option>
|
||||
<option value="2">二年级</option>
|
||||
<option value="3">三年级</option>
|
||||
<option value="4">四年级</option>
|
||||
<option value="5">五年级</option>
|
||||
<option value="6">六年级</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>拼音(多个用逗号分隔)</label>
|
||||
<input type="text" class="form-input" id="edit-pinyins" placeholder="例如: zhōng, zhòng">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>读音</label>
|
||||
<input type="text" class="form-input" id="edit-pronunciations" placeholder="读音标注或音频路径">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>描述</label>
|
||||
<textarea class="form-textarea" id="edit-description" rows="3" placeholder="描述信息"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SVG配置标签页 -->
|
||||
<div class="tab-content" id="svg-tab">
|
||||
<div class="form-group">
|
||||
<label>SVG 数据</label>
|
||||
<div class="svg-editor">
|
||||
<textarea class="form-textarea svg-code" id="edit-svg" rows="15"
|
||||
placeholder="输入或粘贴 SVG 代码..."></textarea>
|
||||
<div class="svg-preview">
|
||||
<div class="preview-label">预览</div>
|
||||
<div id="svg-preview-container" class="svg-preview-box"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片管理标签页 -->
|
||||
<div class="tab-content" id="images-tab">
|
||||
<div class="image-upload-area" id="image-upload-area">
|
||||
<input type="file" id="image-upload-input" multiple accept="image/*" style="display: none;">
|
||||
<div class="upload-placeholder" onclick="document.getElementById('image-upload-input').click()">
|
||||
<span class="upload-icon">📷</span>
|
||||
<p>点击上传图片(支持多选)</p>
|
||||
<p class="upload-hint">图片将自动处理为半透明背景,统一尺寸</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-list" id="image-list">
|
||||
<!-- 图片列表通过 JS 动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关联管理标签页 -->
|
||||
<div class="tab-content" id="relations-tab">
|
||||
<div class="relation-section">
|
||||
<h4>关联词语</h4>
|
||||
<div class="relation-input-group">
|
||||
<input type="text" class="form-input" id="related-word-input" placeholder="输入关联词语,按回车添加">
|
||||
<button class="btn btn-secondary" onclick="addRelatedWord()">添加</button>
|
||||
</div>
|
||||
<div class="relation-tags" id="related-words-list">
|
||||
<!-- 关联词语标签 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="relation-section">
|
||||
<h4>关联句子</h4>
|
||||
<div class="sentence-input-group">
|
||||
<textarea class="form-textarea" id="sentence-input" rows="2"
|
||||
placeholder="输入句子,按回车添加"></textarea>
|
||||
<button class="btn btn-secondary" onclick="addSentence()">添加</button>
|
||||
</div>
|
||||
<div class="sentence-list" id="sentences-list">
|
||||
<!-- 句子列表 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeEditModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="saveWord()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户编辑模态框 -->
|
||||
<div id="user-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="user-modal-title">添加管理员</h3>
|
||||
<button class="modal-close" onclick="closeUserModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>用户名 *</label>
|
||||
<input type="text" class="form-input" id="user-username" placeholder="输入用户名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>邮箱 *</label>
|
||||
<input type="email" class="form-input" id="user-email" placeholder="输入邮箱">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码 *</label>
|
||||
<input type="password" class="form-input" id="user-password" placeholder="输入密码">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>角色 *</label>
|
||||
<select class="form-select" id="user-role">
|
||||
<option value="admin">一般管理员</option>
|
||||
<option value="super_admin">超级管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeUserModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="saveUser()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<div class="modal-overlay" onclick="closeAllModals()"></div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,486 @@
|
||||
/* 全局样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color: #4f46e5;
|
||||
--primary-hover: #4338ca;
|
||||
--secondary-color: #64748b;
|
||||
--success-color: #10b981;
|
||||
--danger-color: #ef4444;
|
||||
--warning-color: #f59e0b;
|
||||
--bg-color: #f8fafc;
|
||||
--sidebar-bg: #1e293b;
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--border-color: #e2e8f0;
|
||||
--card-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
--card-shadow-hover: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: var(--sidebar-bg);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: rgba(79, 70, 229, 0.2);
|
||||
color: white;
|
||||
border-left: 3px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 1.2rem;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
margin-left: 260px;
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
background: white;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.data-table thead {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
/* 卡片视图 */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.word-card {
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.word-card:hover {
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.word-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.word-content {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.word-type {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: var(--bg-color);
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.word-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.word-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.word-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
background: white;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.page-btn:hover:not(:disabled) {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 模态框 */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
z-index: 1001;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.modal-large {
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* 表单 */
|
||||
.form-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
Reference in New Issue
Block a user