Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe4d8f6d62 | ||
|
|
1c8035d935 | ||
|
|
2fad025496 | ||
|
|
02b4720b34 | ||
|
|
8964fcdea7 | ||
|
|
771fb807eb | ||
|
|
782c61a38c | ||
|
|
40a6850c3f | ||
|
|
9d801ba6b4 | ||
|
|
11f448cedc | ||
|
|
41f7ba7b8a | ||
|
|
b9c5886437 | ||
|
|
11fb5481d3 |
@@ -7,6 +7,10 @@ dist-ssr
|
||||
apps/*/dist
|
||||
packages/*/dist
|
||||
|
||||
# --- 分析报告文件 ---
|
||||
report.html
|
||||
apps/*/report.html
|
||||
|
||||
# --- 日志文件 ---
|
||||
logs
|
||||
*.log
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
"totvsLanguageServer.welcomePage": false,
|
||||
"cSpell.words": [
|
||||
"activityinfo",
|
||||
"Echarts"
|
||||
] // `
|
||||
"Echarts",
|
||||
"Typeit"
|
||||
],
|
||||
"kiroAgent.configureMCP": "Disabled" // `
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 应用的基础URL,默认为 "/"
|
||||
# 如果使用子目录,必须以 "/" 结尾,例如 "/admin/" 而不是 "/admin"
|
||||
# VITE_BASE_URL='/admin'
|
||||
VITE_BASE_URL='/'
|
||||
|
||||
# VITE_BASE_URL="https://api.qyzhjy.com"
|
||||
|
||||
@@ -28,7 +28,7 @@ VITE_AUTH_ROUTE_MODE=static
|
||||
VITE_AUTH_ROUTE_FORCE_LOGIN=N
|
||||
|
||||
# 静态认证路由的主页
|
||||
VITE_ROUTE_HOME=home
|
||||
VITE_ROUTE_HOME=user_home
|
||||
|
||||
# 默认菜单图标
|
||||
VITE_MENU_ICON=mdi:menu
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
# VITE_SERVICE_BASE_URL=http://192.168.5.140:9999
|
||||
|
||||
# VITE_SERVICE_BASE_URL=https://api.qyzhjy.com
|
||||
# VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com
|
||||
|
||||
VITE_SERVICE_BASE_URL=https://172.16.10.130:5000
|
||||
VITE_SERVICE_BASE_URL=http://172.16.10.130:5000
|
||||
|
||||
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||
"demo": "http://localhost:9528"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# backend service base url, prod environment
|
||||
VITE_SERVICE_BASE_URL=https://mock.apifox.cn/m1/3109515-0-default
|
||||
VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com
|
||||
|
||||
# other backend service base url, prod environment
|
||||
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||
|
||||
@@ -6,7 +6,9 @@ export function getBuildTime() {
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
const buildTime = dayjs.tz(Date.now(), 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss')
|
||||
// 添加随机毫秒确保每次构建时间唯一
|
||||
const now = Date.now()
|
||||
const buildTime = dayjs.tz(now, 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||
|
||||
return buildTime
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { PluginOption } from 'vite'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
|
||||
export function setupAnalyzer() {
|
||||
return visualizer({
|
||||
open: true,
|
||||
gzipSize: true,
|
||||
brotliSize: true,
|
||||
filename: 'report.html',
|
||||
}) as PluginOption
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import viteCompression from 'vite-plugin-compression'
|
||||
|
||||
export function setupCompression() {
|
||||
return viteCompression({
|
||||
verbose: true,
|
||||
disable: false,
|
||||
threshold: 10240,
|
||||
algorithm: 'gzip',
|
||||
ext: '.gz',
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { viteAutoDeployPlugin } from 'vite-auto-deploy'
|
||||
import { name } from '../../package.json'
|
||||
|
||||
export function setupDeploy(isBuild: boolean) {
|
||||
return viteAutoDeployPlugin({
|
||||
/** 上传地址 */
|
||||
uploadUrl: 'http://47.109.17.238:8200/api/upload/code/deploy',
|
||||
/** 项目名称 (如果多个项目都是用的同上传地址时,服务端可用此字段区分是哪一个项目) */
|
||||
projectKey: name,
|
||||
/** 上传完成后,是否保留压缩包到本地 */
|
||||
saveLocalZip: true,
|
||||
/** 是否开启自动部署 */
|
||||
autoDeploy: isBuild,
|
||||
headers: {
|
||||
'Custom-Timestamp': new Date().getTime().toString(),
|
||||
'Custom-Run-Platform': 'app',
|
||||
'Custom-Device-Id': Math.random().toString(36).substring(2),
|
||||
'Custom-Os-Name': 'windows',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||
|
||||
export function setupImageOptimizer() {
|
||||
return ViteImageOptimizer({
|
||||
logStats: true,
|
||||
|
||||
// SVG 优化配置
|
||||
svg: {
|
||||
multipass: true,
|
||||
plugins: [
|
||||
{
|
||||
name: 'preset-default',
|
||||
params: {
|
||||
overrides: {
|
||||
removeViewBox: false,
|
||||
cleanupIds: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
'sortAttrs',
|
||||
],
|
||||
},
|
||||
|
||||
// PNG 高质量压缩
|
||||
png: {
|
||||
quality: 90,
|
||||
compressionLevel: 9,
|
||||
},
|
||||
|
||||
// JPEG 优化
|
||||
jpeg: {
|
||||
quality: 85,
|
||||
mozjpeg: true,
|
||||
},
|
||||
jpg: {
|
||||
quality: 85,
|
||||
mozjpeg: true,
|
||||
},
|
||||
|
||||
// WebP 转换
|
||||
webp: {
|
||||
quality: 80,
|
||||
lossless: false,
|
||||
force: true,
|
||||
},
|
||||
|
||||
includePublic: true,
|
||||
cache: true,
|
||||
cacheLocation: 'node_modules/.cache/image-optimizer',
|
||||
})
|
||||
}
|
||||
@@ -2,14 +2,20 @@ import type { PluginOption } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import progress from 'vite-plugin-progress'
|
||||
import { setupAnalyzer } from './analyzer'
|
||||
import { setupCompression } from './compression'
|
||||
import { setupDeploy } from './deploy'
|
||||
import { setupDevtoolsPlugin } from './devtools'
|
||||
import { setupHtmlPlugin } from './html'
|
||||
import { setupImageOptimizer } from './image-optimizer'
|
||||
import { setupPWA } from './pwa'
|
||||
import { setupElegantRouter } from './router'
|
||||
import { setupUnocss } from './unocss'
|
||||
import { setupUnplugin } from './unplugin'
|
||||
|
||||
export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
|
||||
export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string, isBuild: boolean) {
|
||||
const plugins: PluginOption = [
|
||||
// 基础插件
|
||||
vue(),
|
||||
vueJsx(),
|
||||
setupDevtoolsPlugin(viteEnv),
|
||||
@@ -18,7 +24,18 @@ export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
|
||||
...setupUnplugin(viteEnv),
|
||||
progress(),
|
||||
setupHtmlPlugin(buildTime),
|
||||
|
||||
// 优化插件
|
||||
setupImageOptimizer(),
|
||||
setupCompression(),
|
||||
setupPWA(),
|
||||
setupDeploy(isBuild),
|
||||
]
|
||||
|
||||
// 构建时添加打包分析插件
|
||||
if (isBuild) {
|
||||
plugins.push(setupAnalyzer())
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { PluginOption } from 'vite'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
export function setupPWA(): PluginOption {
|
||||
return VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
// 添加 devOptions 以便在开发环境调试
|
||||
devOptions: {
|
||||
enabled: false, // 开发环境启用
|
||||
type: 'module',
|
||||
},
|
||||
workbox: {
|
||||
// 排除大型 SVG 文件,避免预缓存
|
||||
globIgnores: [
|
||||
'**/team-title-bg*.svg',
|
||||
'**/cover-bg-active*.svg',
|
||||
'**/*-bg-animation*.svg',
|
||||
],
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,webp,jpg,jpeg}', '**/*.svg'],
|
||||
runtimeCaching: [
|
||||
// 字体缓存
|
||||
{
|
||||
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'google-fonts-cache',
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
|
||||
},
|
||||
},
|
||||
},
|
||||
// 大型 SVG 文件运行时缓存
|
||||
{
|
||||
urlPattern: /.*\.(svg)$/,
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'svg-cache',
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days
|
||||
},
|
||||
},
|
||||
},
|
||||
// 图片资源缓存
|
||||
{
|
||||
urlPattern: /.*\.(png|jpg|jpeg|webp|gif)$/,
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'images-cache',
|
||||
expiration: {
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
|
||||
manifest: {
|
||||
name: '阅读之星竞赛系统',
|
||||
short_name: '阅读之星',
|
||||
description: '树人研究院阅读之星竞赛系统',
|
||||
theme_color: '#ffffff',
|
||||
background_color: '#ffffff',
|
||||
display: 'standalone',
|
||||
orientation: 'landscape',
|
||||
icons: [
|
||||
{ src: 'images/pwa_logo_192.png', sizes: '192x192', type: 'image/png' },
|
||||
{ src: 'images/pwa_logo_512.png', sizes: '512x512', type: 'image/png' },
|
||||
{ src: 'images/pwa_logo_512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
||||
{ src: 'images/pwa_logo_512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-cmn-Hans">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# 设置根目录为打包后的 dist 文件夹
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# 开启 gzip 压缩
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
|
||||
gzip_vary on;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
# 核心配置:处理 SPA 路由 fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 静态资源缓存配置(可选)
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
# 代理接口配置(如果需要)
|
||||
# location /api/ {
|
||||
# proxy_pass http://backend-api.com/;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# }
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
"dev": "vite --mode dev",
|
||||
"dev:prod": "vite --mode prod",
|
||||
"gen-route": "sa gen-route",
|
||||
"lint": "eslint . --fix",
|
||||
"lint": "eslint src --fix",
|
||||
"prepare": "simple-git-hooks",
|
||||
"preview": "vite preview",
|
||||
"release": "sa release",
|
||||
@@ -82,6 +82,7 @@
|
||||
"eslint": "9.39.2",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"kolorist": "1.8.0",
|
||||
"rollup-plugin-visualizer": "^7.0.1",
|
||||
"sass": "1.97.1",
|
||||
"simple-git-hooks": "2.13.1",
|
||||
"tsx": "4.21.0",
|
||||
@@ -89,7 +90,11 @@
|
||||
"unplugin-icons": "22.5.0",
|
||||
"unplugin-vue-components": "30.0.0",
|
||||
"vite": "7.3.0",
|
||||
"vite-auto-deploy": "^2.0.34",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-image-optimizer": "^2.0.3",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-plugin-vue-devtools": "8.0.5",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -1,60 +0,0 @@
|
||||
<svg width="100%" height="100%" version="1.1" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<path
|
||||
d="M 200,866 C 100,866 50,779.4 100,692.8 L 200,519.6 C 220,485 240,490 265,499.6 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
|
||||
fill="url(#LinearGradient)" fill-rule="nonzero" opacity="1" stroke="none" />
|
||||
<path
|
||||
d="M 420,866 C 455,861 478,846 500,827 C 614,696 615,597 500,517 C 394,444 333,374 380,207.82 L 260,415.67 C 240.22,450 254.37,465.1 275.28,481.79 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
|
||||
fill="url(#LinearGradient_2)" fill-rule="nonzero" opacity="1" stroke="none" />
|
||||
<path d="M 500,517 C 394,444 333,374 380,207.82 L 400,173.2 C 367,295 421,350 603,428 C 572,440 524,474 500,517"
|
||||
fill="url(#LinearGradient_3)" fill-rule="nonzero" opacity="1" stroke="none" />
|
||||
<path d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 572,440 524,474 500,517 C 615,597 614,696 500,827"
|
||||
fill="url(#LinearGradient_4)" fill-rule="nonzero" opacity="1" stroke="none" />
|
||||
<path d="M 400,173.2 C 367,295 421,350 603,428 C 690,389, 750,445 788,500 L 600,173.2 C 550,86.6 450,86.6 400,173.2"
|
||||
fill="url(#LinearGradient_5)" fill-rule="nonzero" opacity="1" stroke="none" />
|
||||
<path
|
||||
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 690,389, 750,445 788,500 C 816,554 797,606 750,640 L 500,827"
|
||||
fill="url(#LinearGradient_6)" fill-rule="nonzero" opacity="1" stroke="none" />
|
||||
<path
|
||||
d="M 788,500 C 816,554 797,606 750,640 L 500,827 C 497,851 513,862 540,866 L 800,866 C 900,866 950,779.4 900,692.8 L 788,500"
|
||||
fill="url(#LinearGradient_7)" fill-rule="nonzero" opacity="1" stroke="none" />
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient gradientTransform="matrix(104.391 -73.3432 73.3432 104.391 277.441 710.122)"
|
||||
gradientUnits="userSpaceOnUse" id="LinearGradient" x1="0" x2="1" y1="0" y2="0">
|
||||
<stop offset="0" stop-color="#373ebf" />
|
||||
<stop offset="1" stop-color="#5058e6" />
|
||||
</linearGradient>
|
||||
<linearGradient gradientTransform="matrix(-173.747 557.324 -557.324 -173.747 508.829 258.172)"
|
||||
gradientUnits="userSpaceOnUse" id="LinearGradient_2" x1="0" x2="1" y1="0" y2="0">
|
||||
<stop offset="0" stop-color="#c2d6ff" />
|
||||
<stop offset="1" stop-color="#646cff" />
|
||||
</linearGradient>
|
||||
<linearGradient gradientTransform="matrix(157.951 295.666 -295.666 157.951 382.944 193.642)"
|
||||
gradientUnits="userSpaceOnUse" id="LinearGradient_3" x1="0" x2="1" y1="0" y2="0">
|
||||
<stop offset="0" stop-color="#5058e6" />
|
||||
<stop offset="1" stop-color="#373ebf" />
|
||||
</linearGradient>
|
||||
<linearGradient gradientTransform="matrix(-44.3023 219.578 -219.578 -44.3023 619.69 469.652)"
|
||||
gradientUnits="userSpaceOnUse" id="LinearGradient_4" x1="0" x2="1" y1="0" y2="0">
|
||||
<stop offset="0" stop-color="#91a7ff" />
|
||||
<stop offset="1" stop-color="#5058e6" />
|
||||
</linearGradient>
|
||||
<linearGradient gradientTransform="matrix(125.52 334.256 -334.256 125.52 539.723 235.139)"
|
||||
gradientUnits="userSpaceOnUse" id="LinearGradient_5" x1="0" x2="1" y1="0" y2="0">
|
||||
<stop offset="0" stop-color="#646cff" />
|
||||
<stop offset="1" stop-color="#c2d6ff" />
|
||||
</linearGradient>
|
||||
<linearGradient gradientTransform="matrix(-241.23 357.206 -357.206 -241.23 754.054 449.312)"
|
||||
gradientUnits="userSpaceOnUse" id="LinearGradient_6" x1="0" x2="1" y1="0" y2="0">
|
||||
<stop offset="0" stop-color="#c2d6ff" />
|
||||
<stop offset="1" stop-color="#646cff" />
|
||||
</linearGradient>
|
||||
<linearGradient gradientTransform="matrix(125.978 210.065 -210.065 125.978 596.433 613.665)"
|
||||
gradientUnits="userSpaceOnUse" id="LinearGradient_7" x1="0" x2="1" y1="0" y2="0">
|
||||
<stop offset="0" stop-color="#373ebf" />
|
||||
<stop offset="1" stop-color="#5058e6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 578 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 487 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 487 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.8 MiB |
|
After Width: | Height: | Size: 740 KiB |
|
Before Width: | Height: | Size: 7.2 MiB |
|
After Width: | Height: | Size: 445 KiB |
|
After Width: | Height: | Size: 790 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 797 KiB |
|
After Width: | Height: | Size: 811 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 5.2 MiB |
|
After Width: | Height: | Size: 126 KiB |
@@ -10,6 +10,16 @@
|
||||
<rect x="-406.921" y="-212.194" width="1977.2" height="1318.13" fill="url(#pattern0_1099_5207)"/>
|
||||
</g>
|
||||
<g filter="url(#filter0_d_1099_5207)">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="translate"
|
||||
values="0 0; 0 -15; 0 0"
|
||||
dur="4s"
|
||||
repeatCount="indefinite"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 0.5; 1"
|
||||
keySplines="0.45 0 0.55 1; 0.45 0 0.55 1"
|
||||
/>
|
||||
<rect x="-66" y="-25" width="1364.67" height="806" fill="url(#pattern1_1099_5207)" shape-rendering="crispEdges"/>
|
||||
</g>
|
||||
</g>
|
||||
|
Before Width: | Height: | Size: 16 MiB After Width: | Height: | Size: 16 MiB |
|
After Width: | Height: | Size: 907 KiB |
|
After Width: | Height: | Size: 907 KiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 549 KiB |
|
Before Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 449 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 468 KiB After Width: | Height: | Size: 468 KiB |
@@ -17,6 +17,8 @@ interface Props {
|
||||
icon?: string
|
||||
/** 本地 svg 图标名称 */
|
||||
localIcon?: string
|
||||
/** 图标大小 */
|
||||
size?: string | number
|
||||
}
|
||||
|
||||
const attrs = useAttrs()
|
||||
@@ -44,13 +46,13 @@ const renderLocalIcon = computed(() => props.localIcon || !props.icon)
|
||||
<template>
|
||||
<!-- 渲染本地 svg 图标 -->
|
||||
<template v-if="renderLocalIcon">
|
||||
<svg aria-hidden="true" width="1em" height="1em" v-bind="bindAttrs">
|
||||
<svg aria-hidden="true" :width="size || '1em'" :height="size || '1em'" v-bind="bindAttrs">
|
||||
<use :xlink:href="symbolId" fill="currentColor" />
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- 渲染 iconify 图标 -->
|
||||
<Icon v-if="icon" :icon="icon" v-bind="bindAttrs" />
|
||||
<Icon v-if="icon" :icon="icon" :width="size" :height="size" v-bind="bindAttrs" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,44 +2,84 @@
|
||||
import type { Options } from 'typeit'
|
||||
import type { El } from 'typeit/dist/types'
|
||||
import TypeIt from 'typeit'
|
||||
import { onMounted, shallowRef } from 'vue'
|
||||
import { onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'
|
||||
|
||||
defineOptions({ name: 'TypeIt' })
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
speed: 100,
|
||||
loop: false,
|
||||
cursor: true,
|
||||
lifeLike: true,
|
||||
strings: () => [],
|
||||
})
|
||||
|
||||
interface Props {
|
||||
/** 打字速度 */
|
||||
speed?: number
|
||||
/** 是否循环 */
|
||||
loop?: boolean
|
||||
/** 是否显示光标 */
|
||||
cursor?: boolean
|
||||
/** 打字内容 */
|
||||
strings?: string | string[]
|
||||
/** 像人一样打字 */
|
||||
lifeLike?: boolean
|
||||
/** 开始前的延迟 */
|
||||
startDelay?: number
|
||||
/** 下一段文字的延迟 */
|
||||
nextStringDelay?: number
|
||||
/** 循环延迟 */
|
||||
loopDelay?: number
|
||||
/** 兼容旧代码或用户自定义延迟参数 */
|
||||
typeDelay?: number
|
||||
}
|
||||
|
||||
const textRef = shallowRef<El>()
|
||||
const typeItInstance = shallowRef<TypeIt | null>(null)
|
||||
|
||||
function init() {
|
||||
if (!textRef.value)
|
||||
return
|
||||
|
||||
const options: Options = {
|
||||
strings: 'SoybeanAdmin是一个清新优雅、高颜值且功能强大的后台管理模板',
|
||||
lifeLike: true,
|
||||
speed: 120,
|
||||
loop: true,
|
||||
if (typeItInstance.value) {
|
||||
typeItInstance.value.destroy()
|
||||
}
|
||||
|
||||
const initTypeIt = new TypeIt(textRef.value, options)
|
||||
// 清空内容,防止重叠
|
||||
textRef.value.textContent = ''
|
||||
|
||||
initTypeIt.go()
|
||||
const options: Options = {
|
||||
strings: props.strings,
|
||||
lifeLike: props.lifeLike,
|
||||
speed: props.speed,
|
||||
loop: props.loop,
|
||||
cursor: props.cursor,
|
||||
startDelay: props.startDelay,
|
||||
nextStringDelay: props.nextStringDelay ?? props.typeDelay,
|
||||
loopDelay: props.loopDelay,
|
||||
}
|
||||
|
||||
typeItInstance.value = new TypeIt(textRef.value, options).go()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (typeItInstance.value) {
|
||||
typeItInstance.value.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => [props.strings, props.speed, props.loop], () => {
|
||||
init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NCard title="打字机 插件" :bordered="false" class="h-full card-wrapper">
|
||||
<NSpace :vertical="true">
|
||||
<GithubLink link="https://github.com/alexmacarthur/typeit" />
|
||||
<WebSiteLink label="文档地址:" link="https://www.typeitjs.com/docs/vanilla/usage/" />
|
||||
</NSpace>
|
||||
<NDivider title-placement="left">
|
||||
基本示例
|
||||
</NDivider>
|
||||
<span ref="textRef" class="text-18px" />
|
||||
</NCard>
|
||||
</div>
|
||||
<span ref="textRef" class="typeit-container" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['click'])
|
||||
|
||||
const isCustom = computed(() => props.type === 'custom')
|
||||
|
||||
const customBg = computed(() => `url(${props.btnBg}) no-repeat center / 100% 100%`)
|
||||
|
||||
function handleClick() {
|
||||
if (!props.disabled) {
|
||||
emits('click')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="action-btn"
|
||||
:class="[
|
||||
isCustom ? 'custom-btn' : 'standard-btn',
|
||||
theme,
|
||||
type,
|
||||
{ 'is-disabled': disabled },
|
||||
]"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span v-if="type === 'text' || type === 'custom'" class="btn-text">{{ text }}</span>
|
||||
<slot v-else />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(1);
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
transform: none !important;
|
||||
box-shadow: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// standard: type="button" | type="text"
|
||||
.standard-btn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
background: #eb5e55;
|
||||
box-shadow: 0 8px 0 #fcc64f;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
filter: brightness(1.05);
|
||||
box-shadow: 0 10px 0 #fcc64f;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(4px);
|
||||
box-shadow: 0 2px 0 #fcc64f;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background: white;
|
||||
color: #eb5e55;
|
||||
border: 2px solid #eb5e55;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.text {
|
||||
width: auto;
|
||||
min-width: 180px;
|
||||
padding: 0 30px;
|
||||
border-radius: 40px;
|
||||
|
||||
.btn-text {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// custom: type="custom",背景完全由 btnBg 决定
|
||||
.custom-btn {
|
||||
background: v-bind(customBg);
|
||||
background-color: transparent;
|
||||
min-width: 210px;
|
||||
width: auto;
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
box-shadow: none;
|
||||
color: rgb(0, 94, 174);
|
||||
|
||||
.btn-text {
|
||||
font-size: v-bind('`${fontSize}px`');
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: #eb5e55;
|
||||
filter: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,19 @@
|
||||
<!-- eslint-disable unused-imports/no-unused-vars -->
|
||||
<script lang="ts" setup>
|
||||
import VScaleScreen from 'v-scale-screen'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import _defaultBg from '@/assets/imgs/user/home-bg.svg'
|
||||
import titleBg2 from '@/assets/imgs/user/title-bg2.svg'
|
||||
import _defaultTitleBg from '@/assets/imgs/user/title-bg.svg'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import cqLogo from '@/assets/imgs/logo/cq-logo.webp'
|
||||
import srLogo from '@/assets/imgs/logo/qw-logo.webp'
|
||||
import qwLogo from '@/assets/imgs/logo/text-logo.webp'
|
||||
import _defaultBg from '@/assets/imgs/user/home-bg1.webp'
|
||||
import titleBg2 from '@/assets/imgs/user/title-bg2.webp'
|
||||
import titleBg3 from '@/assets/imgs/user/title-bg3.png'
|
||||
import titleBg4 from '@/assets/imgs/user/title-bg4.png'
|
||||
import _defaultTitleBg from '@/assets/imgs/user/title-bg.webp'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
|
||||
import ActionButton from './ActionButton.vue'
|
||||
|
||||
interface Props {
|
||||
/** 是否显示返回按钮 */
|
||||
showBack?: boolean
|
||||
@@ -27,8 +35,16 @@ interface Props {
|
||||
btnTheme?: 'primary' | 'light'
|
||||
/** 是否禁用下一步按钮 */
|
||||
nextDisabled?: boolean
|
||||
/** 标题区域背景图片URL,不传则使用默认 title-bg.svg */
|
||||
/** 标题区域背景图片URL,不传则使用默认 title-bg.webp */
|
||||
titleBgType?: number
|
||||
/** 是否显示第二个下一步按钮 */
|
||||
showNextBtn2?: boolean
|
||||
/** 第二个按钮的文本 */
|
||||
nextBtnText2?: string
|
||||
/** title 颜色 */
|
||||
titleTextColor?: string
|
||||
/* 按钮字体大小 */
|
||||
fontSize?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -43,21 +59,31 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
btnTheme: 'primary',
|
||||
nextDisabled: false,
|
||||
titleBgType: 1,
|
||||
showNextBtn2: false,
|
||||
nextBtnText2: undefined,
|
||||
titleTextColor: '#000000',
|
||||
fontSize: 30,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'back'): void
|
||||
(e: 'next'): void
|
||||
(e: 'next2'): void
|
||||
}>()
|
||||
|
||||
const titleBgUrlMapping: Record<number, string> = {
|
||||
1: _defaultTitleBg,
|
||||
2: titleBg2,
|
||||
3: titleBg3,
|
||||
4: titleBg4,
|
||||
}
|
||||
|
||||
const { routerPushByKey } = useRouterPush()
|
||||
|
||||
const titleBgUrl = computed(() => titleBgUrlMapping[props.titleBgType] || _defaultBg)
|
||||
const titleBgLoaded = ref(false)
|
||||
const currentTitleBg = ref('')
|
||||
const imageLoadStatus = ref<Record<string, boolean>>({})
|
||||
|
||||
// const router = useRouter()
|
||||
const innerBgUrl = ref(_defaultBg)
|
||||
@@ -78,6 +104,8 @@ async function fetchSystemConfig() {
|
||||
}
|
||||
|
||||
const showContent = ref(false)
|
||||
const showSlotContent = ref(false)
|
||||
const showButton = ref(false)
|
||||
|
||||
function handleBack() {
|
||||
emit('back')
|
||||
@@ -89,16 +117,105 @@ function handleNext() {
|
||||
emit('next')
|
||||
}
|
||||
|
||||
function handleNext2() {
|
||||
emit('next2')
|
||||
}
|
||||
|
||||
/** 点击返回首页 */
|
||||
function handleBackHome() {
|
||||
routerPushByKey('user_home')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSystemConfig()
|
||||
// 预加载所有标题背景图,并跟踪加载状态
|
||||
const preloadAllImages = () => {
|
||||
const promises = Object.entries(titleBgUrlMapping).map(([_key, url]) => {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
imageLoadStatus.value[url] = true
|
||||
resolve(true)
|
||||
}
|
||||
img.onerror = () => {
|
||||
imageLoadStatus.value[url] = false
|
||||
resolve(false)
|
||||
}
|
||||
img.src = url
|
||||
})
|
||||
})
|
||||
|
||||
// 也预加载默认背景
|
||||
const defaultBgPromise = new Promise((resolve) => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
imageLoadStatus.value[_defaultBg] = true
|
||||
resolve(true)
|
||||
}
|
||||
img.onerror = () => {
|
||||
imageLoadStatus.value[_defaultBg] = false
|
||||
resolve(false)
|
||||
}
|
||||
img.src = _defaultBg
|
||||
})
|
||||
|
||||
return Promise.all([...promises, defaultBgPromise])
|
||||
}
|
||||
|
||||
// 开始预加载
|
||||
preloadAllImages()
|
||||
.then(() => {
|
||||
// 所有图片预加载完成后,设置当前背景
|
||||
currentTitleBg.value = titleBgUrl.value
|
||||
titleBgLoaded.value = true
|
||||
|
||||
fetchSystemConfig()
|
||||
setTimeout(() => {
|
||||
showContent.value = true
|
||||
}, 100)
|
||||
})
|
||||
.catch(() => {
|
||||
// 即使预加载失败也继续
|
||||
currentTitleBg.value = titleBgUrl.value
|
||||
titleBgLoaded.value = true
|
||||
|
||||
fetchSystemConfig()
|
||||
setTimeout(() => {
|
||||
showContent.value = true
|
||||
}, 100)
|
||||
})
|
||||
|
||||
// 延迟显示内容,等待标题卷轴展开
|
||||
const delay = props.showTitle ? 500 : 100
|
||||
setTimeout(() => {
|
||||
showContent.value = true
|
||||
}, 100)
|
||||
showSlotContent.value = true
|
||||
}, delay)
|
||||
|
||||
// 延迟显示按钮,等待内容显示
|
||||
setTimeout(() => {
|
||||
showButton.value = true
|
||||
}, delay + 500)
|
||||
})
|
||||
|
||||
// 监听 titleBgUrl 变化,确保图片切换平滑
|
||||
watch(titleBgUrl, (newUrl) => {
|
||||
if (newUrl && imageLoadStatus.value[newUrl]) {
|
||||
// 如果图片已经加载过,直接切换
|
||||
currentTitleBg.value = newUrl
|
||||
}
|
||||
else if (newUrl) {
|
||||
// 如果图片还没加载,预加载它
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
imageLoadStatus.value[newUrl] = true
|
||||
currentTitleBg.value = newUrl
|
||||
}
|
||||
img.onerror = () => {
|
||||
imageLoadStatus.value[newUrl] = false
|
||||
// 加载失败时使用默认背景
|
||||
currentTitleBg.value = _defaultBg
|
||||
}
|
||||
img.src = newUrl
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -107,23 +224,45 @@ onMounted(() => {
|
||||
<!-- Background Layer (Outside Scale to cover full screen) -->
|
||||
<div class="fixed-bg" :style="innerBgUrl ? { backgroundImage: `url(${innerBgUrl})` } : {}" />
|
||||
|
||||
<VScaleScreen width="1920" height="1080">
|
||||
<VScaleScreen width="2048" height="1080">
|
||||
<div class="competition-layout">
|
||||
<!-- Header -->
|
||||
<header class="page-header text-[30px] text-[#333333] font-bold">
|
||||
<span class="org-name" @click="handleBackHome">教育学会</span>
|
||||
<span class="org-name">树人研究院</span>
|
||||
<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>
|
||||
|
||||
<!-- 中间文字 -->
|
||||
<div class="flex flex-1 justify-center">
|
||||
<img :src="qwLogo" alt="重庆logo" class="h-14 w-auto">
|
||||
</div>
|
||||
|
||||
<!-- 右侧logo组 -->
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<img :src="srLogo" alt="树人logo" class="h-19 w-auto">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="glass-overlay">
|
||||
<!-- title -->
|
||||
<section v-if="showTitle" class="title-section">
|
||||
<Transition name="fade-slide-down" appear>
|
||||
<div v-if="showContent" class="title-section">
|
||||
<div class="scroll-bg" :style="{ backgroundImage: `url(${titleBgUrl})` }">
|
||||
<h1 class="main-title">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<Transition name="scroll-unfold" appear>
|
||||
<div v-if="showContent" class="title-wrapper">
|
||||
<div class="scroll-bg">
|
||||
<!-- 预渲染所有背景图,用 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"
|
||||
>
|
||||
<Transition name="fade-in-delay" appear>
|
||||
<h1 v-if="showContent" class="main-title" :style="{ color: titleTextColor }">
|
||||
{{ title }}
|
||||
</h1>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -131,29 +270,40 @@ onMounted(() => {
|
||||
|
||||
<!-- Main Content Slot -->
|
||||
<main class="main-content">
|
||||
<slot />
|
||||
<div v-if="showSlotContent" class="h-full w-full flex flex-col items-center justify-center">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer Action -->
|
||||
<footer
|
||||
class="page-footer w-full flex items-center justify-between px-20"
|
||||
v-if="showButton" class="page-footer w-full flex items-center justify-between px-20"
|
||||
:class="[actionPosition, btnTheme]"
|
||||
>
|
||||
<button
|
||||
class="action-btn back-btn" :class="{ 'text-btn': backBtnText }"
|
||||
:style="{ visibility: showBack ? 'visible' : 'hidden' }" @click="handleBack"
|
||||
>
|
||||
<span v-if="backBtnText" class="btn-text">{{ backBtnText }}</span>
|
||||
<SvgIcon v-else icon="mdi:arrow-left" class="arrow-icon" />
|
||||
</button>
|
||||
<div class="left">
|
||||
<ActionButton
|
||||
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" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="showNext" class="action-btn next-btn text-btn" :class="{ 'is-disabled': nextDisabled }"
|
||||
@click="handleNext"
|
||||
>
|
||||
<span class="btn-text">{{ nextBtnText || '下一步' }}</span>
|
||||
<!-- <SvgIcon v-else icon="mdi:arrow-right" class="arrow-icon" /> -->
|
||||
</button>
|
||||
<div class="right">
|
||||
<ActionButton
|
||||
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" />
|
||||
</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" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,24 +316,62 @@ onMounted(() => {
|
||||
margin: 10px auto;
|
||||
width: 412px;
|
||||
height: 165px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* Ensure wrapper stays centered during expansion */
|
||||
|
||||
.scroll-bg {
|
||||
.title-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* Keep the background image centered */
|
||||
}
|
||||
|
||||
.scroll-bg {
|
||||
width: 400px !important;
|
||||
/* Fixed width to prevent squishing during animation */
|
||||
height: 155px !important;
|
||||
position: relative;
|
||||
background: no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 10px; // 微调垂直位置,因为卷轴可能有视觉重心偏移
|
||||
padding-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
/* Prevent shrinking */
|
||||
|
||||
.scroll-bg-img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
pointer-events: none;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
opacity: 0;
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 2.6rem;
|
||||
font-size: 2.5rem;
|
||||
color: $text-color;
|
||||
margin: 0;
|
||||
letter-spacing: 4px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
/* Prevent text wrapping */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,9 +407,11 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
z-index: 1; // 确保内容在背景之上
|
||||
z-index: 2; // 确保内容在背景之上
|
||||
// margin: 10px auto;
|
||||
margin-top: 28px;
|
||||
|
||||
// Default background decoration (Bottom Wave)
|
||||
// 底部波浪背景
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -240,7 +430,7 @@ onMounted(() => {
|
||||
.glass-overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(2px);
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
|
||||
@@ -254,7 +444,7 @@ onMounted(() => {
|
||||
|
||||
.page-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: -20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -280,105 +470,42 @@ onMounted(() => {
|
||||
.page-footer {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
right: 0; // 默认在右下角
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
z-index: 20;
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&.top {
|
||||
bottom: auto;
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.1s ease;
|
||||
background: #eb5e55;
|
||||
box-shadow: 0 8px 0 #fcc64f;
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(4px);
|
||||
box-shadow: 0 2px 0 #fcc64f;
|
||||
}
|
||||
|
||||
// 文字按钮通用样式
|
||||
&.text-btn {
|
||||
width: auto;
|
||||
min-width: 180px;
|
||||
padding: 0 30px;
|
||||
border-radius: 40px;
|
||||
|
||||
.btn-text {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.next-btn {
|
||||
// 保留原有 next-btn 特定逻辑(如果有)
|
||||
}
|
||||
|
||||
&.back-btn {
|
||||
// 保持统一风格
|
||||
}
|
||||
|
||||
// 禁用状态
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
// opacity: 0.8; // 移除透明度变化
|
||||
// filter: grayscale(0.5); // 移除置灰效果
|
||||
// box-shadow: none; // 保持阴影,或者根据需要调整
|
||||
|
||||
&:hover {
|
||||
transform: none; // 禁止 hover 位移
|
||||
// filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: none;
|
||||
// box-shadow: none; // 保持阴影
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Light Theme Style (White bg, Red text/border, Yellow shadow)
|
||||
&.light {
|
||||
.action-btn {
|
||||
background: white;
|
||||
color: #eb5e55;
|
||||
border: 2px solid #eb5e55;
|
||||
box-shadow: 0 8px 0 #fcc64f; // 还原黄色立体阴影
|
||||
|
||||
&:hover {
|
||||
background-color: #fff; // 保持白色
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(4px);
|
||||
box-shadow: 0 2px 0 #fcc64f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transitions
|
||||
.scroll-unfold-enter-active {
|
||||
transition: width 1.5s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
.scroll-unfold-enter-from {
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
.fade-in-delay-enter-active {
|
||||
transition: opacity 0.8s ease;
|
||||
transition-delay: 1s;
|
||||
/* Wait for scroll to unfold */
|
||||
}
|
||||
|
||||
.fade-in-delay-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-slide-down-enter-active,
|
||||
.fade-slide-down-leave-active {
|
||||
transition: all 0.8s ease;
|
||||
|
||||
@@ -43,6 +43,3 @@ const _props = withDefaults(defineProps<Props>(), {
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
@@ -21,11 +21,17 @@ export enum QuestionType {
|
||||
}
|
||||
|
||||
export enum QuestionCategoryEnum {
|
||||
ChineseCharacterDictation1 = 'ChineseCharacterDictation1', // 汉字听写-提示 (jiu表示小鸟的叫声)
|
||||
ChineseCharacterDictation2 = 'ChineseCharacterDictation2', // 根据拼音写汉字-提示 (tang)
|
||||
CharacterRadicalAddition = 'CharacterRadicalAddition', // 汉字加一加 (车)
|
||||
WordDictation = 'WordDictation', // 词语听写 (zhi re)
|
||||
IdiomWriting1 = 'IdiomWriting1', // 成语-文字要求 (反义字)
|
||||
IdiomWriting2 = 'IdiomWriting2', // 成语-文字要求 (看图写成语)
|
||||
PoetryComprehension = 'PoetryComprehension', // 诗词理解-选择题
|
||||
ChineseCharacterDictation1 = 'ChineseCharacterDictation1', // 1. 汉字听写-提示 (jiu表示小鸟的叫声)
|
||||
ChineseCharacterDictation2 = 'ChineseCharacterDictation2', // 2. 根据拼音写汉字-提示 (tang)
|
||||
CharacterRadicalAddition = 'CharacterRadicalAddition', // 3. 汉字加一加 (车)
|
||||
WordDictation = 'WordDictation', // 4. 词语听写 (zhi re)
|
||||
IdiomWriting1 = 'IdiomWriting1', // 5. 成语-文字要求
|
||||
IdiomWriting2 = 'IdiomWriting2', // 6. 成语-文字要求 (看图写成语)
|
||||
PoetryComprehension = 'PoetryComprehension', // 7. 诗词理解-选择题
|
||||
ReversePoemMatching = 'ReversePoemMatching', // 8. 逆向接诗句
|
||||
SceneBasedPoemGuessing = 'SceneBasedPoemGuessing', // 9. 场景猜诗句
|
||||
AssociationMatching = 'AssociationMatching', // 10. 联想对对碰
|
||||
CharacterWordFlowerDrinkingGame = 'CharacterWordFlowerDrinkingGame', // 11. 字词飞花令
|
||||
PoetryCommonKnowledge = 'PoetryCommonKnowledge', // 12. 诗词常识
|
||||
ChangeWords = 'ChangeWords', // 13. 换字组成语
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export function useRouterPush(inSetup = true) {
|
||||
* @param key 路由名称
|
||||
* @param options 路由参数
|
||||
*/
|
||||
async function routerPushByKey(key: RouteKey, options?: App.Global.RouterPushOptions) {
|
||||
async function routerPushByKey(key: RouteKey, options?: App.Global.RouterPushOptions, newWindow?: boolean) {
|
||||
const { query, params } = options || {}
|
||||
|
||||
const routeLocation: RouteLocationRaw = {
|
||||
@@ -40,7 +40,13 @@ export function useRouterPush(inSetup = true) {
|
||||
routeLocation.params = params
|
||||
}
|
||||
|
||||
return routerPush(routeLocation)
|
||||
if (newWindow) {
|
||||
const routeData = router.resolve(routeLocation)
|
||||
window.open(routeData.href, '_blank')
|
||||
}
|
||||
else {
|
||||
return routerPush(routeLocation)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
||||
user_groups: () => import("@/views/user/groups/index.vue"),
|
||||
user_home: () => import("@/views/user/home/index.vue"),
|
||||
"user_rank-list": () => import("@/views/user/rank-list/index.vue"),
|
||||
"user_rank-pending": () => import("@/views/user/rank-pending/index.vue"),
|
||||
user_rules: () => import("@/views/user/rules/index.vue"),
|
||||
user_teams: () => import("@/views/user/teams/index.vue"),
|
||||
};
|
||||
|
||||
@@ -249,8 +249,11 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
path: '/test',
|
||||
component: 'layout.base$view.test',
|
||||
meta: {
|
||||
title: 'test',
|
||||
i18nKey: 'route.test'
|
||||
title: '测试',
|
||||
i18nKey: 'route.test',
|
||||
icon: 'material-symbols:settings',
|
||||
order: 7,
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -340,15 +343,6 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
i18nKey: 'route.user_rank-list'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user_rank-pending',
|
||||
path: '/user/rank-pending',
|
||||
component: 'view.user_rank-pending',
|
||||
meta: {
|
||||
title: 'user_rank-pending',
|
||||
i18nKey: 'route.user_rank-pending'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user_rules',
|
||||
path: '/user/rules',
|
||||
|
||||
@@ -193,7 +193,6 @@ const routeMap: RouteMap = {
|
||||
"user_groups": "/user/groups",
|
||||
"user_home": "/user/home",
|
||||
"user_rank-list": "/user/rank-list",
|
||||
"user_rank-pending": "/user/rank-pending",
|
||||
"user_rules": "/user/rules",
|
||||
"user_teams": "/user/teams"
|
||||
};
|
||||
|
||||
@@ -58,9 +58,9 @@ export function fetchCreateQuestion(data: Api.Competition.CreateQuestionRequest[
|
||||
}
|
||||
|
||||
/** 根据活动ID获取活动题目列表 */
|
||||
export function fetchGetQuestionList(id: number) {
|
||||
export function fetchGetQuestionList(id: number, roundType = 0) {
|
||||
return request<App.Service.Response<Api.Competition.QuestionListRecord[]>>({
|
||||
url: `/Base/ActivityMain/GetActivity_QuestionByActivityID?ID=${id}`,
|
||||
url: `/Base/ActivityMain/GetActivity_QuestionByActivityID?ID=${id}&RoundType=${roundType}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,9 +78,9 @@ export function fetchCheckNextStep(TeamGroup_QuestionID: number) {
|
||||
}
|
||||
|
||||
/** 根据活动id查询题目列表 */
|
||||
export function fetchGetQuestionTableByActivityID(ActivityID: string) {
|
||||
export function fetchGetQuestionTableByActivityID(ActivityID: string, RoundType: number = 0) {
|
||||
return request<App.Service.Response<Api.Competition.QuestionListDetailRound[]>>({
|
||||
url: `/Base/ActivityMain/GetActivity_QuestionTableByActivityID/?ID=${ActivityID}`,
|
||||
url: `/Base/ActivityMain/GetActivity_QuestionTableByActivityID/?ID=${ActivityID}&RoundType=${RoundType}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
@@ -103,3 +103,12 @@ export function fetchGetQuestionDetailByID(QuestionDetailID: number) {
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 加时赛选择组小队 */
|
||||
export function fetchAddExtraTime(data: Api.Competition.AddExtraTimeParams) {
|
||||
return request<App.Service.Response<any>>({
|
||||
url: `/Base/ActivityMain/AddOverTimeTeamGroup`,
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,60 +49,29 @@ export function fetchGetQuestionLibraryListAll(params: Api.Question.GetQuestionL
|
||||
}
|
||||
|
||||
/** 新增一条问题库 */
|
||||
export function fetchAddQuestionLibrary(params: Api.Question.AddQuestionLibraryParams, file?: File | null) {
|
||||
const formData = new FormData()
|
||||
if (file) {
|
||||
formData.append('Image', file)
|
||||
}
|
||||
export function fetchAddQuestionLibrary(params: Api.Question.AddQuestionLibraryParams) {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
url: '/Base/ActivityMain/AddQuestionListDetail',
|
||||
method: 'post',
|
||||
// params: {
|
||||
// QuestionId: params.questionId,
|
||||
// Name: params.name,
|
||||
// Answer: params.answer,
|
||||
// Type: params.type,
|
||||
// IsGood: params.IsGood,
|
||||
// ImageUrl: params.imageUrl,
|
||||
// },
|
||||
data: {
|
||||
QuestionId: params.questionId,
|
||||
Name: params.name,
|
||||
Answer: params.answer,
|
||||
Type: params.type,
|
||||
IsGood: params.IsGood,
|
||||
ImageUrl: params.imageUrl,
|
||||
},
|
||||
data: params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 禁用/启用 一条问题库 */
|
||||
export function fetchDisableQuestionLibrary(params: Api.Question.DisableQuestionLibraryParams[]) {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
url: '/Base/ActivityMain/UpdateQuestionListDetailList',
|
||||
method: 'post',
|
||||
data: params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 更新一条问题库 */
|
||||
export function fetchUpdateQuestionLibrary(params: Api.Question.AddQuestionLibraryParams, file?: File | null) {
|
||||
const formData = new FormData()
|
||||
if (file) {
|
||||
formData.append('Image', file)
|
||||
}
|
||||
export function fetchUpdateQuestionLibrary(params: Api.Question.UpdateQuestionLibraryParams) {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
url: '/Base/ActivityMain/UpdateQuestionListDetail',
|
||||
method: 'post',
|
||||
// params: {
|
||||
// Id: params.id,
|
||||
// QuestionId: params.questionId,
|
||||
// Name: params.name,
|
||||
// Answer: params.answer,
|
||||
// Type: params.type,
|
||||
// IsGood: params.IsGood,
|
||||
// ImageUrl: params.imageUrl,
|
||||
// },
|
||||
data: {
|
||||
Id: params.id,
|
||||
QuestionId: params.questionId,
|
||||
Name: params.name,
|
||||
Answer: params.answer,
|
||||
Type: params.type,
|
||||
IsGood: params.IsGood,
|
||||
ImageUrl: params.imageUrl,
|
||||
},
|
||||
data: params,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -93,9 +93,9 @@ export const useCompetitionStore = defineStore('competition', () => {
|
||||
timerInterval.value = window.setInterval(() => {
|
||||
if (timeLeft.value > 0) {
|
||||
timeLeft.value--
|
||||
// 剩余5秒播放倒计时音效
|
||||
if (timeLeft.value <= 5) {
|
||||
audioController.play('tick')
|
||||
// 5秒内播放倒计时音效
|
||||
if (timeLeft.value === 5) {
|
||||
audioController.play('boy5')
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -52,7 +52,7 @@ declare namespace Api {
|
||||
type QuestionScoreType = 0 | 1 /** 固定分数 | 答题个数 */
|
||||
|
||||
/** competition round type */
|
||||
type CompetitionRoundType = 0 | 1 /** 题包环节 | 加时环节 */
|
||||
type CompetitionRoundType = 0 | 1 | 3 /** 题包环节 | 加时环节 | 决赛环节 */
|
||||
|
||||
/**
|
||||
* competition publish status
|
||||
@@ -106,6 +106,8 @@ declare namespace Api {
|
||||
Image: string
|
||||
ImageUrl: string
|
||||
Answer: string
|
||||
KeyWordsIndex?: number
|
||||
KeyWord?: string
|
||||
}
|
||||
|
||||
/** get current question params */
|
||||
@@ -329,7 +331,7 @@ declare namespace Api {
|
||||
/** 答题时间(秒) */
|
||||
QuestionTime: number
|
||||
/** 赛题包类型 */
|
||||
RoundType: number
|
||||
RoundType: CompetitionRoundType
|
||||
/** 题目模板 id */
|
||||
TemplateID: number
|
||||
/** UI 类型 */
|
||||
@@ -351,6 +353,7 @@ declare namespace Api {
|
||||
id: number
|
||||
/** 分组名称(兼容字段) */
|
||||
name: string
|
||||
RoundType: number
|
||||
}
|
||||
|
||||
interface CreateQuestionRequest {
|
||||
@@ -380,6 +383,19 @@ declare namespace Api {
|
||||
roundType: number
|
||||
}
|
||||
|
||||
interface AddExtraTimeParams extends Array<ExtraTimeItem> {}
|
||||
|
||||
interface ExtraTimeItem {
|
||||
GroupName: string
|
||||
ActivityID: number
|
||||
TeamInfos: TeamInfo[]
|
||||
}
|
||||
|
||||
interface TeamInfo {
|
||||
TeamID: number
|
||||
TeamName: string
|
||||
}
|
||||
|
||||
/** common record */
|
||||
type CommonRecord<T = any> = {
|
||||
/** record id */
|
||||
|
||||
@@ -20,6 +20,14 @@ declare namespace Api {
|
||||
data: T[]
|
||||
}
|
||||
|
||||
/** disable question library params */
|
||||
interface DisableQuestionLibraryParams {
|
||||
/** question library id */
|
||||
Id: number
|
||||
/** is disabled 0: no 1: yes */
|
||||
IsDisabled: boolean
|
||||
}
|
||||
|
||||
/** get question library list all params */
|
||||
interface GetQuestionLibraryListAllParams {
|
||||
/** question id */
|
||||
@@ -57,9 +65,21 @@ declare namespace Api {
|
||||
/** question library image url */
|
||||
imageUrl: string
|
||||
/** question library type */
|
||||
type: QuestionScoreType
|
||||
// type?: QuestionScoreType
|
||||
/** is priority 0: no 1: yes */
|
||||
IsGood: number
|
||||
/** is disabled */
|
||||
IsDisabled: boolean
|
||||
/** keyword */
|
||||
KeyWord: string
|
||||
/** keyword index */
|
||||
KeyWordsIndex: number
|
||||
}
|
||||
|
||||
/** update question library params */
|
||||
interface UpdateQuestionLibraryParams extends AddQuestionLibraryParams {
|
||||
/** question library id */
|
||||
id: number
|
||||
}
|
||||
|
||||
/** common search params of table */
|
||||
|
||||
@@ -12,6 +12,7 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ActionButton: typeof import('./../components/custom/user/ActionButton.vue')['default']
|
||||
AppProvider: typeof import('./../components/common/app-provider.vue')['default']
|
||||
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
|
||||
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
|
||||
@@ -30,8 +31,6 @@ declare module 'vue' {
|
||||
'IconIc:roundPublish': typeof import('~icons/ic/round-publish')['default']
|
||||
'IconIc:roundStopCircle': typeof import('~icons/ic/round-stop-circle')['default']
|
||||
'IconIc:twotoneRule': typeof import('~icons/ic/twotone-rule')['default']
|
||||
IconIcBaselineAdd: typeof import('~icons/ic/baseline-add')['default']
|
||||
IconIcBaselineAddPlus: typeof import('~icons/ic/baseline-add-plus')['default']
|
||||
IconIcBaselineArrowBack: typeof import('~icons/ic/baseline-arrow-back')['default']
|
||||
IconIcBaselineArrowForward: typeof import('~icons/ic/baseline-arrow-forward')['default']
|
||||
IconIcBaselineCalendarMonth: typeof import('~icons/ic/baseline-calendar-month')['default']
|
||||
@@ -40,60 +39,37 @@ declare module 'vue' {
|
||||
IconIcBaselineContentCopy: typeof import('~icons/ic/baseline-content-copy')['default']
|
||||
IconIcBaselineDelete: typeof import('~icons/ic/baseline-delete')['default']
|
||||
IconIcBaselineDownload: typeof import('~icons/ic/baseline-download')['default']
|
||||
IconIcBaselineEdit: typeof import('~icons/ic/baseline-edit')['default']
|
||||
IconIcBaselineFlash: typeof import('~icons/ic/baseline-flash')['default']
|
||||
IconIcBaselineGroup: typeof import('~icons/ic/baseline-group')['default']
|
||||
IconIcBaselineRefresh: typeof import('~icons/ic/baseline-refresh')['default']
|
||||
IconIcBaselineRocketLaunch: typeof import('~icons/ic/baseline-rocket-launch')['default']
|
||||
IconIcBaselineSave: typeof import('~icons/ic/baseline-save')['default']
|
||||
IconIcBaselineScan: typeof import('~icons/ic/baseline-scan')['default']
|
||||
IconIcBaselineSettingsBackupRestore: typeof import('~icons/ic/baseline-settings-backup-restore')['default']
|
||||
IconIcBaselineUpload: typeof import('~icons/ic/baseline-upload')['default']
|
||||
IconIcBaselineUploadFile: typeof import('~icons/ic/baseline-upload-file')['default']
|
||||
IconIcCarbonUserAvatarFilledAlt: typeof import('~icons/ic/carbon-user-avatar-filled-alt')['default']
|
||||
IconIcOutlineAccessTime: typeof import('~icons/ic/outline-access-time')['default']
|
||||
IconIcOutlineAddToPhotos: typeof import('~icons/ic/outline-add-to-photos')['default']
|
||||
IconIcOutlineArchive: typeof import('~icons/ic/outline-archive')['default']
|
||||
IconIcOutlineAttachMoney: typeof import('~icons/ic/outline-attach-money')['default']
|
||||
IconIcOutlineDelete: typeof import('~icons/ic/outline-delete')['default']
|
||||
IconIcOutlineFolderOff: typeof import('~icons/ic/outline-folder-off')['default']
|
||||
IconIcOutlineFormatListNumbered: typeof import('~icons/ic/outline-format-list-numbered')['default']
|
||||
IconIcOutlineTimer: typeof import('~icons/ic/outline-timer')['default']
|
||||
IconIcOutlineTitle: typeof import('~icons/ic/outline-title')['default']
|
||||
IconIcRoundAccessTime: typeof import('~icons/ic/round-access-time')['default']
|
||||
IconIcRoundArrowBack: typeof import('~icons/ic/round-arrow-back')['default']
|
||||
IconIcRoundArrowForward: typeof import('~icons/ic/round-arrow-forward')['default']
|
||||
IconIcRoundArrowLeft: typeof import('~icons/ic/round-arrow-left')['default']
|
||||
IconIcRoundArrowRight: typeof import('~icons/ic/round-arrow-right')['default']
|
||||
IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
|
||||
IconIcRoundLibraryAdd: typeof import('~icons/ic/round-library-add')['default']
|
||||
IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
|
||||
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
|
||||
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
|
||||
IconIcRoundStarBorder: typeof import('~icons/ic/round-star-border')['default']
|
||||
IconIcRoundUpload: typeof import('~icons/ic/round-upload')['default']
|
||||
IconLocalBanner: typeof import('~icons/local/banner')['default']
|
||||
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
|
||||
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
|
||||
IconMdiDelete: typeof import('~icons/mdi/delete')['default']
|
||||
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
||||
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
|
||||
IconMdiPlus: typeof import('~icons/mdi/plus')['default']
|
||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
IconSolarAddSquareLinear: typeof import('~icons/solar/add-square-linear')['default']
|
||||
IconSolarArchiveUpMinimlisticOutline: typeof import('~icons/solar/archive-up-minimlistic-outline')['default']
|
||||
IconSolarCloudUploadLinear: typeof import('~icons/solar/cloud-upload-linear')['default']
|
||||
IconSolarPen2Linear: typeof import('~icons/solar/pen2-linear')['default']
|
||||
IconSolarQuestionCircleLinear: typeof import('~icons/solar/question-circle-linear')['default']
|
||||
IconSolarQuestionCircleOutline: typeof import('~icons/solar/question-circle-outline')['default']
|
||||
IconSolarTrashBinMinimalisticOutline: typeof import('~icons/solar/trash-bin-minimalistic-outline')['default']
|
||||
'IconStreamlineSharp:typeAreaRemix': typeof import('~icons/streamline-sharp/type-area-remix')['default']
|
||||
IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default']
|
||||
IconUilSearch: typeof import('~icons/uil/search')['default']
|
||||
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
|
||||
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
|
||||
MathIframeDialog: typeof import('./../components/common/rest-basic-editor/components/math-iframe-dialog.vue')['default']
|
||||
MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
@@ -111,7 +87,6 @@ declare module 'vue' {
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NEditor: typeof import('naive-ui')['NEditor']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
@@ -119,14 +94,9 @@ declare module 'vue' {
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NImageGroup: typeof import('naive-ui')['NImageGroup']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||
@@ -136,38 +106,24 @@ declare module 'vue' {
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSelectOption: typeof import('naive-ui')['NSelectOption']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
NTable: typeof import('naive-ui')['NTable']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NTextarea: typeof import('naive-ui')['NTextarea']
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NTree: typeof import('naive-ui')['NTree']
|
||||
NTreeSelect: typeof import('naive-ui')['NTreeSelect']
|
||||
NUpload: typeof import('naive-ui')['NUpload']
|
||||
NWatermark: typeof import('naive-ui')['NWatermark']
|
||||
OssImageUpload: typeof import('./../components/common/oss-image-upload/index.vue')['default']
|
||||
PageHeader: typeof import('./../components/common/page-header.vue')['default']
|
||||
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
|
||||
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
|
||||
RestBasicEditor: typeof import('./../components/common/rest-basic-editor/index.vue')['default']
|
||||
RestDraggableResizable: typeof import('./../components/common/rest-draggable-resizable/rest-draggable-resizable.vue')['default']
|
||||
RestHoverAciton: typeof import('./../components/common/rest-hover-aciton/rest-hover-aciton.vue')['default']
|
||||
RestImgViewer: typeof import('./../components/common/rest-img-viewer/rest-img-viewer.vue')['default']
|
||||
RestSelectFile: typeof import('./../components/common/rest-select-file/rest-select-file.vue')['default']
|
||||
RestUpload: typeof import('./../components/common/rest-upload/rest-upload.vue')['default']
|
||||
RestVideoViewer: typeof import('./../components/common/rest-video-viewer/rest-video-viewer.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SoybeanAvatar: typeof import('./../components/custom/soybean-avatar.vue')['default']
|
||||
@@ -178,7 +134,6 @@ declare module 'vue' {
|
||||
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
|
||||
TianZiGe: typeof import('./../components/custom/user/TianZiGe.vue')['default']
|
||||
Typeit: typeof import('./../components/custom/typeit/index.vue')['default']
|
||||
UpFileDialog: typeof import('./../components/common/rest-basic-editor/components/up-file-dialog.vue')['default']
|
||||
WangEditor: typeof import('./../components/common/wang-editor.vue')['default']
|
||||
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
|
||||
}
|
||||
@@ -186,6 +141,7 @@ declare module 'vue' {
|
||||
|
||||
// For TSX support
|
||||
declare global {
|
||||
const ActionButton: typeof import('./../components/custom/user/ActionButton.vue')['default']
|
||||
const AppProvider: typeof import('./../components/common/app-provider.vue')['default']
|
||||
const BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
|
||||
const ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
|
||||
@@ -204,8 +160,6 @@ declare global {
|
||||
const 'IconIc:roundPublish': typeof import('~icons/ic/round-publish')['default']
|
||||
const 'IconIc:roundStopCircle': typeof import('~icons/ic/round-stop-circle')['default']
|
||||
const 'IconIc:twotoneRule': typeof import('~icons/ic/twotone-rule')['default']
|
||||
const IconIcBaselineAdd: typeof import('~icons/ic/baseline-add')['default']
|
||||
const IconIcBaselineAddPlus: typeof import('~icons/ic/baseline-add-plus')['default']
|
||||
const IconIcBaselineArrowBack: typeof import('~icons/ic/baseline-arrow-back')['default']
|
||||
const IconIcBaselineArrowForward: typeof import('~icons/ic/baseline-arrow-forward')['default']
|
||||
const IconIcBaselineCalendarMonth: typeof import('~icons/ic/baseline-calendar-month')['default']
|
||||
@@ -214,60 +168,37 @@ declare global {
|
||||
const IconIcBaselineContentCopy: typeof import('~icons/ic/baseline-content-copy')['default']
|
||||
const IconIcBaselineDelete: typeof import('~icons/ic/baseline-delete')['default']
|
||||
const IconIcBaselineDownload: typeof import('~icons/ic/baseline-download')['default']
|
||||
const IconIcBaselineEdit: typeof import('~icons/ic/baseline-edit')['default']
|
||||
const IconIcBaselineFlash: typeof import('~icons/ic/baseline-flash')['default']
|
||||
const IconIcBaselineGroup: typeof import('~icons/ic/baseline-group')['default']
|
||||
const IconIcBaselineRefresh: typeof import('~icons/ic/baseline-refresh')['default']
|
||||
const IconIcBaselineRocketLaunch: typeof import('~icons/ic/baseline-rocket-launch')['default']
|
||||
const IconIcBaselineSave: typeof import('~icons/ic/baseline-save')['default']
|
||||
const IconIcBaselineScan: typeof import('~icons/ic/baseline-scan')['default']
|
||||
const IconIcBaselineSettingsBackupRestore: typeof import('~icons/ic/baseline-settings-backup-restore')['default']
|
||||
const IconIcBaselineUpload: typeof import('~icons/ic/baseline-upload')['default']
|
||||
const IconIcBaselineUploadFile: typeof import('~icons/ic/baseline-upload-file')['default']
|
||||
const IconIcCarbonUserAvatarFilledAlt: typeof import('~icons/ic/carbon-user-avatar-filled-alt')['default']
|
||||
const IconIcOutlineAccessTime: typeof import('~icons/ic/outline-access-time')['default']
|
||||
const IconIcOutlineAddToPhotos: typeof import('~icons/ic/outline-add-to-photos')['default']
|
||||
const IconIcOutlineArchive: typeof import('~icons/ic/outline-archive')['default']
|
||||
const IconIcOutlineAttachMoney: typeof import('~icons/ic/outline-attach-money')['default']
|
||||
const IconIcOutlineDelete: typeof import('~icons/ic/outline-delete')['default']
|
||||
const IconIcOutlineFolderOff: typeof import('~icons/ic/outline-folder-off')['default']
|
||||
const IconIcOutlineFormatListNumbered: typeof import('~icons/ic/outline-format-list-numbered')['default']
|
||||
const IconIcOutlineTimer: typeof import('~icons/ic/outline-timer')['default']
|
||||
const IconIcOutlineTitle: typeof import('~icons/ic/outline-title')['default']
|
||||
const IconIcRoundAccessTime: typeof import('~icons/ic/round-access-time')['default']
|
||||
const IconIcRoundArrowBack: typeof import('~icons/ic/round-arrow-back')['default']
|
||||
const IconIcRoundArrowForward: typeof import('~icons/ic/round-arrow-forward')['default']
|
||||
const IconIcRoundArrowLeft: typeof import('~icons/ic/round-arrow-left')['default']
|
||||
const IconIcRoundArrowRight: typeof import('~icons/ic/round-arrow-right')['default']
|
||||
const IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
|
||||
const IconIcRoundLibraryAdd: typeof import('~icons/ic/round-library-add')['default']
|
||||
const IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
|
||||
const IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
|
||||
const IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
|
||||
const IconIcRoundStarBorder: typeof import('~icons/ic/round-star-border')['default']
|
||||
const IconIcRoundUpload: typeof import('~icons/ic/round-upload')['default']
|
||||
const IconLocalBanner: typeof import('~icons/local/banner')['default']
|
||||
const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
|
||||
const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
|
||||
const IconMdiDelete: typeof import('~icons/mdi/delete')['default']
|
||||
const IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
||||
const IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
const IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
|
||||
const IconMdiPlus: typeof import('~icons/mdi/plus')['default']
|
||||
const IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
const IconSolarAddSquareLinear: typeof import('~icons/solar/add-square-linear')['default']
|
||||
const IconSolarArchiveUpMinimlisticOutline: typeof import('~icons/solar/archive-up-minimlistic-outline')['default']
|
||||
const IconSolarCloudUploadLinear: typeof import('~icons/solar/cloud-upload-linear')['default']
|
||||
const IconSolarPen2Linear: typeof import('~icons/solar/pen2-linear')['default']
|
||||
const IconSolarQuestionCircleLinear: typeof import('~icons/solar/question-circle-linear')['default']
|
||||
const IconSolarQuestionCircleOutline: typeof import('~icons/solar/question-circle-outline')['default']
|
||||
const IconSolarTrashBinMinimalisticOutline: typeof import('~icons/solar/trash-bin-minimalistic-outline')['default']
|
||||
const 'IconStreamlineSharp:typeAreaRemix': typeof import('~icons/streamline-sharp/type-area-remix')['default']
|
||||
const IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default']
|
||||
const IconUilSearch: typeof import('~icons/uil/search')['default']
|
||||
const LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
|
||||
const LookForward: typeof import('./../components/custom/look-forward.vue')['default']
|
||||
const MathIframeDialog: typeof import('./../components/common/rest-basic-editor/components/math-iframe-dialog.vue')['default']
|
||||
const MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
|
||||
const NAlert: typeof import('naive-ui')['NAlert']
|
||||
const NBadge: typeof import('naive-ui')['NBadge']
|
||||
@@ -285,7 +216,6 @@ declare global {
|
||||
const NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
const NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
const NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
const NEditor: typeof import('naive-ui')['NEditor']
|
||||
const NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
const NForm: typeof import('naive-ui')['NForm']
|
||||
const NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
@@ -293,14 +223,9 @@ declare global {
|
||||
const NGi: typeof import('naive-ui')['NGi']
|
||||
const NGrid: typeof import('naive-ui')['NGrid']
|
||||
const NImage: typeof import('naive-ui')['NImage']
|
||||
const NImageGroup: typeof import('naive-ui')['NImageGroup']
|
||||
const NInput: typeof import('naive-ui')['NInput']
|
||||
const NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||
const NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
|
||||
const NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
const NLayout: typeof import('naive-ui')['NLayout']
|
||||
const NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||
const NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||
const NList: typeof import('naive-ui')['NList']
|
||||
const NListItem: typeof import('naive-ui')['NListItem']
|
||||
const NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||
@@ -310,38 +235,24 @@ declare global {
|
||||
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
const NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||
const NPopover: typeof import('naive-ui')['NPopover']
|
||||
const NProgress: typeof import('naive-ui')['NProgress']
|
||||
const NRadio: typeof import('naive-ui')['NRadio']
|
||||
const NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
const NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
const NSelect: typeof import('naive-ui')['NSelect']
|
||||
const NSelectOption: typeof import('naive-ui')['NSelectOption']
|
||||
const NSpace: typeof import('naive-ui')['NSpace']
|
||||
const NSpin: typeof import('naive-ui')['NSpin']
|
||||
const NStatistic: typeof import('naive-ui')['NStatistic']
|
||||
const NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
const NTab: typeof import('naive-ui')['NTab']
|
||||
const NTable: typeof import('naive-ui')['NTable']
|
||||
const NTabs: typeof import('naive-ui')['NTabs']
|
||||
const NTag: typeof import('naive-ui')['NTag']
|
||||
const NTextarea: typeof import('naive-ui')['NTextarea']
|
||||
const NThing: typeof import('naive-ui')['NThing']
|
||||
const NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
const NTree: typeof import('naive-ui')['NTree']
|
||||
const NTreeSelect: typeof import('naive-ui')['NTreeSelect']
|
||||
const NUpload: typeof import('naive-ui')['NUpload']
|
||||
const NWatermark: typeof import('naive-ui')['NWatermark']
|
||||
const OssImageUpload: typeof import('./../components/common/oss-image-upload/index.vue')['default']
|
||||
const PageHeader: typeof import('./../components/common/page-header.vue')['default']
|
||||
const PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
|
||||
const ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
|
||||
const RestBasicEditor: typeof import('./../components/common/rest-basic-editor/index.vue')['default']
|
||||
const RestDraggableResizable: typeof import('./../components/common/rest-draggable-resizable/rest-draggable-resizable.vue')['default']
|
||||
const RestHoverAciton: typeof import('./../components/common/rest-hover-aciton/rest-hover-aciton.vue')['default']
|
||||
const RestImgViewer: typeof import('./../components/common/rest-img-viewer/rest-img-viewer.vue')['default']
|
||||
const RestSelectFile: typeof import('./../components/common/rest-select-file/rest-select-file.vue')['default']
|
||||
const RestUpload: typeof import('./../components/common/rest-upload/rest-upload.vue')['default']
|
||||
const RestVideoViewer: typeof import('./../components/common/rest-video-viewer/rest-video-viewer.vue')['default']
|
||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||
const RouterView: typeof import('vue-router')['RouterView']
|
||||
const SoybeanAvatar: typeof import('./../components/custom/soybean-avatar.vue')['default']
|
||||
@@ -352,7 +263,6 @@ declare global {
|
||||
const ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
|
||||
const TianZiGe: typeof import('./../components/custom/user/TianZiGe.vue')['default']
|
||||
const Typeit: typeof import('./../components/custom/typeit/index.vue')['default']
|
||||
const UpFileDialog: typeof import('./../components/common/rest-basic-editor/components/up-file-dialog.vue')['default']
|
||||
const WangEditor: typeof import('./../components/common/wang-editor.vue')['default']
|
||||
const WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
|
||||
}
|
||||
@@ -47,7 +47,6 @@ declare module "@elegant-router/types" {
|
||||
"user_groups": "/user/groups";
|
||||
"user_home": "/user/home";
|
||||
"user_rank-list": "/user/rank-list";
|
||||
"user_rank-pending": "/user/rank-pending";
|
||||
"user_rules": "/user/rules";
|
||||
"user_teams": "/user/teams";
|
||||
};
|
||||
@@ -136,7 +135,6 @@ declare module "@elegant-router/types" {
|
||||
| "user_groups"
|
||||
| "user_home"
|
||||
| "user_rank-list"
|
||||
| "user_rank-pending"
|
||||
| "user_rules"
|
||||
| "user_teams"
|
||||
>;
|
||||
|
||||
@@ -1,23 +1,58 @@
|
||||
import boy5 from '@/assets/audio/boy-5s.mp3'
|
||||
import system10 from '@/assets/audio/boy-10s.mp3'
|
||||
|
||||
export type AudioType = 'start' | 'tick10' | 'flip' | 'boy5'
|
||||
|
||||
export class AudioController {
|
||||
private audioContext: AudioContext | null = null
|
||||
private static audioContext: AudioContext | null = null
|
||||
private soundFiles: Record<string, string> = {}
|
||||
private audioCache: Record<string, HTMLAudioElement> = {}
|
||||
|
||||
constructor(soundFiles: Record<string, string> = {}) {
|
||||
this.soundFiles = soundFiles
|
||||
this.soundFiles = {
|
||||
tick10: system10,
|
||||
boy5,
|
||||
...soundFiles,
|
||||
}
|
||||
// 预加载关键音频
|
||||
this.preload(['tick10', 'boy5'])
|
||||
}
|
||||
|
||||
private initAudioContext() {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
|
||||
if (!AudioController.audioContext) {
|
||||
const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext
|
||||
if (AudioContextClass) {
|
||||
AudioController.audioContext = new AudioContextClass()
|
||||
}
|
||||
}
|
||||
// 尝试恢复上下文(如果被挂起)
|
||||
if (AudioController.audioContext?.state === 'suspended') {
|
||||
AudioController.audioContext.resume().catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载音频文件
|
||||
*/
|
||||
preload(types: AudioType[]) {
|
||||
types.forEach((type) => {
|
||||
const url = this.soundFiles[type]
|
||||
if (url && !this.audioCache[type]) {
|
||||
const audio = new Audio(url)
|
||||
audio.preload = 'auto'
|
||||
this.audioCache[type] = audio
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放指定类型的音效
|
||||
* 优先使用预设的音频文件,如果没有配置或加载失败,则使用 Web Audio API 合成音效
|
||||
*/
|
||||
async play(type: 'start' | 'tick' | 'flip') {
|
||||
async play(type: AudioType) {
|
||||
// 确保音频上下文已初始化
|
||||
this.initAudioContext()
|
||||
|
||||
// 尝试播放音频文件
|
||||
if (this.soundFiles[type]) {
|
||||
try {
|
||||
@@ -54,68 +89,83 @@ export class AudioController {
|
||||
})
|
||||
}
|
||||
|
||||
private playSynth(type: 'start' | 'tick' | 'flip') {
|
||||
this.initAudioContext()
|
||||
if (!this.audioContext)
|
||||
private playSynth(type: AudioType) {
|
||||
if (!AudioController.audioContext)
|
||||
return
|
||||
|
||||
const ctxTime = this.audioContext.currentTime
|
||||
const gain = this.audioContext.createGain()
|
||||
gain.connect(this.audioContext.destination)
|
||||
const ctx = AudioController.audioContext
|
||||
const ctxTime = ctx.currentTime
|
||||
const gain = ctx.createGain()
|
||||
gain.connect(ctx.destination)
|
||||
|
||||
if (type === 'start') {
|
||||
const osc = this.audioContext.createOscillator()
|
||||
osc.connect(gain)
|
||||
// 开始音效:高音短促提示
|
||||
osc.type = 'sine'
|
||||
osc.frequency.setValueAtTime(880, ctxTime)
|
||||
osc.frequency.exponentialRampToValueAtTime(440, ctxTime + 0.3)
|
||||
gain.gain.setValueAtTime(0.5, ctxTime)
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctxTime + 0.3)
|
||||
osc.start(ctxTime)
|
||||
osc.stop(ctxTime + 0.3)
|
||||
}
|
||||
else if (type === 'tick') {
|
||||
const osc = this.audioContext.createOscillator()
|
||||
osc.connect(gain)
|
||||
// 倒计时音效:急促的滴答声
|
||||
osc.type = 'triangle'
|
||||
osc.frequency.setValueAtTime(600, ctxTime)
|
||||
gain.gain.setValueAtTime(0.3, ctxTime)
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctxTime + 0.1)
|
||||
osc.start(ctxTime)
|
||||
osc.stop(ctxTime + 0.1)
|
||||
}
|
||||
else if (type === 'flip') {
|
||||
// 翻书音效:使用白噪声模拟纸张摩擦
|
||||
const bufferSize = this.audioContext.sampleRate * 0.5 // 0.5秒缓冲
|
||||
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate)
|
||||
const data = buffer.getChannelData(0)
|
||||
|
||||
// 生成白噪声
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = Math.random() * 2 - 1
|
||||
}
|
||||
|
||||
const noise = this.audioContext.createBufferSource()
|
||||
noise.buffer = buffer
|
||||
|
||||
// 滤波器:低通滤波器,模拟纸张的闷声
|
||||
const filter = this.audioContext.createBiquadFilter()
|
||||
filter.type = 'lowpass'
|
||||
filter.frequency.setValueAtTime(400, ctxTime)
|
||||
filter.frequency.exponentialRampToValueAtTime(3000, ctxTime + 0.1) // 频率快速扫过,模拟快速翻动
|
||||
|
||||
noise.connect(filter)
|
||||
filter.connect(gain)
|
||||
|
||||
// 音量包络:快速淡入淡出
|
||||
gain.gain.setValueAtTime(0, ctxTime)
|
||||
gain.gain.linearRampToValueAtTime(0.8, ctxTime + 0.05)
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctxTime + 0.3)
|
||||
|
||||
noise.start(ctxTime)
|
||||
noise.stop(ctxTime + 0.3)
|
||||
switch (type) {
|
||||
case 'start':
|
||||
this.playStartSound(ctx, gain, ctxTime)
|
||||
break
|
||||
case 'tick10':
|
||||
case 'boy5':
|
||||
this.playTickSound(ctx, gain, ctxTime)
|
||||
break
|
||||
case 'flip':
|
||||
this.playFlipSound(ctx, gain, ctxTime)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private playStartSound(ctx: AudioContext, gain: GainNode, time: number) {
|
||||
const osc = ctx.createOscillator()
|
||||
osc.connect(gain)
|
||||
// 开始音效:高音短促提示
|
||||
osc.type = 'sine'
|
||||
osc.frequency.setValueAtTime(880, time)
|
||||
osc.frequency.exponentialRampToValueAtTime(440, time + 0.3)
|
||||
gain.gain.setValueAtTime(0.5, time)
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, time + 0.3)
|
||||
osc.start(time)
|
||||
osc.stop(time + 0.3)
|
||||
}
|
||||
|
||||
private playTickSound(ctx: AudioContext, gain: GainNode, time: number) {
|
||||
const osc = ctx.createOscillator()
|
||||
osc.connect(gain)
|
||||
// 倒计时音效:急促的滴答声
|
||||
osc.type = 'triangle'
|
||||
osc.frequency.setValueAtTime(600, time)
|
||||
gain.gain.setValueAtTime(0.3, time)
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, time + 0.1)
|
||||
osc.start(time)
|
||||
osc.stop(time + 0.1)
|
||||
}
|
||||
|
||||
private playFlipSound(ctx: AudioContext, gain: GainNode, time: number) {
|
||||
// 翻书音效:使用白噪声模拟纸张摩擦
|
||||
const bufferSize = ctx.sampleRate * 0.5 // 0.5秒缓冲
|
||||
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate)
|
||||
const data = buffer.getChannelData(0)
|
||||
|
||||
// 生成白噪声
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = Math.random() * 2 - 1
|
||||
}
|
||||
|
||||
const noise = ctx.createBufferSource()
|
||||
noise.buffer = buffer
|
||||
|
||||
// 滤波器:低通滤波器,模拟纸张的闷声
|
||||
const filter = ctx.createBiquadFilter()
|
||||
filter.type = 'lowpass'
|
||||
filter.frequency.setValueAtTime(400, time)
|
||||
filter.frequency.exponentialRampToValueAtTime(3000, time + 0.1) // 频率快速扫过,模拟快速翻动
|
||||
|
||||
noise.connect(filter)
|
||||
filter.connect(gain)
|
||||
|
||||
// 音量包络:快速淡入淡出
|
||||
gain.gain.setValueAtTime(0, time)
|
||||
gain.gain.linearRampToValueAtTime(0.8, time + 0.05)
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, time + 0.3)
|
||||
|
||||
noise.start(time)
|
||||
noise.stop(time + 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ const statisticData = computed<StatisticData[]>(() => [
|
||||
<NGi span="24 s:24 m:18">
|
||||
<div class="flex-y-center">
|
||||
<div class="size-72px shrink-0 overflow-hidden rd-1/2">
|
||||
<img src="@/assets/imgs/reader-star.jpg" class="size-full">
|
||||
<img src="@/assets/imgs/user/read-logo.png" class="size-full">
|
||||
</div>
|
||||
<div class="pl-12px">
|
||||
<h3 class="text-18px font-semibold">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NForm, NFormItem, NInputNumber, NModal, NSelect, type SelectOption } from 'naive-ui'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { useDict } from '@/hooks/business/useDict'
|
||||
import { fetchGetQuestionListAll } from '@/service/api/question'
|
||||
@@ -29,7 +29,10 @@ const props = defineProps<{
|
||||
|
||||
const { options: roundTypeOptions } = useDict('round_type')
|
||||
const { options: scoreTypeOptions } = useDict('score_type')
|
||||
const { options: timeOptions } = useDict('time_out')
|
||||
const { options: timeOptionsRaw } = useDict('time_out')
|
||||
const timeOptions = computed(() =>
|
||||
[...timeOptionsRaw.value].sort((a, b) => Number(a.value) - Number(b.value)),
|
||||
)
|
||||
// const { options: uiTypeOptions } = useDict('ui_type', 'string')
|
||||
const { options: uiTypeOptions } = useDict('question_category_name', 'string')
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ async function fetchTeamListByGroupId(GroupID: number) {
|
||||
|
||||
/** 获取活动题目列表 */
|
||||
async function getQuestionList(id: number) {
|
||||
const { data, error } = await fetchGetQuestionList(id)
|
||||
const { data, error } = await fetchGetQuestionList(id, 3)
|
||||
if (!error) {
|
||||
questionsList.value = data?.data || []
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NCard, NForm, NFormItem, NInput, NInputNumber, NModal, NSelect, NTag, type SelectOption } from 'naive-ui'
|
||||
import { NButton, NCard, NForm, NFormItem, NInputNumber, NModal, NSelect, NTag, type SelectOption } from 'naive-ui'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { PublishStatus } from '@/enum/business'
|
||||
@@ -166,7 +166,7 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
function getRoundTypeName(roundType: string) {
|
||||
function getRoundTypeName(roundType: number) {
|
||||
const option = roundTypeOptions.value?.find((item: any) => item.value === roundType)
|
||||
return option?.label || '未知类型'
|
||||
}
|
||||
@@ -268,14 +268,14 @@ defineExpose({ validate, reset, modelValue })
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="h-8 w-1.5 rounded-full bg-blue-500" />
|
||||
<NTag type="primary" class="!text-blue-500">
|
||||
{{ getRoundTypeName(round.roundType) }}
|
||||
{{ getRoundTypeName(Number(round.roundType)) }}
|
||||
</NTag>
|
||||
<div class="flex items-center gap-3">
|
||||
<NInput
|
||||
<!-- <NInput
|
||||
v-model:value="round.name" placeholder="请输入环节名称" :bordered="false"
|
||||
:disabled="!isEdit"
|
||||
class="border transition-all !w-80 !border-transparent !rounded-lg !bg-transparent !text-xl !font-bold focus-within:shadow-sm focus-within:!border-blue-500 hover:!border-gray-200 focus-within:!bg-white"
|
||||
/>
|
||||
/> -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-500 font-bold"
|
||||
|
||||
@@ -23,7 +23,6 @@ function handleAdd() {
|
||||
}
|
||||
|
||||
async function handleCopy(item: Api.Competition.CompetitionItem) {
|
||||
// TODO: 实现复制逻辑
|
||||
try {
|
||||
const { error } = await fetchCopyActivity(item.Id)
|
||||
if (error) {
|
||||
@@ -163,7 +162,3 @@ onMounted(() => fetchActivityList())
|
||||
<CompetitionAddModal v-model:show="showAddModal" @success="fetchActivityList" />
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 针对不同屏幕微调间距 */
|
||||
</style>
|
||||
|
||||
@@ -57,6 +57,14 @@ async function fetchData() {
|
||||
|
||||
// 列定义
|
||||
const columns: DataTableColumns<Api.Dictionary.DictionaryItem> = [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: 80,
|
||||
render(_, index) {
|
||||
return index + 1
|
||||
},
|
||||
},
|
||||
{ title: 'ID', key: 'Id' },
|
||||
{
|
||||
title: '字典键 (Key)',
|
||||
|
||||
@@ -59,6 +59,14 @@ watch(() => props.visible, (val) => {
|
||||
})
|
||||
|
||||
const columns: DataTableColumns<any> = [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: 80,
|
||||
render(_, index) {
|
||||
return index + 1
|
||||
},
|
||||
},
|
||||
{ title: 'ID', key: 'ID', width: 80 },
|
||||
{ title: '键 (Key)', key: 'UI_Key' },
|
||||
{ title: '值 (Value)', key: 'UI_Value', width: 100 },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import CategoryTree from './modules/CategoryTree.vue'
|
||||
import QuestionList from './modules/QuestionList.vue'
|
||||
|
||||
@@ -8,7 +8,8 @@ defineOptions({ name: 'QuestionStore' })
|
||||
const currentCategory = ref<any>(null)
|
||||
|
||||
// Resize logic
|
||||
const siderWidth = ref(450)
|
||||
const siderWidth = ref(525)
|
||||
const LOCAL_STORAGE_KEY = 'question-store-sider-width'
|
||||
const minWidth = 300
|
||||
const maxWidth = 800
|
||||
const isDragging = ref(false)
|
||||
@@ -43,7 +44,15 @@ function onMouseUp() {
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.body.style.userSelect = ''
|
||||
document.body.style.cursor = ''
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, siderWidth.value.toString())
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const savedWidth = localStorage.getItem(LOCAL_STORAGE_KEY)
|
||||
if (savedWidth) {
|
||||
siderWidth.value = Number(savedWidth)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
NInput,
|
||||
NModal,
|
||||
NSelect,
|
||||
type SelectOption,
|
||||
useDialog,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
@@ -20,9 +21,10 @@ const emit = defineEmits<{
|
||||
(e: 'dragStart', event: MouseEvent): void
|
||||
}>()
|
||||
|
||||
const { options: questionTypeOptions } = useDict('question_type', 'string')
|
||||
const { options: categoryOptions } = useDict('question_category_name', 'string')
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'question-store-selected-category-id'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
|
||||
@@ -46,6 +48,7 @@ const currentOperationNode = ref<CategoryItem | null>(null)
|
||||
watch(selectedKey, (newKey) => {
|
||||
if (!newKey) {
|
||||
emit('update:category', null)
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEY)
|
||||
return
|
||||
}
|
||||
const node = categoryList.value.find(item => item.Id === newKey)
|
||||
@@ -53,9 +56,11 @@ watch(selectedKey, (newKey) => {
|
||||
emit('update:category', {
|
||||
...node,
|
||||
})
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, newKey.toString())
|
||||
}
|
||||
else {
|
||||
emit('update:category', null)
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEY)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -63,6 +68,16 @@ function handleSelect(key: number) {
|
||||
selectedKey.value = key
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类名称选择更新
|
||||
* @param _value
|
||||
* @param option
|
||||
*/
|
||||
function handleCategoryNameChange(_value: string, option: SelectOption) {
|
||||
if (option)
|
||||
categoryForm.value.questionType = option.value as QuestionType
|
||||
}
|
||||
|
||||
function handleAddCategory() {
|
||||
categoryOperation.value = 'add'
|
||||
categoryForm.value.questionContent = ''
|
||||
@@ -100,7 +115,7 @@ function handleDeleteCategory(item: CategoryItem) {
|
||||
selectedKey.value = null
|
||||
}
|
||||
// 获取最新分类列表
|
||||
await fetchCategoryList()
|
||||
await loadAndSelectCategory()
|
||||
message.success('删除成功')
|
||||
}
|
||||
},
|
||||
@@ -128,7 +143,7 @@ async function submitCategory() {
|
||||
message.error('添加分类失败')
|
||||
return
|
||||
}
|
||||
await fetchCategoryList()
|
||||
await loadAndSelectCategory()
|
||||
message.success('分类添加成功')
|
||||
}
|
||||
else if (categoryOperation.value === 'edit' && currentOperationNode.value) {
|
||||
@@ -144,7 +159,7 @@ async function submitCategory() {
|
||||
}
|
||||
currentOperationNode.value.QuestionContent = categoryForm.value.questionContent
|
||||
message.success('分类修改成功')
|
||||
await fetchCategoryList()
|
||||
await loadAndSelectCategory()
|
||||
// 更新选中分类的信息
|
||||
if (selectedKey.value === currentOperationNode.value.Id) {
|
||||
const node = currentOperationNode.value
|
||||
@@ -160,21 +175,35 @@ async function submitCategory() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类列表
|
||||
* 获取并选择分类列表
|
||||
*/
|
||||
async function fetchCategoryList() {
|
||||
async function loadAndSelectCategory() {
|
||||
try {
|
||||
const { data } = await fetchGetQuestionListAll()
|
||||
categoryList.value = data?.data || []
|
||||
// Ensure Id is always a number to prevent type mismatch issues
|
||||
categoryList.value = (data?.data || []).map((c: any) => ({ ...c, Id: Number(c.Id) }))
|
||||
|
||||
const savedKeyRaw = localStorage.getItem(LOCAL_STORAGE_KEY)
|
||||
const savedKey = savedKeyRaw ? Number(savedKeyRaw) : null
|
||||
const savedCategoryExists = savedKey ? categoryList.value.some(c => c.Id === savedKey) : false
|
||||
|
||||
if (savedKey && savedCategoryExists) {
|
||||
selectedKey.value = savedKey
|
||||
}
|
||||
else if (categoryList.value.length > 0) {
|
||||
selectedKey.value = categoryList.value[0].Id
|
||||
}
|
||||
else {
|
||||
selectedKey.value = null
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
catch (error) {
|
||||
message.error('获取分类列表失败')
|
||||
catch (error: any) {
|
||||
message.error(error.message || '获取分类列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCategoryList()
|
||||
loadAndSelectCategory()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -243,11 +272,12 @@ onMounted(() => {
|
||||
:options="categoryOptions"
|
||||
value-field="label"
|
||||
placeholder="请选择分类名称"
|
||||
@update:value="handleCategoryNameChange"
|
||||
/>
|
||||
</NFormItem>
|
||||
<!-- 题目类型 -->
|
||||
<NFormItem label="题目类型">
|
||||
<NSelect v-model:value="categoryForm.questionType" :options="questionTypeOptions" placeholder="请选择题目类型" />
|
||||
<NInput v-model:value="categoryForm.questionType" placeholder="请选择分类名称自动带出" :readonly="true" />
|
||||
</NFormItem>
|
||||
<!-- 分类描述 -->
|
||||
<NFormItem label="分类描述">
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInst, FormItemRule } from 'naive-ui'
|
||||
import Clipboard from 'clipboard'
|
||||
import {
|
||||
NButton,
|
||||
NCard,
|
||||
NDrawer,
|
||||
NDrawerContent,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NRadio,
|
||||
NRadioGroup,
|
||||
} from 'naive-ui'
|
||||
import { pinyin } from 'pinyin-pro'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import OssImageUpload from '@/components/common/oss-image-upload/index.vue'
|
||||
import WangEditor from '@/components/common/wang-editor.vue'
|
||||
import { QuestionCategoryEnum, QuestionType } from '@/enum/business'
|
||||
import { fetchAddQuestionLibrary, fetchUpdateQuestionLibrary } from '@/service/api/question'
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
operation: 'add' | 'edit'
|
||||
questionData?: any
|
||||
currentCategory: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:show', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const showQuestionModal = computed({
|
||||
get: () => props.show,
|
||||
set: val => emit('update:show', val),
|
||||
})
|
||||
|
||||
// 拼音转换工具
|
||||
const pinyinTool = ref({
|
||||
input: '',
|
||||
output: '',
|
||||
})
|
||||
|
||||
// 监听输入变化自动转换
|
||||
watch(() => pinyinTool.value.input, (val) => {
|
||||
if (val) {
|
||||
pinyinTool.value.output = pinyin(val)
|
||||
}
|
||||
else {
|
||||
pinyinTool.value.output = ''
|
||||
}
|
||||
})
|
||||
|
||||
// 复制拼音
|
||||
const copyBtnRef = ref<any>(null)
|
||||
let clipboard: Clipboard | null = null
|
||||
|
||||
watch(copyBtnRef, (inst) => {
|
||||
if (clipboard) {
|
||||
clipboard.destroy()
|
||||
clipboard = null
|
||||
}
|
||||
if (inst) {
|
||||
const domEl = inst.$el || inst
|
||||
clipboard = new Clipboard(domEl)
|
||||
clipboard.on('success', () => {
|
||||
window?.$message?.success('拼音已复制到剪贴板')
|
||||
})
|
||||
clipboard.on('error', () => {
|
||||
window?.$message?.error('复制失败,请手动复制')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard?.destroy()
|
||||
})
|
||||
|
||||
const questionForm = ref({
|
||||
name: '',
|
||||
answer: '',
|
||||
type: QuestionCategoryEnum.ChineseCharacterDictation1 as QuestionCategoryEnum | number,
|
||||
imageUrl: '',
|
||||
IsGood: 0,
|
||||
IsDisabled: false,
|
||||
KeyWord: '',
|
||||
KeyWordsIndex: 0,
|
||||
})
|
||||
|
||||
const questionFormRef = ref<FormInst | null>(null)
|
||||
|
||||
const rules = computed(() => {
|
||||
const dynamicRules: any = {
|
||||
answer: [{ required: true, message: '请输入题目答案', trigger: ['blur'] }],
|
||||
type: [{ required: true, message: '请选择题目类型', trigger: ['change'] }],
|
||||
}
|
||||
|
||||
// 题目正文内容和图片至少要有一个
|
||||
if (!questionForm.value.imageUrl) {
|
||||
dynamicRules.name = [{ required: true, message: '请输入题目正文内容或上传图片', trigger: ['blur'] }]
|
||||
}
|
||||
|
||||
// 只有“联想对对碰”类型才需要关键词
|
||||
if (questionForm.value.type === QuestionCategoryEnum.AssociationMatching) {
|
||||
dynamicRules.KeyWord = [{ required: true, message: '请输入关键词', trigger: ['blur'] }]
|
||||
dynamicRules.KeyWordsIndex = [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule: FormItemRule, value: number) => {
|
||||
return typeof value === 'number'
|
||||
},
|
||||
message: '请输入关键词索引',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return dynamicRules
|
||||
})
|
||||
|
||||
watch(() => props.show, (val) => {
|
||||
if (val) {
|
||||
if (props.operation === 'add') {
|
||||
questionForm.value = {
|
||||
name: '',
|
||||
answer: '',
|
||||
type: props.currentCategory?.QuestionType || QuestionType.SingleChoice,
|
||||
imageUrl: '',
|
||||
IsGood: 0,
|
||||
IsDisabled: false,
|
||||
KeyWord: '',
|
||||
KeyWordsIndex: 0,
|
||||
}
|
||||
pinyinTool.value.input = ''
|
||||
pinyinTool.value.output = ''
|
||||
}
|
||||
else if (props.operation === 'edit' && props.questionData) {
|
||||
const question = props.questionData
|
||||
questionForm.value = {
|
||||
name: question.Name,
|
||||
answer: question.Answer,
|
||||
type: Number(question.Type),
|
||||
KeyWord: question.KeyWord || '',
|
||||
KeyWordsIndex: question.KeyWordsIndex || 0,
|
||||
imageUrl: question.ImageUrl || '',
|
||||
IsGood: question.IsPriority || 0,
|
||||
IsDisabled: !!question.IsDisabled,
|
||||
}
|
||||
pinyinTool.value.input = ''
|
||||
pinyinTool.value.output = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function submitQuestion() {
|
||||
const valid = await questionFormRef.value?.validate()
|
||||
|
||||
if (!valid) {
|
||||
window?.$message?.error('请填写完整信息')
|
||||
return
|
||||
}
|
||||
|
||||
const params = {
|
||||
...questionForm.value,
|
||||
questionId: props.currentCategory.Id,
|
||||
// type: String(questionForm.value.type) as Api.Question.QuestionScoreType,
|
||||
}
|
||||
|
||||
if (questionForm.value.type !== QuestionCategoryEnum.AssociationMatching) {
|
||||
params.KeyWord = ''
|
||||
params.KeyWordsIndex = 0
|
||||
}
|
||||
|
||||
if (props.operation === 'add') {
|
||||
const { error } = await fetchAddQuestionLibrary({ ...params, id: 0 })
|
||||
if (!error) {
|
||||
window?.$message?.success('题目添加成功')
|
||||
emit('success')
|
||||
}
|
||||
else {
|
||||
window?.$message?.error('添加失败')
|
||||
}
|
||||
}
|
||||
else if (props.operation === 'edit' && props.questionData) {
|
||||
const { error } = await fetchUpdateQuestionLibrary({ ...params, id: props.questionData.Id })
|
||||
if (!error) {
|
||||
window?.$message?.success('题目修改成功')
|
||||
emit('success')
|
||||
}
|
||||
else {
|
||||
window?.$message?.error('修改失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="showQuestionModal" :width="800">
|
||||
<NDrawerContent :title="props.operation === 'edit' ? '编辑题目详情' : '新增题目详情'">
|
||||
<div class="mb-4">
|
||||
<NCard size="small" title="🛠️ 拼音助手 (输入汉字获取带音标拼音)" class="bg-gray-50/50">
|
||||
<div class="flex gap-2">
|
||||
<NInput v-model:value="pinyinTool.input" placeholder="输入汉字..." class="flex-1" />
|
||||
<div class="flex flex-1 items-center justify-between border border-gray-200 rounded bg-white px-3 py-1">
|
||||
<span class="text-gray-600">{{ pinyinTool.output || '拼音结果将显示在这里' }}</span>
|
||||
<textarea id="pinyinCopyTarget" v-model="pinyinTool.output" class="absolute opacity-0 -z-1" />
|
||||
<div v-if="pinyinTool.output" ref="copyBtnRef" data-clipboard-target="#pinyinCopyTarget">
|
||||
<NButton size="tiny" type="primary" secondary>
|
||||
复制
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
<NForm ref="questionFormRef" label-placement="top" :rules="rules" :model="questionForm" size="small">
|
||||
<NFormItem label="题目正文内容" path="name">
|
||||
<div
|
||||
v-if="[QuestionCategoryEnum.PoetryComprehension, QuestionCategoryEnum.PoetryCommonKnowledge, QuestionCategoryEnum.ReversePoemMatching, QuestionCategoryEnum.ChineseCharacterDictation1].includes(props.currentCategory?.QuestionType)"
|
||||
class="w-full overflow-hidden border border-gray-200 rounded-lg"
|
||||
>
|
||||
<WangEditor
|
||||
v-model="questionForm.name" placeholder="请输入题目内容..." height="300px"
|
||||
:exclude-keys="['header1', 'fontSize', 'fontFamily', 'lineHeight', 'justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify', 'group-image', 'group-video', 'insertTable', 'headerSelect']"
|
||||
/>
|
||||
</div>
|
||||
<NInput v-else v-model:value="questionForm.name" type="textarea" placeholder="请输入题目内容" :rows="3" />
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="是否优先使用" path="IsGood">
|
||||
<NRadioGroup v-model:value="questionForm.IsGood">
|
||||
<NRadio :value="0">
|
||||
否
|
||||
</NRadio>
|
||||
<NRadio :value="1">
|
||||
是
|
||||
</NRadio>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem label="是否禁用" path="IsDisabled">
|
||||
<NRadioGroup v-model:value="questionForm.IsDisabled">
|
||||
<NRadio :value="false">
|
||||
否
|
||||
</NRadio>
|
||||
<NRadio :value="true">
|
||||
是
|
||||
</NRadio>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem label="上传图片">
|
||||
<OssImageUpload v-model="questionForm.imageUrl" />
|
||||
</NFormItem>
|
||||
<template v-if="currentCategory?.QuestionType === QuestionCategoryEnum.AssociationMatching">
|
||||
<NFormItem label="关键词" path="KeyWord">
|
||||
<NInput v-model:value="questionForm.KeyWord" placeholder="请输入关键词" />
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="关键词索引" path="KeyWordsIndex">
|
||||
<NInputNumber v-model:value="questionForm.KeyWordsIndex" placeholder="请输入关键词索引" />
|
||||
</NFormItem>
|
||||
</template>
|
||||
|
||||
<NFormItem label="参考标准答案" path="answer">
|
||||
<NInput v-model:value="questionForm.answer" placeholder="正确答案" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<NButton @click="showQuestionModal = false">
|
||||
取消并返回
|
||||
</NButton>
|
||||
<NButton type="primary" @click="submitQuestion">
|
||||
保存并入库
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
@@ -1,84 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInst, UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui'
|
||||
import Clipboard from 'clipboard'
|
||||
import {
|
||||
NBreadcrumb,
|
||||
NBreadcrumbItem,
|
||||
NButton,
|
||||
NCard,
|
||||
NCheckbox,
|
||||
NDrawer,
|
||||
NDrawerContent,
|
||||
NEmpty,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NImage,
|
||||
NInput,
|
||||
NPagination,
|
||||
NRadio,
|
||||
NRadioGroup,
|
||||
NTag,
|
||||
NUpload,
|
||||
} from 'naive-ui'
|
||||
import { pinyin } from 'pinyin-pro'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import WangEditor from '@/components/common/wang-editor.vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue'
|
||||
import { QuestionCategoryEnum, QuestionType } from '@/enum/business'
|
||||
import { fetchAddQuestionLibrary, fetchDeleteQuestionLibrary, fetchGetQuestionLibraryListAll, fetchUpdateQuestionLibrary } from '@/service/api/question'
|
||||
/**
|
||||
* 为了user端题目展示的效果以及布局可控,指定规则:
|
||||
* 1.普通类型 =》用存文字存 ['汉字加一加','词语听写','汉字听写','成语写一写']
|
||||
* 2.复杂样式 =》用富文本 ['诗词理解']
|
||||
*
|
||||
*/
|
||||
import { fetchDeleteQuestionLibrary, fetchDisableQuestionLibrary, fetchGetQuestionLibraryListAll } from '@/service/api/question'
|
||||
import QuestionDrawer from './QuestionDrawer.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
currentCategory: any
|
||||
}>()
|
||||
|
||||
// 拼音转换工具
|
||||
const pinyinTool = ref({
|
||||
input: '',
|
||||
output: '',
|
||||
})
|
||||
|
||||
// 监听输入变化自动转换
|
||||
watch(() => pinyinTool.value.input, (val) => {
|
||||
if (val) {
|
||||
pinyinTool.value.output = pinyin(val)
|
||||
}
|
||||
else {
|
||||
pinyinTool.value.output = ''
|
||||
}
|
||||
})
|
||||
|
||||
// 复制拼音
|
||||
const copyBtnRef = ref<any>(null)
|
||||
let clipboard: Clipboard | null = null
|
||||
|
||||
watch(copyBtnRef, (inst) => {
|
||||
if (clipboard) {
|
||||
clipboard.destroy()
|
||||
clipboard = null
|
||||
}
|
||||
if (inst) {
|
||||
const domEl = inst.$el || inst
|
||||
clipboard = new Clipboard(domEl)
|
||||
clipboard.on('success', () => {
|
||||
window?.$message?.success('拼音已复制到剪贴板')
|
||||
})
|
||||
clipboard.on('error', () => {
|
||||
window?.$message?.error('复制失败,请手动复制')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard?.destroy()
|
||||
})
|
||||
|
||||
// 题目列表数据
|
||||
const questionList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -106,18 +47,7 @@ const pagination = ref({
|
||||
const searchText = ref('')
|
||||
const showQuestionModal = ref(false)
|
||||
const questionOperation = ref<'add' | 'edit'>('add')
|
||||
const currentQuestionId = ref<number | null>(null)
|
||||
const questionForm = ref({
|
||||
name: '',
|
||||
answer: '',
|
||||
type: QuestionType.SingleChoice as QuestionType | number, // 题目类型
|
||||
imageUrl: '',
|
||||
IsGood: 0,
|
||||
})
|
||||
|
||||
const fileList = ref<UploadFileInfo[]>([])
|
||||
|
||||
const questionFormRef = ref<FormInst | null>(null)
|
||||
const currentQuestion = ref<any>(null)
|
||||
|
||||
// 批量选择
|
||||
const selectedQuestionIds = ref<number[]>([])
|
||||
@@ -177,10 +107,29 @@ function handleBatchDelete() {
|
||||
})
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入题目正文内容', trigger: ['blur'] }],
|
||||
answer: [{ required: true, message: '请输入题目答案', trigger: ['blur'] }],
|
||||
type: [{ required: true, message: '请选择题目类型', trigger: ['change'] }],
|
||||
async function handleBatchDisable(disabled: boolean) {
|
||||
if (selectedQuestionIds.value.length === 0) {
|
||||
window?.$message?.warning('请先选择要操作的题目')
|
||||
return
|
||||
}
|
||||
|
||||
const payload = selectedQuestionIds.value.map(id => ({
|
||||
id,
|
||||
Id: id,
|
||||
IsDisabled: disabled,
|
||||
}))
|
||||
|
||||
const { error } = await fetchDisableQuestionLibrary(payload)
|
||||
if (!error) {
|
||||
window?.$message?.success(`批量${disabled ? '禁用' : '启用'}成功`)
|
||||
selectedQuestionIds.value = []
|
||||
if (props.currentCategory?.Id) {
|
||||
fetchData(props.currentCategory.Id)
|
||||
}
|
||||
}
|
||||
else {
|
||||
window?.$message?.error(`批量${disabled ? '禁用' : '启用'}失败`)
|
||||
}
|
||||
}
|
||||
|
||||
const hasCategory = computed(() => {
|
||||
@@ -198,14 +147,6 @@ watch(() => props.currentCategory, (newVal) => {
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
function customRequest({ onFinish }: UploadCustomRequestOptions) {
|
||||
onFinish()
|
||||
}
|
||||
|
||||
function handleUploadChange(data: { fileList: UploadFileInfo[] }) {
|
||||
fileList.value = data.fileList
|
||||
}
|
||||
|
||||
async function fetchData(id: number) {
|
||||
loading.value = true
|
||||
const { data, error } = await fetchGetQuestionLibraryListAll({
|
||||
@@ -234,47 +175,15 @@ function handleAddQuestion() {
|
||||
return
|
||||
}
|
||||
questionOperation.value = 'add'
|
||||
currentQuestionId.value = null
|
||||
questionForm.value = {
|
||||
name: '',
|
||||
answer: '',
|
||||
type: props.currentCategory?.QuestionType || QuestionType.SingleChoice,
|
||||
imageUrl: '',
|
||||
IsGood: 0,
|
||||
}
|
||||
fileList.value = []
|
||||
currentQuestion.value = null
|
||||
showQuestionModal.value = true
|
||||
pinyinTool.value.input = ''
|
||||
pinyinTool.value.output = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理编辑题目
|
||||
* @param id 题目ID
|
||||
*/
|
||||
function handleEditQuestion(id: number) {
|
||||
pinyinTool.value.input = ''
|
||||
pinyinTool.value.output = ''
|
||||
const question = questionList.value.find(q => q.Id === id)
|
||||
if (question) {
|
||||
questionOperation.value = 'edit'
|
||||
currentQuestionId.value = id
|
||||
questionForm.value = {
|
||||
name: question.Name,
|
||||
answer: question.Answer,
|
||||
type: question.Type,
|
||||
imageUrl: question.ImageUrl || '',
|
||||
IsGood: question.IsPriority || 0,
|
||||
}
|
||||
fileList.value = []
|
||||
if (question.ImageUrl) {
|
||||
fileList.value = [{
|
||||
id: 'existing',
|
||||
name: 'Existing Image',
|
||||
status: 'finished',
|
||||
url: question.ImageUrl,
|
||||
}]
|
||||
}
|
||||
currentQuestion.value = question
|
||||
showQuestionModal.value = true
|
||||
}
|
||||
}
|
||||
@@ -300,67 +209,10 @@ function handleDeleteQuestion(id: number) {
|
||||
})
|
||||
}
|
||||
|
||||
async function submitQuestion() {
|
||||
const valid = await questionFormRef.value?.validate()
|
||||
|
||||
if (!questionForm.value.name) {
|
||||
if (!valid) {
|
||||
window?.$message?.error('请填写完整信息')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const commonParams = {
|
||||
name: questionForm.value.name,
|
||||
answer: questionForm.value.answer,
|
||||
imageUrl: '',
|
||||
type: questionForm.value.type as any,
|
||||
questionId: props.currentCategory.Id,
|
||||
IsGood: questionForm.value.IsGood,
|
||||
}
|
||||
|
||||
let fileToUpload: File | null = null
|
||||
if (questionForm.value.type === QuestionType.Image) { // 图片类型
|
||||
if (fileList.value.length > 0 && fileList.value[0].file) {
|
||||
fileToUpload = fileList.value[0].file
|
||||
}
|
||||
// else if (questionOperation.value === 'add' && fileList.value.length === 0) {
|
||||
// window?.$message?.error('请上传图片')
|
||||
// return
|
||||
// }
|
||||
// else if (questionOperation.value === 'edit' && fileList.value.length === 0) {
|
||||
// window?.$message?.error('请上传图片')
|
||||
// return
|
||||
// }
|
||||
}
|
||||
|
||||
if (questionOperation.value === 'add') {
|
||||
const { error } = await fetchAddQuestionLibrary({
|
||||
...commonParams,
|
||||
id: 0,
|
||||
}, fileToUpload)
|
||||
if (!error) {
|
||||
window?.$message?.success('题目添加成功')
|
||||
showQuestionModal.value = false
|
||||
fetchData(props.currentCategory.Id)
|
||||
}
|
||||
else {
|
||||
window?.$message?.error('添加失败')
|
||||
}
|
||||
}
|
||||
else if (questionOperation.value === 'edit' && currentQuestionId.value) {
|
||||
const { error } = await fetchUpdateQuestionLibrary({
|
||||
...commonParams,
|
||||
id: currentQuestionId.value,
|
||||
}, fileToUpload)
|
||||
if (!error) {
|
||||
window?.$message?.success('题目修改成功')
|
||||
showQuestionModal.value = false
|
||||
fetchData(props.currentCategory.Id)
|
||||
}
|
||||
else {
|
||||
window?.$message?.error('修改失败')
|
||||
}
|
||||
function onDrawerSuccess() {
|
||||
showQuestionModal.value = false
|
||||
if (props.currentCategory?.Id) {
|
||||
fetchData(props.currentCategory.Id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -385,11 +237,7 @@ async function submitQuestion() {
|
||||
<!-- 搜索框 -->
|
||||
<div v-if="currentCategory" class="mt-2 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<NCheckbox
|
||||
:checked="isAllSelected"
|
||||
:indeterminate="isIndeterminate"
|
||||
@update:checked="handleSelectAll"
|
||||
>
|
||||
<NCheckbox :checked="isAllSelected" :indeterminate="isIndeterminate" @update:checked="handleSelectAll">
|
||||
全选 ({{ selectedQuestionIds.length }})
|
||||
</NCheckbox>
|
||||
<NInput v-model:value="searchText" placeholder="输入题目关键字在当前分类下搜索..." class="!w-80" @keyup.enter="handleSearch">
|
||||
@@ -400,6 +248,24 @@ async function submitQuestion() {
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<NButton
|
||||
v-if="selectedQuestionIds.length > 0" type="warning" secondary
|
||||
@click="() => handleBatchDisable(true)"
|
||||
>
|
||||
<template #icon>
|
||||
<SvgIcon icon="carbon:view-off" />
|
||||
</template>
|
||||
批量禁用
|
||||
</NButton>
|
||||
<NButton
|
||||
v-if="selectedQuestionIds.length > 0" type="success" secondary
|
||||
@click="() => handleBatchDisable(false)"
|
||||
>
|
||||
<template #icon>
|
||||
<SvgIcon icon="carbon:view" />
|
||||
</template>
|
||||
批量启用
|
||||
</NButton>
|
||||
<NButton v-if="selectedQuestionIds.length > 0" type="error" secondary @click="handleBatchDelete">
|
||||
<template #icon>
|
||||
<SvgIcon icon="carbon:trash-can" />
|
||||
@@ -440,8 +306,8 @@ async function submitQuestion() {
|
||||
<NCard
|
||||
v-for="q in questionList" :key="q.Id" size="small" hoverable class="rounded-xl transition-all" :class="{
|
||||
'ring-2 ring-primary-500 bg-primary-50': selectedQuestionIds.includes(q.Id),
|
||||
}"
|
||||
@click="handleSelect(q.Id, !selectedQuestionIds.includes(q.Id))"
|
||||
'opacity-50 grayscale': q.IsDisabled === 1,
|
||||
}" @click="handleSelect(q.Id, !selectedQuestionIds.includes(q.Id))"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2" @click.stop>
|
||||
@@ -455,6 +321,9 @@ async function submitQuestion() {
|
||||
<NTag size="small" :type="q.IsGood === 1 ? 'info' : 'default'" :bordered="false">
|
||||
{{ q.IsGood === 1 ? '优先' : '随机' }}
|
||||
</NTag>
|
||||
<NTag v-if="q.IsDisabled" size="small" type="error" :bordered="false">
|
||||
已禁用
|
||||
</NTag>
|
||||
</div>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
@@ -509,88 +378,13 @@ async function submitQuestion() {
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Add Question Drawer -->
|
||||
<NDrawer v-model:show="showQuestionModal" :width="800">
|
||||
<NDrawerContent :title="questionOperation === 'edit' ? '编辑题目详情' : '新增题目详情'">
|
||||
<div class="mb-4">
|
||||
<NCard size="small" title="🛠️ 拼音助手 (输入汉字获取带音标拼音)" class="bg-gray-50/50">
|
||||
<div class="flex gap-2">
|
||||
<NInput v-model:value="pinyinTool.input" placeholder="输入汉字..." class="flex-1" />
|
||||
<div class="flex flex-1 items-center justify-between border border-gray-200 rounded bg-white px-3 py-1">
|
||||
<span class="text-gray-600">{{ pinyinTool.output || '拼音结果将显示在这里' }}</span>
|
||||
<textarea id="pinyinCopyTarget" v-model="pinyinTool.output" class="absolute opacity-0 -z-1" />
|
||||
<div v-if="pinyinTool.output" ref="copyBtnRef" data-clipboard-target="#pinyinCopyTarget">
|
||||
<NButton size="tiny" type="primary" secondary>
|
||||
复制
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
<NForm ref="questionFormRef" label-placement="top" :rules="rules" :model="questionForm" size="small">
|
||||
<NFormItem label="题目正文内容" path="name">
|
||||
<div v-if="[QuestionCategoryEnum.PoetryComprehension].includes(props.currentCategory?.QuestionType)" class="w-full overflow-hidden border border-gray-200 rounded-lg">
|
||||
<WangEditor
|
||||
v-model="questionForm.name" placeholder="请输入题目内容..." height="300px"
|
||||
:exclude-keys="['header1', 'fontSize', 'fontFamily', 'lineHeight', 'justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify', 'group-image', 'group-video', 'insertTable', 'headerSelect']"
|
||||
/>
|
||||
</div>
|
||||
<NInput v-else v-model:value="questionForm.name" type="textarea" placeholder="请输入题目内容" :rows="3" />
|
||||
</NFormItem>
|
||||
|
||||
<!-- <div class="grid grid-cols-2 gap-4">
|
||||
<NFormItem label="参考分值 (Pts)">
|
||||
<NInputNumber v-model:value="questionForm.score" class="w-full" :min="1" />
|
||||
</NFormItem>
|
||||
<NFormItem label="限时 (S)">
|
||||
<NInputNumber v-model:value="questionForm.time" class="w-full" :min="1" />
|
||||
</NFormItem>
|
||||
</div> -->
|
||||
|
||||
<!-- 类型 -->
|
||||
<!-- <NFormItem label="类型" path="type">
|
||||
<NSelect v-model:value="questionForm.type" class="w-full" :options="questionTypeOptionsDict" />
|
||||
</NFormItem> -->
|
||||
|
||||
<!-- 是否优先使用 -->
|
||||
<NFormItem label="是否优先使用" path="IsGood">
|
||||
<NRadioGroup v-model:value="questionForm.IsGood">
|
||||
<NRadio :value="0">
|
||||
否
|
||||
</NRadio>
|
||||
<NRadio :value="1">
|
||||
是
|
||||
</NRadio>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<!-- 上传图片 -->
|
||||
<NFormItem v-if="questionForm.type === QuestionType.Image" label="上传图片">
|
||||
<NUpload
|
||||
v-model:file-list="fileList" accept="image/*" :max="1" list-type="image-card"
|
||||
:custom-request="customRequest" @change="handleUploadChange"
|
||||
>
|
||||
点击上传
|
||||
</NUpload>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="参考标准答案" path="answer">
|
||||
<NInput v-model:value="questionForm.answer" placeholder="正确答案" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<NButton @click="showQuestionModal = false">
|
||||
取消并返回
|
||||
</NButton>
|
||||
<NButton type="primary" @click="submitQuestion">
|
||||
保存并入库
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
<QuestionDrawer
|
||||
v-model:show="showQuestionModal"
|
||||
:operation="questionOperation"
|
||||
:question-data="currentQuestion"
|
||||
:current-category="currentCategory"
|
||||
@success="onDrawerSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -150,8 +150,8 @@ function edit(item: any) {
|
||||
<NCard :bordered="false" page-size="small" class="card-wrapper sm:flex-1-hidden">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-16px font-bold">
|
||||
<div class="mid-title">
|
||||
<div class="text-2xl font-bold">
|
||||
{{ rankTitle }}
|
||||
</div>
|
||||
<div class="mt-4px text-12px text-gray-500">
|
||||
|
||||