Compare commits
14 Commits
release/v1
...
feat/v1.0.
| Author | SHA1 | Date | |
|---|---|---|---|
| f4c3734639 | |||
| 38a243af15 | |||
| 0d03fdd782 | |||
| a76263375a | |||
| 9bc58c6270 | |||
| 7b505ec4aa | |||
| a7cf35e6bf | |||
| c617307c79 | |||
| 3d3f7afcd2 | |||
| 18e3f6f7f2 | |||
| e061f46e66 | |||
| 7d49143ae0 | |||
| d1f6044a6c | |||
| 9a80fd9fa1 |
9
.kiro/settings/kiro.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"kiro.ai.language": "zh-CN",
|
||||
"kiro.ai.commitMessage.language": "chinese",
|
||||
"kiro.ai.commitMessage.forceLanguage": "zh-CN",
|
||||
"kiro.git.commitMessageLanguage": "zh-CN",
|
||||
"kiro.assistant.locale": "zh-CN",
|
||||
"kiro.assistant.language": "chinese",
|
||||
"kiro.assistant.alwaysUseChinese": true
|
||||
}
|
||||
110
.kiro/steering/chinese-commit-messages.md
Normal file
@ -0,0 +1,110 @@
|
||||
---
|
||||
inclusion: auto
|
||||
---
|
||||
|
||||
# Git 提交消息规范
|
||||
|
||||
**重要:所有提交消息必须使用中文!**
|
||||
|
||||
本项目严格遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范。
|
||||
|
||||
## 提交消息格式
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
### 必需部分
|
||||
|
||||
#### Type (类型)
|
||||
提交的类型必须是以下之一:
|
||||
|
||||
- **feat**: 新功能 (feature)
|
||||
- **fix**: 修复 bug
|
||||
- **docs**: 仅文档更改
|
||||
- **style**: 不影响代码含义的更改(空格、格式化、缺少分号等)
|
||||
- **refactor**: 既不修复 bug 也不添加功能的代码更改
|
||||
- **perf**: 提高性能的代码更改
|
||||
- **test**: 添加缺失的测试或更正现有测试
|
||||
- **build**: 影响构建系统或外部依赖的更改(示例范围:webpack, npm, vite)
|
||||
- **ci**: 对 CI 配置文件和脚本的更改(示例范围:GitHub Actions, GitLab CI)
|
||||
- **chore**: 其他不修改 src 或测试文件的更改
|
||||
- **revert**: 回退之前的提交
|
||||
|
||||
#### Subject (主题)
|
||||
- **必须使用中文描述**
|
||||
- 使用简洁的语言描述本次提交的更改
|
||||
- 不超过 50 个字符
|
||||
- 首字母小写
|
||||
- 结尾不加句号
|
||||
|
||||
### 可选部分
|
||||
|
||||
#### Scope (范围)
|
||||
用于说明提交影响的范围,例如:
|
||||
- **component**: 组件相关
|
||||
- **api**: API 相关
|
||||
- **router**: 路由相关
|
||||
- **store**: 状态管理相关
|
||||
- **utils**: 工具函数相关
|
||||
- **styles**: 样式相关
|
||||
- **deps**: 依赖项相关
|
||||
|
||||
#### Body (正文)
|
||||
- 详细描述本次提交的动机和更改内容
|
||||
- 可以分多行
|
||||
- 应该说明"是什么"和"为什么",而不是"怎么做"
|
||||
|
||||
#### Footer (页脚)
|
||||
- 用于关闭 Issue 或描述破坏性变更
|
||||
- Breaking Changes 必须在页脚中以 `BREAKING CHANGE:` 开头
|
||||
- 关闭 Issue 使用 `Closes #issue号`
|
||||
|
||||
## 示例(必须使用中文)
|
||||
|
||||
### 简单的功能添加
|
||||
```
|
||||
feat(article): 添加文章搜索功能
|
||||
```
|
||||
|
||||
### 修复 bug
|
||||
```
|
||||
fix(login): 修复登录状态未持久化的问题
|
||||
```
|
||||
|
||||
### 包含详细描述
|
||||
```
|
||||
feat(editor): 支持 markdown 实时预览
|
||||
|
||||
添加了一个新的编辑器组件,支持 markdown 的实时预览功能。
|
||||
用户在编辑时可以同时看到渲染后的效果,提升编辑体验。
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
### 破坏性变更
|
||||
```
|
||||
feat(api): 重构用户认证 api
|
||||
|
||||
BREAKING CHANGE: 用户认证接口从 /api/auth 迁移到 /api/v2/auth,
|
||||
旧接口将在下一个版本中移除。客户端需要更新 API 端点。
|
||||
```
|
||||
|
||||
## 重要规则
|
||||
1. **严格遵循格式**: `<type>(<scope>): <subject>`
|
||||
2. **type 必须小写**: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
||||
3. **subject 必须使用中文**: 不要使用英文描述
|
||||
4. **subject 首字母小写**: 不要大写开头
|
||||
5. **subject 不加句号**: 结尾不要加标点符号
|
||||
6. **不超过 50 字符**: subject 部分要简洁
|
||||
7. **破坏性变更**: 必须在 body 中以 `BREAKING CHANGE:` 开头说明
|
||||
|
||||
## 生成提交消息时的要求
|
||||
- 始终使用中文
|
||||
- 动词开头(如:添加、修复、更新、优化、重构)
|
||||
- 描述要具体,避免模糊的表述
|
||||
- 如果有多个更改,选择最主要的作为 subject
|
||||
14
.vscode/settings.json
vendored
@ -42,5 +42,17 @@
|
||||
"Echarts",
|
||||
"Typeit"
|
||||
],
|
||||
"kiroAgent.configureMCP": "Disabled" // `
|
||||
|
||||
// Kiro AI 助手配置
|
||||
"kiroAgent.configureMCP": "Disabled",
|
||||
"kiro.ai.language": "zh-CN",
|
||||
"kiro.ai.commitMessage.language": "chinese",
|
||||
"kiro.git.commitMessageLanguage": "zh-CN",
|
||||
"kiro.assistant.locale": "zh-CN",
|
||||
|
||||
// Git 配置
|
||||
"git.inputValidationLength": 72,
|
||||
|
||||
// GitHub Copilot 语言设置(如果安装了)
|
||||
"github.copilot.chat.localeOverride": "zh-CN"
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ VITE_APP_TITLE="阅读比赛配置后台"
|
||||
VITE_BASE_OSS_URL = https://oss.qyzhjy.com/
|
||||
|
||||
# 上传图片的URL
|
||||
VITE_BASE_UPLOAD_URL="http://192.168.20.150:8080"
|
||||
VITE_BASE_UPLOAD_URL="https://api.qyzhjy.com"
|
||||
|
||||
VITE_APP_DESC="阅读比赛配置"
|
||||
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
|
||||
# VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com
|
||||
|
||||
VITE_SERVICE_BASE_URL=http://172.16.10.130:5000
|
||||
VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com/
|
||||
# VITE_SERVICE_BASE_URL=http://172.16.10.130:5000
|
||||
|
||||
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||
"demo": "http://localhost:9528"
|
||||
|
||||
110
apps/admin/LARGE_SCREEN_DEV_GUIDE.md
Normal file
@ -0,0 +1,110 @@
|
||||
# 大屏开发调试指南
|
||||
|
||||
## 项目配置
|
||||
|
||||
- **设计基准**: 2048 x 1080
|
||||
- **缩放方案**: VScaleScreen
|
||||
- **目标设备**: 超级大屏(投屏显示)
|
||||
|
||||
## 开发调试方法
|
||||
|
||||
### 方法 1: Chrome DevTools 自定义设备(推荐)
|
||||
|
||||
1. 打开 Chrome DevTools (F12)
|
||||
2. 点击设备模拟按钮 (Ctrl/Cmd + Shift + M)
|
||||
3. 在设备下拉菜单中选择 "Edit..."
|
||||
4. 添加自定义设备:
|
||||
- Device Name: `Large Screen 2048x1080`
|
||||
- Width: `2048`
|
||||
- Height: `1080`
|
||||
- Device pixel ratio: `1`
|
||||
- User agent string: (保持默认)
|
||||
5. 选择刚创建的设备进行调试
|
||||
|
||||
### 方法 2: 浏览器窗口缩放
|
||||
|
||||
1. 将浏览器窗口调整为合适大小
|
||||
2. 使用浏览器缩放功能 (Ctrl/Cmd + 鼠标滚轮)
|
||||
3. 调整到能看到完整布局的比例
|
||||
|
||||
### 方法 3: 响应式设计模式
|
||||
|
||||
在 DevTools 中:
|
||||
1. 选择 "Responsive" 模式
|
||||
2. 手动输入宽度: `2048`
|
||||
3. 手动输入高度: `1080`
|
||||
|
||||
## 字体大小说明
|
||||
|
||||
当前字体配置已针对 2048x1080 优化:
|
||||
|
||||
| Class | 实际大小 | 适用场景 |
|
||||
|-------|---------|---------|
|
||||
| `text-xs` | 12.8px | 辅助文字、标注 |
|
||||
| `text-sm` | 15.2px | 次要文字 |
|
||||
| `text-base` | 17.6px | 正文 |
|
||||
| `text-lg` | 20.8px | 小标题 |
|
||||
| `text-xl` | 24px | 中标题 |
|
||||
| `text-2xl` | 28px | 大标题 |
|
||||
| `text-3xl` | 33.6px | 特大标题 |
|
||||
| `text-4xl` | 40px | 主标题 |
|
||||
| `text-5xl` | 51.2px | 超大标题 |
|
||||
| `text-6xl` | 64px | 巨大标题 |
|
||||
|
||||
## VScaleScreen 工作原理
|
||||
|
||||
```
|
||||
设计基准 (2048x1080)
|
||||
↓
|
||||
VScaleScreen 自动缩放
|
||||
↓
|
||||
实际显示设备 (任意分辨率)
|
||||
```
|
||||
|
||||
**示例:**
|
||||
- 在 1920x1080 屏幕上:内容会略微缩小
|
||||
- 在 3840x2160 (4K) 屏幕上:内容会放大约 2 倍
|
||||
- 在更大的 LED 屏上:内容会等比例放大
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 在我的电脑上字体看起来太大了?
|
||||
A: 这是正常的!因为你的设计基准 (2048x1080) 比普通电脑屏幕大。请使用上述方法在正确的分辨率下调试。
|
||||
|
||||
### Q: 如何确认实际大屏的分辨率?
|
||||
A:
|
||||
1. 询问现场技术人员
|
||||
2. 常见大屏分辨率:
|
||||
- 1920x1080 (Full HD)
|
||||
- 2560x1440 (2K)
|
||||
- 3840x2160 (4K)
|
||||
- 7680x4320 (8K)
|
||||
|
||||
### Q: 需要修改 VScaleScreen 的尺寸吗?
|
||||
A: 通常不需要。保持 2048x1080 即可,VScaleScreen 会自动适配。除非:
|
||||
- 实际大屏比例不是 16:9
|
||||
- 需要针对特定分辨率优化
|
||||
|
||||
### Q: 如何微调字体大小?
|
||||
A: 修改 `uno.config.ts` 中的 `fontSize` 配置,然后重启开发服务器。
|
||||
|
||||
## 测试清单
|
||||
|
||||
部署前请确认:
|
||||
|
||||
- [ ] 在 2048x1080 分辨率下所有文字清晰可读
|
||||
- [ ] 所有布局元素位置正确
|
||||
- [ ] 动画效果流畅
|
||||
- [ ] 图片清晰不模糊(建议使用 2x 或 3x 分辨率的图片)
|
||||
- [ ] 在实际大屏上测试(如果可能)
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
1. **图片资源**: 使用 WebP 格式,准备高分辨率版本
|
||||
2. **字体**: 使用 Web Font,确保在大屏上渲染清晰
|
||||
3. **动画**: 使用 CSS transform 和 opacity,避免触发重排
|
||||
4. **测试**: 在接近实际大屏的设备上测试性能
|
||||
|
||||
## 联系与支持
|
||||
|
||||
如有问题,请联系开发团队。
|
||||
239
apps/admin/font-size-preview.html
Normal file
@ -0,0 +1,239 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>字体大小预览 - 2048x1080</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
|
||||
sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 2048px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 60px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 60px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 200px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.size-info {
|
||||
width: 150px;
|
||||
color: #999;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.sample {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* UnoCSS 字体大小映射 */
|
||||
.text-xs {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.text-sm {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.text-base {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.text-lg {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.text-xl {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.text-2xl {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
.text-3xl {
|
||||
font-size: 2.1rem;
|
||||
}
|
||||
.text-4xl {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
.text-5xl {
|
||||
font-size: 3.2rem;
|
||||
}
|
||||
.text-6xl {
|
||||
font-size: 4rem;
|
||||
}
|
||||
.text-7xl {
|
||||
font-size: 5rem;
|
||||
}
|
||||
.text-8xl {
|
||||
font-size: 6.5rem;
|
||||
}
|
||||
.text-9xl {
|
||||
font-size: 8.5rem;
|
||||
|
||||
.controls {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.controls button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
margin-bottom: 10px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.controls button:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
|
||||
.current-size {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="controls">
|
||||
<button onclick="setViewport(2048, 1080)">2048x1080 (设计基准)</button>
|
||||
<button onclick="setViewport(1920, 1080)">1920x1080 (Full HD)</button>
|
||||
<button onclick="setViewport(3840, 2160)">3840x2160 (4K)</button>
|
||||
<div class="current-size" id="currentSize">当前窗口: <span id="sizeDisplay"></span></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h1>字体大小预览</h1>
|
||||
<div class="info">设计基准: 2048 x 1080 | 使用 VScaleScreen 自动缩放</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="label">text-xs</div>
|
||||
<div class="size-info">0.8rem (12.8px)</div>
|
||||
<div class="sample text-xs">辅助文字、标注信息 - The quick brown fox jumps over the lazy dog</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="label">text-sm</div>
|
||||
<div class="size-info">0.95rem (15.2px)</div>
|
||||
<div class="sample text-sm">次要文字内容 - The quick brown fox jumps over the lazy dog</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="label">text-base</div>
|
||||
<div class="size-info">1.1rem (17.6px)</div>
|
||||
<div class="sample text-base">正文内容 - The quick brown fox jumps over the lazy dog</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="label">text-lg</div>
|
||||
<div class="size-info">1.3rem (20.8px)</div>
|
||||
<div class="sample text-lg">小标题 - The quick brown fox jumps over the lazy dog</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="label">text-xl</div>
|
||||
<div class="size-info">1.5rem (24px)</div>
|
||||
<div class="sample text-xl">中标题 - The quick brown fox jumps over the lazy dog</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="label">text-2xl</div>
|
||||
<div class="size-info">1.75rem (28px)</div>
|
||||
<div class="sample text-2xl">大标题 - 重庆市教育学会</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="label">text-3xl</div>
|
||||
<div class="size-info">2.1rem (33.6px)</div>
|
||||
<div class="sample text-3xl">特大标题 - 阅读之星展示活动</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="label">text-4xl</div>
|
||||
<div class="size-info">2.5rem (40px)</div>
|
||||
<div class="sample text-4xl">主标题 - 队伍名单</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="label">text-5xl</div>
|
||||
<div class="size-info">3.2rem (51.2px)</div>
|
||||
<div class="sample text-5xl">超大标题 - 星辞海遨游</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="label">text-6xl</div>
|
||||
<div class="size-info">4rem (64px)</div>
|
||||
<div class="sample text-6xl">巨大标题 - 开始答题</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function setViewport(width, height) {
|
||||
// 这个函数只是提示,实际需要在浏览器 DevTools 中手动设置
|
||||
alert(
|
||||
`请在浏览器 DevTools 中设置视口为 ${width}x${height}\n\n步骤:\n1. 按 F12 打开 DevTools\n2. 按 Ctrl+Shift+M 进入响应式模式\n3. 输入宽度: ${width}\n4. 输入高度: ${height}`,
|
||||
)
|
||||
|
||||
function updateSize() {
|
||||
const width = window.innerWidth
|
||||
const height = window.innerHeight
|
||||
document.getElementById('sizeDisplay').textContent = `${width}x${height}`
|
||||
}
|
||||
|
||||
updateSize();
|
||||
window.addEventListener('resize', updateSize)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
apps/admin/src/assets/audio/checkout.mp3
Normal file
BIN
apps/admin/src/assets/audio/timeout3.mp3
Normal file
33
apps/admin/src/assets/fonts/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# 拼音字体说明
|
||||
|
||||
## 需要的字体文件
|
||||
|
||||
为了正确显示拼音声调符号,需要使用支持拼音的字体。
|
||||
|
||||
### 推荐字体:
|
||||
1. **GB Pinyinok-B** - 专业拼音字体
|
||||
2. **方正楷体拼音** - 楷体风格拼音字体
|
||||
3. **华文楷体拼音** - 华文系列拼音字体
|
||||
|
||||
### 字体文件放置位置:
|
||||
```
|
||||
apps/admin/src/assets/fonts/
|
||||
├── GBPinyinok-B.ttf
|
||||
├── GBPinyinok-B.woff2
|
||||
└── GBPinyinok-B.woff
|
||||
```
|
||||
|
||||
### 如何获取字体:
|
||||
1. 从系统字体库复制(macOS: /Library/Fonts/)
|
||||
2. 从字体网站下载(需要商业授权)
|
||||
3. 使用开源拼音字体
|
||||
|
||||
### 字体格式转换:
|
||||
如果只有 .ttf 文件,可以使用在线工具转换为 woff2 格式:
|
||||
- https://cloudconvert.com/ttf-to-woff2
|
||||
- https://www.fontsquirrel.com/tools/webfont-generator
|
||||
|
||||
### 注意事项:
|
||||
- 确保字体文件有合法的使用授权
|
||||
- woff2 格式文件最小,加载最快
|
||||
- 字体文件较大时考虑使用 CDN
|
||||
BIN
apps/admin/src/assets/imgs/user/rank-bg-kuang.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
apps/admin/src/assets/imgs/user/rank-title.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/admin/src/assets/imgs/user/team-title-bg.png
Normal file
|
After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 177 KiB |
@ -2,39 +2,42 @@
|
||||
import { computed } from 'vue'
|
||||
import defaultBtnBg from '@/assets/imgs/user/btn-bg.png'
|
||||
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: '下一步',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
theme: {
|
||||
type: String as () => 'primary' | 'light',
|
||||
default: 'primary',
|
||||
},
|
||||
type: {
|
||||
type: String as () => 'button' | 'text' | 'custom',
|
||||
default: 'button',
|
||||
},
|
||||
btnBg: {
|
||||
type: String,
|
||||
default: defaultBtnBg,
|
||||
},
|
||||
fontSize: {
|
||||
type: Number,
|
||||
default: 30,
|
||||
},
|
||||
interface Props {
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
theme?: 'primary' | 'light'
|
||||
type?: 'button' | 'text' | 'custom'
|
||||
btnBg?: string
|
||||
fontSize?: number | string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
text: '下一步',
|
||||
disabled: false,
|
||||
theme: 'primary',
|
||||
type: 'button',
|
||||
btnBg: defaultBtnBg,
|
||||
fontSize: '2.4rem',
|
||||
})
|
||||
|
||||
const emits = defineEmits(['click'])
|
||||
const emits = defineEmits<{
|
||||
click: []
|
||||
}>()
|
||||
|
||||
const isCustom = computed(() => props.type === 'custom')
|
||||
|
||||
const customBg = computed(() => `url(${props.btnBg}) no-repeat center / 100% 100%`)
|
||||
|
||||
const fontSizeValue = computed(() => {
|
||||
// 如果已经是带单位的字符串(如 '2.4rem', '30px'),直接返回
|
||||
if (typeof props.fontSize === 'string' && /^[\d.]+(?:px|rem|em)$/.test(props.fontSize)) {
|
||||
return props.fontSize
|
||||
}
|
||||
// 如果是纯数字或数字字符串,添加 px 单位
|
||||
const size = typeof props.fontSize === 'number' ? props.fontSize : Number.parseFloat(props.fontSize)
|
||||
return Number.isNaN(size) ? '2.4rem' : `${size}px`
|
||||
})
|
||||
|
||||
function handleClick() {
|
||||
if (!props.disabled) {
|
||||
emits('click')
|
||||
@ -68,11 +71,13 @@ function handleClick() {
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
user-select: none;
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(1);
|
||||
// filter: grayscale(1);
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
@ -82,7 +87,7 @@ function handleClick() {
|
||||
}
|
||||
}
|
||||
|
||||
// standard: type="button" | type="text"
|
||||
// Standard button styles: type="button" | type="text"
|
||||
.standard-btn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
@ -91,13 +96,13 @@ function handleClick() {
|
||||
background: #eb5e55;
|
||||
box-shadow: 0 8px 0 #fcc64f;
|
||||
|
||||
&:hover {
|
||||
&:hover:not(.is-disabled) {
|
||||
transform: translateY(-2px);
|
||||
filter: brightness(1.05);
|
||||
box-shadow: 0 10px 0 #fcc64f;
|
||||
}
|
||||
|
||||
&:active {
|
||||
&:active:not(.is-disabled) {
|
||||
transform: translateY(4px);
|
||||
box-shadow: 0 2px 0 #fcc64f;
|
||||
}
|
||||
@ -107,7 +112,7 @@ function handleClick() {
|
||||
color: #eb5e55;
|
||||
border: 2px solid #eb5e55;
|
||||
|
||||
&:hover {
|
||||
&:hover:not(.is-disabled) {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
@ -126,35 +131,36 @@ function handleClick() {
|
||||
}
|
||||
}
|
||||
|
||||
// custom: type="custom",背景完全由 btnBg 决定
|
||||
// Custom button: type="custom" - background controlled by btnBg prop
|
||||
.custom-btn {
|
||||
background: v-bind(customBg);
|
||||
background-color: transparent;
|
||||
min-width: 210px;
|
||||
min-width: 220px;
|
||||
width: auto;
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
box-shadow: none;
|
||||
color: rgb(0, 94, 174);
|
||||
font-weight: bold;
|
||||
|
||||
.btn-text {
|
||||
font-size: v-bind('`${fontSize}px`');
|
||||
font-size: v-bind(fontSizeValue);
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: #eb5e55;
|
||||
filter: none;
|
||||
opacity: 1;
|
||||
color: red;
|
||||
// filter: grayscale(0.5);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover:not(.is-disabled) {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
&:active:not(.is-disabled) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,12 +229,12 @@ watch(titleBgUrl, (newUrl) => {
|
||||
<!-- Header -->
|
||||
<header class="page-header flex items-center px-8 text-[30px] text-[#333333] font-bold">
|
||||
<!-- 左侧logo -->
|
||||
<div class="flex cursor-pointer items-center" @click="handleBackHome">
|
||||
<img :src="cqLogo" alt="重庆logo" class="h-20 w-auto">
|
||||
<div class="!z-index-99 flex cursor-pointer items-center" @click="handleBackHome">
|
||||
<img :src="cqLogo" alt="重庆logo" class="h-18 w-auto">
|
||||
</div>
|
||||
|
||||
<!-- 中间文字 -->
|
||||
<div class="flex flex-1 justify-center">
|
||||
<div class="!z-index-99 flex flex-1 justify-center">
|
||||
<img :src="qwLogo" alt="重庆logo" class="h-14 w-auto">
|
||||
</div>
|
||||
|
||||
@ -249,17 +249,23 @@ watch(titleBgUrl, (newUrl) => {
|
||||
<section v-if="showTitle" class="title-section">
|
||||
<Transition name="scroll-unfold" appear>
|
||||
<div v-if="showContent" class="title-wrapper">
|
||||
<div class="scroll-bg">
|
||||
<div
|
||||
class="scroll-bg"
|
||||
:style="{ width: props.titleBgType === 3 ? '400px' : props.titleBgType === 4 ? '420px' : '480px' }"
|
||||
>
|
||||
<!-- 预渲染所有背景图,用 opacity 切换避免加载抖动 -->
|
||||
<img
|
||||
v-for="(url, _key) in titleBgUrlMapping" :key="_key" :src="url" class="scroll-bg-img" :class="{
|
||||
active: titleBgUrl === url && imageLoadStatus[url] !== false,
|
||||
loading: imageLoadStatus[url] === undefined,
|
||||
}" :style="{ visibility: imageLoadStatus[url] === false ? 'hidden' : 'visible' }" aria-hidden="true"
|
||||
@load="imageLoadStatus[url] = true" @error="imageLoadStatus[url] = false"
|
||||
}" :style="{ visibility: imageLoadStatus[url] === false ? 'hidden' : 'visible' }"
|
||||
aria-hidden="true" @load="imageLoadStatus[url] = true" @error="imageLoadStatus[url] = false"
|
||||
>
|
||||
<Transition name="fade-in-delay" appear>
|
||||
<h1 v-if="showContent" class="main-title" :style="{ color: titleTextColor }">
|
||||
<h1
|
||||
v-if="showContent" class="main-title"
|
||||
:style="{ color: titleTextColor, letterSpacing: props.titleBgType === 2 ? '6px' : props.titleBgType === 4 ? '1px' : '4px', fontSize: props.titleBgType === 4 ? '38px' : '' }"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
</Transition>
|
||||
@ -285,7 +291,7 @@ watch(titleBgUrl, (newUrl) => {
|
||||
class="back-btn" :style="{ visibility: showBack ? 'visible' : 'hidden' }" type="custom"
|
||||
:text="backBtnText || ''" :theme="btnTheme" :font-size="fontSize" @click="handleBack"
|
||||
>
|
||||
<SvgIcon v-if="!backBtnText" icon="mdi:arrow-left" size="40" class="arrow-icon" />
|
||||
<SvgIcon v-if="!backBtnText" icon="mdi:arrow-left" size="42" class="arrow-icon" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
@ -294,14 +300,14 @@ watch(titleBgUrl, (newUrl) => {
|
||||
v-if="showNext" type="custom" :text="nextBtnText || '下一步'" :theme="btnTheme"
|
||||
:disabled="nextDisabled" :font-size="fontSize" @click="handleNext"
|
||||
>
|
||||
<SvgIcon v-if="!nextBtnText" icon="mdi:arrow-right" class="arrow-icon" size="40" />
|
||||
<SvgIcon v-if="!nextBtnText" icon="mdi:arrow-right" class="arrow-icon" size="42" />
|
||||
</ActionButton>
|
||||
|
||||
<ActionButton
|
||||
v-if="showNextBtn2" :type="nextBtnText2 ? 'text' : 'button'" :text="nextBtnText2 || '下一步'"
|
||||
:theme="btnTheme" :disabled="nextDisabled" @click="handleNext2"
|
||||
>
|
||||
<SvgIcon v-if="!nextBtnText2" icon="mdi:arrow-right" class="arrow-icon" size="40" />
|
||||
<SvgIcon v-if="!nextBtnText2" icon="mdi:arrow-right" class="arrow-icon" size="42" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</footer>
|
||||
@ -330,9 +336,9 @@ watch(titleBgUrl, (newUrl) => {
|
||||
}
|
||||
|
||||
.scroll-bg {
|
||||
width: 400px !important;
|
||||
width: 400px;
|
||||
/* Fixed width to prevent squishing during animation */
|
||||
height: 155px !important;
|
||||
height: 180px !important;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -363,10 +369,10 @@ watch(titleBgUrl, (newUrl) => {
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 2.5rem;
|
||||
font-size: 2.66rem;
|
||||
color: $text-color;
|
||||
margin: 0;
|
||||
letter-spacing: 4px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
/* Prevent text wrapping */
|
||||
@ -444,15 +450,15 @@ watch(titleBgUrl, (newUrl) => {
|
||||
|
||||
.page-header {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
top: -58px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 196px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px 40px;
|
||||
padding: 20px 44px;
|
||||
font-weight: 700;
|
||||
color: $secondary-color;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
|
||||
@ -214,7 +214,7 @@ init()
|
||||
<template #prefix>
|
||||
<SvgIcon :icon="tab.icon" :local-icon="tab.localIcon" class="inline-block align-text-bottom text-16px" />
|
||||
</template>
|
||||
<div class="max-w-240px ellipsis-text">
|
||||
<div class="max-w-240px ellipsis-text text-14px">
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</PageTab>
|
||||
|
||||
@ -7,6 +7,7 @@ body,
|
||||
#app {
|
||||
height: 100%;
|
||||
text-autospace: normal;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
html {
|
||||
|
||||
16
apps/admin/src/utils/uuid.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 生成UUID的兼容函数
|
||||
* 优先使用原生 crypto.randomUUID,如果不支持则使用兼容方案
|
||||
*/
|
||||
export function generateUUID(): string {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
|
||||
// 兼容性回退方案 - 生成符合 UUID v4 格式的字符串
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = Math.random() * 16 | 0
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
@ -41,8 +41,10 @@ const modelValue = ref<{
|
||||
|
||||
const { options: roundTypeOptions } = useDict('round_type')
|
||||
const { options: scoreTypeOptions } = useDict('score_type')
|
||||
const { options: timeOptions } = useDict('time_out')
|
||||
// const { options: uiTypeOptions } = useDict('ui_type', 'string')
|
||||
const { options: timeOptionsRaw } = useDict('time_out')
|
||||
const timeOptions = computed(() =>
|
||||
[...timeOptionsRaw.value].sort((a, b) => Number(a.value) - Number(b.value)),
|
||||
)
|
||||
const { options: uiTypeOptions } = useDict('question_category_name', 'string')
|
||||
|
||||
const hookProps = reactive({
|
||||
|
||||
@ -8,7 +8,7 @@ defineOptions({ name: 'QuestionStore' })
|
||||
const currentCategory = ref<any>(null)
|
||||
|
||||
// Resize logic
|
||||
const siderWidth = ref(525)
|
||||
const siderWidth = ref(535)
|
||||
const LOCAL_STORAGE_KEY = 'question-store-sider-width'
|
||||
const minWidth = 300
|
||||
const maxWidth = 800
|
||||
|
||||
@ -231,10 +231,10 @@ onMounted(() => {
|
||||
>
|
||||
<div class="flex items-center gap-2 overflow-hidden">
|
||||
<SvgIcon
|
||||
icon="carbon:folder" class="flex-shrink-0 text-lg"
|
||||
icon="carbon:folder" class="flex-shrink-0 text-sm"
|
||||
:class="selectedKey === item.Id ? 'text-primary' : 'text-gray-400'"
|
||||
/>
|
||||
<span class="truncate font-medium">{{ item.Name }} - {{ item.QuestionContent }}</span>
|
||||
<span class="truncate text-16px font-medium">{{ item.Name }} - {{ item.QuestionContent }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<!-- eslint-disable no-console -->
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { useIntervalFn, useStorage } from '@vueuse/core'
|
||||
import { NButton, NCard, NImage, NInputNumber, NTag, useMessage } from 'naive-ui'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
@ -32,6 +32,10 @@ const currentQuestion = ref<Api.Competition.GetCurrentQuestionInfo>() // 当前
|
||||
const questionInfo = ref<Api.Competition.QuestionListDetailRound>() // 题目详情
|
||||
const currentQuestionDetailID = ref<number | string>('') // 当前选中的题目详情 ID
|
||||
|
||||
const nowTs = ref(Date.now())
|
||||
useIntervalFn(() => {
|
||||
nowTs.value = Date.now()
|
||||
}, 1000)
|
||||
// 判断当前activityId是正在比赛的活动
|
||||
const isActiveActivityId = computed(() => {
|
||||
if (!currentQuestion.value)
|
||||
@ -253,7 +257,9 @@ async function fetchCurrentQuestion(shouldSync = true) {
|
||||
return
|
||||
}
|
||||
currentQuestion.value = data?.data || {}
|
||||
const endTime = new Date(currentQuestion.value.EndTime)
|
||||
console.warn('current question', currentQuestion.value)
|
||||
console.log('当前题目结束时间是:', endTime.toLocaleString())
|
||||
// 只在初始化时同步选中题,轮询时不同步
|
||||
if (shouldSync) {
|
||||
syncCurrentQuestionSelection()
|
||||
@ -264,6 +270,42 @@ async function fetchCurrentQuestion(shouldSync = true) {
|
||||
}
|
||||
}
|
||||
|
||||
const blobSizeCache = new Map<string, number>()
|
||||
const objectUrlCache = new Map<string, string>()
|
||||
|
||||
async function checkAndUpdateImage(uniqueId: string, originalUrl: string, item: any) {
|
||||
try {
|
||||
const timestamp = Date.now()
|
||||
const separator = originalUrl.includes('?') ? '&' : '?'
|
||||
const fetchUrl = `${originalUrl}${separator}_t=${timestamp}`
|
||||
|
||||
const response = await fetch(fetchUrl)
|
||||
if (!response.ok)
|
||||
return
|
||||
const blob = await response.blob()
|
||||
|
||||
const prevSize = blobSizeCache.get(uniqueId)
|
||||
if (prevSize === blob.size) {
|
||||
return // 内容大小未变,认为图片没变,不更新 src,避免闪烁
|
||||
}
|
||||
|
||||
blobSizeCache.set(uniqueId, blob.size)
|
||||
const objectUrl = URL.createObjectURL(blob)
|
||||
|
||||
const oldUrl = objectUrlCache.get(uniqueId)
|
||||
if (oldUrl) {
|
||||
URL.revokeObjectURL(oldUrl)
|
||||
}
|
||||
objectUrlCache.set(uniqueId, objectUrl)
|
||||
|
||||
// 替换为本地 Blob URL,由于数据已在内存中,更新时不会闪烁
|
||||
item.AnswerValuePicture = objectUrl
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Silent image update failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取答题卡数据
|
||||
*/
|
||||
@ -365,13 +407,22 @@ async function fetchAnswerData() {
|
||||
// 强制更新图片 URL,防止缓存
|
||||
// 优先使用 ans 中的图片,如果没有则回退到 answerData 中的图片
|
||||
const imageUrl = ans.AnswerValuePicture || (parsedAnswers.length === 1 && parsedAnswers[0] === ans ? answerData.UserAnswerPicture : '')
|
||||
|
||||
const uniqueId = `${answerData.TeamId}-${ans.Guid}`
|
||||
let finalImageUrl = imageUrl
|
||||
|
||||
if (imageUrl) {
|
||||
if (oldAns && oldAns._originalUrl === imageUrl) {
|
||||
finalImageUrl = oldAns.AnswerValuePicture
|
||||
}
|
||||
else {
|
||||
finalImageUrl = imageUrl
|
||||
}
|
||||
}
|
||||
|
||||
const cleanBaseUrl = ''
|
||||
const timestamp = new Date().getTime()
|
||||
|
||||
finalImageUrl = `${finalImageUrl}?_t=${timestamp}`
|
||||
console.log(finalImageUrl, 'finalImageUrl')
|
||||
|
||||
// 判断是否手动干预:
|
||||
// 1. 本地存储有状态 (hasStoredStatus) 或有分数 (hasStoredScore)
|
||||
// 2. 内存中有手动标记 (oldAns._isManual)
|
||||
@ -401,6 +452,7 @@ async function fetchAnswerData() {
|
||||
QuestionDetailID: answerData.QuestionDetaiID, // [新增] 注意后端字段拼写
|
||||
AnswerText: ans.AnswerText,
|
||||
AnswerValuePicture: finalImageUrl,
|
||||
_originalUrl: imageUrl,
|
||||
_baseImgUrl: cleanBaseUrl,
|
||||
_t: timestamp, // 保存时间戳
|
||||
Score: finalScore,
|
||||
@ -409,17 +461,21 @@ async function fetchAnswerData() {
|
||||
_aiStatus: aiStatus,
|
||||
_aiScore: aiScore,
|
||||
}
|
||||
if (imageUrl) {
|
||||
checkAndUpdateImage(uniqueId, imageUrl, resultItem)
|
||||
}
|
||||
return resultItem
|
||||
}
|
||||
|
||||
// 如果没有手动编辑
|
||||
return {
|
||||
const resultItem = {
|
||||
...ans,
|
||||
TeamId: answerData.TeamId, // [新增] 注入 TeamId
|
||||
QuestionId: answerData.QuestionId, // [新增]
|
||||
QuestionDetailID: answerData.QuestionDetaiID, // [新增] 注意后端字段拼写
|
||||
AnswerText: ans.AnswerText,
|
||||
AnswerValuePicture: finalImageUrl,
|
||||
_originalUrl: imageUrl,
|
||||
_baseImgUrl: cleanBaseUrl,
|
||||
_t: timestamp, // 保存时间戳
|
||||
Status: ans.Status,
|
||||
@ -428,6 +484,10 @@ async function fetchAnswerData() {
|
||||
Score: null, // 默认 null
|
||||
_isManual: false,
|
||||
}
|
||||
if (imageUrl) {
|
||||
checkAndUpdateImage(uniqueId, imageUrl, resultItem)
|
||||
}
|
||||
return resultItem
|
||||
})
|
||||
|
||||
return {
|
||||
@ -469,7 +529,7 @@ async function handleNexCurrentQuestion() {
|
||||
|
||||
function handlePublish() {
|
||||
const endTimeStr = (currentQuestion.value as any)?.EndTime
|
||||
if (endTimeStr && Date.now() < new Date(endTimeStr).getTime() + 5000) {
|
||||
if (endTimeStr && Date.now() < new Date(endTimeStr).getTime() + 1000) {
|
||||
window.$message?.warning('倒计时未结束(需等待5秒延迟),暂不能发布')
|
||||
return
|
||||
}
|
||||
@ -580,13 +640,12 @@ function handlePublish() {
|
||||
* 消费并发布当前题目
|
||||
*/
|
||||
const pollingTimer = ref<NodeJS.Timeout | null>(null)
|
||||
const nowTs = ref(Date.now())
|
||||
const canPublish = computed(() => {
|
||||
const endTime = (currentQuestion.value as any)?.EndTime
|
||||
if (!endTime)
|
||||
return true
|
||||
// 结束时间 + 5秒延迟才能发布
|
||||
const end = new Date(endTime).getTime() + 5000
|
||||
const end = new Date(endTime).getTime() + 2000
|
||||
return nowTs.value >= end
|
||||
})
|
||||
|
||||
@ -596,8 +655,8 @@ function startPolling() {
|
||||
// 检查是否已经结束
|
||||
const endTime = (currentQuestion.value as any)?.EndTime
|
||||
if (endTime) {
|
||||
// 增加 12s 缓冲时间
|
||||
const end = new Date(endTime).getTime() + 12000
|
||||
// 增加 5s 缓冲时间
|
||||
const end = new Date(endTime).getTime() + 3000
|
||||
const now = new Date().getTime()
|
||||
if (now > end) {
|
||||
console.log('当前题目已结束(含12s缓冲),仅获取一次最终结果,不启动轮询', endTime)
|
||||
@ -614,25 +673,23 @@ function startPolling() {
|
||||
}
|
||||
|
||||
fetchAnswerData() // 立即获取
|
||||
nowTs.value = Date.now()
|
||||
pollingTimer.value = setInterval(() => {
|
||||
// 每次轮询前再次检查是否过期
|
||||
const currentEndTime = (currentQuestion.value as any)?.EndTime
|
||||
if (currentEndTime) {
|
||||
// 增加 12s 缓冲时间
|
||||
const endTs = new Date(currentEndTime).getTime() + 12000
|
||||
// 增加 1s 缓冲时间
|
||||
const endTs = new Date(currentEndTime).getTime() + 1000
|
||||
if (Date.now() > endTs) {
|
||||
console.log('题目时间已到(含12s缓冲),停止轮询', currentEndTime)
|
||||
console.log('题目时间已到(含1s缓冲),停止轮询', currentEndTime)
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
}
|
||||
nowTs.value = Date.now()
|
||||
|
||||
// 轮询时同时更新当前题目信息(获取最新的 EndTime),但不触发同步
|
||||
fetchCurrentQuestion(false)
|
||||
fetchAnswerData()
|
||||
}, 3000)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
@ -808,27 +865,29 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- 图片展示 -->
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="relative min-h-30 flex items-center justify-center">
|
||||
<NImage
|
||||
:src="item.AnswerValuePicture || item.imageUrl" object-fit="contain"
|
||||
class="max-h-[300px] w-full"
|
||||
:src="item.AnswerValuePicture || item.imageUrl"
|
||||
object-fit="contain"
|
||||
class="pointer-events-auto max-h-[300px] w-full cursor-pointer"
|
||||
show-toolbar-tooltip
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Button Overlay (Center) -->
|
||||
<div
|
||||
class="absolute inset-0 flex flex-col items-center justify-center gap-2 opacity-0 transition-opacity duration-200 hover:opacity-100"
|
||||
class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-2 opacity-0 transition-opacity duration-200 hover:opacity-100"
|
||||
>
|
||||
<!-- 如果未校准,显示两个按钮 -->
|
||||
<template v-if="!item._isManual">
|
||||
<NButton
|
||||
strong secondary round size="medium" type="success" class="shadow-xl"
|
||||
strong secondary round size="medium" type="success" class="pointer-events-auto shadow-xl"
|
||||
@click="setStatus(team, item, 1)"
|
||||
>
|
||||
确认正确 (绿)
|
||||
</NButton>
|
||||
<NButton
|
||||
strong secondary round size="medium" type="error" class="shadow-xl"
|
||||
strong secondary round size="medium" type="error" class="pointer-events-auto shadow-xl"
|
||||
@click="setStatus(team, item, 0)"
|
||||
>
|
||||
确认错误 (红)
|
||||
@ -839,7 +898,7 @@ onUnmounted(() => {
|
||||
<template v-else>
|
||||
<NButton
|
||||
strong secondary round size="large" :type="item.Status === 1 ? 'error' : 'success'"
|
||||
class="shadow-xl" @click="setStatus(team, item, item.Status === 1 ? 0 : 1)"
|
||||
class="pointer-events-auto shadow-xl" @click="setStatus(team, item, item.Status === 1 ? 0 : 1)"
|
||||
>
|
||||
切换为 {{ item.Status === 1 ? '错误 (红)' : '正确 (绿)' }}
|
||||
</NButton>
|
||||
@ -891,8 +950,10 @@ onUnmounted(() => {
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<NImage
|
||||
v-if="item.AnswerValuePicture || item.imageUrl"
|
||||
:src="item.AnswerValuePicture || item.imageUrl" object-fit="contain"
|
||||
class="max-h-[800px] w-full"
|
||||
:src="item.AnswerValuePicture || item.imageUrl"
|
||||
object-fit="contain"
|
||||
class="pointer-events-auto max-h-[800px] w-full cursor-pointer"
|
||||
show-toolbar-tooltip
|
||||
/>
|
||||
<div v-else class="text-gray-300">
|
||||
<div class="i-carbon-no-image text-6xl" />
|
||||
|
||||
@ -7,6 +7,7 @@ import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchAddTemplate, fetchTemplateDetail } from '@/service/api/template'
|
||||
import { generateUUID } from '@/utils/uuid'
|
||||
import { useUpload } from './hooks/useUpload'
|
||||
import TemplateCanvas from './modules/TemplateCanvas.vue'
|
||||
import TemplateSettings from './modules/TemplateSettings.vue'
|
||||
@ -64,7 +65,7 @@ function handleUploadTrigger() {
|
||||
* 处理区域生成
|
||||
*/
|
||||
function handleGenerate() {
|
||||
const groupId = crypto.randomUUID()
|
||||
const groupId = generateUUID()
|
||||
const newRegions: Region[] = []
|
||||
|
||||
// 计算起始编号:查找现有的最大 Qx 编号
|
||||
@ -85,7 +86,7 @@ function handleGenerate() {
|
||||
for (let c = 0; c < genSettings.value.cols; c++) {
|
||||
const index = maxNum + r * genSettings.value.cols + c + 1 // 计算当前区域的编号
|
||||
newRegions.push({
|
||||
id: crypto.randomUUID(),
|
||||
id: generateUUID(),
|
||||
groupId,
|
||||
x: currentX + c * (genSettings.value.w + genSettings.value.gapX),
|
||||
y: currentY + r * (genSettings.value.h + genSettings.value.gapY),
|
||||
|
||||
@ -66,6 +66,11 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
|
||||
title: '模板ID',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
key: 'PageNo',
|
||||
title: '页码',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
key: 'competitionId',
|
||||
title: '关联比赛',
|
||||
|
||||
@ -117,6 +117,42 @@ async function checkNextStep() {
|
||||
}
|
||||
}
|
||||
|
||||
const blobSizeCache = new Map<string, number>()
|
||||
const objectUrlCache = new Map<string, string>()
|
||||
|
||||
async function checkAndUpdateImage(uniqueId: string, originalUrl: string, team: any) {
|
||||
try {
|
||||
const timestamp = Date.now()
|
||||
const separator = originalUrl.includes('?') ? '&' : '?'
|
||||
const fetchUrl = `${originalUrl}${separator}_t=${timestamp}`
|
||||
|
||||
const response = await fetch(fetchUrl)
|
||||
if (!response.ok)
|
||||
return
|
||||
const blob = await response.blob()
|
||||
|
||||
const prevSize = blobSizeCache.get(uniqueId)
|
||||
if (prevSize === blob.size) {
|
||||
return // 内容大小未变,认为图片没变,不更新 src,避免闪烁
|
||||
}
|
||||
|
||||
blobSizeCache.set(uniqueId, blob.size)
|
||||
const objectUrl = URL.createObjectURL(blob)
|
||||
|
||||
const oldUrl = objectUrlCache.get(uniqueId)
|
||||
if (oldUrl) {
|
||||
URL.revokeObjectURL(oldUrl)
|
||||
}
|
||||
objectUrlCache.set(uniqueId, objectUrl)
|
||||
|
||||
// 替换为本地 Blob URL,由于数据已在内存中,更新时不会闪烁
|
||||
team.UserAnswerPicture = objectUrl
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Silent image update failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取游戏现场统计结果 */
|
||||
async function fetchGetGameStatisticsData() {
|
||||
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
||||
@ -127,7 +163,30 @@ async function fetchGetGameStatisticsData() {
|
||||
QuestionDetaiID: Number(QuestionDetailID.value),
|
||||
}
|
||||
const { data } = await fetchGetGameStatistics(_params)
|
||||
teams.value = (data?.data as any[]) || []
|
||||
const rawTeams = (data?.data as any[]) || []
|
||||
|
||||
teams.value = rawTeams.map((team) => {
|
||||
const uniqueId = team.TeamID || team.TeamName || Math.random().toString()
|
||||
team._uniqueId = uniqueId
|
||||
|
||||
if (team.UserAnswerPicture) {
|
||||
const originalUrl = team.UserAnswerPicture
|
||||
const oldTeam = teams.value.find(t => t._uniqueId === uniqueId)
|
||||
|
||||
if (oldTeam && oldTeam.UserAnswerPicture && oldTeam._originalUrl === originalUrl) {
|
||||
// 保持原有的 URL,避免每次轮询都刷新
|
||||
team.UserAnswerPicture = oldTeam.UserAnswerPicture
|
||||
}
|
||||
else {
|
||||
team.UserAnswerPicture = originalUrl
|
||||
}
|
||||
team._originalUrl = originalUrl
|
||||
|
||||
// 后台静默获取最新图片并对比
|
||||
checkAndUpdateImage(uniqueId, originalUrl, team)
|
||||
}
|
||||
return team
|
||||
})
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('Failed to fetch game statistics', error)
|
||||
@ -150,15 +209,15 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<CompetitionLayout
|
||||
:show-title="true" :show-back="false" :show-next="showNextBtn" title="答题图解" action-position="top"
|
||||
back-btn-text="返回" :next-btn-text="isCompleted ? '该组已完成,回到选组' : '下一题'" btn-theme="light" @back="handleBack"
|
||||
:show-title="true" :show-back="false" :show-next="showNextBtn" :title-bg-type="3" title-text-color="#ffffff" title="答题图解" action-position="top"
|
||||
back-btn-text="返回" :next-btn-text="isCompleted ? '已完成,返选组' : '下一题'" btn-theme="light" @back="handleBack"
|
||||
@next="handleNext"
|
||||
>
|
||||
<div class="relative h-full w-full flex flex-col items-center px-12 pt-10 font-sans">
|
||||
<div class="relative h-full w-full flex flex-col items-center px-10 pt-10px font-sans">
|
||||
<!-- Content -->
|
||||
<div class="w-full flex flex-col flex-1 overflow-hidden">
|
||||
<div class="mb-2 text-xl text-red-600 font-bold">
|
||||
备注:评委正在评分中,请等待。。。
|
||||
备注:评委正在评分中,请等待...
|
||||
</div>
|
||||
|
||||
<div v-if="teams && teams.length > 0" class="grid grid-cols-4 h-full gap-6 pb-8">
|
||||
@ -167,14 +226,17 @@ onBeforeUnmount(() => {
|
||||
class="flex flex-col border-2 border-gray-200 rounded-xl bg-white/90 p-4 shadow-sm"
|
||||
>
|
||||
<div
|
||||
class="mb-3 flex items-center gap-2 rounded-full from-sky-300 to-cyan-200 bg-gradient-to-r px-4 py-2 shadow-sm"
|
||||
class="mb-3 flex items-center justify-between gap-2 rounded-full from-sky-300 to-cyan-200 bg-gradient-to-r px-4 py-2 shadow-sm"
|
||||
>
|
||||
<span class="text-lg text-white font-bold drop-shadow">{{ team.TeamName }}</span>
|
||||
<span class="ml-2 rounded-full bg-white/40 px-2 py-0.5 text-sm">正确 <span class="text-md text-blue">{{
|
||||
team.correctCount }}</span> 字</span>
|
||||
<span class="text-s rounded-full bg-white/40 px-2 py-0.5">积分 <span class="text-md text-blue">{{
|
||||
team.Points
|
||||
}}</span></span>
|
||||
<span class="overflow-hidden text-ellipsis whitespace-nowrap text-lg text-white font-bold drop-shadow">{{ team.TeamName }}</span>
|
||||
<div class="flex shrink-0 gap-2">
|
||||
<span class="whitespace-nowrap rounded-full bg-green-100/90 px-3 py-1.5 text-sm">
|
||||
评委评分 <span class="text-lg text-orange-600 font-bold">{{ team.ResultPotins }}</span>
|
||||
</span>
|
||||
<span class="whitespace-nowrap rounded-full bg-orange-100/60 px-3 py-1.5 text-sm">
|
||||
AI评分 <span class="text-md text-orange-600 font-bold">{{ team.Points }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Answer Image Area -->
|
||||
|
||||
@ -1,20 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import checkoutAudio from '@/assets/audio/checkout.mp3'
|
||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||
import { PublishStatus } from '@/enum/business'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchUpdatePublishStatus } from '@/service/api/competition'
|
||||
import { fetchGetQuestionTableByActivityID } from '@/service/api/game'
|
||||
import { useActivityInfoStore } from '@/store/modules/activityinfo'
|
||||
import { AudioController } from '@/utils/audio'
|
||||
import { filterRepeat } from '@/utils/data'
|
||||
|
||||
const activityInfoStore = useActivityInfoStore()
|
||||
const activityInfo = computed(() => activityInfoStore.activityInfo)
|
||||
|
||||
const { routerPushByKey, routerBack } = useRouterPush()
|
||||
const audioController = new AudioController()
|
||||
|
||||
interface QuestionItem {
|
||||
id: number
|
||||
@ -30,12 +29,12 @@ const activityName = computed(() => activityInfo.value?.ActivityTitle || '星·
|
||||
const flippedCards = ref<Set<number>>(new Set())
|
||||
|
||||
// 计算是否所有卡片都已翻转 (当前显示4张卡片)
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
|
||||
const isAllFlipped = computed(() => flippedCards.value.size === questions.value.length)
|
||||
|
||||
// 控制下一步按钮的显示(带延迟)
|
||||
const showNextButton = ref(true)
|
||||
// eslint-disable-next-line prefer-const, unused-imports/no-unused-vars
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// 获取路由参数
|
||||
@ -77,21 +76,21 @@ async function getQuestions() {
|
||||
}
|
||||
}
|
||||
// 测试先注释掉
|
||||
// watch(isAllFlipped, (val) => {
|
||||
// if (timer) {
|
||||
// clearTimeout(timer)
|
||||
// timer = null
|
||||
// }
|
||||
watch(isAllFlipped, (val) => {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
|
||||
// if (val) {
|
||||
// timer = setTimeout(() => {
|
||||
// showNextButton.value = true
|
||||
// }, 1500)
|
||||
// }
|
||||
// else {
|
||||
// showNextButton.value = false
|
||||
// }
|
||||
// })
|
||||
if (val) {
|
||||
timer = setTimeout(() => {
|
||||
showNextButton.value = true
|
||||
}, 1500)
|
||||
}
|
||||
else {
|
||||
showNextButton.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function handleBack() {
|
||||
routerBack()
|
||||
@ -128,7 +127,8 @@ function toggleFlip(index: number) {
|
||||
}
|
||||
else {
|
||||
flippedCards.value.add(index)
|
||||
audioController.play('flip') // 翻牌动效
|
||||
const audio = new Audio(checkoutAudio)
|
||||
audio.play().catch(e => console.error('Play audio failed', e))
|
||||
}
|
||||
// 每次点击都播放翻牌音效(无论翻开还是翻回去)
|
||||
// audioController.play('flip')
|
||||
@ -150,6 +150,16 @@ async function initSyncRouteParams() {
|
||||
|
||||
const showCards = ref(false)
|
||||
|
||||
// 计算每排卡片数量(根据不同屏幕尺寸)
|
||||
// 默认按最小屏幕(2列)计算,如果超过2排则调整padding
|
||||
const needCompactPadding = computed(() => {
|
||||
// 假设最小屏幕每排2个卡片(w-[44%])
|
||||
// const cardsPerRow = 2
|
||||
// const totalRows = Math.ceil(questions.value.length / cardsPerRow)
|
||||
// return totalRows >= 2
|
||||
return questions.value.length > 7 // 如果超过8张卡片就使用紧凑间距
|
||||
})
|
||||
|
||||
// 3D 悬停效果逻辑
|
||||
const cardStyles = ref<Record<number, any>>({})
|
||||
|
||||
@ -196,12 +206,11 @@ onMounted(async () => {
|
||||
<template>
|
||||
<CompetitionLayout
|
||||
:show-back="true" :show-next="showNextButton" next-btn-text="下一步" :title="activityName"
|
||||
:title-bg-type="2" back-btn-text="返回" @back="handleBack"
|
||||
@next="handleNext"
|
||||
:title-bg-type="2" back-btn-text="返回" @back="handleBack" @next="handleNext"
|
||||
>
|
||||
<div class="h-screen w-full overflow-y-auto font-sans">
|
||||
<div class="relative z-10 min-h-full w-full">
|
||||
<div class="min-h-full w-full flex flex-col items-center pb-12 pt-24">
|
||||
<div class="min-h-full w-full flex flex-col items-center pb-12" :class="needCompactPadding ? 'pt-4' : 'pt-24'">
|
||||
<!-- 卡片列表 -->
|
||||
<TransitionGroup
|
||||
tag="div" name="card-anim"
|
||||
@ -231,7 +240,7 @@ onMounted(async () => {
|
||||
<!-- 文字内容 -->
|
||||
<div class="relative z-10 transform text-center">
|
||||
<div
|
||||
class="card-text text-2xl text-[#333333] font-bold leading-tight font-serif drop-shadow-sm"
|
||||
class="card-text text-26px text-[#333333] font-bold leading-tight font-serif drop-shadow-sm"
|
||||
>
|
||||
<span class="block">{{ node.moduleName.slice(0, 2) }}</span>
|
||||
<span class="block">{{ node.moduleName.slice(2) }}</span>
|
||||
@ -279,7 +288,7 @@ onMounted(async () => {
|
||||
// [过冲状态]
|
||||
// 飞到眼前一点点 (Z=50px)
|
||||
// 角度稍微回弹 (rotateY -15deg)
|
||||
transform: translate3d(0, -20px, 50px) rotateY(-15deg) rotateX(-10deg) scale(1.05);
|
||||
transform: translate3d(0, -20px, 50px) rotateY(-15deg) rotateX(-10deg) scale(1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
@ -319,8 +328,8 @@ onMounted(async () => {
|
||||
// [翻转状态]
|
||||
// rotateY(180deg): 绕Y轴旋转180度,实现翻面
|
||||
// scale(1.05): 翻转同时放大1.05倍,产生"向观众逼近"的视觉冲击力
|
||||
transform: rotateY(180deg) scale(1.06);
|
||||
box-shadow: 0 20px 40px -5px rgba(0, 0, 0, 0.3); // [动态阴影] 翻转浮起时阴影更深、更扩散
|
||||
transform: rotateY(180deg) scale(1.08);
|
||||
box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.3); // [动态阴影] 翻转浮起时阴影更深、更扩散
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,7 +344,7 @@ onMounted(async () => {
|
||||
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
// border: 1px solid rgba(255, 255, 255, 0.5); // 增加微妙的边框
|
||||
// border: 1px solid rgba(231, 215, 144, 0.5); // 增加微妙的边框
|
||||
}
|
||||
|
||||
// 正面(实际是卡背图案)
|
||||
@ -358,7 +367,7 @@ onMounted(async () => {
|
||||
// 背面(实际是内容面)
|
||||
.flip-card-back {
|
||||
// background-color: #fff;
|
||||
transform: rotateY(180deg);
|
||||
transform: rotateY(200deg);
|
||||
// border: 4px solid #fff;
|
||||
|
||||
.card-content-design {
|
||||
@ -400,7 +409,7 @@ onMounted(async () => {
|
||||
// animation-delay: 0.2s; 确保卡片翻转到一半(正对屏幕)时才开始闪光,
|
||||
// 模拟光线随着角度变化掠过卡片表面的物理效果
|
||||
animation: shine-sweep 3s ease-out forwards;
|
||||
animation-delay: 0.2s;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
}
|
||||
|
||||
@ -446,14 +455,14 @@ onMounted(async () => {
|
||||
|
||||
10% {
|
||||
// 刚进入时瞬间变亮
|
||||
opacity: 0.8;
|
||||
opacity: 0.8; // 0.8 是一个半透明值,模拟光束的透明度,值越大越透明
|
||||
}
|
||||
|
||||
100% {
|
||||
// 扫出视野
|
||||
top: 50%;
|
||||
left: 150%;
|
||||
opacity: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -143,7 +143,7 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4)
|
||||
@next="handleCardClick(question as Api.Competition.CurrentQuestionResponse)" @back="handleBack"
|
||||
>
|
||||
<div class="h-full w-full flex flex-col items-center pt-10 font-sans">
|
||||
<div v-if="question" class="mb-10 max-w-4xl text-center">
|
||||
<div v-if="question" class="mb-10 max-w-85% text-center">
|
||||
<div class="text-4xl text-gray-800 font-medium leading-relaxed tracking-wide">
|
||||
{{ questionTitle || '' }}
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import start3Audio from '@/assets/audio/timeout3.mp3'
|
||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchGetGameStatistics, fetchGetQuestionDetail, fetchSubmitQuestionResult } from '@/service/api/game'
|
||||
@ -26,6 +27,9 @@ const loading = ref(false)
|
||||
|
||||
// 本地倒计时控制(为了在页面上显示“开始答题”还是倒计时)
|
||||
const isStarted = ref(false)
|
||||
const isPreparing = ref(false) // 是否正在准备中(321倒计时)
|
||||
const showCountdown = ref(false) // 控制倒计时弹窗显示
|
||||
const countdownNum = ref(3) // 倒计时数字
|
||||
const pollingTimer = ref<number | null>(null) // 轮询定时器
|
||||
|
||||
console.log(activityInfo.value, 'activityInfo.value')
|
||||
@ -130,19 +134,10 @@ async function fetchGetQuestionDetailData() {
|
||||
|
||||
async function handleNext() {
|
||||
// 点击开始答题
|
||||
if (!isStarted.value) {
|
||||
isStarted.value = true
|
||||
if (!isStarted.value && !isPreparing.value) {
|
||||
isPreparing.value = true
|
||||
|
||||
// 获取题目时间,优先从 currentQuestionInfo 获取,兼容大小写
|
||||
// 并在获取失败时尝试从 detail 获取(如果后端在详情接口也返回了时间)
|
||||
const infoTime = store.currentQuestionInfo?.QuestionTime || (store.currentQuestionInfo as any)?.questionTime
|
||||
const detailTime = (store.currentQuestionDetail as any)?.QuestionTime || (store.currentQuestionDetail as any)?.questionTime
|
||||
const duration = Number(infoTime || detailTime || 120)
|
||||
|
||||
console.log('开始倒计时,时长:', duration, 'InfoTime:', infoTime, 'DetailTime:', detailTime)
|
||||
store.startTimer(duration)
|
||||
|
||||
// 提交题目使用记录
|
||||
// 提交题目使用记录 (调用开始接口)
|
||||
try {
|
||||
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
||||
const params: Api.Competition.QuestionAddParams = {
|
||||
@ -153,19 +148,45 @@ async function handleNext() {
|
||||
QuestionDetaiID: store.currentQuestionDetail.Id || 0, // 详情 ID
|
||||
TeamGroupQuestionID: store.currentQuestionMainInfo?.TeamGroup_QuestionID || 0, // 题目在分组中的 ID
|
||||
}
|
||||
console.log(params, 'params')
|
||||
await fetchSubmitQuestionResult(params)
|
||||
// 立即拉取一次,然后开启轮询
|
||||
await fetchGetGameStatisticsData(params)
|
||||
startPolling(params)
|
||||
await fetchSubmitQuestionResult(params) // 提交题目使用记录
|
||||
|
||||
// 调用接口成功后,显示 321 倒计时弹窗并播放音乐
|
||||
showCountdown.value = true
|
||||
countdownNum.value = 3
|
||||
|
||||
const audio = new Audio(start3Audio)
|
||||
audio.play().catch(e => console.error('Play audio failed', e))
|
||||
|
||||
const timer = setInterval(() => {
|
||||
countdownNum.value--
|
||||
if (countdownNum.value === 0) {
|
||||
clearInterval(timer)
|
||||
showCountdown.value = false
|
||||
isPreparing.value = false
|
||||
isStarted.value = true
|
||||
|
||||
// 并在获取失败时尝试从 detail 获取(如果后端在详情接口也返回了时间)
|
||||
const infoTime = store.currentQuestionInfo?.QuestionTime || (store.currentQuestionInfo as any)?.questionTime
|
||||
const detailTime = (store.currentQuestionDetail as any)?.QuestionTime || (store.currentQuestionDetail as any)?.questionTime
|
||||
const duration = Number(infoTime || detailTime || 120)
|
||||
|
||||
console.log('开始倒计时,时长:', duration, 'InfoTime:', infoTime, 'DetailTime:', detailTime)
|
||||
store.startTimer(duration)
|
||||
|
||||
// 立即拉取一次,然后开启轮询
|
||||
fetchGetGameStatisticsData(params)
|
||||
startPolling(params)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to submit question result', error)
|
||||
isPreparing.value = false
|
||||
// 不阻塞流程,仅记录错误
|
||||
}
|
||||
}
|
||||
else {
|
||||
else if (isStarted.value) {
|
||||
// 备用逻辑:如果已经在答题中(理论上不会触发,因为上面已经跳转),直接跳转
|
||||
store.stopTimer()
|
||||
stopPolling()
|
||||
@ -255,8 +276,8 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
|
||||
<CompetitionLayout
|
||||
action-position="top" :show-back="false" :show-next="true" :title="mainTitle || ''"
|
||||
:title-bg-type="titleBgTypeNum" title-text-color="#ffffff" back-btn-text="返回"
|
||||
:next-btn-text="isStarted ? formattedTime : '开始答题'" :next-disabled="isStarted" btn-theme="light" @back="handleBack"
|
||||
@next="handleNext"
|
||||
:next-btn-text="(isStarted || isPreparing) ? (isPreparing ? '准备中...' : formattedTime) : '开始答题'"
|
||||
:next-disabled="isStarted || isPreparing" btn-theme="light" @back="handleBack" @next="handleNext"
|
||||
>
|
||||
<div class="relative h-full w-full flex flex-col items-center overflow-hidden px-12 font-sans">
|
||||
<!-- 题目说明 -->
|
||||
@ -270,6 +291,18 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 321 倒计时遮罩层 -->
|
||||
<div
|
||||
v-if="showCountdown"
|
||||
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||
>
|
||||
<Transition name="scale" mode="out-in">
|
||||
<div :key="countdownNum" class="text-[300px] color-#dd0315 font-bold">
|
||||
{{ countdownNum }}
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的 Next 按钮 (方便演示:右上角小区域) -->
|
||||
<div class="absolute right-0 top-0 z-50 h-20 w-20 cursor-pointer" title="Next Step (Debug)" @click="handleNext">
|
||||
<div class="h-full w-full flex items-center justify-center">
|
||||
@ -284,12 +317,11 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
|
||||
class="relative z-20 h-64 max-w-5xl w-full flex flex-col shrink-0 border-2 border-blue-300 rounded-xl bg-white/90 p-4 shadow-lg backdrop-blur"
|
||||
>
|
||||
<!-- 图表标题栏 -->
|
||||
<div v-if="isStarted" class="mb-4 flex items-center justify-between px-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-3 w-3 rotate-45 bg-orange-400" />
|
||||
<span class="text-lg text-blue-800 font-bold">答题进度</span>
|
||||
<span class="text-md text-gray-400">(仅为ai判定,不代表最终结果)</span>
|
||||
</div>
|
||||
<div v-if="isStarted" class="z-index-999 relative mb-4">
|
||||
<img
|
||||
src="@/assets/imgs/user/rank-bg-kuang.png" alt="答题进度"
|
||||
class="z-index-999 absolute left--30px top--25px h-auto max-h-16 w-auto"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 柱状图(ECharts) -->
|
||||
@ -307,4 +339,16 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
|
||||
/* red-500 */
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 321 倒计时动画 */
|
||||
.scale-enter-active,
|
||||
.scale-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.scale-enter-from,
|
||||
.scale-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -40,11 +40,17 @@ function buildOption(list: BarDatum[]): ECOption {
|
||||
const correct = list.map(i => i.correct)
|
||||
|
||||
return {
|
||||
// 禁用动画,避免频繁刷新时的抖动
|
||||
// 启用动画,让柱子变化更平滑
|
||||
animation: false,
|
||||
animationDuration: 800,
|
||||
animationEasing: 'linear', // 动画缓动函数,平滑过渡,cubicOut|linear|quadOut|quadIn
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: '#3b82f6',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#1e3a8a' },
|
||||
},
|
||||
legend: {
|
||||
data: ['答题数量', '预估得分'],
|
||||
@ -52,17 +58,18 @@ function buildOption(list: BarDatum[]): ECOption {
|
||||
right: 0,
|
||||
icon: 'circle',
|
||||
textStyle: {
|
||||
color: '#666',
|
||||
color: '#64748b',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
grid: { left: 16, right: 16, top: 40, bottom: 0, containLabel: true },
|
||||
grid: { left: 16, right: 16, top: 70, bottom: 0, containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisTick: { show: false }, // 隐藏 x 轴刻度线
|
||||
axisLine: { show: false }, // 隐藏 x 轴线
|
||||
axisLabel: {
|
||||
color: '#003366', // 深蓝色字体
|
||||
color: '#1e3a8a', // 深蓝色字体
|
||||
fontWeight: 'bold',
|
||||
fontSize: 14,
|
||||
margin: 16,
|
||||
@ -82,6 +89,7 @@ function buildOption(list: BarDatum[]): ECOption {
|
||||
type: 'bar',
|
||||
data: total,
|
||||
barWidth: 24,
|
||||
barGap: '40%', // 柱子间的间隙
|
||||
itemStyle: {
|
||||
// 从上到下:浅蓝 -> 深蓝
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
@ -89,14 +97,17 @@ function buildOption(list: BarDatum[]): ECOption {
|
||||
{ offset: 1, color: '#3b82f6' }, // blue-500 (底部深色)
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
shadowBlur: 10,
|
||||
shadowColor: 'rgba(59, 130, 246, 0.2)',
|
||||
shadowOffsetY: 4,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
color: '#003366', // 深蓝色
|
||||
color: '#1e3a8a', // 深蓝色
|
||||
fontWeight: 'bold',
|
||||
fontSize: 18,
|
||||
offset: [0, 2],
|
||||
fontSize: 16,
|
||||
offset: [0, -4],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -111,14 +122,17 @@ function buildOption(list: BarDatum[]): ECOption {
|
||||
{ offset: 1, color: '#f97316' }, // orange-500 (底部深色)
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
shadowBlur: 10,
|
||||
shadowColor: 'rgba(249, 115, 22, 0.2)',
|
||||
shadowOffsetY: 4,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
color: '#ea580c', // orange-600
|
||||
fontWeight: 'bold',
|
||||
fontSize: 18,
|
||||
offset: [0, 2],
|
||||
fontSize: 16,
|
||||
offset: [0, -4],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -24,6 +24,35 @@ const dictationParts = computed(() => {
|
||||
return []
|
||||
})
|
||||
|
||||
// 动态计算第二个框的样式(根据文字长度)
|
||||
const secondPartStyle = computed(() => {
|
||||
if (dictationParts.value.length < 2)
|
||||
return { fontSize: 'text-5xl', letterSpacing: 'tracking-widest' }
|
||||
|
||||
const secondPart = dictationParts.value[1]
|
||||
const length = secondPart.length
|
||||
|
||||
// 根据文字长度返回不同的样式配置
|
||||
if (length <= 16) {
|
||||
return {
|
||||
fontSize: 'text-5xl', // 51.2px - 保持大字体
|
||||
letterSpacing: 'tracking-widest', // 0.4em - 正常字间距
|
||||
}
|
||||
}
|
||||
else if (length <= 20) {
|
||||
return {
|
||||
fontSize: 'text-4xl', // 40px - 稍微缩小
|
||||
letterSpacing: 'tracking-wide', // 0.2em - 缩小字间距
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
fontSize: 'text-3xl', // 33.6px - 更小一点
|
||||
letterSpacing: 'tracking-normal', // 0.05em - 最小字间距
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 联想对对碰:上方词组
|
||||
const associationWords = computed(() => {
|
||||
const name = currentQuestionDetail.value?.Name || ''
|
||||
@ -71,13 +100,18 @@ watch(
|
||||
<!-- 模板A: 汉字听写1-提示 (拼音+提示) -->
|
||||
<div v-if="template === QuestionCategoryEnum.ChineseCharacterDictation1" class="text-center">
|
||||
<div class="mb-6 inline-block text-5xl font-bold leading-none">
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<div class="flex items-center justify-start gap-4">
|
||||
<!-- 第一个框:拼音 -->
|
||||
<div class="short-content-bg pinyin-text text-6xl">
|
||||
{{ dictationParts[0] }}
|
||||
</div>
|
||||
|
||||
<!-- 第二个框:意思提示 -->
|
||||
<div
|
||||
v-for="(part, index) in dictationParts" :key="`part-${index}`"
|
||||
class="text-4xl"
|
||||
:class="index === 0 ? 'short-content-bg' : 'content-with-bg'"
|
||||
v-if="dictationParts.length > 1" class="content-with-bg pinyin-text !max-w-85%"
|
||||
:class="[secondPartStyle.fontSize, secondPartStyle.letterSpacing]"
|
||||
>
|
||||
{{ part }}
|
||||
{{ dictationParts[1] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -88,8 +122,11 @@ watch(
|
||||
v-else-if="template === QuestionCategoryEnum.ChineseCharacterDictation2 || template === QuestionCategoryEnum.WordDictation"
|
||||
class="flex flex-col items-center"
|
||||
>
|
||||
<div class="mb-1 text-3xl text-gray-800 font-bold leading-relaxed">
|
||||
<div class="rich-content content-with-bg ml-3 mt-4 flex text-6xl !px-16 !py-10 !tracking-[0.1em]">
|
||||
<div class="mb-2 text-center text-2xl">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="mb-1 text-gray-800 font-bold leading-relaxed">
|
||||
<div class="rich-content content-with-bg pinyin-text ml-3 mt-4 flex text-7xl !max-w-100% !px-16 !py-4 !tracking-[0.08em]">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
@ -98,8 +135,8 @@ watch(
|
||||
<!-- 模板C: 汉字加一加 (提示+部件) -->
|
||||
<div v-else-if="template === QuestionCategoryEnum.CharacterRadicalAddition" class="text-center">
|
||||
<div class="mb-6 inline-block text-5xl font-bold leading-none">
|
||||
<div class="rich-content ml-3 mt-4 flex gap-1 text-5xl tracking-[0.1em]">
|
||||
请写出含有“ <span class="text-color-red !text-6xl">{{ content }}</span>”的汉字,书写时间为60秒。
|
||||
<div class="rich-content ml-3 mt-4 flex gap-1 text-5xl tracking-[0.06em]">
|
||||
请写出含有“ <span class="text-color-red !text-5xl">{{ content }}</span>”的汉字,书写时间为60秒。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -107,7 +144,7 @@ watch(
|
||||
<!-- 模板D: 成语写一写1 (拼音+提示) -->
|
||||
<div v-else-if="template === QuestionCategoryEnum.IdiomWriting1" class="flex flex-col items-center">
|
||||
<div class="mb-1 text-gray-800 font-bold leading-relaxed">
|
||||
<div class="rich-content ml-3 mt-4 flex gap-1 text-4xl">
|
||||
<div class="rich-content ml-3 mt-4 flex gap-1 text-5xl">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
@ -115,8 +152,8 @@ watch(
|
||||
|
||||
<!-- 模板E: 成语-文字要求 -->
|
||||
<div v-else-if="template === QuestionCategoryEnum.IdiomWriting2" class="text-center">
|
||||
<div class="mb-8 text-4xl text-gray-800 font-bold">
|
||||
<NImage :src="ImageUrl" preview-disabled width="300" object-fit="contain" />
|
||||
<div class="mb-2">
|
||||
<img :src="ImageUrl" preview-disabled class="max-h-368px max-w-700px" object-fit="cover">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -136,8 +173,8 @@ watch(
|
||||
|
||||
<!-- 模板: 场景猜诗句 -->
|
||||
<div v-else-if="template === QuestionCategoryEnum.SceneBasedPoemGuessing" class="w-full flex flex-col items-center">
|
||||
<div class="mb-1 p-16 text-3xl text-gray-800 font-bold leading-relaxed line-height-20">
|
||||
<div class="rich-content" v-html="content" />
|
||||
<div class="mb-1px p-16 text-3xl text-gray-800 font-bold leading-relaxed line-height-16">
|
||||
<div class="rich-content indent-[2em]" v-html="content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -148,7 +185,7 @@ watch(
|
||||
>
|
||||
<!-- 上方:词组 -->
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
<div v-for="(word, index) in associationWords" :key="`word-${index}`" class="association-word text-bold">
|
||||
<div v-for="(word, index) in associationWords" :key="`word-${index}`" class="association-word text-bold text-center text-3xl">
|
||||
{{ word }}
|
||||
</div>
|
||||
</div>
|
||||
@ -173,14 +210,14 @@ watch(
|
||||
v-else-if="template === QuestionCategoryEnum.CharacterWordFlowerDrinkingGame"
|
||||
class="w-full flex flex-col items-center"
|
||||
>
|
||||
<div class="options-container text-5xl">
|
||||
<div class="options-container text-6xl">
|
||||
请回答带有“{{ content }}”的诗词。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模板: 诗词常识 -->
|
||||
<!-- 模板: 加时赛诗词常识 -->
|
||||
<div v-else-if="template === QuestionCategoryEnum.PoetryCommonKnowledge" class="w-full flex flex-col items-center">
|
||||
<div class="mb-1 p-16 text-3xl text-gray-800 font-bold leading-relaxed line-height-20">
|
||||
<div class="line-height mb-1 p-16 text-3xl text-gray-800 font-bold leading-relaxed">
|
||||
<div class="rich-content" v-html="content" />
|
||||
</div>
|
||||
</div>
|
||||
@ -408,7 +445,7 @@ watch(
|
||||
:deep(p),
|
||||
:deep(span),
|
||||
:deep(div) {
|
||||
font-size: 2rem; /* text-5xl */
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -416,30 +453,54 @@ watch(
|
||||
background: url('@/assets/imgs/user/content-bg.png') no-repeat center;
|
||||
background-size: 100% 100%;
|
||||
border-radius: 12px;
|
||||
padding: 20px 30px;
|
||||
letter-spacing: 0.4em;
|
||||
padding: 30px 50px;
|
||||
/* 优化长文本显示 */
|
||||
max-width: 80%;
|
||||
/* 限制最大宽度 */
|
||||
min-height: 120px;
|
||||
/* 最小高度,与 short-content-bg 保持一致 */
|
||||
word-break: keep-all;
|
||||
/* 避免中文单词被截断 */
|
||||
white-space: normal;
|
||||
/* 允许换行 */
|
||||
line-height: 1.5;
|
||||
/* 行高,让换行更美观 */
|
||||
display: inline-flex;
|
||||
/* 使用 flex 布局 */
|
||||
align-items: center;
|
||||
/* 垂直居中 */
|
||||
justify-content: center;
|
||||
/* 水平居中 */
|
||||
text-align: center;
|
||||
/* 文字居中 */
|
||||
}
|
||||
|
||||
.short-content-bg {
|
||||
background: url('@/assets/imgs/user/short-content-bg.png') no-repeat center;
|
||||
background-size: 100% 100%;
|
||||
border-radius: 12px;
|
||||
padding: 20px 30px;
|
||||
letter-spacing: 0.1em;
|
||||
padding: 20px 80px;
|
||||
min-height: 120px;
|
||||
/* 最小高度,与 content-with-bg 保持一致 */
|
||||
display: inline-flex;
|
||||
/* 使用 flex 布局 */
|
||||
align-items: center;
|
||||
/* 垂直居中 */
|
||||
justify-content: center;
|
||||
/* 水平居中 */
|
||||
}
|
||||
|
||||
.association-word {
|
||||
background: url('@/assets/imgs/user/dui-kuang.png') no-repeat center;
|
||||
background-size: 100% 100%;
|
||||
min-width: 160px;
|
||||
min-width: 170px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
padding: 0 20px;
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
|
||||
.association-char {
|
||||
@ -453,4 +514,35 @@ watch(
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 拼音字体样式 - 支持声调符号 */
|
||||
.pinyin-text {
|
||||
/* 字体回退顺序:
|
||||
1. GB Pinyinok-B - 专业拼音字体
|
||||
2. Microsoft YaHei - 微软雅黑(Windows 系统自带,支持拼音)
|
||||
3. PingFang SC - 苹方(macOS 系统自带,支持拼音)
|
||||
4. STHeiti - 华文黑体(支持拼音)
|
||||
5. sans-serif - 系统默认无衬线字体
|
||||
*/
|
||||
font-family: 'GB Pinyinok-B', 'Microsoft YaHei', 'PingFang SC', 'STHeiti', 'SimHei', sans-serif;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.08em;
|
||||
/* 确保拼音声调符号不被截断 */
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 如果需要加载自定义拼音字体 */
|
||||
@font-face {
|
||||
font-family: 'GB Pinyinok-B';
|
||||
/* 优先使用本地已安装的字体 */
|
||||
src: local('GB Pinyinok-B'), local('GBPinyinok-B'), local('GB Pinyinok B');
|
||||
/* 如果本地没有,则加载项目中的字体文件(需要先添加字体文件)*/
|
||||
/* src: url('@/assets/fonts/GBPinyinok-B.woff2') format('woff2'),
|
||||
url('@/assets/fonts/GBPinyinok-B.woff') format('woff'),
|
||||
url('@/assets/fonts/GBPinyinok-B.ttf') format('truetype'); */
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
/* 字体加载时先显示系统字体,加载完成后切换 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -3,7 +3,7 @@ import type { DataTableColumns } from 'naive-ui'
|
||||
import { NDataTable, NImage, NTag, useMessage } from 'naive-ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import title from '@/assets/imgs/user/rank-title.svg'
|
||||
import title from '@/assets/imgs/user/rank-title.png'
|
||||
import ActionButton from '@/components/custom/user/ActionButton.vue'
|
||||
import { fetchUserRankList } from '@/service/api/rank'
|
||||
import ExtraTimeModal from './components/ExtraTimeModal.vue'
|
||||
@ -57,6 +57,23 @@ const columns = ref<DataTableColumns<TeamData>>([
|
||||
|
||||
const tableData = ref<TeamData[]>([])
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
const mockUserRankList: Api.Rank.ActivityRankResList = Array.from({ length: 13 }, (_, teamIndex) => {
|
||||
// eslint-disable-next-line ts/no-shadow
|
||||
const questions = Array.from({ length: 10 }, (_, questionIndex) => {
|
||||
const QuestionNO = questionIndex + 1
|
||||
const ResultPotin = Math.max(0, 10 - teamIndex - (questionIndex % 3))
|
||||
return { QuestionNO, ResultPotin }
|
||||
})
|
||||
|
||||
return {
|
||||
TeamID: 1001 + teamIndex,
|
||||
Name: `测试队伍${teamIndex + 1}`,
|
||||
TotalPoint: questions.reduce((sum, item) => sum + item.ResultPotin, 0),
|
||||
Questions: questions,
|
||||
}
|
||||
})
|
||||
|
||||
async function getUserRankList() {
|
||||
const { data, error } = await fetchUserRankList(MainID, RoundType)
|
||||
if (error) {
|
||||
@ -64,9 +81,11 @@ async function getUserRankList() {
|
||||
return
|
||||
}
|
||||
|
||||
if (data?.data && data.data.length > 0) {
|
||||
const rankSource = data?.data || []
|
||||
|
||||
if (rankSource.length > 0) {
|
||||
// 动态生成题目列
|
||||
const firstTeam = data.data[0]
|
||||
const firstTeam = rankSource[0]
|
||||
const questionColumns = firstTeam.Questions.map(q => ({
|
||||
title: `第${q.QuestionNO}题`,
|
||||
key: `q${q.QuestionNO}`,
|
||||
@ -94,12 +113,12 @@ async function getUserRankList() {
|
||||
return <span>{rank}</span>
|
||||
},
|
||||
},
|
||||
{ title: '队伍名称', key: 'teamName', align: 'left', width: 150, className: 'team-name' },
|
||||
{ title: '队伍名称', key: 'teamName', align: 'left', width: 190, className: 'team-name' },
|
||||
...questionColumns,
|
||||
{ title: '总积分', key: 'total', align: 'center', className: 'total-score' },
|
||||
]
|
||||
|
||||
tableData.value = data.data.map((item, index) => {
|
||||
tableData.value = rankSource.map((item, index) => {
|
||||
const questions: Record<string, number> = {}
|
||||
item.Questions.forEach((q) => {
|
||||
questions[`q${q.QuestionNO}`] = q.ResultPotin
|
||||
@ -114,7 +133,7 @@ async function getUserRankList() {
|
||||
} as TeamData
|
||||
})
|
||||
|
||||
teamOptions.value = data.data.map(item => ({ label: item.Name, value: item.TeamID }))
|
||||
teamOptions.value = rankSource.map(item => ({ label: item.Name, value: item.TeamID }))
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,21 +156,21 @@ function rowClassName(row: TeamData) {
|
||||
|
||||
<template>
|
||||
<CompetitionLayout :show-back="false" :show-next="false" :show-title="false">
|
||||
<div class="leaderboard-container">
|
||||
<div class="leaderboard-header">
|
||||
<NImage :src="title" alt="logo" preview-disabled class="mt--60px" object-fit="contain" />
|
||||
<p class="mt--20px text-center text-lg text-#6b778d">
|
||||
<div class="leaderboard-container h-100%">
|
||||
<div class="leaderboard-header !z-index-99">
|
||||
<NImage :src="title" alt="logo" preview-disabled class="mt--42px" object-fit="contain" />
|
||||
<!-- <p class="mt--16px text-center text-18px text-#6b778d">
|
||||
"勤学如春起之苗,不见其增,日有所长"
|
||||
</p>
|
||||
</p> -->
|
||||
<ActionButton
|
||||
v-if="hasExtraTimeBtn" class="position-absolute bottom-20px right-20px" type="text" text="加时赛"
|
||||
@click="handleAddTime"
|
||||
v-if="hasExtraTimeBtn" class="!z-index-100 position-absolute right-20px top-20px" type="text"
|
||||
text="加时赛" @click="handleAddTime"
|
||||
/>
|
||||
</div>
|
||||
<div class="leaderboard-content">
|
||||
<NDataTable
|
||||
:columns="columns" :data="tableData" :bordered="false" :bottom-bordered="false" size="large"
|
||||
:row-class-name="rowClassName" class="rank-table"
|
||||
:columns="columns" :data="tableData" :bordered="false" max-height="100%" :bottom-bordered="false"
|
||||
size="large" :row-class-name="rowClassName" class="rank-table !h-100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -163,7 +182,7 @@ function rowClassName(row: TeamData) {
|
||||
/* 1. 整体容器和背景 */
|
||||
.leaderboard-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
background-image: url('https://raw.githubusercontent.com/0x3f3f3f3f/figure-bed/main/img/20240524150352.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
@ -193,19 +212,12 @@ function rowClassName(row: TeamData) {
|
||||
}
|
||||
|
||||
.leaderboard-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: #1a2a44;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* .leaderboard-header p {
|
||||
font-size: 1rem;
|
||||
color: #6b778d;
|
||||
margin-top: 8px;
|
||||
} */
|
||||
|
||||
/* 3. 表格内容区 */
|
||||
.leaderboard-content {
|
||||
width: 100%;
|
||||
/* max-width: 1200px; */
|
||||
@ -213,6 +225,7 @@ function rowClassName(row: TeamData) {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(26, 42, 68, 0.05);
|
||||
overflow: hidden;
|
||||
height: calc(100% - 220px);
|
||||
}
|
||||
|
||||
/* 4. Naive UI 表格深度定制 */
|
||||
@ -221,7 +234,7 @@ function rowClassName(row: TeamData) {
|
||||
}
|
||||
|
||||
:deep(.rank-table .n-data-table-th) {
|
||||
font-size: 14px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
border-bottom: 2px solid #dee2e6 !important;
|
||||
@ -236,11 +249,11 @@ function rowClassName(row: TeamData) {
|
||||
}
|
||||
|
||||
:deep(.rank-table .n-data-table-td) {
|
||||
font-size: 15px;
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e9ecef !important;
|
||||
padding-top: 16px !important;
|
||||
padding-bottom: 16px !important;
|
||||
/* padding-top: 16px !important; */
|
||||
/* padding-bottom: 16px !important; */
|
||||
}
|
||||
|
||||
/* 奇偶行交替色 */
|
||||
@ -256,7 +269,7 @@ function rowClassName(row: TeamData) {
|
||||
|
||||
:deep(.total-score) {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
font-size: 2.1rem;
|
||||
color: #e67e22;
|
||||
}
|
||||
|
||||
@ -284,4 +297,11 @@ function rowClassName(row: TeamData) {
|
||||
:deep(.rank-top-3 .total-score) {
|
||||
color: #cd7f32;
|
||||
}
|
||||
|
||||
/* 强制表格容器占满父级 */
|
||||
.leaderboard-content :deep(.n-data-table) {
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -49,7 +49,7 @@ function handleNext() {
|
||||
|
||||
<template>
|
||||
<CompetitionLayout
|
||||
:show-back="true" back-btn-text="返回" :show-next="true" :title="activityName" next-btn-text="下一步"
|
||||
:show-back="true" :title-bg-type="2" back-btn-text="返回" :show-next="true" :title="activityName" next-btn-text="下一步"
|
||||
@back="handleBack" @next="handleNext"
|
||||
>
|
||||
<div class="items-flex-start h-full w-full flex justify-center font-sans">
|
||||
|
||||
@ -87,7 +87,10 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CompetitionLayout :show-back="true" back-btn-text="返回" :show-next="true" next-btn-text="下一步" :show-title="false" @next="handleNext" @back="handleBack">
|
||||
<CompetitionLayout
|
||||
:show-back="true" back-btn-text="返回" :show-next="false" next-btn-text="下一步" :show-title="false"
|
||||
@next="handleNext" @back="handleBack"
|
||||
>
|
||||
<!-- Scroll Content -->
|
||||
<div class="scroll-container">
|
||||
<!-- 卷轴背景动画容器 -->
|
||||
@ -113,7 +116,7 @@ onMounted(() => {
|
||||
<div class="icon-wrapper">
|
||||
<img src="@/assets/imgs/user/team-title-icon.webp" alt="team-icon" class="h-full w-full object-cover">
|
||||
</div>
|
||||
<div class="team-name">
|
||||
<div class="team-name text-2xl">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
@ -181,6 +184,7 @@ onMounted(() => {
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
@ -189,7 +193,7 @@ onMounted(() => {
|
||||
.title-section {
|
||||
position: absolute;
|
||||
top: 130px; // 使用固定像素值,确保在卷轴内部
|
||||
left: 40px; // 距离左边缘的固定距离
|
||||
left: 47px; // 距离左边缘的固定距离
|
||||
z-index: 10;
|
||||
|
||||
.scroll-bg {
|
||||
@ -207,8 +211,8 @@ onMounted(() => {
|
||||
// 添加响应式适配
|
||||
@media (max-width: 1920px) {
|
||||
.title-section {
|
||||
top: 70px;
|
||||
left: 50px;
|
||||
top: 140px;
|
||||
left: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -316,14 +320,20 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.team-name {
|
||||
font-size: 1.5rem;
|
||||
// min-width: 130px;
|
||||
// width: 300px;
|
||||
// height: 120px;
|
||||
min-height: 60px;
|
||||
min-width: 250px;
|
||||
line-height: 60px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
||||
background: url('@/assets/imgs/user/team-title-bg.svg') no-repeat center center;
|
||||
background-size: contain;
|
||||
background: url('@/assets/imgs/user/team-title-bg.png') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
padding: 15px 30px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
{
|
||||
"question_bank": [
|
||||
{
|
||||
"QuestionId": 34,
|
||||
"Name": "<p>诗人袁枚写的与<strong>美食</strong>有关的书是( )。</p><p><strong> A.《食经》 B.《食珍录》 C.《红楼梦》 D.《随园食单》</strong></p>",
|
||||
"Answer": "D",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
},
|
||||
{
|
||||
"QuestionId": 34,
|
||||
"Name": "<p>“我家洗砚池头树”引用了一位<strong>书法家</strong>的典故,这位书法家是( )。</p><p><strong> A.王献之 B.王羲之 C.王冕 D.柳公权</strong></p>",
|
||||
"Answer": "B",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
},
|
||||
{
|
||||
"QuestionId": 34,
|
||||
"Name": "<p>“诗中有画,画中有诗”评价的是( )。</p><p><strong> A.杜牧的诗画风格</strong></p><p><strong> B.刘禹锡的诗画风格</strong></p><p><strong> C.苏轼的诗画风格</strong></p><p><strong> D.王维的诗画风格</strong></p>",
|
||||
"Answer": "D",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
},
|
||||
{
|
||||
"QuestionId": 34,
|
||||
"Name": "<p>被闻一多称为“<strong>诗中的诗</strong>,顶峰上的顶峰”的诗歌作品是( )。</p><p><strong> A.刘希夷的《代悲白头翁》</strong></p><p><strong> B.杨广的《春江花月夜》</strong></p><p><strong> C.张若虚的《春江花月夜》</strong></p><p><strong> D.李白的《蜀道难》</strong></p>",
|
||||
"Answer": "C",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
},
|
||||
{
|
||||
"QuestionId": 34,
|
||||
"Name": "<p>“<strong>梅妻鹤子</strong>”指的是( )。</p><p><strong> A.林逋 B.梅尧臣 C.苏舜钦 D.王安石</strong></p>",
|
||||
"Answer": "A",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
},
|
||||
{
|
||||
"QuestionId": 34,
|
||||
"Name": "<p>“一门父子三词客,千古文章八大家。”这副对联中提到的“<strong>三父子</strong>”是( )。</p><p><strong> A.曹操、曹丕、曹植</strong></p><p><strong> B.苏洵、苏轼、苏辙</strong></p><p><strong> C.班彪、班固、班超</strong></p><p><strong> D.阮瑀、阮籍、阮咸</strong></p>",
|
||||
"Answer": "B",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
},
|
||||
{
|
||||
"QuestionId": 34,
|
||||
"Name": "<p>诗人刘禹锡被<strong>称</strong>为( )。</p><p><strong> A.“诗鬼” B.“诗豪” C.“诗圣” D.“诗仙”</strong></p>",
|
||||
"Answer": "B",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
},
|
||||
{
|
||||
"QuestionId": 34,
|
||||
"Name": "<p>有关诗人卢钺的信息,<strong>不正确</strong>的一项是( )。</p><p><strong> A.别号梅坡</strong></p><p><strong> B.代表作《雪梅》</strong></p><p><strong> C.与辛派词人陈亮是好友</strong></p><p><strong> D.宋朝人</strong></p>",
|
||||
"Answer": "C",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
},
|
||||
{
|
||||
"QuestionId": 34,
|
||||
"Name": "<p>诗人王昌龄被<strong>称</strong>为( )。</p><p><strong> A.“五言圣手” B.“七绝圣手” C.“七言圣手” D.“七绝诗人”</strong></p>",
|
||||
"Answer": "B",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
},
|
||||
{
|
||||
"QuestionId": 34,
|
||||
"Name": "<p>李商隐与杜牧<strong>合称</strong>( )。</p><p><strong> A.“李杜” B.“杜李” C.“小李杜” D.“唐诗双杰”</strong></p>",
|
||||
"Answer": "C",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
},
|
||||
{
|
||||
"QuestionId": 34,
|
||||
"Name": "<p>“南朝四百八十寺”中,“<strong>南朝</strong>”指的是( )。</p><p><strong> A.在南方建都的王朝</strong></p><p><strong> B.宋、齐、魏、陈四个朝代的总称</strong></p><p><strong> C.宋、齐、梁、陈四个朝代的总称</strong></p><p><strong> D.在南京建都的王朝</strong></p>",
|
||||
"Answer": "C",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
},
|
||||
{
|
||||
"QuestionId": 34,
|
||||
"Name": "<p>“<strong>初唐四杰</strong>”是指( )。</p><p><strong> A.张若虚、张旭、孔融和贺知章</strong></p><p><strong> B.王勃、杨炯、卢照邻和骆宾王</strong></p><p><strong> C.高适、王昌龄、岑参和王之涣</strong></p><p><strong> D.贺知章、张若虚、张旭和包融</strong></p>",
|
||||
"Answer": "B",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
"question_bank": [
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "表示团结的四字成语",
|
||||
"Name": "请写出表示团结的四字成语",
|
||||
"Answer": "万众一心、众志成城、精诚团结、风雨同舟、患难与共、群策群力、和衷共济、同舟共济、齐心协力、守望相助……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
@ -10,7 +10,7 @@
|
||||
},
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "含有“足”的四字成语",
|
||||
"Name": "请写出含有“足”的四字成语",
|
||||
"Answer": "美中不足、捷足先登、足不出户、酒足饭饱、无足轻重、心满意足、画蛇添足、手足无措、足智多谋、不足挂齿……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
@ -18,7 +18,7 @@
|
||||
},
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "含有“语”和“言”两个字的四字成语",
|
||||
"Name": "请写出含有“语”和“言”两个字的四字成语",
|
||||
"Answer": "不言不语、只言片语、三言两语、千言万语、豪言壮语、少言寡语、自言自语、甜言蜜语、花言巧语、风言风语……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
@ -26,15 +26,15 @@
|
||||
},
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "表示关系好的四字成语",
|
||||
"Answer": "形影不离、情同手足、亲密无间、莫逆之交、刎颈之交、肝胆照照、八拜之交、情深似海、如胶似漆、深情厚谊……",
|
||||
"Name": "请写出表示关系好的四字成语",
|
||||
"Answer": "形影不离、情同手足、亲密无间、莫逆之交、刎颈之交、肝胆相照、八拜之交、情深似海、如胶似漆、深情厚谊……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
"ImageUrl": ""
|
||||
},
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "含有“马”的四字成语",
|
||||
"Name": "请写出含有“马”的四字成语",
|
||||
"Answer": "害群之马、车水马龙、万马齐喑、兵荒马乱、马马虎虎、人仰马翻、走马观花、马到成功、千军万马、单枪匹马……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
@ -42,7 +42,7 @@
|
||||
},
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "含有“安”的四字成语",
|
||||
"Name": "请写出含有“安”的四字成语",
|
||||
"Answer": "随遇而安、安居乐业、安身立命、一路平安、国泰民安、惴惴不安、局促不安、坐卧不安、忐忑不安、安然无恙……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
@ -50,7 +50,7 @@
|
||||
},
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "表示学习刻苦的四字成语",
|
||||
"Name": "请写出表示学习刻苦的四字成语",
|
||||
"Answer": "废寝忘食、勤学苦练、闻鸡起舞、牛角挂书、凿壁偷光、焚膏继晷、囊萤映雪、韦编三绝、孜孜不倦、悬梁刺股……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
@ -58,7 +58,7 @@
|
||||
},
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "表示看的四字成语",
|
||||
"Name": "请写出表示看的四字成语",
|
||||
"Answer": "东张西望、察言观色、走马观花、左顾右盼、侧目而视、目不转睛、极目远眺、目不暇接、一目十行、探头探脑……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
@ -66,7 +66,7 @@
|
||||
},
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "含有两种颜色的四字成语",
|
||||
"Name": "请写出含有两种颜色的四字成语",
|
||||
"Answer": "姹紫嫣红、万紫千红、金碧辉煌、桃红柳绿、黑白分明、青山绿水、唇红齿白、青出于蓝、青黄不接、白纸黑字……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
@ -74,7 +74,7 @@
|
||||
},
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "表示珍贵的四字成语",
|
||||
"Name": "请写出表示珍贵的四字成语",
|
||||
"Answer": "无价之宝、奇珍异宝、凤毛麟角、绝无仅有、价值连城、和璧隋珠、金玉良言、沧海遗珠、千金难买、吉光片羽……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
@ -82,7 +82,7 @@
|
||||
},
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "含有“星”的四字成语",
|
||||
"Name": "请写出含有“星”的四字成语",
|
||||
"Answer": "星星点点、众星拱月、月明星稀、吉星高照、披星戴月、星光灿烂、星罗棋布、星月交辉、斗转星移、大步流星……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
@ -90,7 +90,7 @@
|
||||
},
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "含有“风”和“雨”两个字的四字成语",
|
||||
"Name": "请写出含有“风”和“雨”两个字的四字成语",
|
||||
"Answer": "风雨同舟、春风化雨、和风细雨、呼风唤雨、狂风暴雨、血雨腥风、凄风苦雨、风雨无阻、风雨交加、风吹雨打……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
@ -98,7 +98,7 @@
|
||||
},
|
||||
{
|
||||
"QuestionId": 5,
|
||||
"Name": "表示不安的四字成语",
|
||||
"Name": "请写出表示不安的四字成语",
|
||||
"Answer": "心惊肉跳、忐忑不安、坐立不安、惊慌失措、胆战心惊、诚惶诚恐、惊弓之鸟、七上八下、如坐针毡、提心吊胆……",
|
||||
"Type": "text",
|
||||
"IsGood": 0,
|
||||
|
||||
@ -1,254 +1,254 @@
|
||||
{
|
||||
"question_bank": [
|
||||
{
|
||||
"name": "重阳、习俗、思念",
|
||||
"answer": "遥知兄弟登高处,遍插茱萸少一人。",
|
||||
"imageUrl": "",
|
||||
"Name": "重阳、习俗、思念",
|
||||
"Answer": "遥知兄弟登高处,遍插茱萸少一人。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "处",
|
||||
"KeyWordsIndex": 6,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "边塞、征战、思乡",
|
||||
"answer": "秦时明月汉时关,万里长征人未还。",
|
||||
"imageUrl": "",
|
||||
"Name": "边塞、征战、思乡",
|
||||
"Answer": "秦时明月汉时关,万里长征人未还。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "关",
|
||||
"KeyWordsIndex": 6,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "军中、饮酒、欢快",
|
||||
"answer": "葡萄美酒夜光杯,欲饮琵琶马上催。",
|
||||
"imageUrl": "",
|
||||
"Name": "军中、饮酒、欢快",
|
||||
"Answer": "葡萄美酒夜光杯,欲饮琵琶马上催。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "催",
|
||||
"KeyWordsIndex": 13,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "初夏、田园、蔬果",
|
||||
"answer": "梅子金黄杏子肥,麦花雪白菜花稀。",
|
||||
"imageUrl": "",
|
||||
"Name": "初夏、田园、蔬果",
|
||||
"Answer": "梅子金黄杏子肥,麦花雪白菜花稀。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "肥",
|
||||
"KeyWordsIndex": 6,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "童趣、追跑、田野",
|
||||
"answer": "儿童急走追黄蝶,飞入菜花无处寻。",
|
||||
"imageUrl": "",
|
||||
"Name": "童趣、追跑、田野",
|
||||
"Answer": "儿童急走追黄蝶,飞入菜花无处寻。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "蝶",
|
||||
"KeyWordsIndex": 6,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "送别、赤诚、朋友",
|
||||
"answer": "洛阳亲友如相问,一片冰心在玉壶。",
|
||||
"imageUrl": "",
|
||||
"Name": "送别、赤诚、朋友",
|
||||
"Answer": "洛阳亲友如相问,一片冰心在玉壶。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "心",
|
||||
"KeyWordsIndex": 10,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "梅花、香气、傲骨",
|
||||
"answer": "不要人夸好颜色,只留清气满乾坤。",
|
||||
"imageUrl": "",
|
||||
"Name": "梅花、香气、傲骨",
|
||||
"Answer": "不要人夸好颜色,只留清气满乾坤。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "好",
|
||||
"KeyWordsIndex": 4,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "遗愿、爱国、失地",
|
||||
"answer": "死去元知万事空,但悲不见九州同。",
|
||||
"imageUrl": "",
|
||||
"Name": "遗愿、爱国、失地",
|
||||
"Answer": "死去元知万事空,但悲不见九州同。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "悲",
|
||||
"KeyWordsIndex": 8,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "水滨、笙乐、忧愁",
|
||||
"answer": "山外青山楼外楼,西湖歌舞几时休?",
|
||||
"imageUrl": "",
|
||||
"Name": "水滨、笙乐、忧愁",
|
||||
"Answer": "山外青山楼外楼,西湖歌舞几时休?",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "休",
|
||||
"KeyWordsIndex": 13,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "祈求、变革、贤能",
|
||||
"answer": "我劝天公重抖擞,不拘一格降人材。",
|
||||
"imageUrl": "",
|
||||
"Name": "祈求、变革、贤能",
|
||||
"Answer": "我劝天公重抖擞,不拘一格降人材。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "公",
|
||||
"KeyWordsIndex": 3,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "深夜、羁客、寒冷",
|
||||
"answer": "月落乌啼霜满天,江枫渔火对愁眠。",
|
||||
"imageUrl": "",
|
||||
"Name": "深夜、羁客、寒冷",
|
||||
"Answer": "月落乌啼霜满天,江枫渔火对愁眠。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "愁",
|
||||
"KeyWordsIndex": 12,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "登科、飞驰、盛景",
|
||||
"answer": "春风得意马蹄疾,一日看尽长安花。",
|
||||
"imageUrl": "",
|
||||
"Name": "登科、飞驰、盛景",
|
||||
"Answer": "春风得意马蹄疾,一日看尽长安花。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "花",
|
||||
"KeyWordsIndex": 13,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "耕地、纺织、协作",
|
||||
"answer": "昼出耘田夜绩麻,村庄儿女各当家。",
|
||||
"imageUrl": "",
|
||||
"Name": "耕地、纺织、协作",
|
||||
"Answer": "昼出耘田夜绩麻,村庄儿女各当家。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "村",
|
||||
"KeyWordsIndex": 7,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "悠闲、放牧、吹奏",
|
||||
"answer": "牧童归去横牛背,短笛无腔信口吹。",
|
||||
"imageUrl": "",
|
||||
"Name": "悠闲、放牧、吹奏",
|
||||
"Answer": "牧童归去横牛背,短笛无腔信口吹。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "背",
|
||||
"KeyWordsIndex": 6,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "童趣、玩耍、敲击",
|
||||
"answer": "稚子金盆脱晓冰,彩丝穿取当银钲。",
|
||||
"imageUrl": "",
|
||||
"Name": "童趣、玩耍、敲击",
|
||||
"Answer": "稚子金盆脱晓冰,彩丝穿取当银钲。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "脱",
|
||||
"KeyWordsIndex": 4,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "欣喜、回家、畅饮",
|
||||
"answer": "白日放歌须纵酒,青春作伴好还乡。",
|
||||
"imageUrl": "",
|
||||
"Name": "欣喜、回家、畅饮",
|
||||
"Answer": "白日放歌须纵酒,青春作伴好还乡。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "须",
|
||||
"KeyWordsIndex": 4,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "雄师、豪迈、跋涉",
|
||||
"answer": "红军不怕远征难,万水千山只等闲。",
|
||||
"imageUrl": "",
|
||||
"Name": "雄师、豪迈、跋涉",
|
||||
"Answer": "红军不怕远征难,万水千山只等闲。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "怕",
|
||||
"KeyWordsIndex": 3,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "中秋、怀人、月亮",
|
||||
"answer": "今夜月明人尽望,不知秋思落谁家。",
|
||||
"imageUrl": "",
|
||||
"Name": "中秋、怀人、月亮",
|
||||
"Answer": "今夜月明人尽望,不知秋思落谁家。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "思",
|
||||
"KeyWordsIndex": 10,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "无畏、高尚、咏物",
|
||||
"answer": "粉骨碎身浑不怕,要留清白在人间。",
|
||||
"imageUrl": "",
|
||||
"Name": "无畏、高尚、咏物",
|
||||
"Answer": "粉骨碎身浑不怕,要留清白在人间。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "浑",
|
||||
"KeyWordsIndex": 4,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "坚韧、勇气、咏物",
|
||||
"answer": "咬定青山不放松,立根原在破岩中。",
|
||||
"imageUrl": "",
|
||||
"Name": "坚韧、勇气、咏物",
|
||||
"Answer": "咬定青山不放松,立根原在破岩中。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "根",
|
||||
"KeyWordsIndex": 8,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "送别、边塞、畅饮",
|
||||
"answer": "劝君更尽一杯酒,西出阳关无故人。",
|
||||
"imageUrl": "",
|
||||
"Name": "送别、边塞、畅饮",
|
||||
"Answer": "劝君更尽一杯酒,西出阳关无故人。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "关",
|
||||
"KeyWordsIndex": 10,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "细雨、嫩绿、朦胧",
|
||||
"answer": "天街小雨润如酥,草色遥看近却无。",
|
||||
"imageUrl": "",
|
||||
"Name": "细雨、嫩绿、朦胧",
|
||||
"Answer": "天街小雨润如酥,草色遥看近却无。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "街",
|
||||
"KeyWordsIndex": 1,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "哭泣、沦陷、期盼",
|
||||
"answer": "遗民泪尽胡尘里,南望王师又一年。",
|
||||
"imageUrl": "",
|
||||
"Name": "哭泣、沦陷、期盼",
|
||||
"Answer": "遗民泪尽胡尘里,南望王师又一年。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "师",
|
||||
"KeyWordsIndex": 10,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "酒菜、好客、乡村",
|
||||
"answer": "莫笑农家腊酒浑,丰年留客足鸡豚。",
|
||||
"imageUrl": "",
|
||||
"Name": "酒菜、好客、乡村",
|
||||
"Answer": "莫笑农家腊酒浑,丰年留客足鸡豚。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "足",
|
||||
"KeyWordsIndex": 11,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
},
|
||||
{
|
||||
"name": "初夏、喧闹、江南",
|
||||
"answer": "黄梅时节家家雨,青草池塘处处蛙。",
|
||||
"imageUrl": "",
|
||||
"Name": "初夏、喧闹、江南",
|
||||
"Answer": "黄梅时节家家雨,青草池塘处处蛙。",
|
||||
"ImageUrl": "",
|
||||
"IsGood": 0,
|
||||
"IsDisabled": false,
|
||||
"KeyWord": "池",
|
||||
"KeyWordsIndex": 9,
|
||||
"questionId": 32
|
||||
"QuestionId": 32
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -15,11 +15,27 @@ export default defineConfig<Theme>({
|
||||
theme: {
|
||||
...themeVars,
|
||||
fontSize: {
|
||||
// 图标字体大小
|
||||
'icon-xs': '0.875rem',
|
||||
'icon-small': '1rem',
|
||||
'icon': '1.125rem',
|
||||
'icon-large': '1.5rem',
|
||||
'icon-xl': '2rem',
|
||||
|
||||
// 针对 2048x1080 设计基准优化的字体大小
|
||||
'xs': '0.875rem', // 原 0.75rem
|
||||
'sm': '1rem', // 原 0.875rem
|
||||
'base': '1.125rem', // 原 1rem
|
||||
'lg': '1.25rem', // 原 1.125rem
|
||||
'xl': '1.5rem', // 原 1.25rem
|
||||
'2xl': '1.875rem', // 原 1.5rem
|
||||
'3xl': '2.25rem', // 原 1.875rem
|
||||
'4xl': '2.75rem', // 原 2.25rem
|
||||
'5xl': '3.5rem', // 原 3rem
|
||||
'6xl': '4.5rem', // 原 3.75rem
|
||||
'7xl': '5.5rem', // 原 4.5rem
|
||||
'8xl': '7rem', // 原 6rem
|
||||
'9xl': '9rem', // 原 8rem
|
||||
},
|
||||
},
|
||||
shortcuts: {
|
||||
|
||||