Compare commits
38 Commits
main
...
9d801ba6b4
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d801ba6b4 | |||
| 11f448cedc | |||
| 41f7ba7b8a | |||
| b9c5886437 | |||
| 11fb5481d3 | |||
| 8469e16638 | |||
| e8cd6732d7 | |||
| ed3c815cd7 | |||
| 94af0bec6a | |||
| 81643c61ae | |||
| 2b50bd2c71 | |||
| e70409082a | |||
| f1ff053b68 | |||
| f3136cd9ad | |||
| 61a7412fc1 | |||
| 0fd8ef527a | |||
| b71a758474 | |||
| ca42c6a876 | |||
| ce5ee2a289 | |||
| e1a19c48e0 | |||
| b39761d58e | |||
| beaaee72b4 | |||
| 82b1dd7ebe | |||
| ba38accdb9 | |||
| 16d7ecd172 | |||
| 78e51d96f6 | |||
| 133d9a903a | |||
| c3d23224ba | |||
| f6d2a643e8 | |||
| 5a48a65ef8 | |||
| 264f600250 | |||
| f1dacf7c66 | |||
| 697d606611 | |||
| d950bb4018 | |||
| 89773bd49b | |||
| 3270e24c25 | |||
| e6346a771e | |||
| 764b5727ba |
7
.vscode/settings.json
vendored
@ -36,5 +36,10 @@
|
||||
"./apps/user",
|
||||
"./packages/eslint-config"
|
||||
],
|
||||
"totvsLanguageServer.welcomePage": false // `
|
||||
"totvsLanguageServer.welcomePage": false,
|
||||
"cSpell.words": [
|
||||
"activityinfo",
|
||||
"Echarts",
|
||||
"Typeit"
|
||||
] // `
|
||||
}
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
# 应用的基础URL,默认为 "/"
|
||||
# 如果使用子目录,必须以 "/" 结尾,例如 "/admin/" 而不是 "/admin"
|
||||
VITE_BASE_URL=/
|
||||
# VITE_BASE_URL='/admin'
|
||||
|
||||
# VITE_BASE_URL="https://api.qyzhjy.com"
|
||||
|
||||
VITE_APP_TITLE="阅读比赛配置后台"
|
||||
|
||||
# 阿里云OSS地址前缀(用于拼接完整地址)
|
||||
VITE_BASE_OSS_URL = https://oss.qyzhjy.com/
|
||||
|
||||
# 上传图片的URL
|
||||
VITE_BASE_UPLOAD_URL="http://192.168.20.150:8080"
|
||||
|
||||
VITE_APP_DESC="阅读比赛配置"
|
||||
|
||||
# 图标名称的前缀
|
||||
@ -16,23 +24,26 @@ VITE_ICON_LOCAL_PREFIX=icon-local
|
||||
# 认证路由模式: static (静态) | dynamic (动态)
|
||||
VITE_AUTH_ROUTE_MODE=static
|
||||
|
||||
# 是否强制登录: Y (开启) | N (关闭)
|
||||
VITE_AUTH_ROUTE_FORCE_LOGIN=N
|
||||
|
||||
# 静态认证路由的主页
|
||||
VITE_ROUTE_HOME=home
|
||||
VITE_ROUTE_HOME=user_home
|
||||
|
||||
# 默认菜单图标
|
||||
VITE_MENU_ICON=mdi:menu
|
||||
|
||||
# 在开发模式下是否启用http代理
|
||||
VITE_HTTP_PROXY=Y
|
||||
VITE_HTTP_PROXY=Y # Y 开启后,所有请求都将通过代理服务器,N 关闭后,所有请求都将直接发送到后端服务
|
||||
|
||||
# vue-router 模式: hash | history | memory
|
||||
VITE_ROUTER_HISTORY_MODE=history
|
||||
|
||||
# 后端服务成功代码,收到此代码表示请求成功
|
||||
VITE_SERVICE_SUCCESS_CODE=0000
|
||||
VITE_SERVICE_SUCCESS_CODE=200
|
||||
|
||||
# 后端服务登出代码,收到此代码将登出并重定向到登录页
|
||||
VITE_SERVICE_LOGOUT_CODES=8888,8889
|
||||
VITE_SERVICE_LOGOUT_CODES=401,403
|
||||
|
||||
# 后端服务模态框登出代码,收到此代码将通过显示模态框登出
|
||||
VITE_SERVICE_MODAL_LOGOUT_CODES=7777,7778
|
||||
|
||||
12
apps/admin/.env.dev
Normal file
@ -0,0 +1,12 @@
|
||||
# backend service base url, test environment
|
||||
# VITE_SERVICE_BASE_URL=https://mock.apifox.cn/m1/3109515-0-default
|
||||
|
||||
# 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_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= `{
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
# backend service base url, test environment
|
||||
VITE_SERVICE_BASE_URL=https://mock.apifox.cn/m1/3109515-0-default
|
||||
|
||||
# other backend service base url, test environment
|
||||
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||
"demo": "http://localhost:9528"
|
||||
}`
|
||||
@ -4,10 +4,10 @@ import { bgRed, bgYellow, green, lightBlue } from 'kolorist'
|
||||
import { createServiceConfig } from '../../src/utils/service'
|
||||
|
||||
/**
|
||||
* Set http proxy
|
||||
* 设置 HTTP 代理
|
||||
*
|
||||
* @param env - The current env
|
||||
* @param enable - If enable http proxy
|
||||
* @param env - 当前环境变量
|
||||
* @param enable - 是否启用 HTTP 代理
|
||||
*/
|
||||
export function createViteProxy(env: Env.ImportMeta, enable: boolean) {
|
||||
const isEnableHttpProxy = enable && env.VITE_HTTP_PROXY === 'Y'
|
||||
@ -28,27 +28,34 @@ export function createViteProxy(env: Env.ImportMeta, enable: boolean) {
|
||||
return proxy
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 HTTP 代理项
|
||||
*
|
||||
* @param item - 服务配置项
|
||||
* @param enableLog - 是否启用日志记录
|
||||
*/
|
||||
function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean) {
|
||||
const proxy: Record<string, ProxyOptions> = {}
|
||||
|
||||
proxy[item.proxyPattern] = {
|
||||
target: item.baseURL,
|
||||
changeOrigin: true,
|
||||
secure: false, // 如果是https接口,需要配置这个参数
|
||||
configure: (_proxy, options) => {
|
||||
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
|
||||
if (!enableLog)
|
||||
return
|
||||
|
||||
const requestUrl = `${lightBlue('[proxy url]')}: ${bgYellow(` ${req.method} `)} ${green(`${item.proxyPattern}${req.url}`)}`
|
||||
const requestUrl = `${lightBlue('[代理地址]')}: ${bgYellow(` ${req.method} `)} ${green(`${item.proxyPattern}${req.url}`)}`
|
||||
|
||||
const proxyUrl = `${lightBlue('[real request url]')}: ${green(`${options.target}${req.url}`)}`
|
||||
const proxyUrl = `${lightBlue('[真实请求地址]')}: ${green(`${options.target}${req.url}`)}`
|
||||
|
||||
consola.log(`${requestUrl}\n${proxyUrl}`)
|
||||
})
|
||||
_proxy.on('error', (_err, req, _res) => {
|
||||
if (!enableLog)
|
||||
return
|
||||
consola.log(bgRed(`Error: ${req.method} `), green(`${options.target}${req.url}`))
|
||||
consola.log(bgRed(`错误: ${req.method} `), green(`${options.target}${req.url}`))
|
||||
})
|
||||
},
|
||||
rewrite: path => path.replace(new RegExp(`^${item.proxyPattern}`), ''),
|
||||
|
||||
34
apps/admin/nginx.conf
Normal file
@ -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;
|
||||
# }
|
||||
}
|
||||
@ -9,11 +9,11 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build --mode prod",
|
||||
"build:test": "vite build --mode test",
|
||||
"build:dev": "vite build --mode dev",
|
||||
"cleanup": "sa cleanup",
|
||||
"commit": "sa git-commit",
|
||||
"commit:zh": "sa git-commit -l=zh-cn",
|
||||
"dev": "vite --mode test",
|
||||
"dev": "vite --mode dev",
|
||||
"dev:prod": "vite --mode prod",
|
||||
"gen-route": "sa gen-route",
|
||||
"lint": "eslint . --fix",
|
||||
@ -25,13 +25,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-scroll/core": "2.5.1",
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@iconify/vue": "5.0.0",
|
||||
"@opentiny/fluent-editor": "^4.0.1",
|
||||
"@sa/axios": "workspace:*",
|
||||
"@sa/color": "workspace:*",
|
||||
"@sa/hooks": "workspace:*",
|
||||
"@sa/materials": "workspace:*",
|
||||
"@sa/utils": "workspace:*",
|
||||
"@sapphire/snowflake": "^3.5.5",
|
||||
"@vueuse/core": "14.1.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"@zumer/snapdom": "^2.0.1",
|
||||
"ali-oss": "^6.23.0",
|
||||
"clipboard": "2.0.11",
|
||||
"dayjs": "1.11.19",
|
||||
"defu": "6.1.4",
|
||||
@ -39,20 +46,30 @@
|
||||
"json5": "2.2.3",
|
||||
"naive-ui": "2.43.2",
|
||||
"nprogress": "0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pdfjs-dist": "^5.4.530",
|
||||
"pinia": "3.0.4",
|
||||
"pinyin-pro": "^3.28.0",
|
||||
"quill-toolbar-tip": "^0.1.0",
|
||||
"tailwind-merge": "3.4.0",
|
||||
"typeit": "^8.8.7",
|
||||
"v-scale-screen": "^2.3.0",
|
||||
"vue": "3.5.26",
|
||||
"vue-draggable-plus": "0.6.0",
|
||||
"vue-i18n": "11.2.7",
|
||||
"vue-router": "4.6.4"
|
||||
"vue-router": "4.6.4",
|
||||
"vue3-draggable-resizable": "^1.6.5",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elegant-router/vue": "0.3.8",
|
||||
"@iconify/json": "2.2.417",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@types/ali-oss": "^6.16.13",
|
||||
"@types/node": "25.0.3",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@unocss/eslint-config": "66.5.10",
|
||||
"@unocss/preset-icons": "66.5.10",
|
||||
"@unocss/preset-uno": "66.5.10",
|
||||
@ -72,6 +89,7 @@
|
||||
"unplugin-icons": "22.5.0",
|
||||
"unplugin-vue-components": "30.0.0",
|
||||
"vite": "7.3.0",
|
||||
"vite-auto-deploy": "^2.0.34",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-plugin-vue-devtools": "8.0.5",
|
||||
|
||||
@ -4,6 +4,7 @@ import { darkTheme, NConfigProvider } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
import { naiveDateLocales, naiveLocales } from './locales/naive'
|
||||
import { useAppStore } from './store/modules/app'
|
||||
import { useBusinessStore } from './store/modules/business'
|
||||
import { useThemeStore } from './store/modules/theme'
|
||||
|
||||
defineOptions({
|
||||
@ -12,6 +13,9 @@ defineOptions({
|
||||
|
||||
const appStore = useAppStore()
|
||||
const themeStore = useThemeStore()
|
||||
const businessStore = useBusinessStore()
|
||||
/* 初始化字典 */
|
||||
businessStore.initDict()
|
||||
|
||||
const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined))
|
||||
|
||||
|
||||
BIN
apps/admin/src/assets/audio/boy-10s.mp3
Normal file
BIN
apps/admin/src/assets/audio/boy-5s.mp3
Normal file
BIN
apps/admin/src/assets/audio/girl-10.mp3
Normal file
BIN
apps/admin/src/assets/audio/start3.mp3
Normal file
BIN
apps/admin/src/assets/audio/system-10.mp3
Normal file
BIN
apps/admin/src/assets/imgs/add-f1.png
Normal file
|
After Width: | Height: | Size: 656 KiB |
9
apps/admin/src/assets/imgs/user/cover-back.svg
Normal file
|
After Width: | Height: | Size: 8.8 MiB |
9
apps/admin/src/assets/imgs/user/cover-bg-active.svg
Normal file
|
After Width: | Height: | Size: 11 MiB |
26
apps/admin/src/assets/imgs/user/draw.svg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
9
apps/admin/src/assets/imgs/user/event-card-bg-icon.svg
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
9
apps/admin/src/assets/imgs/user/event-card-bg.svg
Normal file
|
After Width: | Height: | Size: 7.2 MiB |
35
apps/admin/src/assets/imgs/user/group-type-1.svg
Normal file
|
After Width: | Height: | Size: 790 KiB |
23
apps/admin/src/assets/imgs/user/group-type-2.svg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
9
apps/admin/src/assets/imgs/user/home-bg.svg
Normal file
|
After Width: | Height: | Size: 19 MiB |
BIN
apps/admin/src/assets/imgs/user/q5-img.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
32
apps/admin/src/assets/imgs/user/rank-title.svg
Normal file
|
After Width: | Height: | Size: 429 KiB |
BIN
apps/admin/src/assets/imgs/user/read-logo.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
66
apps/admin/src/assets/imgs/user/star-icon.svg
Normal file
|
After Width: | Height: | Size: 10 MiB |
46
apps/admin/src/assets/imgs/user/team-bg-animation.svg
Normal file
|
After Width: | Height: | Size: 16 MiB |
36
apps/admin/src/assets/imgs/user/team-bg.svg
Normal file
|
After Width: | Height: | Size: 16 MiB |
14
apps/admin/src/assets/imgs/user/team-title-bg.svg
Normal file
|
After Width: | Height: | Size: 4.0 MiB |
48
apps/admin/src/assets/imgs/user/team-title-icon.svg
Normal file
|
After Width: | Height: | Size: 5.5 MiB |
15
apps/admin/src/assets/imgs/user/tianzige.svg
Normal file
|
After Width: | Height: | Size: 12 MiB |
9
apps/admin/src/assets/imgs/user/title-bg.svg
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
9
apps/admin/src/assets/imgs/user/title-bg2.svg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
apps/admin/src/assets/imgs/副本第九届“阅读之星”题目示例 (1).xlsx
Normal file
BIN
apps/admin/src/assets/imgs/微信图片_20260204103609_668_5591.png
Normal file
|
After Width: | Height: | Size: 468 KiB |
@ -10,10 +10,10 @@ const ContextHolder = defineComponent({
|
||||
name: 'ContextHolder',
|
||||
setup() {
|
||||
function register() {
|
||||
window.$loadingBar = useLoadingBar()
|
||||
window.$dialog = useDialog()
|
||||
window.$message = useMessage()
|
||||
window.$notification = useNotification()
|
||||
window.$loadingBar = useLoadingBar() // 注册全局 loading bar
|
||||
window.$dialog = useDialog()// 注册全局 dialog
|
||||
window.$message = useMessage()// 注册全局 message
|
||||
window.$notification = useNotification() // 注册全局 notification
|
||||
}
|
||||
|
||||
register()
|
||||
|
||||
153
apps/admin/src/components/common/oss-image-upload/index.vue
Normal file
@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadCustomRequestOptions } from 'naive-ui'
|
||||
import { NButton, NSpin, NUpload, useMessage } from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
import { getAliOssTokenAxios } from '@/service/api/upload'
|
||||
import { browserPathJoin } from '@/utils/date'
|
||||
import { initOSSClient, uploadFileToOSS } from '@/utils/oss'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** 图片地址 */
|
||||
modelValue?: string
|
||||
/** 上传路径 */
|
||||
path?: string // 上传路径,默认 temp/ 目录,同名文件会覆盖
|
||||
/** 提示文字 */
|
||||
hint?: string
|
||||
/** 最大文件大小 (KB) */
|
||||
maxSize?: number
|
||||
/** 接受的文件类型 */
|
||||
accept?: string
|
||||
/** 模式: default-大图上传(带文字), mini-小图上传(仅图标) */
|
||||
variant?: 'default' | 'mini'
|
||||
}>(), {
|
||||
hint: '支持 JPG/PNG 格式',
|
||||
accept: 'image/*',
|
||||
variant: 'default',
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const loading = ref(false)
|
||||
const message = useMessage()
|
||||
|
||||
async function customRequest({ file, onFinish, onError }: UploadCustomRequestOptions) {
|
||||
try {
|
||||
// 校验文件大小
|
||||
if (props.maxSize && file.file) {
|
||||
const sizeKB = file.file.size / 1024
|
||||
if (sizeKB > props.maxSize) {
|
||||
const maxSizeText = props.maxSize >= 1024 ? `${(props.maxSize / 1024).toFixed(2)}MB` : `${props.maxSize}KB`
|
||||
throw new Error(`文件大小不能超过 ${maxSizeText}`)
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
// 1. 获取上传凭证
|
||||
const { data: tokenData, error: tokenError } = await getAliOssTokenAxios()
|
||||
if (tokenError || !tokenData) {
|
||||
throw new Error('获取上传凭证失败')
|
||||
}
|
||||
|
||||
// 2. 初始化 OSS 客户端
|
||||
const client = initOSSClient(tokenData.data || {})
|
||||
|
||||
// 3. 准备路径
|
||||
let path = props.path || `temp/${Date.now()}`
|
||||
path = browserPathJoin(path, file.name)
|
||||
|
||||
// 4. 上传文件
|
||||
await uploadFileToOSS(client, file.file as File, path)
|
||||
|
||||
// 5. 获取 URL
|
||||
const url = browserPathJoin(import.meta.env.VITE_BASE_OSS_URL, path)
|
||||
|
||||
emit('update:modelValue', url)
|
||||
onFinish()
|
||||
}
|
||||
catch (error: any) {
|
||||
message.error(error.message || '上传失败')
|
||||
onError()
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
emit('update:modelValue', '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NUpload
|
||||
:accept="accept"
|
||||
:show-file-list="false"
|
||||
:custom-request="customRequest"
|
||||
class="block"
|
||||
:class="{ 'w-full': props.variant === 'default' }"
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-col cursor-pointer items-center justify-center overflow-hidden transition-all hover:bg-gray-100"
|
||||
:class="[
|
||||
props.variant === 'default' ? 'h-[400px] w-full rounded-3xl bg-[#F5F8FF]' : 'h-full w-full bg-[#F5F8FF] rounded-lg',
|
||||
!modelValue && props.variant === 'default' ? 'border-2 border-dashed border-gray-300' : '',
|
||||
!modelValue && props.variant === 'mini' ? 'border border-dashed border-gray-300' : '',
|
||||
]"
|
||||
>
|
||||
<div v-if="loading" class="absolute inset-0 z-50 flex items-center justify-center bg-white/50">
|
||||
<NSpin :size="props.variant === 'mini' ? 'small' : 'large'" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="modelValue"
|
||||
class="group absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity hover:opacity-100"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<!-- NUpload 的 trigger 会自动处理点击事件 -->
|
||||
<NButton v-if="props.variant === 'default'" ghost color="#fff" size="small">
|
||||
更换
|
||||
</NButton>
|
||||
<div v-else class="cursor-pointer text-xs text-white">
|
||||
更换
|
||||
</div>
|
||||
<NButton v-if="props.variant === 'default'" ghost color="#fff" size="small" @click.stop="handleRemove">
|
||||
删除
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
v-if="modelValue"
|
||||
:src="modelValue"
|
||||
class="h-full w-full object-cover"
|
||||
alt="uploaded"
|
||||
>
|
||||
|
||||
<div v-else class="flex flex-col items-center justify-center text-gray-400">
|
||||
<template v-if="props.variant === 'default'">
|
||||
<slot name="icon">
|
||||
<icon-ic-baseline-upload class="mb-4 text-6xl" />
|
||||
</slot>
|
||||
<div class="text-lg text-gray-600 font-medium">
|
||||
点击上传
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-gray-400">
|
||||
{{ hint }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<icon-ic-baseline-upload class="text-xl" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</NUpload>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.n-upload-trigger) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
488
apps/admin/src/components/common/rest-upload/rest-upload.vue
Normal file
@ -0,0 +1,488 @@
|
||||
<!-- eslint-disable no-console -->
|
||||
<script setup lang="ts"
|
||||
generic="K extends string | string[] | Record<string, unknown> | Record<string, unknown>[] | null | undefined"
|
||||
>
|
||||
import type OSS from 'ali-oss'
|
||||
import type { UploadFileInfo } from 'naive-ui'
|
||||
import type { PropType, Ref } from 'vue'
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/vue'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import { getAliOssTokenAxios } from '@/service/api/upload'
|
||||
import { getOnlyId } from '@/utils/data'
|
||||
import { browserPathJoin } from '@/utils/date'
|
||||
import { fileToBase64, getFirstFrameOfVideo } from '@/utils/file'
|
||||
import { initOSSClient, uploadFileToOSS } from '@/utils/oss'
|
||||
import { FileUploadService } from '@/utils/upload-util'
|
||||
import { isObject } from '@/utils/verify'
|
||||
import { verifyAddIntRoZeroNumb } from '@/utils/verify-number'
|
||||
|
||||
export interface FileItem {
|
||||
/** 唯一 ID */
|
||||
__id: number | string
|
||||
/** 浏览器中的文件对象 */
|
||||
file?: File
|
||||
/** 进度 */
|
||||
percentage: number
|
||||
/** 当前 index */
|
||||
index: number
|
||||
/** 上传状态; 0:正在上传; 1:上传成功; -1:上传失败; */
|
||||
uploadStatus: -1 | 0 | 1
|
||||
/** 错误原因 */
|
||||
errMsg?: string
|
||||
/** url */
|
||||
url: string
|
||||
/** 海报 */
|
||||
poster: string
|
||||
}
|
||||
|
||||
export interface CustomKeys {
|
||||
/** 绑定上传文件链接的 key(即modelValue为对象或对象数组时绑定的上传文件链接的key) */
|
||||
bindFileUrlKey?: string
|
||||
/** 绑定返回海报链接的 key(即modelValue为对象或对象数组时绑定的海报链接的key) */
|
||||
bindPosterUrlKey?: string
|
||||
/** 上传成功后服务器返回链接中文件链接的 key */
|
||||
upFileUrlKey?: string
|
||||
/** 上传成功后服务器返回海报字段的的 key */
|
||||
upPosterUrlKey?: string
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
/** 上传图片的URL */
|
||||
path: { type: String, default: undefined },
|
||||
/** 自定义 key */
|
||||
customKeys: { type: Object as PropType<CustomKeys>, default: (): CustomKeys => ({}) },
|
||||
/** 提示文字 */
|
||||
hint: { type: String, default: '点击上传' },
|
||||
/** 图片对象数组 */
|
||||
modelValue: { type: [Array, String, Object, null, undefined] as PropType<K>, default: () => [] },
|
||||
/** 宽度 */
|
||||
width: { type: [String, Number], default: 100 },
|
||||
/** 高度 */
|
||||
height: { type: [String, Number], default: 100 },
|
||||
/** 最多上传几张图片 */
|
||||
limit: { type: Number, default: 1 },
|
||||
/** 限制可以选择的文件类型(根据后缀限制) 如:['.png', 'jpg'] */
|
||||
accept: { type: Array as PropType<`.${string}`[]>, default: () => ['.png', '.jpg'] },
|
||||
/** 加载状态 */
|
||||
loading: { type: Boolean, default: false },
|
||||
/** 选择图片完成后的回调 */
|
||||
afterSelectFile: { type: Function as PropType<(data: File[]) => File[] | Promise<File[]>>, default: undefined },
|
||||
/** 上传成功后的回调 */
|
||||
afterUpload: { type: Function as PropType<(data: OSS.MultipartUploadResult) => Record<string, unknown> | string>, default: undefined },
|
||||
/** 限制视频最大允许上传多少 kb, */
|
||||
maxVideoSize: { type: Number, default: undefined },
|
||||
/** 限制图片最小允许上传多少 kb,默认 1 */
|
||||
minImageSize: { type: Number, default: 1 },
|
||||
/** 限制图片最大允许上传多少 kb,默认 800 */
|
||||
maxImageSize: { type: Number, default: undefined },
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', list: FileItem[]): void
|
||||
(e: 'update:loading', state: boolean): void
|
||||
(e: 'update:modelValue', list: any): void
|
||||
(e: 'error', myFile: FileItem): void
|
||||
}>()
|
||||
|
||||
const [parent] = useAutoAnimate()
|
||||
const upLoading = ref(props.loading)
|
||||
|
||||
// 视频预览状态
|
||||
const showVideoPreview = ref(false)
|
||||
const videoPreviewSrc = ref('')
|
||||
const videoPreviewPoster = ref('')
|
||||
|
||||
const bindFileUrlKey = computed(() => props.customKeys.bindFileUrlKey || 'url')
|
||||
const bindPosterUrlKey = computed(() => props.customKeys.bindPosterUrlKey || 'frameUrl')
|
||||
const upFileUrlKey = computed(() => props.customKeys.upFileUrlKey || 'url')
|
||||
const upPosterUrlKey = computed(() => props.customKeys.upPosterUrlKey || 'frameUrl')
|
||||
|
||||
const newWidth = computed(() => (verifyAddIntRoZeroNumb(`${props.width}`) || typeof props.width === 'number' ? `${props.width}px` : props.width))
|
||||
const newHeight = computed(() => (verifyAddIntRoZeroNumb(`${props.height}`) || typeof props.height === 'number' ? `${props.height}px` : props.height))
|
||||
|
||||
/** 全部的文件(包含上传和未上传的) */
|
||||
const fileList: Ref<FileItem[]> = ref(getDefFiles(props.modelValue))
|
||||
/** 得到上传成功的文件(包含本次上传成功和之前上传成功的) */
|
||||
const uploadedList = shallowRef<Record<string, string>[] | string[]>(getUploadedList())
|
||||
/** 最终需要得到文件(根据是多选还是单选整理成需要的文件) */
|
||||
const uploadedFiles = computed(() => (props.limit > 1 ? (uploadedList.value as K) : (uploadedList.value[0] as K)))
|
||||
/** 现在还可以添加多少个 */
|
||||
const efficientNumb = computed(() => props.limit - uploadedList.value.length - fileList.value.length)
|
||||
|
||||
// NUpload 文件列表,用于选择后清除
|
||||
const nUploadFileList = ref<UploadFileInfo[]>([])
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
fileList.value = getDefFiles(val)
|
||||
},
|
||||
)
|
||||
watch(
|
||||
() => upLoading.value,
|
||||
(val) => {
|
||||
emit('update:loading', val)
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => uploadedFiles.value,
|
||||
(val) => {
|
||||
emit('update:modelValue', val)
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* 视频预览
|
||||
*/
|
||||
function previewVideoClick(src: string, poster: string) {
|
||||
if (src) {
|
||||
videoPreviewSrc.value = src
|
||||
videoPreviewPoster.value = poster
|
||||
showVideoPreview.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 (已经上传的)
|
||||
*/
|
||||
function deleteClick(index: number) {
|
||||
fileList.value.splice(index, 1)
|
||||
emit('change', fileList.value)
|
||||
uploadedList.value = getUploadedList()
|
||||
}
|
||||
/**
|
||||
* 重新上传
|
||||
*/
|
||||
async function reUploadClick(index: number) {
|
||||
try {
|
||||
upLoading.value = true
|
||||
await uploadFile(fileList.value[index]!)
|
||||
upLoading.value = false
|
||||
emit('change', fileList.value)
|
||||
uploadedList.value = getUploadedList()
|
||||
}
|
||||
catch (error) {
|
||||
upLoading.value = false
|
||||
uploadedList.value = getUploadedList()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('重新上传失败====', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
function reset() {
|
||||
fileList.value = []
|
||||
uploadedList.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
function handleUploadChange(options: { fileList: UploadFileInfo[] }) {
|
||||
// 获取新添加的文件
|
||||
const newFiles = options.fileList.map(item => item.file).filter(Boolean) as File[]
|
||||
|
||||
// 清除 NUpload 列表以避免重复并允许重新选择
|
||||
nUploadFileList.value = []
|
||||
|
||||
if (newFiles.length > 0) {
|
||||
uploadFileList(newFiles)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理上传所有图片
|
||||
*/
|
||||
async function uploadFileList(files: File[] | null) {
|
||||
try {
|
||||
if (files && files.length > 0) {
|
||||
let processFiles = files
|
||||
if (props.afterSelectFile) {
|
||||
processFiles = await props.afterSelectFile(files)
|
||||
}
|
||||
|
||||
const promiseArr: FileItem[] = []
|
||||
// 使用临时列表以避免循环期间 fileList 发生变化的问题(尽管 push 是安全的)
|
||||
// 但我们需要将其推入 fileList 以立即显示它们
|
||||
for (let i = 0, len = processFiles.length; i < len; i++) {
|
||||
if (efficientNumb.value > 0 && processFiles[i]) {
|
||||
const newItem: FileItem = {
|
||||
file: processFiles[i]!,
|
||||
percentage: 0,
|
||||
uploadStatus: 0,
|
||||
index: fileList.value.length,
|
||||
__id: getOnlyId(),
|
||||
url: '',
|
||||
poster: '',
|
||||
}
|
||||
fileList.value.push(newItem)
|
||||
// 必须获取响应式对象,否则进度条不会更新
|
||||
promiseArr.push(fileList.value[fileList.value.length - 1])
|
||||
}
|
||||
}
|
||||
|
||||
if (promiseArr.length > 0) {
|
||||
upLoading.value = true
|
||||
const uploadService = new FileUploadService(uploadFile, 5)
|
||||
await uploadService.upload(promiseArr)
|
||||
upLoading.value = false
|
||||
emit('change', fileList.value)
|
||||
uploadedList.value = getUploadedList()
|
||||
}
|
||||
else {
|
||||
window.$message?.warning('暂无可上传文件或已达上限')
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
uploadedList.value = getUploadedList()
|
||||
window.$message?.error('上传失败')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
async function uploadFile(myFile: FileItem): Promise<FileItem> {
|
||||
try {
|
||||
const fileSize = myFile.file!.size / 1024
|
||||
if (myFile.file!.type.includes('image')) {
|
||||
if (typeof props.maxImageSize === 'number' && fileSize > props.maxImageSize) {
|
||||
const msg = getRestrictSizeMsg(props.maxImageSize, 'max')
|
||||
myFile.uploadStatus = -1
|
||||
myFile.errMsg = msg
|
||||
return Promise.reject(new Error(msg))
|
||||
}
|
||||
else if (fileSize < props.minImageSize) {
|
||||
const msg = getRestrictSizeMsg(props.minImageSize, 'min')
|
||||
myFile.uploadStatus = -1
|
||||
myFile.errMsg = msg
|
||||
return Promise.reject(new Error(msg))
|
||||
}
|
||||
}
|
||||
if (typeof props.maxVideoSize === 'number' && myFile.file!.type.includes('video') && fileSize > props.maxVideoSize) {
|
||||
const msg = getRestrictSizeMsg(props.maxVideoSize, 'max')
|
||||
myFile.uploadStatus = -1
|
||||
myFile.errMsg = msg
|
||||
return Promise.reject(new Error(msg))
|
||||
}
|
||||
const { data: tokenData, error: tokenError } = await getAliOssTokenAxios()
|
||||
if (tokenError || !tokenData) {
|
||||
throw new Error('获取上传凭证失败')
|
||||
}
|
||||
// 初始化OSS客户端
|
||||
const client = initOSSClient(tokenData?.data || {})
|
||||
let path = typeof props.path === 'string' ? '' : `temp/${Date.now()}`
|
||||
path = browserPathJoin(path, myFile.file!.name)
|
||||
// 上传文件
|
||||
const res: OSS.MultipartUploadResult = await uploadFileToOSS(client, myFile.file!, path, (progress) => {
|
||||
myFile.percentage = Math.floor((progress || 0) * 10000) / 100
|
||||
})
|
||||
const url = browserPathJoin(import.meta.env.VITE_BASE_OSS_URL, path)
|
||||
const data = typeof props.afterUpload === 'function' ? props.afterUpload(res) : res
|
||||
if (isObject(data)) {
|
||||
const newData = data as Record<string, string>
|
||||
myFile.url = upFileUrlKey.value in newData ? String(newData[upFileUrlKey.value] || url) : url
|
||||
myFile.poster = upPosterUrlKey.value in newData ? String(newData[upPosterUrlKey.value] || '') : ''
|
||||
if (myFile.file!.type.includes('video') && !myFile.poster) {
|
||||
try {
|
||||
const info = await getFirstFrameOfVideo(myFile.file!)
|
||||
myFile.poster = (await fileToBase64(info.firstFrame)).img || ''
|
||||
}
|
||||
catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('error====', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
myFile.url = url
|
||||
}
|
||||
myFile.uploadStatus = 1
|
||||
return Promise.resolve(myFile)
|
||||
}
|
||||
catch (error: any) {
|
||||
console.log('上传错误====', error)
|
||||
myFile.uploadStatus = -1
|
||||
myFile.errMsg = typeof error.msg === 'string' ? error.msg : '上传失败'
|
||||
emit('error', error)
|
||||
return Promise.reject(new Error(myFile.errMsg))
|
||||
}
|
||||
}
|
||||
|
||||
/** 得到初始已上传的文件 */
|
||||
function getDefFiles(data: unknown): FileItem[] {
|
||||
const val: unknown[] = Array.isArray(data) ? data : data ? [data] : []
|
||||
const newVal: FileItem[] = val.map((item, index: number) => {
|
||||
const _item: FileItem = isObject(item)
|
||||
? {
|
||||
url: String(item[bindFileUrlKey.value] || ''),
|
||||
poster: String(item[bindPosterUrlKey.value] || ''),
|
||||
uploadStatus: 1,
|
||||
index,
|
||||
__id: getOnlyId(),
|
||||
percentage: 0,
|
||||
}
|
||||
: { url: String(item), poster: '', uploadStatus: 1, index, __id: getOnlyId(), percentage: 0 }
|
||||
return _item
|
||||
})
|
||||
return newVal
|
||||
}
|
||||
/** 得到已上传成功的文件 */
|
||||
function getUploadedList() {
|
||||
const list = fileList.value.filter(item => item.uploadStatus === 1 && item.url)
|
||||
// 不能用 bindFileUrlKey 代替
|
||||
if (props.customKeys.bindFileUrlKey) {
|
||||
return list.map((item) => {
|
||||
const obj: Record<string, string> = {}
|
||||
obj[bindFileUrlKey.value] = item.url
|
||||
obj[bindPosterUrlKey.value] = item.poster
|
||||
return obj
|
||||
})
|
||||
}
|
||||
else {
|
||||
return list.map(item => item.url)
|
||||
}
|
||||
}
|
||||
/** 得到不符合大小限制的提示语 */
|
||||
function getRestrictSizeMsg(maxSize: number, type: 'max' | 'min') {
|
||||
const maxSizeText = maxSize >= 1024 ? `${Number((maxSize / 1024).toFixed(2))}M` : `${maxSize}KB`
|
||||
return type === 'max' ? `文件大小不能超过${maxSizeText}` : `文件大小不能小于${maxSizeText}`
|
||||
}
|
||||
|
||||
defineExpose({ reset, getLoadState: () => upLoading.value })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rest-upload-container">
|
||||
<NImageGroup>
|
||||
<div ref="parent" class="flex flex-wrap gap-4">
|
||||
<!-- 文件列表 -->
|
||||
<div
|
||||
v-for="(item, index) in fileList" :key="item.__id"
|
||||
class="group relative overflow-hidden border border-gray-200 rounded-lg transition-all duration-300 hover:border-primary"
|
||||
:style="{ width: newWidth, height: newHeight }"
|
||||
>
|
||||
<!-- 图片预览 -->
|
||||
<div
|
||||
v-if="!item.url.includes('.mp4') && !item.url.includes('.webm') && (item.file?.type?.includes('image') || !item.file)"
|
||||
class="h-full w-full flex items-center justify-center bg-gray-50"
|
||||
>
|
||||
<NImage
|
||||
:src="item.url" class="h-full w-full flex items-center justify-center"
|
||||
:img-props="{ style: { width: '100%', height: '100%', objectFit: 'contain' } }" object-fit="cover"
|
||||
>
|
||||
<template #error>
|
||||
<div class="h-full w-full flex items-center justify-center bg-gray-50 text-gray-400">
|
||||
<SvgIcon icon="mdi:image-broken-variant" class="text-2xl" />
|
||||
</div>
|
||||
</template>
|
||||
</NImage>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览 -->
|
||||
<div
|
||||
v-else class="h-full w-full flex items-center justify-center bg-black bg-cover bg-center bg-no-repeat"
|
||||
:style="{ backgroundImage: item.poster ? `url(${item.poster})` : undefined }"
|
||||
>
|
||||
<SvgIcon icon="mdi:play-circle-outline" class="text-4xl text-white opacity-80" />
|
||||
</div>
|
||||
|
||||
<!-- 进度覆盖层 -->
|
||||
<div
|
||||
v-if="item.uploadStatus === 0"
|
||||
class="absolute inset-0 z-20 flex flex-col items-center justify-center bg-black/50 p-1"
|
||||
>
|
||||
<NProgress
|
||||
type="circle" :percentage="item.percentage" :color="{ stops: ['#E3F2FD', '#2080f0'] }"
|
||||
style="width: 60px; height: 60px"
|
||||
>
|
||||
<template #default>
|
||||
<span class="text-xs text-white">{{ item.percentage }}%</span>
|
||||
</template>
|
||||
</NProgress>
|
||||
</div>
|
||||
|
||||
<!-- 错误覆盖层 -->
|
||||
<div
|
||||
v-if="item.uploadStatus === -1"
|
||||
class="absolute inset-0 z-20 flex flex-col items-center justify-center bg-red-50/90 p-2 text-center"
|
||||
>
|
||||
<span class="mb-1 text-xs text-red-500">{{ item.errMsg || '上传失败' }}</span>
|
||||
<NButton size="tiny" type="error" ghost @click="reUploadClick(index)">
|
||||
重试
|
||||
</NButton>
|
||||
<div class="absolute right-1 top-1 cursor-pointer" @click="deleteClick(index)">
|
||||
<SvgIcon icon="mdi:close" class="text-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作覆盖层(悬停) -->
|
||||
<div
|
||||
v-if="item.uploadStatus === 1"
|
||||
class="pointer-events-none absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black/0 opacity-0 transition-all duration-300 group-hover:bg-black/50 group-hover:opacity-100"
|
||||
>
|
||||
<NButton
|
||||
v-if="item.url.includes('.mp4') || item.url.includes('.webm') || item.file?.type?.includes('video')"
|
||||
circle secondary type="info" class="pointer-events-auto"
|
||||
@click="previewVideoClick(item.url, item.poster)"
|
||||
>
|
||||
<template #icon>
|
||||
<SvgIcon icon="mdi:eye" />
|
||||
</template>
|
||||
</NButton>
|
||||
<NButton v-else circle secondary type="info" class="pointer-events-none">
|
||||
<template #icon>
|
||||
<SvgIcon icon="mdi:eye" />
|
||||
</template>
|
||||
</NButton>
|
||||
|
||||
<NButton circle secondary type="error" class="pointer-events-auto" @click="deleteClick(index)">
|
||||
<template #icon>
|
||||
<SvgIcon icon="mdi:delete" />
|
||||
</template>
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传触发器 -->
|
||||
<div v-if="efficientNumb > 0" :style="{ width: newWidth, height: newHeight }">
|
||||
<NUpload
|
||||
:accept="accept.join(',')" :multiple="limit > 1" :max="limit" :show-file-list="false"
|
||||
:file-list="nUploadFileList" :default-upload="false" class="h-full w-full" @change="handleUploadChange"
|
||||
>
|
||||
<template #default>
|
||||
<div
|
||||
class="h-full w-full flex flex-col cursor-pointer items-center justify-center border border-gray-300 rounded-lg border-dashed bg-gray-50 transition-colors hover:border-primary hover:bg-primary-50"
|
||||
:style="{ width: newWidth, height: newHeight }"
|
||||
>
|
||||
<SvgIcon icon="mdi:plus" class="text-3xl text-gray-400" />
|
||||
<span class="mt-1 text-xs text-gray-400">{{ hint }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</NUpload>
|
||||
</div>
|
||||
</div>
|
||||
</NImageGroup>
|
||||
|
||||
<!-- 视频预览弹窗 -->
|
||||
<NModal v-model:show="showVideoPreview" preset="card" title="视频预览" class="max-w-full w-[800px]">
|
||||
<div class="flex items-center justify-center rounded-lg bg-black/5 p-4">
|
||||
<video
|
||||
v-if="videoPreviewSrc" :src="videoPreviewSrc" :poster="videoPreviewPoster" controls
|
||||
class="max-h-[600px] max-w-full w-auto"
|
||||
/>
|
||||
</div>
|
||||
</NModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rest-upload-container {
|
||||
/* 确保容器不会塌陷 */
|
||||
min-height: v-bind(newHeight);
|
||||
}
|
||||
</style>
|
||||
128
apps/admin/src/components/common/wang-editor.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import { nextTick, onBeforeUnmount, onMounted, shallowRef } from 'vue'
|
||||
import { getAliOssTokenAxios } from '@/service/api/upload'
|
||||
import { browserPathJoin } from '@/utils/date'
|
||||
import { initOSSClient, uploadFileToOSS } from '@/utils/oss'
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
mode?: 'default' | 'simple'
|
||||
height?: string
|
||||
path?: string
|
||||
excludeKeys?: string[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
placeholder: '请输入内容...',
|
||||
mode: 'default',
|
||||
height: '300px',
|
||||
path: 'temp',
|
||||
excludeKeys: () => [],
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// Editor Logic
|
||||
const editorRef = shallowRef()
|
||||
const toolbarConfig = {
|
||||
excludeKeys: props.excludeKeys || [],
|
||||
}
|
||||
|
||||
type InsertFnType = (url: string, alt: string, href: string) => void
|
||||
|
||||
async function customUpload(file: File, insertFn: InsertFnType) {
|
||||
try {
|
||||
const { data: tokenData, error: tokenError } = await getAliOssTokenAxios()
|
||||
if (tokenError || !tokenData) {
|
||||
window.$message?.error('获取上传凭证失败')
|
||||
return
|
||||
}
|
||||
const client = initOSSClient(tokenData?.data || {})
|
||||
const path = `${props.path}/${Date.now()}/${file.name}`
|
||||
|
||||
await uploadFileToOSS(client, file, path)
|
||||
|
||||
const url = browserPathJoin(import.meta.env.VITE_BASE_OSS_URL, path)
|
||||
insertFn(url, file.name, url)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('上传失败', error)
|
||||
window.$message?.error('上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const editorConfig = {
|
||||
placeholder: props.placeholder,
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
customUpload,
|
||||
},
|
||||
uploadVideo: {
|
||||
customUpload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 监听 placeholder 变化
|
||||
// watch(() => props.placeholder, (_newVal) => {
|
||||
// if (editorRef.value) {
|
||||
// // 似乎 wangeditor 不支持动态修改 placeholder,这里作为占位
|
||||
// }
|
||||
// })
|
||||
onMounted(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(toolbarConfig, 'toolbarConfig')
|
||||
nextTick(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(editorRef.value)
|
||||
})
|
||||
})
|
||||
|
||||
function handleCreated(editor: any) {
|
||||
editorRef.value = editor
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Toolbar keys:', editor.getAllMenuKeys())
|
||||
}
|
||||
|
||||
function handleChange(editor: any) {
|
||||
emit('update:modelValue', editor.getHtml())
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value
|
||||
if (editor == null)
|
||||
return
|
||||
editor.destroy()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-hidden border border-gray-200 rounded-lg bg-white">
|
||||
<Toolbar
|
||||
style="border-bottom: 1px solid #eee"
|
||||
:editor="editorRef"
|
||||
:default-config="toolbarConfig"
|
||||
:mode="mode"
|
||||
/>
|
||||
<div :style="{ height: props.height }">
|
||||
<Editor
|
||||
:model-value="modelValue"
|
||||
:style="{ height: '100%', overflowY: 'hidden' }"
|
||||
:default-config="editorConfig"
|
||||
:mode="mode"
|
||||
@on-created="handleCreated"
|
||||
@on-change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.w-e-text-container) {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
126
apps/admin/src/components/custom/README.md
Normal file
@ -0,0 +1,126 @@
|
||||
# SvgIcon 图标组件使用指南
|
||||
|
||||
`SvgIcon` 是本项目统一使用的图标组件,底层基于 `@iconify/vue`,支持渲染 **Iconify 开源图标** 和 **本地 SVG 图标**。
|
||||
|
||||
## 1. 快速开始
|
||||
|
||||
### 1.1 使用 Iconify 图标 (推荐)
|
||||
|
||||
本项目集成了 [Iconify](https://iconify.design/),可直接使用海量开源图标库(如 Material Design, Carbon, Phosphor 等)。
|
||||
|
||||
**语法**:
|
||||
|
||||
```vue
|
||||
<SvgIcon icon="图集名:图标名" />
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```vue
|
||||
<!-- Material Design Icons -->
|
||||
<SvgIcon icon="mdi:home" class="text-xl text-blue-500" />
|
||||
|
||||
<!-- Carbon Icons -->
|
||||
<SvgIcon icon="carbon:user" />
|
||||
```
|
||||
|
||||
### 1.2 使用本地 SVG 图标
|
||||
|
||||
当需要使用设计师提供的自定义图标或彩色图标时。
|
||||
|
||||
1. **存放**:将 `.svg` 文件放入 `src/assets/svg-icon/` 目录。
|
||||
2. **引用**:使用 `local-icon` 属性引用文件名(不含 `.svg` 后缀)。
|
||||
|
||||
**语法**:
|
||||
|
||||
```vue
|
||||
<SvgIcon local-icon="文件名" />
|
||||
```
|
||||
|
||||
**示例**:
|
||||
假设文件位于 `src/assets/svg-icon/custom-logo.svg`:
|
||||
|
||||
```vue
|
||||
<SvgIcon local-icon="custom-logo" class="text-32px" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 属性说明 (Props)
|
||||
|
||||
该组件定义在 `src/components/custom/svg-icon.vue`。
|
||||
|
||||
| 属性名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
| :---------- | :------- | :--- | :----- | :------------------------------------------- |
|
||||
| `icon` | `string` | 否 | - | Iconify 图标名称,格式为 `collection:name`。 |
|
||||
| `localIcon` | `string` | 否 | - | 本地 SVG 文件名。**优先级高于 `icon`**。 |
|
||||
|
||||
> **提示**:
|
||||
>
|
||||
> - 组件设置了 `inheritAttrs: false`,但会手动绑定 `class` 和 `style` 到根元素。
|
||||
> - 你可以通过 Tailwind CSS 类名(如 `text-xl`, `text-red-500`)直接控制图标的大小和颜色。
|
||||
|
||||
---
|
||||
|
||||
## 3. 如何查找与使用图标
|
||||
|
||||
推荐使用 [Icones.js.org](https://icones.js.org/) 图标搜索引擎。
|
||||
|
||||
### 3.1 查找步骤
|
||||
|
||||
1. 打开 [Icones.js.org](https://icones.js.org/)。
|
||||
2. 输入关键词搜索(如 `user`, `setting`)。
|
||||
3. 点击选中的图标,复制底部的 **ID**(例如 `mdi:shield-airplane-outline`)。
|
||||
|
||||
### 3.2 使用示例对照
|
||||
|
||||
| Icones 图标 ID | 在本项目中的写法 (推荐) |
|
||||
| :--------------------------------------------- | :---------------------------------------------------------------- |
|
||||
| `mdi:shield-airplane-outline` | `<SvgIcon icon="mdi:shield-airplane-outline" />` |
|
||||
| `material-symbols:android-wifi-4-bar-question` | `<SvgIcon icon="material-symbols:android-wifi-4-bar-question" />` |
|
||||
| `solar:minimize-square-minimalistic-outline` | `<SvgIcon icon="solar:minimize-square-minimalistic-outline" />` |
|
||||
| `tabler:align-box-top-right` | `<SvgIcon icon="tabler:align-box-top-right" />` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 常见问题与进阶
|
||||
|
||||
### 4.1 什么是 `icon-ic-baseline-refresh`?
|
||||
|
||||
在项目中(如 `competition-add/index.vue`)你可能会看到这种写法:
|
||||
|
||||
```vue
|
||||
<icon-ic-baseline-refresh class="text-icon" />
|
||||
```
|
||||
|
||||
这是 `unplugin-icons` 插件提供的**自动组件导入**功能。
|
||||
|
||||
- **命名规则**:`{Prefix}-{Collection}-{Name}`
|
||||
- **配置来源**:`.env` 文件中的 `VITE_ICON_PREFIX=icon`。
|
||||
- **解析**:`icon-ic-baseline-refresh` 对应图集 `ic` 下的 `baseline-refresh` 图标。
|
||||
|
||||
**建议**:虽然支持这种写法,但为了统一性和灵活性(支持动态变量),**推荐统一使用 `<SvgIcon />` 组件**。
|
||||
|
||||
### 4.2 在 Render 函数中使用 (TSX)
|
||||
|
||||
在 Naive UI 的 `NTree`, `NDataTable` 或 `NMenu` 等需要渲染函数的场景中:
|
||||
|
||||
```ts
|
||||
import { h } from 'vue'
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue'
|
||||
|
||||
// 渲染 Iconify 图标
|
||||
function renderIcon() {
|
||||
return h(SvgIcon, {
|
||||
icon: 'carbon:folder',
|
||||
class: 'text-gray-500'
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染本地图标
|
||||
function renderLocalIcon() {
|
||||
return h(SvgIcon, {
|
||||
localIcon: 'custom-logo'
|
||||
})
|
||||
}
|
||||
```
|
||||
@ -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>
|
||||
|
||||
|
||||
85
apps/admin/src/components/custom/typeit/index.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Options } from 'typeit'
|
||||
import type { El } from 'typeit/dist/types'
|
||||
import TypeIt from 'typeit'
|
||||
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
|
||||
|
||||
if (typeItInstance.value) {
|
||||
typeItInstance.value.destroy()
|
||||
}
|
||||
|
||||
// 清空内容,防止重叠
|
||||
textRef.value.textContent = ''
|
||||
|
||||
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>
|
||||
<span ref="textRef" class="typeit-container" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
116
apps/admin/src/components/custom/user/ActionButton.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
// import { defineProps, defineEmits } from 'vue';
|
||||
|
||||
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',
|
||||
default: 'button',
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['click'])
|
||||
|
||||
function handleClick() {
|
||||
if (!props.disabled) {
|
||||
emits('click')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="action-btn"
|
||||
:class="[theme, { 'is-disabled': disabled, 'text-btn': type === 'text' }]"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span v-if="type === 'text'" class="btn-text">{{ text }}</span>
|
||||
<slot v-else />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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.2s ease;
|
||||
background: #eb5e55;
|
||||
box-shadow: 0 8px 0 #fcc64f;
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
filter: brightness(1.05);
|
||||
box-shadow: 0 10px 0 #fcc64f;
|
||||
}
|
||||
|
||||
&: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: 30px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.light {
|
||||
background: white;
|
||||
color: #eb5e55;
|
||||
border: 2px solid #eb5e55;
|
||||
box-shadow: 0 8px 0 #fcc64f;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 0 #fcc64f;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(4px);
|
||||
box-shadow: 0 2px 0 #fcc64f;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
388
apps/admin/src/components/custom/user/CompetitionLayout.vue
Normal file
@ -0,0 +1,388 @@
|
||||
<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 { useRouterPush } from '@/hooks/common/router'
|
||||
|
||||
import ActionButton from './ActionButton.vue'
|
||||
|
||||
interface Props {
|
||||
/** 是否显示返回按钮 */
|
||||
showBack?: boolean
|
||||
/** 是否显示下一步按钮 */
|
||||
showNext?: boolean
|
||||
/** 背景图片URL,如果不传则尝试内部获取或使用默认 */
|
||||
bgUrl?: string
|
||||
/** 下一步按钮的文本,如果存在则显示为文字按钮,否则显示为图标按钮 */
|
||||
nextBtnText?: string
|
||||
/** 页面标题 */
|
||||
title?: string
|
||||
/** 是否显示标题 */
|
||||
showTitle?: boolean
|
||||
/** 按钮操作栏位置:'bottom' | 'top',默认 'bottom' */
|
||||
actionPosition?: 'bottom' | 'top'
|
||||
/** 返回按钮的文本,如果存在则显示为文字按钮,否则显示为图标按钮 */
|
||||
backBtnText?: string
|
||||
/** 按钮主题:'primary' (红色填充) | 'light' (白色背景红字),默认 'primary' */
|
||||
btnTheme?: 'primary' | 'light'
|
||||
/** 是否禁用下一步按钮 */
|
||||
nextDisabled?: boolean
|
||||
/** 标题区域背景图片URL,不传则使用默认 title-bg.svg */
|
||||
titleBgType?: number
|
||||
/** 是否显示第二个下一步按钮 */
|
||||
showNextBtn2?: boolean
|
||||
/** 第二个按钮的文本 */
|
||||
nextBtnText2?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showBack: true,
|
||||
showNext: true,
|
||||
bgUrl: undefined,
|
||||
nextBtnText: undefined,
|
||||
title: undefined,
|
||||
showTitle: true,
|
||||
actionPosition: 'bottom',
|
||||
backBtnText: undefined,
|
||||
btnTheme: 'primary',
|
||||
nextDisabled: false,
|
||||
titleBgType: 1,
|
||||
showNextBtn2: false,
|
||||
nextBtnText2: undefined,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'back'): void
|
||||
(e: 'next'): void
|
||||
(e: 'next2'): void
|
||||
}>()
|
||||
|
||||
const titleBgUrlMapping: Record<number, string> = {
|
||||
1: _defaultTitleBg,
|
||||
2: titleBg2,
|
||||
}
|
||||
|
||||
const { routerPushByKey } = useRouterPush()
|
||||
|
||||
const titleBgUrl = computed(() => titleBgUrlMapping[props.titleBgType] || _defaultBg)
|
||||
|
||||
// const router = useRouter()
|
||||
const innerBgUrl = ref(_defaultBg)
|
||||
|
||||
async function fetchSystemConfig() {
|
||||
// 如果外部传入了 bgUrl,则优先使用外部的
|
||||
if (props.bgUrl) {
|
||||
innerBgUrl.value = props.bgUrl
|
||||
}
|
||||
|
||||
// 模拟接口请求系统配置
|
||||
// const res = await fetchConfig()
|
||||
// if (res.bgUrl) innerBgUrl.value = res.bgUrl
|
||||
|
||||
// 这里的逻辑保持与原 Home 页面一致:如果没有配置,则使用默认
|
||||
if (!innerBgUrl.value)
|
||||
innerBgUrl.value = _defaultBg
|
||||
}
|
||||
|
||||
const showContent = ref(false)
|
||||
const showSlotContent = ref(false)
|
||||
const showButton = ref(false)
|
||||
|
||||
function handleBack() {
|
||||
emit('back')
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
if (props.nextDisabled)
|
||||
return
|
||||
emit('next')
|
||||
}
|
||||
|
||||
function handleNext2() {
|
||||
emit('next2')
|
||||
}
|
||||
|
||||
/** 点击返回首页 */
|
||||
function handleBackHome() {
|
||||
routerPushByKey('user_home')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSystemConfig()
|
||||
setTimeout(() => {
|
||||
showContent.value = true
|
||||
}, 100)
|
||||
|
||||
// 延迟显示内容,等待标题卷轴展开
|
||||
const delay = props.showTitle ? 500 : 100
|
||||
setTimeout(() => {
|
||||
showSlotContent.value = true
|
||||
}, delay)
|
||||
|
||||
// 延迟显示按钮,等待内容显示
|
||||
setTimeout(() => {
|
||||
showButton.value = true
|
||||
}, delay + 500)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="competition-layout-wrapper">
|
||||
<!-- Background Layer (Outside Scale to cover full screen) -->
|
||||
<div class="fixed-bg" :style="innerBgUrl ? { backgroundImage: `url(${innerBgUrl})` } : {}" />
|
||||
|
||||
<VScaleScreen width="1920" 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>
|
||||
|
||||
<div class="glass-overlay">
|
||||
<!-- title -->
|
||||
<section v-if="showTitle" class="title-section">
|
||||
<Transition name="scroll-unfold" appear>
|
||||
<div v-if="showContent" class="title-wrapper">
|
||||
<div class="scroll-bg" :style="{ backgroundImage: `url(${titleBgUrl})` }">
|
||||
<Transition name="fade-in-delay" appear>
|
||||
<h1 v-if="showContent" class="main-title">
|
||||
{{ title }}
|
||||
</h1>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</section>
|
||||
|
||||
<!-- Main Content Slot -->
|
||||
<main class="main-content">
|
||||
<div v-if="showSlotContent" class="h-full w-full flex flex-col items-center justify-center">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer Action -->
|
||||
<footer
|
||||
v-if="showButton"
|
||||
class="page-footer w-full flex items-center justify-between px-20"
|
||||
:class="[actionPosition, btnTheme]"
|
||||
>
|
||||
<div class="left">
|
||||
<ActionButton
|
||||
class="back-btn" :style="{ visibility: showBack ? 'visible' : 'hidden' }"
|
||||
:type="backBtnText ? 'text' : 'button'" :text="backBtnText || ''" :theme="btnTheme" @click="handleBack"
|
||||
>
|
||||
<SvgIcon v-if="!backBtnText" icon="mdi:arrow-left" size="40" class="arrow-icon" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<ActionButton
|
||||
v-if="showNext" :type="nextBtnText ? 'text' : 'button'" :text="nextBtnText || '下一步'"
|
||||
:theme="btnTheme" :disabled="nextDisabled" @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>
|
||||
</VScaleScreen>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title-section {
|
||||
margin: 10px auto;
|
||||
width: 412px;
|
||||
height: 165px;
|
||||
display: flex;
|
||||
justify-content: center; /* Ensure wrapper stays centered during expansion */
|
||||
|
||||
.title-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center; /* Keep the background image centered */
|
||||
}
|
||||
|
||||
.scroll-bg {
|
||||
width: 412px !important; /* Fixed width to prevent squishing during animation */
|
||||
height: 165px !important;
|
||||
position: relative;
|
||||
background: no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 10px;
|
||||
flex-shrink: 0; /* Prevent shrinking */
|
||||
|
||||
.main-title {
|
||||
font-size: 2.5rem;
|
||||
color: $text-color;
|
||||
margin: 0;
|
||||
letter-spacing: 4px;
|
||||
text-align: center;
|
||||
white-space: nowrap; /* Prevent text wrapping */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.competition-layout-wrapper {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-color: #f0f2f5; // 给一个底色,防止背景图加载前白屏
|
||||
}
|
||||
|
||||
.fixed-bg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
z-index: 0; // 提升 z-index,确保不被底层遮挡
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.competition-layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 70px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
z-index: 1; // 确保内容在背景之上
|
||||
|
||||
// Default background decoration (Bottom Wave)
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 40%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom;
|
||||
background-size: cover;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px 40px;
|
||||
font-weight: 700;
|
||||
color: $secondary-color;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center; // 让内容决定位置,通常是 margin-top
|
||||
// margin: 40px auto;
|
||||
z-index: 10;
|
||||
padding-bottom: 20px;
|
||||
width: 100%; // 确保内容宽度
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
z-index: 20;
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&.top {
|
||||
bottom: auto;
|
||||
top: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
.fade-slide-down-enter-from,
|
||||
.fade-slide-down-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
padding: 15px 20px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
48
apps/admin/src/components/custom/user/TianZiGe.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
char?: string
|
||||
width?: string | number
|
||||
height?: string | number
|
||||
fontSize?: string | number
|
||||
showChar?: boolean
|
||||
}
|
||||
|
||||
const _props = withDefaults(defineProps<Props>(), {
|
||||
char: '',
|
||||
width: '10.8rem', // 对应 w-42 (42 * 0.25rem = 10.5rem)
|
||||
height: '9.5rem', // 对应 h-38 (38 * 0.25rem = 9.5rem)
|
||||
// fontSize: '3.25rem', // 对应 text-4xl
|
||||
showChar: true,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex select-none items-center justify-center border-2 border-red-500 bg-white"
|
||||
:style="{ width: typeof width === 'number' ? `${width}px` : width, height: typeof height === 'number' ? `${height}px` : height }"
|
||||
>
|
||||
<!-- 田字格背景线 -->
|
||||
<div class="pointer-events-none absolute inset-0 z-0 h-full w-full">
|
||||
<!-- 横向虚线 -->
|
||||
<div class="absolute left-0 top-1/2 h-[1px] w-full border-t border-red-400 border-dashed opacity-60" />
|
||||
<!-- 纵向虚线 -->
|
||||
<div class="absolute left-1/2 top-0 h-full w-[1px] border-l border-red-400 border-dashed opacity-60" />
|
||||
<!-- 对角线 -->
|
||||
<svg width="100%" height="100%" class="absolute inset-0 opacity-60">
|
||||
<line x1="0" y1="0" x2="100%" y2="100%" stroke="#f87171" stroke-width="1" stroke-dasharray="4 2" />
|
||||
<line x1="100%" y1="0" x2="0" y2="100%" stroke="#f87171" stroke-width="1" stroke-dasharray="4 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 文字内容 -->
|
||||
<span
|
||||
v-if="showChar"
|
||||
class="z-10 text-6xl text-gray-800 font-bold font-sans"
|
||||
>
|
||||
{{ char }}
|
||||
</span>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
81
apps/admin/src/constants/business.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import type { SelectOption } from 'naive-ui'
|
||||
import { transformRecordToOption } from '@/utils/common'
|
||||
|
||||
export const enableStatusRecord: Record<Api.Common.EnableStatus, string> = {
|
||||
1: '启用',
|
||||
2: '禁用',
|
||||
}
|
||||
|
||||
export const enableStatusOptions: SelectOption[] = transformRecordToOption(enableStatusRecord) as unknown as SelectOption[]
|
||||
|
||||
export const userGenderRecord: Record<Api.SystemManage.UserGender, string> = {
|
||||
1: '男',
|
||||
2: '女',
|
||||
}
|
||||
|
||||
export const userGenderOptions: SelectOption[] = transformRecordToOption(userGenderRecord) as unknown as SelectOption[]
|
||||
|
||||
export const menuTypeRecord: Record<Api.SystemManage.MenuType, string> = {
|
||||
1: '目录',
|
||||
2: '菜单',
|
||||
}
|
||||
|
||||
export const menuTypeOptions: SelectOption[] = transformRecordToOption(menuTypeRecord) as unknown as SelectOption[]
|
||||
|
||||
export const menuIconTypeRecord: Record<Api.SystemManage.IconType, string> = {
|
||||
1: 'iconify',
|
||||
2: '本地图标',
|
||||
}
|
||||
|
||||
/**
|
||||
* 题目分数类型
|
||||
*/
|
||||
export const scoreTypeRecord: Record<Api.Competition.QuestionScoreType, string> = {
|
||||
1: '固定分数',
|
||||
2: '答题个数',
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮次类型
|
||||
*/
|
||||
export const roundTypeRecord: Record<Api.Competition.CompetitionRoundType, string> = {
|
||||
1: '题包环节',
|
||||
2: '加时环节',
|
||||
}
|
||||
|
||||
export const uiTypeRecord: Record<Api.Competition.QuestionTemplateId, string> = {
|
||||
TEMPLATE_DICTATION_HINT: '汉字听写-提示',
|
||||
TEMPLATE_DICTATION_HOMOPHONE: '汉字听写-同音',
|
||||
TEMPLATE_COMPONENT_ADD: '汉字加一加',
|
||||
TEMPLATE_WORD_DICTATION: '词语听写',
|
||||
TEMPLATE_IDIOM_TEXT_REQ: '成语-文字要求',
|
||||
TEMPLATE_IDIOM_IMAGE: '成语-看图',
|
||||
}
|
||||
|
||||
export const timeRecord: Record<Api.Competition.QuestionTimeType, string> = {
|
||||
5: '5s',
|
||||
10: '10s',
|
||||
15: '15s',
|
||||
20: '20s',
|
||||
30: '30s',
|
||||
45: '45s',
|
||||
60: '60s',
|
||||
90: '90s',
|
||||
}
|
||||
|
||||
export const questionTypeRecord: Record<number, string> = {
|
||||
0: '文字',
|
||||
1: '图片',
|
||||
}
|
||||
|
||||
export const questionTypeOptions: SelectOption[] = transformRecordToOption(questionTypeRecord, true) as unknown as SelectOption[]
|
||||
|
||||
export const timeOptions: SelectOption[] = transformRecordToOption(timeRecord) as unknown as SelectOption[]
|
||||
|
||||
export const uiTypeOptions: SelectOption[] = transformRecordToOption(uiTypeRecord) as unknown as SelectOption[]
|
||||
|
||||
export const roundTypeOptions: SelectOption[] = transformRecordToOption(roundTypeRecord) as unknown as SelectOption[]
|
||||
|
||||
export const scoreTypeOptions: SelectOption[] = transformRecordToOption(scoreTypeRecord) as unknown as SelectOption[]
|
||||
|
||||
export const menuIconTypeOptions: SelectOption[] = transformRecordToOption(menuIconTypeRecord) as unknown as SelectOption[]
|
||||
31
apps/admin/src/enum/business.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export enum PublishStatus {
|
||||
Unpublished = 0,
|
||||
Published = 1,
|
||||
Processing = 2,
|
||||
Finished = 3,
|
||||
}
|
||||
|
||||
export enum RankPublishStatus {
|
||||
Unpublished = 0,
|
||||
Published = 1,
|
||||
}
|
||||
|
||||
export enum QuestionType {
|
||||
SingleChoice = 'SingleChoice', // 单选题
|
||||
MultipleChoice = 'MultipleChoice', // 多选题
|
||||
TrueFalse = 'TrueFalse', // 判断题
|
||||
Text = 'Text', // 文本类型
|
||||
FillBlank = 'FillBlank', // 填空题
|
||||
Image = 'image', // 图片类型
|
||||
// 其他类型...
|
||||
}
|
||||
|
||||
export enum QuestionCategoryEnum {
|
||||
ChineseCharacterDictation1 = 'ChineseCharacterDictation1', // 汉字听写-提示 (jiu表示小鸟的叫声)
|
||||
ChineseCharacterDictation2 = 'ChineseCharacterDictation2', // 根据拼音写汉字-提示 (tang)
|
||||
CharacterRadicalAddition = 'CharacterRadicalAddition', // 汉字加一加 (车)
|
||||
WordDictation = 'WordDictation', // 词语听写 (zhi re)
|
||||
IdiomWriting1 = 'IdiomWriting1', // 成语-文字要求 (反义字)
|
||||
IdiomWriting2 = 'IdiomWriting2', // 成语-文字要求 (看图写成语)
|
||||
PoetryComprehension = 'PoetryComprehension', // 诗词理解-选择题
|
||||
}
|
||||
36
apps/admin/src/hooks/business/useDict.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useBusinessStore } from '@/store/modules/business'
|
||||
|
||||
/**
|
||||
* 字典项 hook
|
||||
* @param key 字典项 key
|
||||
* @param valueType 字典项 value 类型,默认 number
|
||||
*/
|
||||
export function useDict(key: string, valueType: 'string' | 'number' = 'number') {
|
||||
const store = useBusinessStore()
|
||||
|
||||
const data = computed(() => store.dictData[key] || [])
|
||||
const loading = computed(() => store.loadingMap[key] || false)
|
||||
const options = computed(() => {
|
||||
if (data.value && data.value.length > 0) {
|
||||
return data.value.map((item: any) => {
|
||||
const rawValue = item.uI_Value || item.UI_Value || item.DicValue
|
||||
return {
|
||||
label: item.uI_Key || item.UI_Key || item.DicKey,
|
||||
value: valueType === 'number' ? Number(rawValue) : String(rawValue),
|
||||
}
|
||||
})
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
store.getDict(key)
|
||||
})
|
||||
|
||||
return {
|
||||
data,
|
||||
options,
|
||||
loading,
|
||||
}
|
||||
}
|
||||
@ -19,7 +19,13 @@ export function useRouterPush(inSetup = true) {
|
||||
|
||||
const routerBack = router.back
|
||||
|
||||
async function routerPushByKey(key: RouteKey, options?: App.Global.RouterPushOptions) {
|
||||
/**
|
||||
* 路由跳转
|
||||
*
|
||||
* @param key 路由名称
|
||||
* @param options 路由参数
|
||||
*/
|
||||
async function routerPushByKey(key: RouteKey, options?: App.Global.RouterPushOptions, newWindow?: boolean) {
|
||||
const { query, params } = options || {}
|
||||
|
||||
const routeLocation: RouteLocationRaw = {
|
||||
@ -34,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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -13,13 +13,13 @@ export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boole
|
||||
'pagination' | 'getColumnChecks' | 'getColumns'
|
||||
> & {
|
||||
/**
|
||||
* get column visible
|
||||
* 获取列的可见性
|
||||
*
|
||||
* @param column
|
||||
*
|
||||
* @default true
|
||||
*
|
||||
* @returns true if the column is visible, false otherwise
|
||||
* @returns 如果列可见返回 true,否则返回 false
|
||||
*/
|
||||
getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean
|
||||
}
|
||||
@ -28,8 +28,12 @@ const SELECTION_KEY = '__selection__'
|
||||
|
||||
const EXPAND_KEY = '__expand__'
|
||||
|
||||
/**
|
||||
* NaiveUI 表格
|
||||
* @param options 表格选项
|
||||
*/
|
||||
export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptions<ResponseData, ApiData, false>) {
|
||||
const scope = effectScope()
|
||||
const scope = effectScope() // 表格作用域,用于管理表格的响应式数据
|
||||
const appStore = useAppStore()
|
||||
|
||||
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
|
||||
@ -38,7 +42,7 @@ export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptio
|
||||
getColumns,
|
||||
})
|
||||
|
||||
// calculate the total width of the table this is used for horizontal scrolling
|
||||
// 计算表格的总宽度,用于水平滚动
|
||||
const scrollX = computed(() => {
|
||||
return result.columns.value.reduce((acc, column) => {
|
||||
return acc + Number(column.width ?? column.minWidth ?? 120)
|
||||
@ -69,7 +73,7 @@ type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>
|
||||
type UseNaivePaginatedTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, true> & {
|
||||
paginationProps?: Omit<PaginationProps, 'page' | 'pageSize' | 'itemCount'>
|
||||
/**
|
||||
* whether to show the total count of the table
|
||||
* 是否显示表格的总条数
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
@ -77,6 +81,10 @@ type UseNaivePaginatedTableOptions<ResponseData, ApiData> = UseNaiveTableOptions
|
||||
onPaginationParamsChange?: (params: PaginationParams) => void | Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* NaiveUI 分页表格
|
||||
* @param options 表格选项
|
||||
*/
|
||||
export function useNaivePaginatedTable<ResponseData, ApiData>(
|
||||
options: UseNaivePaginatedTableOptions<ResponseData, ApiData>,
|
||||
) {
|
||||
@ -104,7 +112,7 @@ export function useNaivePaginatedTable<ResponseData, ApiData>(
|
||||
...options.paginationProps,
|
||||
}) as PaginationProps
|
||||
|
||||
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
|
||||
// 针对移动端,如果系统不支持移动端,可以直接使用 `pagination`
|
||||
const mobilePagination = computed(() => {
|
||||
const p: PaginationProps = {
|
||||
...pagination,
|
||||
@ -172,11 +180,28 @@ export function useNaivePaginatedTable<ResponseData, ApiData>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NaiveUI 表格操作
|
||||
* @param data 表格数据
|
||||
* @param idKey 表格数据的主键字段
|
||||
* @param getData 获取表格数据的函数
|
||||
*/
|
||||
export function useTableOperate<TableData>(
|
||||
data: Ref<TableData[]>,
|
||||
idKey: keyof TableData,
|
||||
getData: () => Promise<void>,
|
||||
) {
|
||||
): {
|
||||
drawerVisible: Ref<boolean> /** 抽屉是否可见 */
|
||||
openDrawer: () => void /** 打开抽屉 */
|
||||
closeDrawer: () => void /** 关闭抽屉 */
|
||||
operateType: Ref<NaiveUI.TableOperateType> /** 操作类型 */
|
||||
handleAdd: () => void /** 新增操作 */
|
||||
editingData: Ref<TableData | null> /** 编辑行数据 */
|
||||
handleEdit: (id: TableData[keyof TableData]) => void /** 编辑操作 */
|
||||
checkedRowKeys: Ref<string[]> /** 表格的选中行 keys */
|
||||
onBatchDeleted: () => Promise<void> /** 批量删除操作完成后的钩子 */
|
||||
onDeleted: () => Promise<void> /** 删除操作完成后的钩子 */
|
||||
} {
|
||||
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean()
|
||||
|
||||
const operateType = shallowRef<NaiveUI.TableOperateType>('add')
|
||||
@ -186,7 +211,7 @@ export function useTableOperate<TableData>(
|
||||
openDrawer()
|
||||
}
|
||||
|
||||
/** the editing row data */
|
||||
/** 编辑行数据 */
|
||||
const editingData = shallowRef<TableData | null>(null)
|
||||
|
||||
function handleEdit(id: TableData[keyof TableData]) {
|
||||
@ -197,10 +222,10 @@ export function useTableOperate<TableData>(
|
||||
openDrawer()
|
||||
}
|
||||
|
||||
/** the checked row keys of table */
|
||||
/** 表格的选中行 keys */
|
||||
const checkedRowKeys = shallowRef<string[]>([])
|
||||
|
||||
/** the hook after the batch delete operation is completed */
|
||||
/** 批量删除操作完成后的钩子 */
|
||||
async function onBatchDeleted() {
|
||||
window.$message?.success($t('common.deleteSuccess'))
|
||||
|
||||
@ -209,7 +234,7 @@ export function useTableOperate<TableData>(
|
||||
await getData()
|
||||
}
|
||||
|
||||
/** the hook after the delete operation is completed */
|
||||
/** 删除操作完成后的钩子 */
|
||||
async function onDeleted() {
|
||||
window.$message?.success($t('common.deleteSuccess'))
|
||||
|
||||
@ -230,17 +255,23 @@ export function useTableOperate<TableData>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认的表格数据转换函数
|
||||
*
|
||||
* @param response 原始的 API 响应数据
|
||||
* @returns 转换后的分页数据
|
||||
*/
|
||||
export function defaultTransform<ApiData>(
|
||||
response: FlatResponseData<any, Api.Common.PaginatingQueryRecord<ApiData>>,
|
||||
): PaginationData<ApiData> {
|
||||
const { data, error } = response
|
||||
|
||||
if (!error) {
|
||||
const { records, current, size, total } = data
|
||||
const { data: list, currentPage: pageNum, pageSize: size, total = 0 } = data
|
||||
|
||||
return {
|
||||
data: records,
|
||||
pageNum: current,
|
||||
data: list,
|
||||
pageNum,
|
||||
pageSize: size,
|
||||
total,
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ interface Props {
|
||||
|
||||
<template>
|
||||
<RouterLink to="/" class="w-full flex-center nowrap-hidden">
|
||||
<SystemLogo class="size-32px" />
|
||||
<SystemLogo class="size-42px" />
|
||||
<h2 v-show="showTitle" class="pl-8px text-16px text-primary font-bold transition duration-300 ease-in-out">
|
||||
{{ $t('system.title') }}
|
||||
</h2>
|
||||
|
||||
@ -82,6 +82,10 @@ const local: App.I18n.Schema = {
|
||||
warning: 'Warning',
|
||||
error: 'Error',
|
||||
followPrimary: 'Follow Primary',
|
||||
unpublished: 'Unpublished',
|
||||
published: 'Published',
|
||||
processing: 'Processing',
|
||||
finished: 'Finished',
|
||||
},
|
||||
themeRadius: {
|
||||
title: 'Theme Radius',
|
||||
@ -228,11 +232,33 @@ const local: App.I18n.Schema = {
|
||||
'404': 'Page Not Found',
|
||||
'500': 'Server Error',
|
||||
'iframe-page': 'Iframe',
|
||||
'home': 'Home',
|
||||
'competition': 'Competition',
|
||||
'question-store': 'Question Store',
|
||||
'template': 'Template',
|
||||
'rank': 'Rank',
|
||||
'rank_rank-detail': 'Rank Detail',
|
||||
'rank_rank-list': 'Rank List',
|
||||
'template_template-detail': 'Template Detail',
|
||||
'template_template-list': 'Template List',
|
||||
'user': 'User',
|
||||
'results': 'Results',
|
||||
'competition_competition-add': 'Competition Add',
|
||||
'competition_competition-detail': 'Competition Detail',
|
||||
'competition_competition-list': 'Competition List',
|
||||
'dictionary': 'Dictionary',
|
||||
'admin-home': 'Admin Home',
|
||||
'user_analysis': 'User Analysis',
|
||||
'user_cover': 'User Cover',
|
||||
'user_draw': 'User Draw',
|
||||
'user_game': 'User Game',
|
||||
'user_teams': 'User Teams',
|
||||
'user_rules': 'User Rules',
|
||||
'user_groups': 'User Groups',
|
||||
'user_home': 'User Home',
|
||||
'results_results-list': 'Results List',
|
||||
'results_results-detail': 'Results Detail',
|
||||
'test': 'Test',
|
||||
'user_rank-list': 'User Rank List',
|
||||
},
|
||||
page: {
|
||||
login: {
|
||||
|
||||
@ -82,6 +82,10 @@ const local: App.I18n.Schema = {
|
||||
warning: '警告色',
|
||||
error: '错误色',
|
||||
followPrimary: '跟随主色',
|
||||
unpublished: '未发布',
|
||||
published: '已发布',
|
||||
processing: '处理中',
|
||||
finished: '已完成',
|
||||
},
|
||||
themeRadius: {
|
||||
title: '主题圆角',
|
||||
@ -224,11 +228,33 @@ const local: App.I18n.Schema = {
|
||||
'404': '页面不存在',
|
||||
'500': '服务器错误',
|
||||
'iframe-page': '外链页面',
|
||||
'home': '首页',
|
||||
'competition': '比赛',
|
||||
'competition': '比赛配置',
|
||||
'question-store': '题库',
|
||||
'template': '模板制作',
|
||||
'rank': '名次排行',
|
||||
'rank_rank-detail': '名次详情',
|
||||
'rank_rank-list': '名次列表',
|
||||
'template_template-detail': '模板详情',
|
||||
'template_template-list': '模板列表',
|
||||
'user': '用户管理',
|
||||
'results': '实时结果',
|
||||
'competition_competition-add': '比赛添加',
|
||||
'competition_competition-detail': '比赛详情',
|
||||
'competition_competition-list': '比赛列表',
|
||||
'dictionary': '字典管理',
|
||||
'admin-home': '管理员首页',
|
||||
'user_analysis': '用户分析',
|
||||
'user_cover': '用户封面',
|
||||
'user_draw': '用户绘制',
|
||||
'user_game': '用户游戏',
|
||||
'user_teams': '用户队伍',
|
||||
'user_rules': '用户规则',
|
||||
'user_groups': '用户组',
|
||||
'user_home': '用户首页',
|
||||
'results_results-list': '实时结果列表',
|
||||
'results_results-detail': '实时结果详情',
|
||||
'test': 'Test',
|
||||
'user_rank-list': '用户排名列表',
|
||||
},
|
||||
page: {
|
||||
login: {
|
||||
|
||||
@ -89,6 +89,10 @@ export function setupAppVersionNotification() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取HTML构建时间
|
||||
* @returns 构建时间字符串或null
|
||||
*/
|
||||
async function getHtmlBuildTime(): Promise<string | null> {
|
||||
const baseUrl = import.meta.env.VITE_BASE_URL || '/'
|
||||
|
||||
|
||||
@ -20,8 +20,26 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
||||
500: () => import("@/views/_builtin/500/index.vue"),
|
||||
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
|
||||
login: () => import("@/views/_builtin/login/index.vue"),
|
||||
"admin-home": () => import("@/views/admin-home/index.vue"),
|
||||
"competition_competition-add": () => import("@/views/competition/competition-add/index.vue"),
|
||||
"competition_competition-detail": () => import("@/views/competition/competition-detail/index.vue"),
|
||||
"competition_competition-list": () => import("@/views/competition/competition-list/index.vue"),
|
||||
home: () => import("@/views/home/index.vue"),
|
||||
dictionary: () => import("@/views/dictionary/index.vue"),
|
||||
"question-store": () => import("@/views/question-store/index.vue"),
|
||||
"rank_rank-detail": () => import("@/views/rank/rank-detail/index.vue"),
|
||||
"rank_rank-list": () => import("@/views/rank/rank-list/index.vue"),
|
||||
"results_results-detail": () => import("@/views/results/results-detail/index.vue"),
|
||||
"results_results-list": () => import("@/views/results/results-list/index.vue"),
|
||||
"template_template-detail": () => import("@/views/template/template-detail/index.vue"),
|
||||
"template_template-list": () => import("@/views/template/template-list/index.vue"),
|
||||
test: () => import("@/views/test/index.vue"),
|
||||
user_analysis: () => import("@/views/user/analysis/index.vue"),
|
||||
user_cover: () => import("@/views/user/cover/index.vue"),
|
||||
user_draw: () => import("@/views/user/draw/index.vue"),
|
||||
user_game: () => import("@/views/user/game/index.vue"),
|
||||
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_rules: () => import("@/views/user/rules/index.vue"),
|
||||
user_teams: () => import("@/views/user/teams/index.vue"),
|
||||
};
|
||||
|
||||
@ -39,13 +39,25 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'admin-home',
|
||||
path: '/admin-home',
|
||||
component: 'layout.base$view.admin-home',
|
||||
meta: {
|
||||
title: 'admin-home',
|
||||
i18nKey: 'route.admin-home',
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'competition',
|
||||
path: '/competition',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'competition',
|
||||
i18nKey: 'route.competition'
|
||||
i18nKey: 'route.competition',
|
||||
icon: 'material-symbols:settings-motion-mode-outline-rounded',
|
||||
order: 1
|
||||
},
|
||||
children: [
|
||||
{
|
||||
@ -82,14 +94,14 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'home',
|
||||
path: '/home',
|
||||
component: 'layout.base$view.home',
|
||||
name: 'dictionary',
|
||||
path: '/dictionary',
|
||||
component: 'layout.base$view.dictionary',
|
||||
meta: {
|
||||
title: 'home',
|
||||
i18nKey: 'route.home',
|
||||
icon: 'mdi:monitor-dashboard',
|
||||
order: 1
|
||||
title: 'dictionary',
|
||||
i18nKey: 'route.dictionary',
|
||||
icon: 'material-symbols:settings',
|
||||
order: 6
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -116,5 +128,243 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
constant: true,
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'question-store',
|
||||
path: '/question-store',
|
||||
component: 'layout.base$view.question-store',
|
||||
meta: {
|
||||
title: 'question-store',
|
||||
i18nKey: 'route.question-store',
|
||||
icon: 'solar:clipboard-text-outline',
|
||||
order: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'rank',
|
||||
path: '/rank',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'rank',
|
||||
i18nKey: 'route.rank',
|
||||
icon: 'mdi:chart-line',
|
||||
order: 5
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'rank_rank-detail',
|
||||
path: '/rank/rank-detail',
|
||||
component: 'view.rank_rank-detail',
|
||||
meta: {
|
||||
title: 'rank_rank-detail',
|
||||
i18nKey: 'route.rank_rank-detail',
|
||||
icon: 'mdi:chart-line-variant',
|
||||
multiTab: true,
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'rank_rank-list',
|
||||
path: '/rank/rank-list',
|
||||
component: 'view.rank_rank-list',
|
||||
meta: {
|
||||
title: 'rank_rank-list',
|
||||
i18nKey: 'route.rank_rank-list',
|
||||
icon: 'mdi:chart-line-variant',
|
||||
order: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'results',
|
||||
path: '/results',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'results',
|
||||
i18nKey: 'route.results',
|
||||
icon: 'mdi:clipboard-check-multiple',
|
||||
order: 2
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'results_results-detail',
|
||||
path: '/results/results-detail',
|
||||
component: 'view.results_results-detail',
|
||||
meta: {
|
||||
title: 'results_results-detail',
|
||||
i18nKey: 'route.results_results-detail',
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'results_results-list',
|
||||
path: '/results/results-list',
|
||||
component: 'view.results_results-list',
|
||||
meta: {
|
||||
title: 'results_results-list',
|
||||
i18nKey: 'route.results_results-list'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'template',
|
||||
path: '/template',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'template',
|
||||
i18nKey: 'route.template',
|
||||
icon: 'material-symbols:settings-motion-mode-outline-rounded',
|
||||
order: 4
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'template_template-detail',
|
||||
path: '/template/template-detail',
|
||||
component: 'view.template_template-detail',
|
||||
meta: {
|
||||
title: 'template_template-detail',
|
||||
i18nKey: 'route.template_template-detail',
|
||||
icon: 'material-symbols:settings-motion-mode-outline-rounded',
|
||||
multiTab: true,
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'template_template-list',
|
||||
path: '/template/template-list',
|
||||
component: 'view.template_template-list',
|
||||
meta: {
|
||||
title: 'template_template-list',
|
||||
i18nKey: 'route.template_template-list',
|
||||
icon: 'material-symbols:settings-motion-mode-outline-rounded',
|
||||
order: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'test',
|
||||
path: '/test',
|
||||
component: 'layout.base$view.test',
|
||||
meta: {
|
||||
title: '测试',
|
||||
i18nKey: 'route.test',
|
||||
icon: 'material-symbols:settings',
|
||||
order: 7,
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
path: '/user',
|
||||
component: 'layout.blank',
|
||||
redirect: '/user/home',
|
||||
meta: {
|
||||
title: 'user',
|
||||
i18nKey: 'route.user',
|
||||
hideInMenu: true,
|
||||
constant: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'user_analysis',
|
||||
path: '/user/analysis',
|
||||
component: 'view.user_analysis',
|
||||
meta: {
|
||||
title: 'user_analysis',
|
||||
i18nKey: 'route.user_analysis',
|
||||
hideInMenu: true,
|
||||
constant: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user_cover',
|
||||
path: '/user/cover',
|
||||
component: 'view.user_cover',
|
||||
meta: {
|
||||
title: 'user_cover',
|
||||
i18nKey: 'route.user_cover',
|
||||
hideInMenu: true,
|
||||
constant: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user_draw',
|
||||
path: '/user/draw',
|
||||
component: 'view.user_draw',
|
||||
meta: {
|
||||
title: 'user_draw',
|
||||
i18nKey: 'route.user_draw',
|
||||
hideInMenu: true,
|
||||
constant: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user_game',
|
||||
path: '/user/game',
|
||||
component: 'view.user_game',
|
||||
meta: {
|
||||
title: 'user_game',
|
||||
i18nKey: 'route.user_game',
|
||||
hideInMenu: true,
|
||||
constant: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user_groups',
|
||||
path: '/user/groups',
|
||||
component: 'view.user_groups',
|
||||
meta: {
|
||||
title: 'user_groups',
|
||||
i18nKey: 'route.user_groups',
|
||||
hideInMenu: true,
|
||||
constant: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user_home',
|
||||
path: '/user/home',
|
||||
component: 'view.user_home',
|
||||
meta: {
|
||||
title: 'user_home',
|
||||
i18nKey: 'route.user_home',
|
||||
hideInMenu: true,
|
||||
constant: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user_rank-list',
|
||||
path: '/user/rank-list',
|
||||
component: 'view.user_rank-list',
|
||||
meta: {
|
||||
title: 'user_rank-list',
|
||||
i18nKey: 'route.user_rank-list'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user_rules',
|
||||
path: '/user/rules',
|
||||
component: 'view.user_rules',
|
||||
meta: {
|
||||
title: 'user_rules',
|
||||
i18nKey: 'route.user_rules',
|
||||
hideInMenu: true,
|
||||
constant: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user_teams',
|
||||
path: '/user/teams',
|
||||
component: 'view.user_teams',
|
||||
meta: {
|
||||
title: 'user_teams',
|
||||
i18nKey: 'route.user_teams',
|
||||
hideInMenu: true,
|
||||
constant: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@ -166,13 +166,35 @@ const routeMap: RouteMap = {
|
||||
"403": "/403",
|
||||
"404": "/404",
|
||||
"500": "/500",
|
||||
"admin-home": "/admin-home",
|
||||
"competition": "/competition",
|
||||
"competition_competition-add": "/competition/competition-add",
|
||||
"competition_competition-detail": "/competition/competition-detail",
|
||||
"competition_competition-list": "/competition/competition-list",
|
||||
"home": "/home",
|
||||
"dictionary": "/dictionary",
|
||||
"iframe-page": "/iframe-page/:url",
|
||||
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"
|
||||
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
|
||||
"question-store": "/question-store",
|
||||
"rank": "/rank",
|
||||
"rank_rank-detail": "/rank/rank-detail",
|
||||
"rank_rank-list": "/rank/rank-list",
|
||||
"results": "/results",
|
||||
"results_results-detail": "/results/results-detail",
|
||||
"results_results-list": "/results/results-list",
|
||||
"template": "/template",
|
||||
"template_template-detail": "/template/template-detail",
|
||||
"template_template-list": "/template/template-list",
|
||||
"test": "/test",
|
||||
"user": "/user",
|
||||
"user_analysis": "/user/analysis",
|
||||
"user_cover": "/user/cover",
|
||||
"user_draw": "/user/draw",
|
||||
"user_game": "/user/game",
|
||||
"user_groups": "/user/groups",
|
||||
"user_home": "/user/home",
|
||||
"user_rank-list": "/user/rank-list",
|
||||
"user_rules": "/user/rules",
|
||||
"user_teams": "/user/teams"
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -12,9 +12,9 @@ import { useRouteStore } from '@/store/modules/route'
|
||||
import { localStg } from '@/utils/storage'
|
||||
|
||||
/**
|
||||
* create route guard
|
||||
* 创建路由守卫
|
||||
*
|
||||
* @param router router instance
|
||||
* @param router 路由实例
|
||||
*/
|
||||
export function createRouteGuard(router: Router) {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
@ -38,39 +38,43 @@ export function createRouteGuard(router: Router) {
|
||||
const hasRole = authStore.userInfo.roles.some(role => routeRoles.includes(role))
|
||||
const hasAuth = authStore.isStaticSuper || !routeRoles.length || hasRole
|
||||
|
||||
// if it is login route when logged in, then switch to the root page
|
||||
// 如果已登录且访问的是登录页,则跳转到根页面
|
||||
if (to.name === loginRoute && isLogin) {
|
||||
next({ name: rootRoute })
|
||||
return
|
||||
}
|
||||
|
||||
// if the route does not need login, then it is allowed to access directly
|
||||
// 如果路由不需要登录,则直接允许访问
|
||||
if (!needLogin) {
|
||||
handleRouteSwitch(to, from, next)
|
||||
return
|
||||
}
|
||||
|
||||
// the route need login but the user is not logged in, then switch to the login page
|
||||
if (!isLogin) {
|
||||
// 如果路由需要登录但用户未登录,则跳转到登录页
|
||||
const isAuthRouteForceLogin = import.meta.env.VITE_AUTH_ROUTE_FORCE_LOGIN === 'Y'
|
||||
const whiteList: string[] = ['user']
|
||||
const isInWhiteList = whiteList.includes(to.name as string)
|
||||
|
||||
if (isAuthRouteForceLogin && !isLogin && !isInWhiteList) {
|
||||
next({ name: loginRoute, query: { redirect: to.fullPath } })
|
||||
return
|
||||
}
|
||||
|
||||
// if the user is logged in but does not have authorization, then switch to the 403 page
|
||||
// 如果用户已登录但没有权限,则跳转到 403 页面
|
||||
if (!hasAuth) {
|
||||
next({ name: noAuthorizationRoute })
|
||||
return
|
||||
}
|
||||
|
||||
// switch route normally
|
||||
// 正常跳转路由
|
||||
handleRouteSwitch(to, from, next)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* initialize route
|
||||
* 初始化路由
|
||||
*
|
||||
* @param to to route
|
||||
* @param to 目标路由
|
||||
*/
|
||||
async function initRoute(to: RouteLocationNormalized): Promise<RouteLocationRaw | null> {
|
||||
const routeStore = useRouteStore()
|
||||
@ -78,12 +82,12 @@ async function initRoute(to: RouteLocationNormalized): Promise<RouteLocationRaw
|
||||
const notFoundRoute: RouteKey = 'not-found'
|
||||
const isNotFoundRoute = to.name === notFoundRoute
|
||||
|
||||
// if the constant route is not initialized, then initialize the constant route
|
||||
// 如果常量路由未初始化,则初始化常量路由
|
||||
if (!routeStore.isInitConstantRoute) {
|
||||
await routeStore.initConstantRoute()
|
||||
|
||||
// the route is captured by the "not-found" route because the constant route is not initialized
|
||||
// after the constant route is initialized, redirect to the original route
|
||||
// 由于常量路由未初始化,路由被“未找到”路由捕获
|
||||
// 常量路由初始化后,重定向到原始路由
|
||||
const path = to.fullPath
|
||||
const location: RouteLocationRaw = {
|
||||
path,
|
||||
@ -97,15 +101,19 @@ async function initRoute(to: RouteLocationNormalized): Promise<RouteLocationRaw
|
||||
|
||||
const isLogin = Boolean(localStg.get('token'))
|
||||
|
||||
if (!isLogin) {
|
||||
// if the user is not logged in and the route is a constant route but not the "not-found" route, then it is allowed to access.
|
||||
const isAuthRouteForceLogin = import.meta.env.VITE_AUTH_ROUTE_FORCE_LOGIN === 'Y'
|
||||
const whiteList: string[] = ['user']
|
||||
const isInWhiteList = whiteList.includes(to.name as string)
|
||||
|
||||
if (isAuthRouteForceLogin && !isLogin && !isInWhiteList) {
|
||||
// 如果用户未登录且路由是常量路由但不是“未找到”路由,则允许访问。
|
||||
if (to.meta.constant && !isNotFoundRoute) {
|
||||
routeStore.onRouteSwitchWhenNotLoggedIn()
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// if the user is not logged in, then switch to the login page
|
||||
// 如果用户未登录,则跳转到登录页
|
||||
const loginRoute: RouteKey = 'login'
|
||||
const query = getRouteQueryOfLoginRoute(to, routeStore.routeHome)
|
||||
|
||||
@ -118,11 +126,11 @@ async function initRoute(to: RouteLocationNormalized): Promise<RouteLocationRaw
|
||||
}
|
||||
|
||||
if (!routeStore.isInitAuthRoute) {
|
||||
// initialize the auth route
|
||||
// 初始化权限路由
|
||||
await routeStore.initAuthRoute()
|
||||
|
||||
// the route is captured by the "not-found" route because the auth route is not initialized
|
||||
// after the auth route is initialized, redirect to the original route
|
||||
// 由于权限路由未初始化,路由被“未找到”路由捕获
|
||||
// 权限路由初始化后,重定向到原始路由
|
||||
if (isNotFoundRoute) {
|
||||
const rootRoute: RouteKey = 'root'
|
||||
const path = to.redirectedFrom?.name === rootRoute ? '/' : to.fullPath
|
||||
@ -140,13 +148,13 @@ async function initRoute(to: RouteLocationNormalized): Promise<RouteLocationRaw
|
||||
|
||||
routeStore.onRouteSwitchWhenLoggedIn()
|
||||
|
||||
// the auth route is initialized
|
||||
// it is not the "not-found" route, then it is allowed to access
|
||||
// 权限路由已初始化
|
||||
// 如果不是“未找到”路由,则允许访问
|
||||
if (!isNotFoundRoute) {
|
||||
return null
|
||||
}
|
||||
|
||||
// it is captured by the "not-found" route, then check whether the route exists
|
||||
// 如果被“未找到”路由捕获,则检查路由是否存在
|
||||
const exist = await routeStore.getIsAuthRouteExist(to.path as RoutePath)
|
||||
const noPermissionRoute: RouteKey = '403'
|
||||
|
||||
@ -162,7 +170,7 @@ async function initRoute(to: RouteLocationNormalized): Promise<RouteLocationRaw
|
||||
}
|
||||
|
||||
function handleRouteSwitch(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
|
||||
// route with href
|
||||
// 外链路由
|
||||
if (to.meta.href) {
|
||||
window.open(to.meta.href, '_blank')
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ const historyCreatorMap: Record<Env.RouterHistoryMode, (base?: string) => Router
|
||||
}
|
||||
|
||||
export const router = createRouter({
|
||||
history: historyCreatorMap[VITE_ROUTER_HISTORY_MODE](VITE_BASE_URL),
|
||||
history: historyCreatorMap[VITE_ROUTER_HISTORY_MODE](VITE_BASE_URL), // 路由模式
|
||||
routes: createBuiltinVueRoutes(),
|
||||
})
|
||||
|
||||
|
||||
@ -8,7 +8,28 @@ import { transformElegantRoutesToVueRoutes } from '../elegant/transform'
|
||||
*
|
||||
* @link https://github.com/reader-starjs/elegant-router?tab=readme-ov-file#custom-route
|
||||
*/
|
||||
const customRoutes: CustomRoute[] = []
|
||||
const customRoutes: CustomRoute[] = [
|
||||
{
|
||||
name: 'root',
|
||||
path: '/',
|
||||
redirect: '/user/home',
|
||||
meta: {
|
||||
title: 'root',
|
||||
constant: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
} as unknown as CustomRoute,
|
||||
{
|
||||
name: 'admin',
|
||||
path: '/admin',
|
||||
redirect: '/competition/competition-list',
|
||||
meta: {
|
||||
title: 'competition/competition-list',
|
||||
constant: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
} as unknown as CustomRoute,
|
||||
]
|
||||
|
||||
/** create routes when the auth route mode is static */
|
||||
export function createStaticRoutes() {
|
||||
|
||||
@ -6,20 +6,20 @@ import { request } from '../request'
|
||||
* @param userName User name
|
||||
* @param password Password
|
||||
*/
|
||||
export function fetchLogin(userName: string, password: string) {
|
||||
export function fetchLogin(account: string, pwd: string) {
|
||||
return request<Api.Auth.LoginToken>({
|
||||
url: '/auth/login',
|
||||
url: '/admin/v1/user/login',
|
||||
method: 'post',
|
||||
data: {
|
||||
userName,
|
||||
password,
|
||||
account,
|
||||
pwd,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** Get user info */
|
||||
export function fetchGetUserInfo() {
|
||||
return request<Api.Auth.UserInfo>({ url: '/auth/getUserInfo' })
|
||||
export function fetchGetUserInfo(userId: number) {
|
||||
return request<Api.Auth.UserInfo>({ url: `admin/v1/manager/detail/${userId}` })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
150
apps/admin/src/service/api/competition.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { request } from '../request'
|
||||
|
||||
/** 房间相关接口 */
|
||||
export function fetchGetRoomList() {
|
||||
return request<App.Service.Response<Api.Competition.Room[]>>({
|
||||
url: '/Base/ActivityMain/GetActivity_RoomList',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 现在服务端把活动新建弄成了三个接口:
|
||||
// 1. 创建活动
|
||||
// 2. 创建活动房间
|
||||
// 3. 创建活动队伍
|
||||
// 但是业务要求最后一步创建
|
||||
|
||||
/** 创建活动基础信息 */
|
||||
export function fetchCreateActivity(data: Api.Competition.CreateRoomRequest) {
|
||||
return request<App.Service.Response<Api.Common.CommonResponse>>({
|
||||
url: '/Base/ActivityMain/AddActivity',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取活动的基础信息 */
|
||||
export function fetchGetActivityDetail(id: number) {
|
||||
return request<App.Service.Response<Api.Competition.ActivityDetail>>({
|
||||
url: `/Base/ActivityMain/GetActivityByID/?ID=${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 创建活动队伍 */
|
||||
export function fetchCreateTeamList(data: Api.Competition.CreateTeamListRequest[]) {
|
||||
return request<App.Service.Response<Api.Common.CommonResponse>>({
|
||||
url: '/Base/ActivityMain/AddActivity_TeamList',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据活动ID获取活动队伍列表 */
|
||||
export function fetchGetTeamList(id: number) {
|
||||
return request<App.Service.Response>({
|
||||
url: `/Base/ActivityMain/GetActivity_TeamsByMainID/?MainID=${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 创建活动题目 */
|
||||
export function fetchCreateQuestion(data: Api.Competition.CreateQuestionRequest[]) {
|
||||
return request<App.Service.Response<Api.Common.CommonResponse>>({
|
||||
url: '/Base/ActivityMain/AddOrUpdateActivity_Question',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据活动ID获取活动题目列表 */
|
||||
export function fetchGetQuestionList(id: number, roundType = 0) {
|
||||
return request<App.Service.Response<Api.Competition.QuestionListRecord[]>>({
|
||||
url: `/Base/ActivityMain/GetActivity_QuestionByActivityID?ID=${id}&RoundType=${roundType}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取活动列表 */
|
||||
export function fetchGetActivityList() {
|
||||
return request<App.Service.Response>({
|
||||
url: '/Base/ActivityMain/Activity_MainList',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 新增活动 (包括基础信息、房间、队伍、题目) */
|
||||
export function fetchCreateCompetition(data: any) {
|
||||
return request<App.Service.Response<Api.Common.CommonResponse>>({
|
||||
url: '/Base/ActivityMain/AddActivityAllData',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据活动id查询分组列表 */
|
||||
export function fetchGetGroupList(id: string) {
|
||||
return request<App.Service.Response>({
|
||||
url: `/Base/ActivityMain/GetActivity_TeamGroupByMainID?ActivityID=${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据组id查询队伍列表 */
|
||||
export function fetchGetTeamListByGroupId(GroupID: number) {
|
||||
return request<App.Service.Response<Api.Competition.TeamListRecord[]>>({
|
||||
url: `/Base/ActivityMain/GetActivity_TeamsByTeamGroupID?GroupID=${GroupID}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 更新活动基础信息 */
|
||||
export function fetchUpdateActivity(data: Api.Competition.ActivityDetail) {
|
||||
return request<App.Service.Response<Api.Common.CommonResponse>>({
|
||||
url: '/Base/ActivityMain/UpdateActivity',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 更新活动队伍 */
|
||||
export function fetchUpdateTeamList(data: Api.Competition.CreateTeamListRequest[]) {
|
||||
return request<App.Service.Response<Api.Common.CommonResponse>>({
|
||||
url: '/Base/ActivityMain/UpdateActivity_TeamList',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 更新活动题目 */
|
||||
export function fetchUpdateQuestion(data: Api.Competition.QuestionListRecord[]) {
|
||||
return request<App.Service.Response<Api.Common.CommonResponse>>({
|
||||
url: '/Base/ActivityMain/AddOrUpdateActivity_Question',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除活动 */
|
||||
export function fetchDeleteActivity(id: number) {
|
||||
return request<App.Service.Response<Api.Common.CommonResponse>>({
|
||||
url: `/Base/ActivityMain/DeleteActivity?ID=${id}`,
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
/** 更新活动状态 */
|
||||
export function fetchUpdatePublishStatus(id: number, status: number) {
|
||||
return request<App.Service.Response<Api.Common.CommonResponse>>({
|
||||
url: `/Base/ActivityMain/UpdateActivityStatus?ActivityID=${id}&status=${status}`,
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
/** 复制活动 */
|
||||
export function fetchCopyActivity(ActivityID: number) {
|
||||
return request<App.Service.Response<Api.Common.CommonResponse>>({
|
||||
url: `/Base/ActivityMain/CopyActivity?ActivityID=${ActivityID}`,
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
70
apps/admin/src/service/api/dictionary.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { request } from '../request'
|
||||
|
||||
/** Get dictionary list */
|
||||
export function getDictionaryList(roomID?: number) {
|
||||
return request<Api.Common.CommonResponse>({
|
||||
url: '/Base/ActivityMain/GetDictionaryList',
|
||||
method: 'get',
|
||||
params: {
|
||||
RoomId: roomID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 新增字段项目 */
|
||||
export function addOrUpdateDictionary(data: Api.Dictionary.AddOrUpdateParams) {
|
||||
return request({
|
||||
url: '/Base/ActivityMain/AddOrUpdateDictionary',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** Delete dictionary */
|
||||
export function deleteDictionary(DicID: number) {
|
||||
return request({
|
||||
url: `/Base/ActivityMain/DeleteDictionary/?DicID=${DicID}`,
|
||||
method: 'post',
|
||||
data: { DicID },
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据字典ID查询字典类型 */
|
||||
export function getDictionaryListUITypeByDicID(dicID: number) {
|
||||
return request<Api.Common.CommonResponse>({
|
||||
url: '/Base/ActivityMain/GetDictionaryListUITypeByDicID',
|
||||
method: 'get',
|
||||
params: {
|
||||
DicID: dicID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据字典ID新建下面的字典类型 */
|
||||
export function addOrUpdateDictionaryUIType(data: Api.Dictionary.AddOrUpdateDictionaryUITypeParams) {
|
||||
return request({
|
||||
url: '/Base/ActivityMain/AddOrUpdateDictionaryUIType',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除字典项 */
|
||||
export function deleteDictionaryItem(DicUIID: number) {
|
||||
return request({
|
||||
url: `/Base/ActivityMain/DeleteDictionaryUIType/?DicUIID=${DicUIID}`,
|
||||
method: 'post',
|
||||
data: { DicUIID },
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据字典value查询字典项 */
|
||||
export function getDictionaryItemListByDicID(DicValue: string) {
|
||||
return request<Api.Common.CommonResponse>({
|
||||
url: `/Base/ActivityMain/GetDictionaryListUITypeByDicValue/?DicValue=${DicValue}`,
|
||||
method: 'get',
|
||||
params: {
|
||||
DicValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
114
apps/admin/src/service/api/game.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { request } from '../request'
|
||||
|
||||
/**
|
||||
* 游戏相关接口 步骤
|
||||
* 1.GetQuestionByActivityIDAndStatus 抽题时候获取题目大纲信息
|
||||
* 2.GetQuestionListDetailRound 获取抽题目详情
|
||||
* 3.fetchSubmitQuestionResult点击开始答题后,把基础信息数据传给service
|
||||
*/
|
||||
|
||||
/** 根据活动id查询分组列表,返回队伍分组id和名称是否已经参赛 */
|
||||
export function fetchGetGroupListByActivityID(id: string, roundType: number = 0) {
|
||||
return request<App.Service.Response<Api.Competition.ActivityTeamGroup[]>>({
|
||||
url: `/Base/ActivityMain/GetActivity_TeamGroupByMainIDAndStatus?ActivityID=${id}&RoundType=${roundType}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 抽题时候获取题目大纲信息 */
|
||||
export function fetchGetQuestionOutline(ActivityID: number, status: number = 0) {
|
||||
return request<App.Service.Response<Api.Competition.QuestionListRecord[]>>({
|
||||
url: `/Base/ActivityMain/GetQuestionByActivityIDAndStatus/?ActivityID=${ActivityID}&status=${status}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
ActivityID,
|
||||
status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 抽题目详情 */
|
||||
export function fetchGetQuestionDetail(ActivityID: string, questionID: number) {
|
||||
return request<App.Service.Response<Api.Competition.QuestionListDetailRound>>({
|
||||
url: `/Base/ActivityMain/GetQuestionListDetailRound/?MainID=${ActivityID}&questionID=${questionID}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
MainID: ActivityID,
|
||||
questionID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 提交抽题结果 */
|
||||
export function fetchSubmitQuestionResult(data: Api.Competition.QuestionAddParams) {
|
||||
return request<App.Service.Response<Api.Competition.QuestionListDetailRound>>({
|
||||
url: `/Base/ActivityMain/AddActivity_TeamQuestionUse/?RoomID=${data.RoomID}&MainID=${data.MainID}&TeamGroupID=${data.TeamGroupID}&QuestionID=${data.QuestionID}&QuestionDetaiID=${data.QuestionDetaiID}&TeamGroupQuestionID=${data.TeamGroupQuestionID}`,
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取游戏现场统计结果 */
|
||||
export function fetchGetGameStatistics(data: { TeamGroupID: number, QuestionID: number }) {
|
||||
return request<App.Service.Response<Api.Competition.TeamAnswerData[]>>({
|
||||
url: `/Base/ActivityMain/GetTeamQuestionUseByTeamGroupIDAndQuestionID/?TeamGroupID=${data.TeamGroupID}&QuestionID=${data.QuestionID}`,
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取游戏现场问题 */
|
||||
export function fetchGetCurrentQuestion(data: Api.Competition.GetCurrentQuestionParams) {
|
||||
return request<App.Service.Response<Api.Competition.CurrentQuestionResponse>>({
|
||||
url: `/Base/ActivityMain/GetCurrentQuestion/?ActivityID=${data.ActivityID}&GroupID=${data.GroupID}&RoundType=${data.RoundType}`,
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 定时刷,评委是否评完结果 判断是否可以下一步 */
|
||||
export function fetchCheckNextStep(TeamGroup_QuestionID: number) {
|
||||
return request<App.Service.Response<any>>({
|
||||
url: `/Base/ActivityMain/GetCurrentQuestionUse/?TeamGroup_QuestionID=${TeamGroup_QuestionID}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
TeamGroup_QuestionID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据活动id查询题目列表 */
|
||||
export function fetchGetQuestionTableByActivityID(ActivityID: string, RoundType: number = 0) {
|
||||
return request<App.Service.Response<Api.Competition.QuestionListDetailRound[]>>({
|
||||
url: `/Base/ActivityMain/GetActivity_QuestionTableByActivityID/?ID=${ActivityID}&RoundType=${RoundType}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据 roomid查询当前正在比赛的题目 */
|
||||
export function fetchGetCurrentQuestionByRoomID(RoomID: number) {
|
||||
return request<App.Service.Response<Api.Competition.GetCurrentQuestionInfo>>({
|
||||
url: `/Base/ActivityMain/SelectTask?RoomID=${RoomID}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
RoomID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据QuestionDetaiID查询题目详情 */
|
||||
export function fetchGetQuestionDetailByID(QuestionDetailID: number) {
|
||||
return request<App.Service.Response<Api.Competition.QuestionListDetailRound>>({
|
||||
url: `/Base/ActivityMain/GetQuestionListDetailByID/?QuestionID=${QuestionDetailID}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 加时赛选择组小队 */
|
||||
export function fetchAddExtraTime(data: Api.Competition.AddExtraTimeParams) {
|
||||
return request<App.Service.Response<any>>({
|
||||
url: `/Base/ActivityMain/AddOverTimeTeamGroup`,
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
@ -1,2 +1,4 @@
|
||||
export * from './auth'
|
||||
export * from './rank'
|
||||
export * from './route'
|
||||
export * from './template'
|
||||
|
||||
127
apps/admin/src/service/api/question.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { request } from '../request'
|
||||
|
||||
/** 获取所有题目列表 */
|
||||
export function fetchGetQuestionListAll() {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
url: '/Base/ActivityMain/GetQuestionListAll',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 新增题目 */
|
||||
export function fetchAddQuestion(data?: Api.Question.AddParams) {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
url: '/Base/ActivityMain/AddBase_QuestionList',
|
||||
method: 'post',
|
||||
data: data || {},
|
||||
})
|
||||
}
|
||||
|
||||
/** 更新题目 */
|
||||
export function fetchUpdateQuestion(data?: Api.Question.AddParams) {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
url: '/Base/ActivityMain/UpdateQuestionList',
|
||||
method: 'post',
|
||||
data: data || {},
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除题目 */
|
||||
export function fetchDeleteQuestion(id: number) {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
url: `/Base/ActivityMain/DeleteQuestionList/?Id=${id}`,
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取所有问题库列表(分页) */
|
||||
export function fetchGetQuestionLibraryListAll(params: Api.Question.GetQuestionLibraryListAllParams) {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
url: '/Base/ActivityMain/GetQuestionListDetail',
|
||||
method: 'get',
|
||||
params: {
|
||||
QuestionId: params.questionId,
|
||||
PageIndex: params.pageIndex,
|
||||
PageSizes: params.pageSizes,
|
||||
keyWords: params.keyWords,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 新增一条问题库 */
|
||||
export function fetchAddQuestionLibrary(params: Api.Question.AddQuestionLibraryParams, file?: File | null) {
|
||||
const formData = new FormData()
|
||||
if (file) {
|
||||
formData.append('Image', file)
|
||||
}
|
||||
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,
|
||||
IsDisabled: params.IsDisabled,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 禁用/启用 一条问题库 */
|
||||
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)
|
||||
}
|
||||
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,
|
||||
IsDisabled: params.IsDisabled,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除一条问题库 */
|
||||
export function fetchDeleteQuestionLibrary(ids: number[]) {
|
||||
return request({
|
||||
url: `/Base/ActivityMain/DeleteQuestionListDetail`,
|
||||
method: 'post',
|
||||
data: ids,
|
||||
})
|
||||
}
|
||||
62
apps/admin/src/service/api/rank.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { request } from '../request'
|
||||
|
||||
/** get user list */
|
||||
export function fetchRankList(MainID: number, RoundType = 0) {
|
||||
return request<Api.Rank.CommonRecord>({
|
||||
url: `/Base/ActivityMain/GetTeamsTotal/list?MainID=${MainID}&RoundType=${RoundType}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取队伍排名详情 */
|
||||
export function fetchRankDetail(MainID: number, RoundType = 0) {
|
||||
return request<Api.Rank.CommonRecord>({
|
||||
url: `/Base/ActivityMain/GetTeamsTotal?MainID=${MainID}&RoundType=${RoundType}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据队伍 id获取具体的题目分数 */
|
||||
export function fetchTeamQuestionScore(TeamID: number) {
|
||||
return request<Api.Rank.CommonRecord>({
|
||||
url: `/Base/ActivityMain/GetTeamQuestionUseByTeamID?TeamID=${TeamID}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取活动排行列表 */
|
||||
export function fetchActivityRankList(PageIndex: number, PageSize: number = 10) {
|
||||
return request<App.Service.Response<Api.Rank.ActivityRankList>>({
|
||||
url: `/Base/ActivityMain/GetActivity_Main_TotalPager?PageIndex=${PageIndex}&PageSizes=${PageSize}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改排名的队伍分数 */
|
||||
export function updateTeamScore(data: Api.Rank.UpdateTeamScoreParams[], activityId: number) {
|
||||
return request<App.Service.Response>({
|
||||
url: `/Base/ActivityMain/UpdateTeamQuestionResultRecore/?ActivityID=${activityId}`,
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 发布排名,更新状态 */
|
||||
export function publishRank(Id: number, Status = 1) {
|
||||
return request<App.Service.Response>({
|
||||
url: `/Base/ActivityMain/UpdateResultRecoreStatus`,
|
||||
method: 'post',
|
||||
data: {
|
||||
Id,
|
||||
Status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取用户排名列表 */
|
||||
export function fetchUserRankList(MainID: number, RoundType: number = 0) {
|
||||
return request<App.Service.Response<Api.Rank.ActivityRankResList>>({
|
||||
url: `/Base/ActivityMain/GetTeamsTotalList?MainID=${MainID}&RoundType=${RoundType}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
28
apps/admin/src/service/api/result.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { request } from '../request'
|
||||
|
||||
/** 评委消费题目,表示用了该题目 */
|
||||
export function updateCurrentQuestionUse(TeamGroupID: number) {
|
||||
return request<App.Service.Response>({
|
||||
url: `/Base/ActivityMain/UpdateCurrentQuestionUse?TeamGroupID=${TeamGroupID}`,
|
||||
method: 'post',
|
||||
data: { TeamGroupID },
|
||||
})
|
||||
}
|
||||
|
||||
/** 评委对其进行打分 */
|
||||
export function updateScore(data: { Id: number, ResultPotins: number, QuestionDetailID: number, QuestionId: number }[]) {
|
||||
return request<App.Service.Response>({
|
||||
url: `/Base/ActivityMain/UpdateTeamQuestionResultList`,
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 结束活动时候调用 */
|
||||
export function endActivity(ActivityID: number) {
|
||||
return request<App.Service.Response>({
|
||||
url: `/Base/ActivityMain/AddOrUpdateActivity_Main_Total?ActivityID=${ActivityID}`,
|
||||
method: 'post',
|
||||
data: { ActivityID },
|
||||
})
|
||||
}
|
||||
55
apps/admin/src/service/api/system-manage.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { request } from '../request'
|
||||
|
||||
/** get role list */
|
||||
export function fetchGetRoleList(params?: Api.SystemManage.RoleSearchParams) {
|
||||
return request<Api.SystemManage.RoleList>({
|
||||
url: '/systemManage/getRoleList',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* get all roles
|
||||
*
|
||||
* these roles are all enabled
|
||||
*/
|
||||
export function fetchGetAllRoles() {
|
||||
return request<Api.SystemManage.AllRole[]>({
|
||||
url: '/systemManage/getAllRoles',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** get user list */
|
||||
export function fetchGetUserList(params?: Api.SystemManage.UserSearchParams) {
|
||||
return request<Api.SystemManage.UserList>({
|
||||
url: '/admin/v1/class/pagelist',
|
||||
method: 'post',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** get menu list */
|
||||
export function fetchGetMenuList() {
|
||||
return request<Api.SystemManage.MenuList>({
|
||||
url: '/systemManage/getMenuList/v2',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** get all pages */
|
||||
export function fetchGetAllPages() {
|
||||
return request<string[]>({
|
||||
url: '/systemManage/getAllPages',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** get menu tree */
|
||||
export function fetchGetMenuTree() {
|
||||
return request<Api.SystemManage.MenuTree[]>({
|
||||
url: '/systemManage/getMenuTree',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
43
apps/admin/src/service/api/template.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { request } from '../request'
|
||||
|
||||
/**
|
||||
* 获取模板列表
|
||||
*/
|
||||
export function fetchTemplateList(data?: Api.Template.TemplateSearchParams) {
|
||||
return request<App.Service.Response<Api.Common.CommonResponse>>({
|
||||
url: '/Base/ActivityMain/GetTemplete_Pag',
|
||||
method: 'get',
|
||||
params: data || {},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增模板, 更新模板
|
||||
*/
|
||||
export function fetchAddTemplate(data?: Api.Template.AddTemplateParams) {
|
||||
return request<App.Service.Response<Api.Common.PaginatingQueryRecord<Api.Template.CommonRecord>>>({
|
||||
url: '/Base/ActivityMain/AddOrUpdateTemplete_Pag',
|
||||
method: 'post',
|
||||
data: data || {},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板详情
|
||||
*/
|
||||
export function fetchTemplateDetail(id: number) {
|
||||
return request<App.Service.Response<Api.Template.TemplateDetail>>({
|
||||
url: `/Base/ActivityMain/GetTemplete_PagByID?id=${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模板
|
||||
*/
|
||||
export function fetchDeleteTemplate(id: number) {
|
||||
return request<App.Service.Response<Api.Common.PaginatingQueryRecord<Api.Template.CommonRecord>>>({
|
||||
url: `/Base/ActivityMain/DeleteTemplete_Pag?id=${id}`,
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
31
apps/admin/src/service/api/upload.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { request } from '../request'
|
||||
|
||||
export interface ReqDeleteAliOssFile {
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface AliOssSTS {
|
||||
AccessKeyId: string
|
||||
AccessKeySecret: string
|
||||
SecurityToken: string
|
||||
Expiration: string
|
||||
BucketName: string
|
||||
Region: string
|
||||
}
|
||||
|
||||
/** 得到上传的token */
|
||||
export function getAliOssTokenAxios() {
|
||||
return request<App.Service.Response<AliOssSTS>>({
|
||||
url: `${import.meta.env.VITE_BASE_UPLOAD_URL}/admin/v1/oss/sts`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除阿里云oss 文件 */
|
||||
export function deleteAliOssFileAxios(data: ReqDeleteAliOssFile) {
|
||||
return request<App.Service.Response<boolean>>({
|
||||
url: `${import.meta.env.VITE_BASE_UPLOAD_URL}/admin/v1/oss/delete`,
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
@ -10,36 +10,50 @@ import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared'
|
||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y'
|
||||
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy)
|
||||
|
||||
/**
|
||||
* 基础请求
|
||||
*/
|
||||
export const request = createFlatRequest(
|
||||
{
|
||||
baseURL,
|
||||
headers: {
|
||||
apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2',
|
||||
'apifoxToken': 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2', // 用于 apifox 调试
|
||||
'Custom-Platform': 'pc',
|
||||
},
|
||||
},
|
||||
{
|
||||
defaultState: {
|
||||
errMsgStack: [],
|
||||
refreshTokenPromise: null,
|
||||
errMsgStack: [], // 错误信息栈
|
||||
refreshTokenPromise: null, // 刷新token Promise
|
||||
} as RequestInstanceState,
|
||||
// 响应数据转换
|
||||
transform(response: AxiosResponse<App.Service.Response<any>>) {
|
||||
return response.data.data
|
||||
// 如果后端返回的data 是string 类型,需要转换为对象
|
||||
if (typeof response.data.data === 'string') {
|
||||
response.data.data = JSON.parse(response.data.data)
|
||||
return response.data
|
||||
}
|
||||
return response.data
|
||||
},
|
||||
// 请求配置转换
|
||||
async onRequest(config) {
|
||||
const Authorization = getAuthorization()
|
||||
Object.assign(config.headers, { Authorization })
|
||||
|
||||
return config
|
||||
},
|
||||
// 响应状态判断
|
||||
isBackendSuccess(response) {
|
||||
// when the backend response code is "0000"(default), it means the request is success
|
||||
// to change this logic by yourself, you can modify the `VITE_SERVICE_SUCCESS_CODE` in `.env` file
|
||||
// 当后端响应状态码为 "200"(默认值)时,表示请求成功
|
||||
// 如果你想自行更改此逻辑,可以在 .env 文件中修改 VITE_SERVICE_SUCCESS_CODE
|
||||
return String(response.data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE
|
||||
},
|
||||
// 响应错误处理
|
||||
async onBackendFail(response, instance) {
|
||||
const authStore = useAuthStore()
|
||||
const responseCode = String(response.data.code)
|
||||
|
||||
//
|
||||
function handleLogout() {
|
||||
authStore.resetStore()
|
||||
}
|
||||
@ -51,19 +65,19 @@ export const request = createFlatRequest(
|
||||
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg)
|
||||
}
|
||||
|
||||
// when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page
|
||||
// 当后端响应状态码在 logoutCodes 中时,表示用户将被登出并重定向到登录页面
|
||||
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || []
|
||||
if (logoutCodes.includes(responseCode)) {
|
||||
handleLogout()
|
||||
return null
|
||||
}
|
||||
|
||||
// when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal
|
||||
// 当后端响应状态码在 modalLogoutCodes 中时,表示将通过显示模态框来使用户登出
|
||||
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || []
|
||||
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) {
|
||||
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg]
|
||||
|
||||
// prevent the user from refreshing the page
|
||||
// 防止用户刷新页面
|
||||
window.addEventListener('beforeunload', handleLogout)
|
||||
|
||||
window.$dialog?.error({
|
||||
@ -83,8 +97,8 @@ export const request = createFlatRequest(
|
||||
return null
|
||||
}
|
||||
|
||||
// when the backend response code is in `expiredTokenCodes`, it means the token is expired, and refresh token
|
||||
// the api `refreshToken` can not return error code in `expiredTokenCodes`, otherwise it will be a dead loop, should return `logoutCodes` or `modalLogoutCodes`
|
||||
// 当后端响应状态码在 expiredTokenCodes 中时,表示 token 已过期,需要刷新 token
|
||||
// refreshToken 接口不能返回 expiredTokenCodes 中的错误码,否则会形成死循环,应该返回 logoutCodes 或 modalLogoutCodes
|
||||
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || []
|
||||
if (expiredTokenCodes.includes(responseCode)) {
|
||||
const success = await handleExpiredRequest(request.state)
|
||||
@ -99,24 +113,24 @@ export const request = createFlatRequest(
|
||||
return null
|
||||
},
|
||||
onError(error) {
|
||||
// when the request is fail, you can show error message
|
||||
// 当请求失败时,可以显示错误信息
|
||||
|
||||
let message = error.message
|
||||
let backendErrorCode = ''
|
||||
|
||||
// get backend error message and code
|
||||
// 获取后端错误信息和状态码
|
||||
if (error.code === BACKEND_ERROR_CODE) {
|
||||
message = error.response?.data?.msg || message
|
||||
backendErrorCode = String(error.response?.data?.code || '')
|
||||
}
|
||||
|
||||
// the error message is displayed in the modal
|
||||
// 错误信息已在模态框中显示
|
||||
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || []
|
||||
if (modalLogoutCodes.includes(backendErrorCode)) {
|
||||
return
|
||||
}
|
||||
|
||||
// when the token is expired, refresh token and retry request, so no need to show error message
|
||||
// 当 token 过期时,刷新 token 并重试请求,因此不需要显示错误信息
|
||||
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || []
|
||||
if (expiredTokenCodes.includes(backendErrorCode)) {
|
||||
return
|
||||
@ -127,39 +141,48 @@ export const request = createFlatRequest(
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* 演示请求
|
||||
*/
|
||||
export const demoRequest = createRequest(
|
||||
{
|
||||
baseURL: otherBaseURL.demo,
|
||||
},
|
||||
/**
|
||||
* 演示请求响应数据转换
|
||||
*/
|
||||
{
|
||||
// 响应数据转换
|
||||
transform(response: AxiosResponse<App.Service.DemoResponse>) {
|
||||
return response.data.result
|
||||
},
|
||||
// 请求配置转换
|
||||
async onRequest(config) {
|
||||
const { headers } = config
|
||||
|
||||
// set token
|
||||
// 设置 token
|
||||
const token = localStg.get('token')
|
||||
const Authorization = token ? `Bearer ${token}` : null
|
||||
Object.assign(headers, { Authorization })
|
||||
|
||||
return config
|
||||
},
|
||||
// 响应状态判断
|
||||
isBackendSuccess(response) {
|
||||
// when the backend response code is "200", it means the request is success
|
||||
// you can change this logic by yourself
|
||||
// 当后端响应状态码为 "200" 时,表示请求成功
|
||||
// 你可以自行更改此逻辑
|
||||
return response.data.status === '200'
|
||||
},
|
||||
async onBackendFail(_response) {
|
||||
// when the backend response code is not "200", it means the request is fail
|
||||
// for example: the token is expired, refresh token and retry request
|
||||
// 当后端响应状态码不是 "200" 时,表示请求失败
|
||||
// 例如:token 过期,刷新 token 并重试请求
|
||||
},
|
||||
onError(error) {
|
||||
// when the request is fail, you can show error message
|
||||
// 当请求失败时,可以显示错误信息
|
||||
|
||||
let message = error.message
|
||||
|
||||
// show backend error message
|
||||
// 显示后端错误信息
|
||||
if (error.code === BACKEND_ERROR_CODE) {
|
||||
message = error.response?.data?.message || message
|
||||
}
|
||||
|
||||
76
apps/admin/src/service/request/readme.md
Normal file
@ -0,0 +1,76 @@
|
||||
## error 字段 不是后端接口直接返回的 ,而是前端请求工具 request (基于 @sa/axios 的 createFlatRequest 封装) 为了简化错误处理而增加的。
|
||||
|
||||
它的机制如下:
|
||||
|
||||
- 请求成功 : error 为 null , data 为后端返回的响应体(包含 code , msg , data , success 等字段)。
|
||||
- 请求失败 (如网络断开、超时): error 会包含错误信息对象, data 为 null 。
|
||||
这种写法让您可以直接解构返回值判断请求是否成功,避免了使用 `try-catch` 包裹。
|
||||
|
||||
总结:
|
||||
|
||||
- response (您重命名的 data ) :包含后端返回的 { success, code, msg, data } 。
|
||||
- error :前端请求库生成的错误对象,用于判断 HTTP 请求层面的成败
|
||||
|
||||
参考:
|
||||
|
||||
- [@sa/axios 文档](https://bytedance.larkoffice.com/wiki/wikcnn33hQ3h3FkqCnqCn33h3Fk)
|
||||
|
||||
例子:
|
||||
|
||||
```typescript
|
||||
import request from '@/service/request'
|
||||
|
||||
// 成功请求
|
||||
const { data, error } = await request.get('/api/success')
|
||||
if (error) {
|
||||
console.error('请求失败:', error)
|
||||
return
|
||||
}
|
||||
console.log('请求成功:', data)
|
||||
|
||||
// 失败请求(如网络断开)
|
||||
const { data: failedData, error: failedError } = await request.get('/api/fail')
|
||||
if (failedError) {
|
||||
console.error('请求失败:', failedError)
|
||||
return
|
||||
}
|
||||
console.log('请求成功:', failedData) // 不会执行到这里
|
||||
|
||||
/**
|
||||
* 初始化模板详情
|
||||
*/
|
||||
async function initData() {
|
||||
const id = Number(route.query.id)
|
||||
if (id) {
|
||||
try {
|
||||
loading.value = true
|
||||
const { data: response, error } = await fetchTemplateDetail(id)
|
||||
if (!error && response) {
|
||||
const { data, success } = response
|
||||
if (success) {
|
||||
const { Name, Width, Height, BackGroundUrl, TempContent } = data || {}
|
||||
templateInfo.value = {
|
||||
name: Name,
|
||||
width: Width,
|
||||
height: Height,
|
||||
backGroundUrl: BackGroundUrl,
|
||||
}
|
||||
imgSrc.value = BackGroundUrl || ''
|
||||
|
||||
if (TempContent) {
|
||||
try {
|
||||
regions.value = JSON.parse(TempContent || '[]')
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to parse tempContent', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -10,7 +10,9 @@ export function getAuthorization() {
|
||||
return Authorization
|
||||
}
|
||||
|
||||
/** refresh token */
|
||||
/**
|
||||
* 刷新token
|
||||
*/
|
||||
async function handleRefreshToken() {
|
||||
const { resetStore } = useAuthStore()
|
||||
|
||||
@ -27,6 +29,9 @@ async function handleRefreshToken() {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理过期token的请求
|
||||
*/
|
||||
export async function handleExpiredRequest(state: RequestInstanceState) {
|
||||
if (!state.refreshTokenPromise) {
|
||||
state.refreshTokenPromise = handleRefreshToken()
|
||||
@ -41,6 +46,9 @@ export async function handleExpiredRequest(state: RequestInstanceState) {
|
||||
return success
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误信息
|
||||
*/
|
||||
export function showErrorMsg(state: RequestInstanceState, message: string) {
|
||||
if (!state.errMsgStack?.length) {
|
||||
state.errMsgStack = []
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export interface RequestInstanceState {
|
||||
/** the promise of refreshing token */
|
||||
/** 刷新token Promise */
|
||||
refreshTokenPromise: Promise<boolean> | null
|
||||
/** the request error message stack */
|
||||
/** 请求错误信息栈 */
|
||||
errMsgStack: string[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { App } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
import { resetSetupStore } from './plugins'
|
||||
|
||||
/** Setup Vue store plugin pinia */
|
||||
@ -7,6 +8,7 @@ export function setupStore(app: App) {
|
||||
const store = createPinia()
|
||||
|
||||
store.use(resetSetupStore)
|
||||
store.use(piniaPluginPersistedstate)
|
||||
|
||||
app.use(store)
|
||||
}
|
||||
|
||||
55
apps/admin/src/store/modules/activityinfo/index.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useActivityInfoStore = defineStore('activity-info', () => {
|
||||
const activityInfo = ref<Api.Competition.ActivityDetail | null>(null)
|
||||
const activityId = ref<number | null>(null)
|
||||
const groupId = ref<number | null>(null)
|
||||
const teamId = ref<number | null>(null)
|
||||
const teamIds = ref<number[]>([])
|
||||
|
||||
function setActivityInfo(info: Api.Competition.ActivityDetail) {
|
||||
activityInfo.value = info
|
||||
activityId.value = info.Id
|
||||
}
|
||||
|
||||
function setActivityId(id: number) {
|
||||
activityId.value = id
|
||||
}
|
||||
|
||||
function setGroupId(id: number) {
|
||||
groupId.value = id
|
||||
}
|
||||
|
||||
function setTeamId(id: number) {
|
||||
teamId.value = id
|
||||
}
|
||||
|
||||
function setTeamIds(ids: number[]) {
|
||||
teamIds.value = ids
|
||||
}
|
||||
|
||||
function clearActivityInfo() {
|
||||
activityInfo.value = null
|
||||
activityId.value = null
|
||||
groupId.value = null
|
||||
teamId.value = null
|
||||
teamIds.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
teamIds,
|
||||
activityInfo,
|
||||
activityId,
|
||||
groupId,
|
||||
teamId,
|
||||
setActivityInfo,
|
||||
setActivityId,
|
||||
setGroupId,
|
||||
setTeamId,
|
||||
setTeamIds,
|
||||
clearActivityInfo,
|
||||
}
|
||||
}, {
|
||||
persist: true,
|
||||
})
|
||||
@ -9,7 +9,7 @@ import { fetchGetUserInfo, fetchLogin } from '@/service/api'
|
||||
import { localStg } from '@/utils/storage'
|
||||
import { useRouteStore } from '../route'
|
||||
import { useTabStore } from '../tab'
|
||||
import { clearAuthStorage, getToken } from './shared'
|
||||
import { clearAuthStorage, getToken, getUserId } from './shared'
|
||||
|
||||
export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
const route = useRoute()
|
||||
@ -98,7 +98,6 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
*/
|
||||
async function login(userName: string, password: string, redirect = true) {
|
||||
startLoading()
|
||||
|
||||
const { data: loginToken, error } = await fetchLogin(userName, password)
|
||||
|
||||
if (!error) {
|
||||
@ -133,9 +132,10 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
// 1. stored in the localStorage, the later requests need it in headers
|
||||
localStg.set('token', loginToken.token)
|
||||
localStg.set('refreshToken', loginToken.refreshToken)
|
||||
localStg.set('userId', loginToken.id)
|
||||
|
||||
// 2. get user info
|
||||
const pass = await getUserInfo()
|
||||
const pass = await getUserInfo(loginToken.id)
|
||||
|
||||
if (pass) {
|
||||
token.value = loginToken.token
|
||||
@ -146,8 +146,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
return false
|
||||
}
|
||||
|
||||
async function getUserInfo() {
|
||||
const { data: info, error } = await fetchGetUserInfo()
|
||||
async function getUserInfo(userId: number) {
|
||||
const { data: info, error } = await fetchGetUserInfo(userId)
|
||||
|
||||
if (!error) {
|
||||
// update store
|
||||
@ -161,9 +161,10 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
|
||||
async function initUserInfo() {
|
||||
const hasToken = getToken()
|
||||
const userId = getUserId()
|
||||
|
||||
if (hasToken) {
|
||||
const pass = await getUserInfo()
|
||||
if (hasToken && userId) {
|
||||
const pass = await getUserInfo(userId)
|
||||
|
||||
if (!pass) {
|
||||
resetStore()
|
||||
|
||||
@ -5,8 +5,14 @@ export function getToken() {
|
||||
return localStg.get('token') || ''
|
||||
}
|
||||
|
||||
export function getUserId() {
|
||||
const userId = localStg.get('userId')
|
||||
return userId || 0
|
||||
}
|
||||
|
||||
/** Clear auth storage */
|
||||
export function clearAuthStorage() {
|
||||
localStg.remove('token')
|
||||
localStg.remove('refreshToken')
|
||||
localStg.remove('userId')
|
||||
}
|
||||
|
||||
59
apps/admin/src/store/modules/business/index.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getDictionaryItemListByDicID, getDictionaryList } from '@/service/api/dictionary'
|
||||
|
||||
export const useBusinessStore = defineStore('business-store', () => {
|
||||
const dictList = ref<Api.Dictionary.DictionaryItem[]>([])
|
||||
const dictData = ref<Record<string, Api.Dictionary.DictionaryUIItem[]>>({})
|
||||
const isInitialized = ref(false)
|
||||
const loadingMap = ref<Record<string, boolean>>({})
|
||||
|
||||
/** 初始化字典 */
|
||||
async function initDict() {
|
||||
if (isInitialized.value)
|
||||
return
|
||||
const { data: res, error } = await getDictionaryList()
|
||||
if (!error && res) {
|
||||
dictList.value = res.data || []
|
||||
isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取字典数据 */
|
||||
async function getDict(key: string) {
|
||||
// 已经加载过,直接返回
|
||||
if (dictData.value[key] && dictData.value[key].length > 0)
|
||||
return dictData.value[key]
|
||||
|
||||
if (loadingMap.value[key]) {
|
||||
return []
|
||||
}
|
||||
|
||||
loadingMap.value[key] = true
|
||||
try {
|
||||
const { data: res, error } = await getDictionaryItemListByDicID(key)
|
||||
if (!error && res) {
|
||||
const list = res.data || []
|
||||
// 使用新对象赋值,确保触发响应式更新
|
||||
dictData.value = {
|
||||
...dictData.value,
|
||||
[key]: list,
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
loadingMap.value[key] = false
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`获取字典数据 ${key}`, dictData.value[key])
|
||||
return dictData.value[key]
|
||||
}
|
||||
|
||||
return {
|
||||
dictList,
|
||||
dictData,
|
||||
loadingMap,
|
||||
initDict,
|
||||
getDict,
|
||||
}
|
||||
})
|
||||
156
apps/admin/src/store/modules/competition/index.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { AudioController } from '@/utils/audio'
|
||||
|
||||
export const useCompetitionStore = defineStore('competition', () => {
|
||||
// ==========================================
|
||||
// State - 核心数据
|
||||
// ==========================================
|
||||
|
||||
/** 从抽题页获取当前题目基础提纲信息 */
|
||||
const currentQuestionMainInfo = ref<Api.Competition.CurrentQuestionResponse>({} as Api.Competition.CurrentQuestionResponse)
|
||||
|
||||
/** 当前选中的题目基础信息 (从抽题页获取) */
|
||||
const currentQuestionInfo = ref<Api.Competition.QuestionListRecord>({} as Api.Competition.QuestionListRecord)
|
||||
|
||||
/** 当前题目的详细信息 (从详情接口获取) */
|
||||
const currentQuestionDetail = ref<Api.Competition.QuestionListDetailRound>({} as Api.Competition.QuestionListDetailRound)
|
||||
|
||||
// ==========================================
|
||||
// State - 流程控制 & 交互状态
|
||||
// ==========================================
|
||||
|
||||
/** 倒计时剩余时间 (秒) */
|
||||
const timeLeft = ref(0)
|
||||
|
||||
/** 定时器引用 */
|
||||
const timerInterval = ref<number | null>(null)
|
||||
|
||||
/** 音效控制器 */
|
||||
const audioController = new AudioController()
|
||||
|
||||
// ==========================================
|
||||
// Actions - 数据操作
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* 设置题目列表
|
||||
*/
|
||||
function setCurrentQuestionMainInfo(obj: Api.Competition.CurrentQuestionResponse) {
|
||||
currentQuestionMainInfo.value = obj as Api.Competition.CurrentQuestionResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空设置题目列表
|
||||
*/
|
||||
function resetCurrentQuestionMainInfo() {
|
||||
currentQuestionMainInfo.value = {} as Api.Competition.CurrentQuestionResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前题目基础信息
|
||||
*/
|
||||
function setCurrentQuestionInfo(info: Api.Competition.QuestionListRecord) {
|
||||
currentQuestionInfo.value = info
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空设置当前题目基础信息
|
||||
*/
|
||||
function resetCurrentQuestionInfo() {
|
||||
currentQuestionInfo.value = {} as Api.Competition.QuestionListRecord
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前题目详情
|
||||
*/
|
||||
function setCurrentQuestionDetail(detail: Api.Competition.QuestionListDetailRound) {
|
||||
currentQuestionDetail.value = detail
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空设置当前题目详情
|
||||
*/
|
||||
function resetCurrentQuestionDetail() {
|
||||
currentQuestionDetail.value = {} as Api.Competition.QuestionListDetailRound
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Actions - 流程控制
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* 启动倒计时
|
||||
* @param duration 持续时间(秒)
|
||||
*/
|
||||
function startTimer(duration: number) {
|
||||
stopTimer()
|
||||
timeLeft.value = duration
|
||||
|
||||
// 播放开始音效
|
||||
audioController.play('start')
|
||||
|
||||
timerInterval.value = window.setInterval(() => {
|
||||
if (timeLeft.value > 0) {
|
||||
timeLeft.value--
|
||||
// 5秒内播放倒计时音效
|
||||
if (timeLeft.value === 5) {
|
||||
audioController.play('boy5')
|
||||
}
|
||||
}
|
||||
else {
|
||||
stopTimer()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止倒计时
|
||||
*/
|
||||
function stopTimer() {
|
||||
if (timerInterval.value) {
|
||||
clearInterval(timerInterval.value)
|
||||
timerInterval.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置当前题目流程状态 (不清除题目基础信息)
|
||||
*/
|
||||
function resetRoundState() {
|
||||
currentQuestionDetail.value = {} as Api.Competition.QuestionListDetailRound
|
||||
stopTimer()
|
||||
timeLeft.value = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化/重置所有数据
|
||||
*/
|
||||
function initData() {
|
||||
currentQuestionMainInfo.value = {} as Api.Competition.CurrentQuestionResponse
|
||||
currentQuestionInfo.value = {} as Api.Competition.QuestionListRecord
|
||||
resetRoundState()
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
currentQuestionMainInfo,
|
||||
currentQuestionInfo,
|
||||
currentQuestionDetail,
|
||||
timeLeft,
|
||||
|
||||
// Actions
|
||||
setCurrentQuestionMainInfo,
|
||||
setCurrentQuestionInfo,
|
||||
setCurrentQuestionDetail,
|
||||
startTimer,
|
||||
stopTimer,
|
||||
resetRoundState,
|
||||
initData,
|
||||
resetCurrentQuestionMainInfo,
|
||||
resetCurrentQuestionInfo,
|
||||
resetCurrentQuestionDetail,
|
||||
}
|
||||
}, {
|
||||
persist: true,
|
||||
})
|
||||
@ -30,27 +30,26 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
const { bool: isInitAuthRoute, setBool: setIsInitAuthRoute } = useBoolean()
|
||||
|
||||
/**
|
||||
* Auth route mode
|
||||
* 权限路由模式
|
||||
*
|
||||
* It recommends to use static mode in the development environment, and use dynamic mode in the production
|
||||
* environment, if use static mode in development environment, the auth routes will be auto generated by plugin
|
||||
* "@elegant-router/vue"
|
||||
* 建议在开发环境中使用静态模式,在生产环境中使用动态模式
|
||||
* 如果在开发环境中使用静态模式,权限路由将由插件 "@elegant-router/vue" 自动生成
|
||||
*/
|
||||
const authRouteMode = ref(import.meta.env.VITE_AUTH_ROUTE_MODE)
|
||||
|
||||
/** Home route key */
|
||||
/** 首页路由 key */
|
||||
const routeHome = ref(import.meta.env.VITE_ROUTE_HOME)
|
||||
|
||||
/**
|
||||
* Set route home
|
||||
* 设置首页路由
|
||||
*
|
||||
* @param routeKey Route key
|
||||
* @param routeKey 路由 key
|
||||
*/
|
||||
function setRouteHome(routeKey: LastLevelRouteKey) {
|
||||
routeHome.value = routeKey
|
||||
}
|
||||
|
||||
/** constant routes */
|
||||
/** 常量路由 */
|
||||
const constantRoutes = shallowRef<ElegantConstRoute[]>([])
|
||||
|
||||
function addConstantRoutes(routes: ElegantConstRoute[]) {
|
||||
@ -63,7 +62,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
constantRoutes.value = Array.from(constantRoutesMap.values())
|
||||
}
|
||||
|
||||
/** auth routes */
|
||||
/** 权限路由 */
|
||||
const authRoutes = shallowRef<ElegantConstRoute[]>([])
|
||||
|
||||
function addAuthRoutes(routes: ElegantConstRoute[]) {
|
||||
@ -78,44 +77,51 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
|
||||
const removeRouteFns: (() => void)[] = []
|
||||
|
||||
/** Global menus */
|
||||
/** 全局菜单 */
|
||||
const menus = ref<App.Global.Menu[]>([])
|
||||
const menusForBreadcrumb = ref<App.Global.Menu[]>([])
|
||||
const searchMenus = computed(() => transformMenuToSearchMenus(menus.value))
|
||||
|
||||
/** Get global menus */
|
||||
/**
|
||||
* 获取全局菜单
|
||||
*
|
||||
* @param routes 路由
|
||||
*/
|
||||
function getGlobalMenus(routes: ElegantConstRoute[]) {
|
||||
menus.value = getGlobalMenusByAuthRoutes(routes)
|
||||
menus.value = getGlobalMenusByAuthRoutes(routes, true, false)
|
||||
menusForBreadcrumb.value = getGlobalMenusByAuthRoutes(routes, false, true)
|
||||
}
|
||||
|
||||
/** Update global menus by locale */
|
||||
/** 根据语言更新全局菜单 */
|
||||
function updateGlobalMenusByLocale() {
|
||||
menus.value = updateLocaleOfGlobalMenus(menus.value)
|
||||
menusForBreadcrumb.value = updateLocaleOfGlobalMenus(menusForBreadcrumb.value)
|
||||
}
|
||||
|
||||
/** Cache routes */
|
||||
/** 缓存路由 */
|
||||
const cacheRoutes = ref<RouteKey[]>([])
|
||||
|
||||
/**
|
||||
* Exclude cache routes
|
||||
* 排除缓存路由
|
||||
*
|
||||
* for reset route cache
|
||||
* 用于重置路由缓存
|
||||
*/
|
||||
const excludeCacheRoutes = ref<RouteKey[]>([])
|
||||
|
||||
/**
|
||||
* Get cache routes
|
||||
* 获取缓存路由
|
||||
*
|
||||
* @param routes Vue routes
|
||||
* @param routes Vue 路由
|
||||
*/
|
||||
function getCacheRoutes(routes: RouteRecordRaw[]) {
|
||||
cacheRoutes.value = getCacheRouteNames(routes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset route cache
|
||||
* 重置路由缓存
|
||||
*
|
||||
* @default
|
||||
* @param routeKey
|
||||
* @param routeKey 路由 key
|
||||
*/
|
||||
async function resetRouteCache(routeKey?: RouteKey) {
|
||||
const routeName = routeKey || (router.currentRoute.value.name as RouteKey)
|
||||
@ -127,10 +133,10 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
excludeCacheRoutes.value = []
|
||||
}
|
||||
|
||||
/** Global breadcrumbs */
|
||||
const breadcrumbs = computed(() => getBreadcrumbsByRoute(router.currentRoute.value, menus.value))
|
||||
/** 全局面包屑 */
|
||||
const breadcrumbs = computed(() => getBreadcrumbsByRoute(router.currentRoute.value, menusForBreadcrumb.value))
|
||||
|
||||
/** Reset store */
|
||||
/** 重置 store */
|
||||
async function resetStore() {
|
||||
const routeStore = useRouteStore()
|
||||
|
||||
@ -138,34 +144,36 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
|
||||
resetVueRoutes()
|
||||
|
||||
// after reset store, need to re-init constant route
|
||||
// 重置 store 后,需要重新初始化常量路由
|
||||
await initConstantRoute()
|
||||
}
|
||||
|
||||
/** Reset vue routes */
|
||||
/** 重置 vue 路由 */
|
||||
function resetVueRoutes() {
|
||||
removeRouteFns.forEach(fn => fn())
|
||||
removeRouteFns.length = 0
|
||||
}
|
||||
|
||||
/** init constant route */
|
||||
/** 初始化常量路由 */
|
||||
async function initConstantRoute() {
|
||||
if (isInitConstantRoute.value)
|
||||
return
|
||||
|
||||
// 静态路由
|
||||
const staticRoute = createStaticRoutes()
|
||||
|
||||
if (authRouteMode.value === 'static') {
|
||||
// 如果是静态路由模式,直接添加静态常量路由
|
||||
if (authRouteMode.value === 'static') { // 静态路由模式
|
||||
addConstantRoutes(staticRoute.constantRoutes)
|
||||
}
|
||||
else {
|
||||
else { // 动态路由模式
|
||||
const { data, error } = await fetchGetConstantRoutes()
|
||||
|
||||
if (!error) {
|
||||
addConstantRoutes(data)
|
||||
}
|
||||
else {
|
||||
// if fetch constant routes failed, use static constant routes
|
||||
// 如果获取常量路由失败,使用静态常量路由
|
||||
addConstantRoutes(staticRoute.constantRoutes)
|
||||
}
|
||||
}
|
||||
@ -177,9 +185,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
tabStore.initHomeTab()
|
||||
}
|
||||
|
||||
/** Init auth route */
|
||||
/** 初始化权限路由 */
|
||||
async function initAuthRoute() {
|
||||
// check if user info is initialized
|
||||
// 检查用户信息是否已初始化
|
||||
if (!authStore.userInfo.userId) {
|
||||
await authStore.initUserInfo()
|
||||
}
|
||||
@ -194,7 +202,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
tabStore.initHomeTab()
|
||||
}
|
||||
|
||||
/** Init static auth route */
|
||||
/** 初始化静态权限路由 */
|
||||
function initStaticAuthRoute() {
|
||||
const { authRoutes: staticAuthRoutes } = createStaticRoutes()
|
||||
|
||||
@ -212,7 +220,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
setIsInitAuthRoute(true)
|
||||
}
|
||||
|
||||
/** Init dynamic auth route */
|
||||
/** 初始化动态权限路由 */
|
||||
async function initDynamicAuthRoute() {
|
||||
const { data, error } = await fetchGetUserRoutes()
|
||||
|
||||
@ -230,12 +238,12 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
setIsInitAuthRoute(true)
|
||||
}
|
||||
else {
|
||||
// if fetch user routes failed, reset store
|
||||
// 如果获取用户路由失败,重置 store
|
||||
authStore.resetStore()
|
||||
}
|
||||
}
|
||||
|
||||
/** handle constant and auth routes */
|
||||
/** 处理常量路由和权限路由 */
|
||||
function handleConstantAndAuthRoutes() {
|
||||
const allRoutes = [...constantRoutes.value, ...authRoutes.value]
|
||||
|
||||
@ -253,9 +261,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add routes to vue router
|
||||
* 添加路由到 vue router
|
||||
*
|
||||
* @param routes Vue routes
|
||||
* @param routes Vue 路由
|
||||
*/
|
||||
function addRoutesToVueRouter(routes: RouteRecordRaw[]) {
|
||||
routes.forEach((route) => {
|
||||
@ -265,18 +273,18 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add remove route fn
|
||||
* 添加删除路由函数
|
||||
*
|
||||
* @param fn
|
||||
* @param fn 删除函数
|
||||
*/
|
||||
function addRemoveRouteFn(fn: () => void) {
|
||||
removeRouteFns.push(fn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update root route redirect when auth route mode is dynamic
|
||||
* 当权限路由模式为动态时,更新根路由重定向
|
||||
*
|
||||
* @param redirectKey Redirect route key
|
||||
* @param redirectKey 重定向路由 key
|
||||
*/
|
||||
function handleUpdateRootRouteRedirect(redirectKey: LastLevelRouteKey) {
|
||||
const redirect = getRoutePath(redirectKey)
|
||||
@ -293,9 +301,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get is auth route exist
|
||||
* 获取权限路由是否存在
|
||||
*
|
||||
* @param routePath Route path
|
||||
* @param routePath 路由路径
|
||||
*/
|
||||
async function getIsAuthRouteExist(routePath: RouteMap[RouteKey]) {
|
||||
const routeName = getRouteName(routePath)
|
||||
@ -315,20 +323,20 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected menu key path
|
||||
* 获取选中的菜单 key 路径
|
||||
*
|
||||
* @param selectedKey Selected menu key
|
||||
* @param selectedKey 选中的菜单 key
|
||||
*/
|
||||
function getSelectedMenuKeyPath(selectedKey: string) {
|
||||
return getSelectedMenuKeyPathByKey(selectedKey, menus.value)
|
||||
}
|
||||
|
||||
async function onRouteSwitchWhenLoggedIn() {
|
||||
// some global init logic when logged in and switch route
|
||||
// 登录并切换路由时的一些全局初始化逻辑
|
||||
}
|
||||
|
||||
async function onRouteSwitchWhenNotLoggedIn() {
|
||||
// some global init logic if it does not need to be logged in
|
||||
// 如果不需要登录时的一些全局初始化逻辑
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -4,28 +4,28 @@ import { useSvgIcon } from '@/hooks/common/icon'
|
||||
import { $t } from '@/locales'
|
||||
|
||||
/**
|
||||
* Filter auth routes by roles
|
||||
* 根据角色过滤权限路由
|
||||
*
|
||||
* @param routes Auth routes
|
||||
* @param roles Roles
|
||||
* @param routes 权限路由
|
||||
* @param roles 角色
|
||||
*/
|
||||
export function filterAuthRoutesByRoles(routes: ElegantConstRoute[], roles: string[]) {
|
||||
return routes.flatMap(route => filterAuthRouteByRoles(route, roles))
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter auth route by roles
|
||||
* 根据角色过滤权限路由
|
||||
*
|
||||
* @param route Auth route
|
||||
* @param roles Roles
|
||||
* @param route 权限路由
|
||||
* @param roles 角色
|
||||
*/
|
||||
function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]): ElegantConstRoute[] {
|
||||
const routeRoles = (route.meta && route.meta.roles) || []
|
||||
|
||||
// if the route's "roles" is empty, then it is allowed to access
|
||||
// 如果路由的 "roles" 为空,则允许访问
|
||||
const isEmptyRoles = !routeRoles.length
|
||||
|
||||
// if the user's role is included in the route's "roles", then it is allowed to access
|
||||
// 如果用户的角色包含在路由的 "roles" 中,则允许访问
|
||||
const hasPermission = routeRoles.some(role => roles.includes(role))
|
||||
|
||||
const filterRoute = { ...route }
|
||||
@ -34,7 +34,7 @@ function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]): Eleg
|
||||
filterRoute.children = filterRoute.children.flatMap(item => filterAuthRouteByRoles(item, roles))
|
||||
}
|
||||
|
||||
// Exclude the route if it has no children after filtering
|
||||
// 如果过滤后没有子路由,则排除该路由
|
||||
if (filterRoute.children?.length === 0) {
|
||||
return []
|
||||
}
|
||||
@ -43,9 +43,9 @@ function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]): Eleg
|
||||
}
|
||||
|
||||
/**
|
||||
* sort route by order
|
||||
* 根据 order 对路由进行排序
|
||||
*
|
||||
* @param route route
|
||||
* @param route 路由
|
||||
*/
|
||||
function sortRouteByOrder(route: ElegantConstRoute) {
|
||||
if (route.children?.length) {
|
||||
@ -57,9 +57,9 @@ function sortRouteByOrder(route: ElegantConstRoute) {
|
||||
}
|
||||
|
||||
/**
|
||||
* sort routes by order
|
||||
* 根据 order 对路由进行排序
|
||||
*
|
||||
* @param routes routes
|
||||
* @param routes 路由
|
||||
*/
|
||||
export function sortRoutesByOrder(routes: ElegantConstRoute[]) {
|
||||
routes.sort((next, prev) => (Number(next.meta?.order) || 0) - (Number(prev.meta?.order) || 0))
|
||||
@ -69,19 +69,30 @@ export function sortRoutesByOrder(routes: ElegantConstRoute[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global menus by auth routes
|
||||
*
|
||||
* @param routes Auth routes
|
||||
* 根据权限路由获取全局菜单
|
||||
* 当嵌套路由里面,有且仅有一个子路由时,将其提升到一级菜单,点击一级菜单时,跳转到子路由
|
||||
* @param routes 权限路由
|
||||
* @param shouldHoist 是否提升
|
||||
* @param includeHidden 是否包含隐藏路由
|
||||
*/
|
||||
export function getGlobalMenusByAuthRoutes(routes: ElegantConstRoute[]) {
|
||||
export function getGlobalMenusByAuthRoutes(routes: ElegantConstRoute[], shouldHoist = false, includeHidden = false) {
|
||||
const menus: App.Global.Menu[] = []
|
||||
|
||||
routes.forEach((route) => {
|
||||
if (!route.meta?.hideInMenu) {
|
||||
if (includeHidden || !route.meta?.hideInMenu) {
|
||||
const menu = getGlobalMenuByBaseRoute(route)
|
||||
|
||||
if (route.children?.some(child => !child.meta?.hideInMenu)) {
|
||||
menu.children = getGlobalMenusByAuthRoutes(route.children)
|
||||
if (route.children?.some(child => includeHidden || !child.meta?.hideInMenu)) {
|
||||
menu.children = getGlobalMenusByAuthRoutes(route.children, shouldHoist, includeHidden)
|
||||
}
|
||||
|
||||
// 如果只有一个子菜单,将其提升
|
||||
if (shouldHoist && menu.children?.length === 1) {
|
||||
const singleChild = menu.children[0]
|
||||
menu.key = singleChild.key
|
||||
menu.routeKey = singleChild.routeKey
|
||||
menu.routePath = singleChild.routePath
|
||||
menu.children = singleChild.children
|
||||
}
|
||||
|
||||
menus.push(menu)
|
||||
@ -92,7 +103,7 @@ export function getGlobalMenusByAuthRoutes(routes: ElegantConstRoute[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update locale of global menus
|
||||
* 更新全局菜单的国际化
|
||||
*
|
||||
* @param menus
|
||||
*/
|
||||
@ -120,7 +131,7 @@ export function updateLocaleOfGlobalMenus(menus: App.Global.Menu[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global menu by route
|
||||
* 根据路由获取全局菜单
|
||||
*
|
||||
* @param route
|
||||
*/
|
||||
@ -145,15 +156,15 @@ function getGlobalMenuByBaseRoute(route: RouteLocationNormalizedLoaded | Elegant
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache route names
|
||||
* 获取缓存路由名称
|
||||
*
|
||||
* @param routes Vue routes (two levels)
|
||||
* @param routes Vue 路由 (两级)
|
||||
*/
|
||||
export function getCacheRouteNames(routes: RouteRecordRaw[]) {
|
||||
const cacheNames: LastLevelRouteKey[] = []
|
||||
|
||||
routes.forEach((route) => {
|
||||
// only get last two level route, which has component
|
||||
// 只获取最后两级有组件的路由
|
||||
route.children?.forEach((child) => {
|
||||
if (child.component && child.meta?.keepAlive) {
|
||||
cacheNames.push(child.name as LastLevelRouteKey)
|
||||
@ -165,7 +176,7 @@ export function getCacheRouteNames(routes: RouteRecordRaw[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Is route exist by route name
|
||||
* 根据路由名称判断路由是否存在
|
||||
*
|
||||
* @param routeName
|
||||
* @param routes
|
||||
@ -175,7 +186,7 @@ export function isRouteExistByRouteName(routeName: RouteKey, routes: ElegantCons
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive get is route exist by route name
|
||||
* 递归判断路由名称是否存在
|
||||
*
|
||||
* @param route
|
||||
* @param routeName
|
||||
@ -195,7 +206,7 @@ function recursiveGetIsRouteExistByRouteName(route: ElegantConstRoute, routeName
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected menu key path
|
||||
* 获取选中的菜单 key 路径
|
||||
*
|
||||
* @param selectedKey
|
||||
* @param menus
|
||||
@ -219,10 +230,10 @@ export function getSelectedMenuKeyPathByKey(selectedKey: string, menus: App.Glob
|
||||
}
|
||||
|
||||
/**
|
||||
* Find menu path
|
||||
* 查找菜单路径
|
||||
*
|
||||
* @param targetKey Target menu key
|
||||
* @param menu Menu
|
||||
* @param targetKey 目标菜单 key
|
||||
* @param menu 菜单
|
||||
*/
|
||||
function findMenuPath(targetKey: string, menu: App.Global.Menu): string[] | null {
|
||||
const path: string[] = []
|
||||
@ -255,7 +266,7 @@ function findMenuPath(targetKey: string, menu: App.Global.Menu): string[] | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform menu to breadcrumb
|
||||
* 将菜单转换为面包屑
|
||||
*
|
||||
* @param menu
|
||||
*/
|
||||
@ -274,7 +285,7 @@ function transformMenuToBreadcrumb(menu: App.Global.Menu) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get breadcrumbs by route
|
||||
* 根据路由获取面包屑
|
||||
*
|
||||
* @param route
|
||||
* @param menus
|
||||
@ -316,9 +327,9 @@ export function getBreadcrumbsByRoute(
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform menu to searchMenus
|
||||
* 将菜单转换为搜索菜单
|
||||
*
|
||||
* @param menus - menus
|
||||
* @param menus - 菜单
|
||||
* @param treeMap
|
||||
*/
|
||||
export function transformMenuToSearchMenus(menus: App.Global.Menu[], treeMap: App.Global.Menu[] = []) {
|
||||
|
||||
@ -76,7 +76,7 @@ abbr:where([title]) {
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
/* h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
@ -84,7 +84,7 @@ h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
} */
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
|
||||
12
apps/admin/src/styles/scss/variables.scss
Normal file
@ -0,0 +1,12 @@
|
||||
// Brand Colors
|
||||
$primary-color: #4db6ac;
|
||||
$secondary-color: #333333;
|
||||
$accent-color: #d4af37;
|
||||
|
||||
// Text Colors
|
||||
$text-color: #004d40;
|
||||
$group-text-color: #5d4037;
|
||||
|
||||
// Backgrounds
|
||||
$card-bg: #b2dfdb;
|
||||
$tag-bg: #d7ccc8;
|
||||
@ -11,6 +11,10 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
success: '#52c41a',
|
||||
warning: '#faad14',
|
||||
error: '#f5222d',
|
||||
unpublished: '#6b7280', // gray-500
|
||||
published: '#2563eb', // blue-600
|
||||
processing: '#16a34a', // green-600
|
||||
finished: '#dc2626', // red-600
|
||||
},
|
||||
isInfoFollowPrimary: true, // 是否开启信息类颜色跟随主题颜色
|
||||
layout: { mode: 'horizontal', scrollMode: 'content' }, // 布局模式 mode: horizontal | vertical
|
||||
@ -38,7 +42,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
mixChildMenuWidth: 200, // 侧边栏混合子菜单宽度
|
||||
autoSelectFirstMenu: false, // 是否自动选择第一个菜单
|
||||
},
|
||||
footer: { visible: true, fixed: false, height: 48, right: true }, // 是否固定底部
|
||||
footer: { visible: false, fixed: false, height: 48, right: true }, // 是否固定底部
|
||||
watermark: {
|
||||
visible: false, // 是否显示水印
|
||||
text: 'reader-starAdmin', // 水印文本
|
||||
|
||||
@ -1,6 +1,16 @@
|
||||
/** Create color palette vars */
|
||||
function createColorPaletteVars() {
|
||||
const colors: App.Theme.ThemeColorKey[] = ['primary', 'info', 'success', 'warning', 'error']
|
||||
const colors: App.Theme.ThemeColorKey[] = [
|
||||
'primary',
|
||||
'info',
|
||||
'success',
|
||||
'warning',
|
||||
'error',
|
||||
'unpublished',
|
||||
'published',
|
||||
'processing',
|
||||
'finished',
|
||||
]
|
||||
const colorPaletteNumbers: App.Theme.ColorPaletteNumber[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
||||
|
||||
const colorPaletteVar = {} as App.Theme.ThemePaletteColor
|
||||
|
||||
413
apps/admin/src/typings/api/Competition.d.ts
vendored
Normal file
@ -0,0 +1,413 @@
|
||||
/**
|
||||
* Namespace Api
|
||||
*
|
||||
* All backend api type
|
||||
*/
|
||||
declare namespace Api {
|
||||
namespace Competition {
|
||||
/** common params of paginating */
|
||||
interface PaginatingCommonParams {
|
||||
/** current page number */
|
||||
current: number
|
||||
/** page size */
|
||||
size: number
|
||||
/** total count */
|
||||
total: number
|
||||
}
|
||||
|
||||
/** common params of paginating query list data */
|
||||
interface PaginatingQueryRecord<T = any> extends PaginatingCommonParams {
|
||||
records: T[]
|
||||
}
|
||||
|
||||
/** common search params of table */
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>
|
||||
|
||||
/**
|
||||
* enable status
|
||||
*
|
||||
* - "1": enabled
|
||||
* - "2": disabled
|
||||
*/
|
||||
type EnableStatus = 1 | 2
|
||||
|
||||
/** question ui type */
|
||||
type QuestionUiType = 1 | 2 | 3 | 4 /** 文本输入 | 单选 | 多选 | 下拉选择 */
|
||||
|
||||
/** question time type */
|
||||
type QuestionTimeType = 5 | 10 | 15 | 20 | 30 | 45 | 60 | 90 /** 5s | 10s | 15s | 20s | 30s | 45s | 60s | 90s */
|
||||
|
||||
/** question template id */
|
||||
type QuestionCategoryName =
|
||||
| 'ChineseCharacterDictation1' // 汉字听写-提示 (jiu表示小鸟的叫声)
|
||||
| 'ChineseCharacterDictation2' // 根据拼音写汉字-提示 (tang)
|
||||
| 'CharacterRadicalAddition' // 汉字加一加 (车)
|
||||
| 'WordDictation' // 词语听写 (zhi re)
|
||||
| 'IdiomWriting1' // 成语-文字要求 (反义字)
|
||||
| 'IdiomWriting2' // 成语-文字要求 (看图写成语)
|
||||
// | 'TEMPLATE_IDIOM_IMAGE' // 成语-看图
|
||||
| 'PoetryComprehension' // 诗词理解-选择题
|
||||
|
||||
/** question score type */
|
||||
type QuestionScoreType = 0 | 1 /** 固定分数 | 答题个数 */
|
||||
|
||||
/** competition round type */
|
||||
type CompetitionRoundType = 0 | 1 | 3 /** 题包环节 | 加时环节 | 决赛环节 */
|
||||
|
||||
/**
|
||||
* competition publish status
|
||||
*
|
||||
* - 0: unpublished
|
||||
* - 1: published
|
||||
* - 2: processing
|
||||
* - 3: finished
|
||||
*/
|
||||
enum PublishStatus {
|
||||
Unpublished = 0,
|
||||
Published = 1,
|
||||
Processing = 2,
|
||||
Finished = 3,
|
||||
}
|
||||
|
||||
interface CompetitionItem {
|
||||
Id: number
|
||||
Name: string
|
||||
StartTime: string
|
||||
EndTime: string
|
||||
TeamGroupNumber: number
|
||||
PublishStatus: number
|
||||
RoomID: number
|
||||
}
|
||||
|
||||
/** room */
|
||||
interface Room {
|
||||
/** room id */
|
||||
ID: number
|
||||
/** room name */
|
||||
Name: string
|
||||
}
|
||||
|
||||
/** question add params */
|
||||
interface QuestionAddParams {
|
||||
RoomID: number
|
||||
MainID: number
|
||||
TeamGroupID: number
|
||||
QuestionID: number
|
||||
QuestionDetaiID: number
|
||||
TeamGroupQuestionID: number
|
||||
}
|
||||
interface QuestionListDetailRound {
|
||||
/** 题目 id */
|
||||
Id: number
|
||||
Name: string
|
||||
Answers: string
|
||||
QuestionId: number
|
||||
IsGood: number
|
||||
Image: string
|
||||
ImageUrl: string
|
||||
Answer: string
|
||||
}
|
||||
|
||||
/** get current question params */
|
||||
interface GetCurrentQuestionParams {
|
||||
/** 活动 id */
|
||||
ActivityID: number
|
||||
/** 队伍分组 id */
|
||||
GroupID: number
|
||||
/** 轮次类型 */
|
||||
RoundType: CompetitionRoundType
|
||||
/** 是否已使用 */
|
||||
isUse?: boolean
|
||||
}
|
||||
|
||||
/** activity detail */
|
||||
interface ActivityDetail {
|
||||
/** 背景图片 */
|
||||
BackgroundImg: string
|
||||
/** 创建时间 */
|
||||
CreatedTime: string
|
||||
/** 结束时间 */
|
||||
EndTime: string
|
||||
/** 活动 id */
|
||||
Id: number
|
||||
/** 活动名称 */
|
||||
Name: string
|
||||
/** 发布状态 */
|
||||
PublishStatus: number
|
||||
/** 房间 id */
|
||||
RoomID: number
|
||||
/** 开始时间 */
|
||||
StartTime: string
|
||||
/** 队伍分组数量 */
|
||||
TeamGroupNumber: number
|
||||
/** 队伍数量 */
|
||||
Teams: number
|
||||
/** 活动副标题 */
|
||||
ActivityTitle?: string
|
||||
/** 普通赛事规则 */
|
||||
ActivityContent?: string
|
||||
/** 加时赛规则 */
|
||||
ExtraTimeContent?: string
|
||||
}
|
||||
|
||||
/** create room request */
|
||||
interface CreateRoomRequest {
|
||||
/** 活动 id */
|
||||
id: number
|
||||
/** 活动名称 */
|
||||
name: string
|
||||
/** 开始时间 */
|
||||
startTime: string
|
||||
/** 结束时间 */
|
||||
endTime: string
|
||||
/** 队伍数量 */
|
||||
teams: number
|
||||
/** 创建时间 */
|
||||
createdTime: string
|
||||
/** 房间 id */
|
||||
roomID: number
|
||||
/** 队伍分组数量 */
|
||||
teamGroupNumber: number
|
||||
/** 背景图片 */
|
||||
backgroundImg: string
|
||||
}
|
||||
|
||||
/** 当前题目响应 */
|
||||
interface CurrentQuestionResponse {
|
||||
/** 活动题目记录 */
|
||||
Activity_Question: QuestionListRecord
|
||||
/** 题目列表 */
|
||||
QuestionList: {
|
||||
/** 题目 id */
|
||||
Id: number
|
||||
/** 题目名称 */
|
||||
Name: string
|
||||
/** 题目内容 */
|
||||
QuestionContent: string
|
||||
/** 题目类型 */
|
||||
QuestionType: string
|
||||
}
|
||||
/** 队伍分组题目 id */
|
||||
TeamGroup_QuestionID: number
|
||||
IsCompleted: boolean
|
||||
}
|
||||
|
||||
/** 获取当前正在比赛的信息 */
|
||||
interface GetCurrentQuestionInfo {
|
||||
/** 答案 */
|
||||
Answer: string
|
||||
/** 结束时间 */
|
||||
EndTime: string
|
||||
/** 高度 */
|
||||
Height: number
|
||||
/** 分值 */
|
||||
Point: number
|
||||
/** 题目详情 id */
|
||||
QuestionDetailID: number
|
||||
QuestionDetaiID: number
|
||||
/** 题目 id */
|
||||
QuestionID: number
|
||||
/** 题目规则 */
|
||||
QuestionRule: number
|
||||
/** 开始时间 */
|
||||
StartTime: string
|
||||
/** 任务模板列表 */
|
||||
TaskTemplete: any[]
|
||||
/** 队伍分组 id */
|
||||
TeamGroupID: number
|
||||
/** 队伍分组题目 id */
|
||||
TeamGroupQuestionID: number
|
||||
/** 队伍列表 */
|
||||
TeamList: any[]
|
||||
/** 宽度 */
|
||||
Width: number
|
||||
ActivityID: number
|
||||
Answer: string
|
||||
}
|
||||
|
||||
/** update room request */
|
||||
interface UpdateRoomRequest {
|
||||
/** 活动 id */
|
||||
id: number
|
||||
/** 活动名称 */
|
||||
name: string
|
||||
/** 开始时间 */
|
||||
startTime: string
|
||||
/** 结束时间 */
|
||||
endTime: string
|
||||
/** 队伍数量 */
|
||||
teams: number
|
||||
/** 队伍分组数量 */
|
||||
teamGroupNumber: number
|
||||
/** 活动副标题 */
|
||||
activityTitle?: string
|
||||
/** 普通赛事规则 */
|
||||
activityContent?: string
|
||||
/** 加时赛规则 */
|
||||
extraTimeContent?: string
|
||||
}
|
||||
|
||||
/** create team list request */
|
||||
interface CreateTeamListRequest {
|
||||
/** 队伍 id */
|
||||
id: number
|
||||
/** 主 id */
|
||||
mainId: number
|
||||
/** 队伍编号 */
|
||||
number: string
|
||||
/** 队伍名称 */
|
||||
name: string
|
||||
/** 笔序列号 */
|
||||
penSerial: string
|
||||
/** 队员名单 */
|
||||
nameList: string
|
||||
/** 学校名称 */
|
||||
schoolName: string
|
||||
/** 队伍分组 id */
|
||||
teamGroupId: number
|
||||
/** 队伍头像 */
|
||||
headImg?: string
|
||||
}
|
||||
|
||||
interface TeamListRecord {
|
||||
/** 队伍 id */
|
||||
Id: number
|
||||
/** 主 id */
|
||||
MainId: number
|
||||
/** 队伍编号 */
|
||||
Number: string
|
||||
/** 队伍名称 */
|
||||
Name: string
|
||||
/** 笔序列号 */
|
||||
PenSerial: string
|
||||
/** 队员名单 */
|
||||
NameList: string
|
||||
/** 学校名称 */
|
||||
SchoolName: string
|
||||
/** 队伍分组 id */
|
||||
TeamGroupId: number
|
||||
/** 队伍头像 */
|
||||
HeadImg?: string
|
||||
}
|
||||
|
||||
interface TeamAnswerData {
|
||||
Id: number
|
||||
MainId: number
|
||||
TeamGroupId: number
|
||||
TeamId: number
|
||||
QuestionId: number
|
||||
QuestionDetailId: number | null
|
||||
UserAnswerPicture: string
|
||||
UserAnswerFont: string // JSON string
|
||||
TeamName: string
|
||||
Points: number
|
||||
ResultPotins: number
|
||||
Status: number
|
||||
// 兼容字段(用于兜底)
|
||||
AnswerValuePicture?: string
|
||||
Guid?: string
|
||||
QuestionDetaiID: number | null
|
||||
}
|
||||
|
||||
interface QuestionListRecord {
|
||||
/** 活动题目名称 */
|
||||
ActitvityQuestionName: string
|
||||
/** 活动 id */
|
||||
ActivityID: number
|
||||
/** 主键 id */
|
||||
ID: number
|
||||
/** 分值 */
|
||||
Point: number
|
||||
/** 题目 id */
|
||||
QuestionID: number
|
||||
/** 题目序号 */
|
||||
QuestionIndex: number
|
||||
/** 题目规则 */
|
||||
QuestionRule: number
|
||||
/** 题目副标题 */
|
||||
QuestionSubTitle: string
|
||||
/** 答题时间(秒) */
|
||||
QuestionTime: number
|
||||
/** 赛题包类型 */
|
||||
RoundType: CompetitionRoundType
|
||||
/** 题目模板 id */
|
||||
TemplateID: number
|
||||
/** UI 类型 */
|
||||
UIType: QuestionCategoryName
|
||||
}
|
||||
|
||||
interface ActivityTeamGroup {
|
||||
/** 队伍分组 id */
|
||||
Id: number
|
||||
/** 是否已结束 */
|
||||
IsEnd: boolean
|
||||
/** 主活动 id */
|
||||
MainId: number
|
||||
/** 分组名称 */
|
||||
Name: string
|
||||
/** 图标 */
|
||||
icon: string
|
||||
/** 队伍分组 id(兼容字段) */
|
||||
id: number
|
||||
/** 分组名称(兼容字段) */
|
||||
name: string
|
||||
RoundType: number
|
||||
}
|
||||
|
||||
interface CreateQuestionRequest {
|
||||
/** 题目 id */
|
||||
id: number
|
||||
/** 活动 id */
|
||||
activityID: number
|
||||
/** 问题 id */
|
||||
questionID: number
|
||||
/** 题目序号 */
|
||||
questionIndex: number
|
||||
/** 活动题目名称 */
|
||||
actitvityQuestionName: string
|
||||
/** 答题时间(秒) */
|
||||
questionTime: number
|
||||
/** 题目规则 */
|
||||
questionRule: number
|
||||
/** UI 类型 */
|
||||
uiType: string
|
||||
/** 分值 */
|
||||
point: number
|
||||
/** 题目副标题 */
|
||||
questionSubTitle: string
|
||||
/** 题目模板 id */
|
||||
templateID: number
|
||||
/** 赛题包类型 */
|
||||
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 */
|
||||
id: number
|
||||
/** record creator */
|
||||
createBy: string
|
||||
/** record create time */
|
||||
createTime: string
|
||||
/** record updater */
|
||||
updateBy: string
|
||||
/** record update time */
|
||||
updateTime: string
|
||||
/** record status */
|
||||
status: EnableStatus | null
|
||||
} & T
|
||||
}
|
||||
}
|
||||
1
apps/admin/src/typings/api/auth.d.ts
vendored
@ -8,6 +8,7 @@ declare namespace Api {
|
||||
interface LoginToken {
|
||||
token: string
|
||||
refreshToken: string
|
||||
id: number
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
|
||||
22
apps/admin/src/typings/api/common.d.ts
vendored
@ -7,21 +7,29 @@ declare namespace Api {
|
||||
namespace Common {
|
||||
/** common params of paginating */
|
||||
interface PaginatingCommonParams {
|
||||
/** current page number */
|
||||
current: number
|
||||
/** page size */
|
||||
size: number
|
||||
/** currentPage page number */
|
||||
currentPage: number
|
||||
/** page pageSize */
|
||||
pageSize: number
|
||||
/** total count */
|
||||
total: number
|
||||
total?: number
|
||||
}
|
||||
|
||||
/** common params of paginating query list data */
|
||||
interface PaginatingQueryRecord<T = any> extends PaginatingCommonParams {
|
||||
records: T[]
|
||||
data: T[]
|
||||
}
|
||||
|
||||
/** common search params of table */
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'currentPage' | 'pageSize'>
|
||||
|
||||
/** common response */
|
||||
export interface CommonResponse {
|
||||
success: boolean
|
||||
code: number
|
||||
msg: string
|
||||
data: any
|
||||
}
|
||||
|
||||
/**
|
||||
* enable status
|
||||
|
||||
37
apps/admin/src/typings/api/dictionary.d.ts
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Namespace Api
|
||||
*
|
||||
* All backend api type
|
||||
*/
|
||||
declare namespace Api {
|
||||
namespace Dictionary {
|
||||
interface DictionaryItem {
|
||||
Id: number
|
||||
DicKey: string
|
||||
DicValue: string
|
||||
bakValue: string
|
||||
}
|
||||
|
||||
interface DictionaryUIItem {
|
||||
id: number
|
||||
uI_Key: string
|
||||
uI_Value: string
|
||||
bakValue: string
|
||||
dictionaryID: number
|
||||
}
|
||||
|
||||
type AddOrUpdateParams = DictionaryItem
|
||||
|
||||
interface AddOrUpdateDictionaryUITypeParams {
|
||||
id?: number
|
||||
uI_Key: string
|
||||
uI_Value: string
|
||||
bakValue: string
|
||||
dictionaryID: number
|
||||
}
|
||||
|
||||
interface SearchParams {
|
||||
RoomID: number
|
||||
}
|
||||
}
|
||||
}
|
||||
108
apps/admin/src/typings/api/question.d.ts
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Namespace Api
|
||||
*
|
||||
* All backend api type
|
||||
*/
|
||||
declare namespace Api {
|
||||
namespace Question {
|
||||
/** common params of paginating */
|
||||
interface PaginatingCommonParams {
|
||||
/** pageIndex page number */
|
||||
pageIndex: number
|
||||
/** page size */
|
||||
pageSizes: number
|
||||
/** total count */
|
||||
total: number
|
||||
}
|
||||
|
||||
/** common params of paginating query list data */
|
||||
interface PaginatingQueryRecord<T = any> extends PaginatingCommonParams {
|
||||
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 */
|
||||
questionId: number
|
||||
/** pageIndex page number */
|
||||
pageIndex: number
|
||||
/** page size */
|
||||
pageSizes: number
|
||||
/** search keyword */
|
||||
keyWords?: string
|
||||
}
|
||||
|
||||
/** add params */
|
||||
interface AddParams {
|
||||
/** question id */
|
||||
id: number
|
||||
/** question name */
|
||||
name: string
|
||||
/** question content */
|
||||
questionContent: string
|
||||
/** question type */
|
||||
questionType: QuestionType
|
||||
}
|
||||
|
||||
/** add question library params */
|
||||
interface AddQuestionLibraryParams {
|
||||
/** question library id */
|
||||
id: number
|
||||
/** question library name */
|
||||
name: string
|
||||
/** question library content */
|
||||
answer: string
|
||||
/** question id */
|
||||
questionId: number
|
||||
/** question library image url */
|
||||
imageUrl: string
|
||||
/** question library type */
|
||||
type: QuestionScoreType
|
||||
/** is priority 0: no 1: yes */
|
||||
IsGood: number
|
||||
/** is disabled */
|
||||
IsDisabled: boolean
|
||||
}
|
||||
|
||||
/** common search params of table */
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>
|
||||
|
||||
/**
|
||||
* enable status
|
||||
*
|
||||
* - "1": enabled
|
||||
* - "2": disabled
|
||||
*/
|
||||
type EnableStatus = '1' | '2'
|
||||
|
||||
/** question score type */
|
||||
type QuestionScoreType = '1' | '2' /** 固定分数 | 答题个数 */
|
||||
|
||||
/** competition round type */
|
||||
type CompetitionRoundType = '1' | '2' /** 题包环节 | 加时环节 */
|
||||
|
||||
/** common record */
|
||||
type CommonRecord<T = any> = {
|
||||
/** record id */
|
||||
id: number
|
||||
/** record creator */
|
||||
createBy: string
|
||||
/** record create time */
|
||||
createTime: string
|
||||
/** record updater */
|
||||
updateBy: string
|
||||
/** record update time */
|
||||
updateTime: string
|
||||
/** record status */
|
||||
status: EnableStatus | null
|
||||
} & T
|
||||
}
|
||||
}
|
||||
115
apps/admin/src/typings/api/rank.d.ts
vendored
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Namespace Api
|
||||
*
|
||||
* All backend api type
|
||||
*/
|
||||
declare namespace Api {
|
||||
namespace Rank {
|
||||
/** common params of paginating */
|
||||
interface PaginatingCommonParams {
|
||||
/** currentPage page number */
|
||||
currentPage: number
|
||||
/** page pageSize */
|
||||
pageSize: number
|
||||
/** total count */
|
||||
total?: number
|
||||
}
|
||||
|
||||
/** user search params */
|
||||
interface UserSearchParams extends PaginatingCommonParams {
|
||||
/** competition activity name */
|
||||
competitionActivityName?: string
|
||||
/** publish status */
|
||||
publishStatus?: EnableStatus
|
||||
}
|
||||
|
||||
/** common params of paginating query list data */
|
||||
interface PaginatingQueryRecord<T = any> extends PaginatingCommonParams {
|
||||
records: T[]
|
||||
}
|
||||
|
||||
/** common search params of table */
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'currentPage' | 'pageSize'>
|
||||
|
||||
interface ActivityRankResList extends Array<ActivityRankItem> {}
|
||||
|
||||
interface ActivityRankItem {
|
||||
TeamID: number
|
||||
Name: string
|
||||
TotalPoint: number
|
||||
Questions: QuestionScore[]
|
||||
}
|
||||
|
||||
interface QuestionScore {
|
||||
QuestionNO: number
|
||||
ResultPotin: number
|
||||
}
|
||||
|
||||
interface ActivityRankList {
|
||||
Items: ActivityRank[]
|
||||
TotalRecords: number
|
||||
PageNumber: number
|
||||
PageSize: number
|
||||
TotalPages: number
|
||||
}
|
||||
|
||||
interface UpdateTeamScoreParams {
|
||||
/** 主活动 ID */
|
||||
ActivityID: number
|
||||
|
||||
Id: number
|
||||
/** 结果得分 */
|
||||
ResultPotins: number
|
||||
}
|
||||
|
||||
/** activity rank */
|
||||
interface ActivityRank {
|
||||
/** activity id */
|
||||
Id: number
|
||||
/** activity id (MainID) */
|
||||
ActivityId: number
|
||||
/** activity name */
|
||||
ActivityName: string
|
||||
/** start time */
|
||||
StartTime: string
|
||||
/** team number */
|
||||
TeamNum: number
|
||||
/** max point */
|
||||
MaxPoint: number
|
||||
/** last update time */
|
||||
LastUpdatetime: string
|
||||
/** is publish */
|
||||
Status: number
|
||||
}
|
||||
|
||||
/**
|
||||
* enable status
|
||||
*
|
||||
* - "1": enabled
|
||||
* - "2": disabled
|
||||
*/
|
||||
type EnableStatus = '1' | '2'
|
||||
|
||||
/** question score type */
|
||||
type QuestionScoreType = '1' | '2' /** 固定分数 | 答题个数 */
|
||||
|
||||
/** competition round type */
|
||||
type CompetitionRoundType = '1' | '2' /** 题包环节 | 加时环节 */
|
||||
|
||||
/** common record */
|
||||
type CommonRecord<T = any> = {
|
||||
/** record id */
|
||||
id: number
|
||||
/** record creator */
|
||||
createBy: string
|
||||
/** record create time */
|
||||
createTime: string
|
||||
/** record updater */
|
||||
updateBy: string
|
||||
/** record update time */
|
||||
updateTime: string
|
||||
/** record status */
|
||||
status: EnableStatus | null
|
||||
} & T
|
||||
}
|
||||
}
|
||||
139
apps/admin/src/typings/api/system-manage.d.ts
vendored
Normal file
@ -0,0 +1,139 @@
|
||||
declare namespace Api {
|
||||
/**
|
||||
* namespace SystemManage
|
||||
*
|
||||
* backend api module: "systemManage"
|
||||
*/
|
||||
namespace SystemManage {
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>
|
||||
|
||||
/** role */
|
||||
type Role = Common.CommonRecord<{
|
||||
/** role name */
|
||||
roleName: string
|
||||
/** role code */
|
||||
roleCode: string
|
||||
/** role description */
|
||||
roleDesc: string
|
||||
}>
|
||||
|
||||
/** role search params */
|
||||
type RoleSearchParams = CommonType.RecordNullable<
|
||||
Pick<Api.SystemManage.Role, 'roleName' | 'roleCode' | 'status'> & CommonSearchParams
|
||||
>
|
||||
|
||||
/** role list */
|
||||
type RoleList = Common.PaginatingQueryRecord<Role>
|
||||
|
||||
/** all role */
|
||||
type AllRole = Pick<Role, 'id' | 'roleName' | 'roleCode'>
|
||||
|
||||
/**
|
||||
* user gender
|
||||
*
|
||||
* - "1": "male"
|
||||
* - "2": "female"
|
||||
*/
|
||||
type UserGender = '1' | '2'
|
||||
|
||||
/** user */
|
||||
type User = Common.CommonRecord<{
|
||||
/** user name */
|
||||
userName: string
|
||||
/** user gender */
|
||||
userGender: UserGender | null
|
||||
/** user nick name */
|
||||
nickName: string
|
||||
/** user phone */
|
||||
userPhone: string
|
||||
/** user email */
|
||||
userEmail: string
|
||||
/** user role code collection */
|
||||
userRoles: string[]
|
||||
}>
|
||||
|
||||
/** user search params */
|
||||
type UserSearchParams = CommonType.RecordNullable<
|
||||
Pick<Api.SystemManage.User, 'userName' | 'userGender' | 'nickName' | 'userPhone' | 'userEmail' | 'status'> &
|
||||
CommonSearchParams
|
||||
>
|
||||
|
||||
/** user list */
|
||||
type UserList = Common.PaginatingQueryRecord<User>
|
||||
|
||||
/**
|
||||
* menu type
|
||||
*
|
||||
* - "1": directory
|
||||
* - "2": menu
|
||||
*/
|
||||
type MenuType = '1' | '2'
|
||||
|
||||
interface MenuButton {
|
||||
/**
|
||||
* button code
|
||||
*
|
||||
* it can be used to control the button permission
|
||||
*/
|
||||
code: string
|
||||
/** button description */
|
||||
desc: string
|
||||
}
|
||||
|
||||
/**
|
||||
* icon type
|
||||
*
|
||||
* - "1": iconify icon
|
||||
* - "2": local icon
|
||||
*/
|
||||
type IconType = '1' | '2'
|
||||
|
||||
type MenuPropsOfRoute = Pick<
|
||||
import('vue-router').RouteMeta,
|
||||
| 'i18nKey'
|
||||
| 'keepAlive'
|
||||
| 'constant'
|
||||
| 'order'
|
||||
| 'href'
|
||||
| 'hideInMenu'
|
||||
| 'activeMenu'
|
||||
| 'multiTab'
|
||||
| 'fixedIndexInTab'
|
||||
| 'query'
|
||||
>
|
||||
|
||||
type Menu = Common.CommonRecord<{
|
||||
/** parent menu id */
|
||||
parentId: number
|
||||
/** menu type */
|
||||
menuType: MenuType
|
||||
/** menu name */
|
||||
menuName: string
|
||||
/** route name */
|
||||
routeName: string
|
||||
/** route path */
|
||||
routePath: string
|
||||
/** component */
|
||||
component?: string
|
||||
/** iconify icon name or local icon name */
|
||||
icon: string
|
||||
/** icon type */
|
||||
iconType: IconType
|
||||
/** buttons */
|
||||
buttons?: MenuButton[] | null
|
||||
/** children menu */
|
||||
children?: Menu[] | null
|
||||
}> &
|
||||
MenuPropsOfRoute
|
||||
|
||||
/** menu list */
|
||||
type MenuList = Common.PaginatingQueryRecord<Menu>
|
||||
|
||||
interface MenuTree {
|
||||
id: number
|
||||
label: string
|
||||
pId: number
|
||||
children?: MenuTree[]
|
||||
}
|
||||
}
|
||||
}
|
||||
109
apps/admin/src/typings/api/template.d.ts
vendored
Normal file
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Namespace Api
|
||||
*
|
||||
* All backend api type
|
||||
*/
|
||||
declare namespace Api {
|
||||
namespace Template {
|
||||
/** common params of paginating */
|
||||
interface PaginatingCommonParams {
|
||||
/** currentPage page number */
|
||||
currentPage: number
|
||||
/** page pageSize */
|
||||
pageSize: number
|
||||
/** total count */
|
||||
total?: number
|
||||
}
|
||||
|
||||
interface AddTemplateParams {
|
||||
/** template id */
|
||||
id: number | null
|
||||
/** page number */
|
||||
pageNo: number | null | string
|
||||
/** template name */
|
||||
name: string
|
||||
/** background url */
|
||||
backGroundUrl: string
|
||||
/** width */
|
||||
width: number
|
||||
/** height */
|
||||
height: number
|
||||
/** template content */
|
||||
tempContent: string
|
||||
}
|
||||
|
||||
/** template detail */
|
||||
interface TemplateDetail {
|
||||
/** template id */
|
||||
ID: number
|
||||
/** template name */
|
||||
Name: string
|
||||
/** width */
|
||||
Width: number
|
||||
/** height */
|
||||
Height: number
|
||||
/** background url */
|
||||
BackGroundUrl: string
|
||||
/** template content */
|
||||
TempContent: string
|
||||
/** page number */
|
||||
PageNo: string
|
||||
/** competition id */
|
||||
CompetitionId?: number
|
||||
}
|
||||
|
||||
/** template search params */
|
||||
interface TemplateSearchParams extends PaginatingCommonParams {
|
||||
/** competition activity name */
|
||||
competitionActivityName?: string
|
||||
/** publish status */
|
||||
publishStatus?: EnableStatus
|
||||
/** template name */
|
||||
templateName?: string
|
||||
/** competition id */
|
||||
competitionId?: string
|
||||
/** size */
|
||||
size?: string
|
||||
/** status */
|
||||
status?: EnableStatus | null
|
||||
}
|
||||
|
||||
/** common params of paginating query list data */
|
||||
interface PaginatingQueryRecord<T = any> extends PaginatingCommonParams {
|
||||
records: T[]
|
||||
}
|
||||
|
||||
/** common search params of table */
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'currentPage' | 'pageSize'>
|
||||
|
||||
/**
|
||||
* enable status
|
||||
*
|
||||
* - "1": enabled
|
||||
* - "2": disabled
|
||||
*/
|
||||
type EnableStatus = '1' | '2'
|
||||
|
||||
/** question score type */
|
||||
type QuestionScoreType = '1' | '2' /** 固定分数 | 答题个数 */
|
||||
|
||||
/** competition round type */
|
||||
type CompetitionRoundType = '1' | '2' /** 题包环节 | 加时环节 */
|
||||
|
||||
/** common record */
|
||||
type CommonRecord<T = any> = {
|
||||
/** record id */
|
||||
id: number
|
||||
/** record creator */
|
||||
createBy: string
|
||||
/** record create time */
|
||||
createTime: string
|
||||
/** record updater */
|
||||
updateBy: string
|
||||
/** record update time */
|
||||
updateTime: string
|
||||
/** record status */
|
||||
status: EnableStatus | null
|
||||
} & T
|
||||
}
|
||||
}
|
||||
10
apps/admin/src/typings/app.d.ts
vendored
@ -140,6 +140,10 @@ declare namespace App {
|
||||
success: string
|
||||
warning: string
|
||||
error: string
|
||||
unpublished: string
|
||||
published: string
|
||||
processing: string
|
||||
finished: string
|
||||
}
|
||||
|
||||
interface ThemeColor extends OtherColor {
|
||||
@ -194,7 +198,7 @@ declare namespace App {
|
||||
|
||||
/** The router push options */
|
||||
interface RouterPushOptions {
|
||||
query?: Record<string, string>
|
||||
query?: Record<string, string | number>
|
||||
params?: Record<string, string>
|
||||
}
|
||||
|
||||
@ -633,11 +637,13 @@ declare namespace App {
|
||||
/** The backend service response data */
|
||||
interface Response<T = unknown> {
|
||||
/** The backend service response code */
|
||||
code: string
|
||||
code: number
|
||||
/** The backend service response message */
|
||||
msg: string
|
||||
/** The backend service response data */
|
||||
data: T
|
||||
/** The backend service response success status */
|
||||
success?: boolean
|
||||
}
|
||||
|
||||
/** The demo backend service response data */
|
||||
|
||||
112
apps/admin/src/typings/components.d.ts
vendored
@ -12,35 +12,60 @@ 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']
|
||||
copy: typeof import('./../components/custom/wave-bg copy.vue')['default']
|
||||
CompetitionLayout: typeof import('./../components/custom/user/CompetitionLayout.vue')['default']
|
||||
CountTo: typeof import('./../components/custom/count-to.vue')['default']
|
||||
DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
|
||||
ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
|
||||
FullScreen: typeof import('./../components/common/full-screen.vue')['default']
|
||||
IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
|
||||
IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
|
||||
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
|
||||
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
|
||||
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
|
||||
IconIcBaselineAdd: typeof import('~icons/ic/baseline-add')['default']
|
||||
'IconIc:baselineArrowRightAlt': typeof import('~icons/ic/baseline-arrow-right-alt')['default']
|
||||
'IconIc:baselineDeleteForever': typeof import('~icons/ic/baseline-delete-forever')['default']
|
||||
'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']
|
||||
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']
|
||||
IconIcBaselineCheck: typeof import('~icons/ic/baseline-check')['default']
|
||||
IconIcBaselineClose: typeof import('~icons/ic/baseline-close')['default']
|
||||
IconIcBaselineContentCopy: typeof import('~icons/ic/baseline-content-copy')['default']
|
||||
IconIcBaselineDelete: typeof import('~icons/ic/baseline-delete')['default']
|
||||
IconIcBaselineEdit: typeof import('~icons/ic/baseline-edit')['default']
|
||||
IconIcBaselineRocketLaunch: typeof import('~icons/ic/baseline-rocket-launch')['default']
|
||||
IconIcBaselineSave: typeof import('~icons/ic/baseline-save')['default']
|
||||
IconIcBaselineDownload: typeof import('~icons/ic/baseline-download')['default']
|
||||
IconIcBaselineGroup: typeof import('~icons/ic/baseline-group')['default']
|
||||
IconIcBaselineRefresh: typeof import('~icons/ic/baseline-refresh')['default']
|
||||
IconIcBaselineUpload: typeof import('~icons/ic/baseline-upload')['default']
|
||||
IconIcBaselineUploadFile: typeof import('~icons/ic/baseline-upload-file')['default']
|
||||
IconIcOutlineArchive: typeof import('~icons/ic/outline-archive')['default']
|
||||
IconIcOutlineAccessTime: typeof import('~icons/ic/outline-access-time')['default']
|
||||
IconIcOutlineAddToPhotos: typeof import('~icons/ic/outline-add-to-photos')['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']
|
||||
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']
|
||||
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']
|
||||
IconLocalActivity: typeof import('~icons/local/activity')['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']
|
||||
IconLocalBanner: typeof import('~icons/local/banner')['default']
|
||||
IconLocalReadLogo: typeof import('~icons/local/read-logo')['default']
|
||||
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
|
||||
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
|
||||
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
||||
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
|
||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['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']
|
||||
@ -53,7 +78,10 @@ declare module 'vue' {
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
@ -62,9 +90,10 @@ declare module 'vue' {
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
@ -75,6 +104,10 @@ declare module 'vue' {
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
@ -86,9 +119,11 @@ declare module 'vue' {
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
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']
|
||||
RestUpload: typeof import('./../components/common/rest-upload/rest-upload.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SoybeanAvatar: typeof import('./../components/custom/soybean-avatar.vue')['default']
|
||||
@ -97,41 +132,69 @@ declare module 'vue' {
|
||||
TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default']
|
||||
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
|
||||
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']
|
||||
WangEditor: typeof import('./../components/common/wang-editor.vue')['default']
|
||||
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
// 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']
|
||||
const copy: typeof import('./../components/custom/wave-bg copy.vue')['default']
|
||||
const CompetitionLayout: typeof import('./../components/custom/user/CompetitionLayout.vue')['default']
|
||||
const CountTo: typeof import('./../components/custom/count-to.vue')['default']
|
||||
const DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
|
||||
const ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
|
||||
const FullScreen: typeof import('./../components/common/full-screen.vue')['default']
|
||||
const IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
|
||||
const IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
|
||||
const IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
|
||||
const IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
|
||||
const IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
|
||||
const IconIcBaselineAdd: typeof import('~icons/ic/baseline-add')['default']
|
||||
const 'IconIc:baselineArrowRightAlt': typeof import('~icons/ic/baseline-arrow-right-alt')['default']
|
||||
const 'IconIc:baselineDeleteForever': typeof import('~icons/ic/baseline-delete-forever')['default']
|
||||
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 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']
|
||||
const IconIcBaselineCheck: typeof import('~icons/ic/baseline-check')['default']
|
||||
const IconIcBaselineClose: typeof import('~icons/ic/baseline-close')['default']
|
||||
const IconIcBaselineContentCopy: typeof import('~icons/ic/baseline-content-copy')['default']
|
||||
const IconIcBaselineDelete: typeof import('~icons/ic/baseline-delete')['default']
|
||||
const IconIcBaselineEdit: typeof import('~icons/ic/baseline-edit')['default']
|
||||
const IconIcBaselineRocketLaunch: typeof import('~icons/ic/baseline-rocket-launch')['default']
|
||||
const IconIcBaselineSave: typeof import('~icons/ic/baseline-save')['default']
|
||||
const IconIcBaselineDownload: typeof import('~icons/ic/baseline-download')['default']
|
||||
const IconIcBaselineGroup: typeof import('~icons/ic/baseline-group')['default']
|
||||
const IconIcBaselineRefresh: typeof import('~icons/ic/baseline-refresh')['default']
|
||||
const IconIcBaselineUpload: typeof import('~icons/ic/baseline-upload')['default']
|
||||
const IconIcBaselineUploadFile: typeof import('~icons/ic/baseline-upload-file')['default']
|
||||
const IconIcOutlineArchive: typeof import('~icons/ic/outline-archive')['default']
|
||||
const IconIcOutlineAccessTime: typeof import('~icons/ic/outline-access-time')['default']
|
||||
const IconIcOutlineAddToPhotos: typeof import('~icons/ic/outline-add-to-photos')['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 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 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 IconLocalActivity: typeof import('~icons/local/activity')['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 IconLocalBanner: typeof import('~icons/local/banner')['default']
|
||||
const IconLocalReadLogo: typeof import('~icons/local/read-logo')['default']
|
||||
const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
|
||||
const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['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 IconMdiRefresh: typeof import('~icons/mdi/refresh')['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']
|
||||
@ -144,7 +207,10 @@ declare global {
|
||||
const NButton: typeof import('naive-ui')['NButton']
|
||||
const NCard: typeof import('naive-ui')['NCard']
|
||||
const NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
const NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
const NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
const NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
const NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
const NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
const NDivider: typeof import('naive-ui')['NDivider']
|
||||
const NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
@ -153,9 +219,10 @@ declare global {
|
||||
const NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
const NForm: typeof import('naive-ui')['NForm']
|
||||
const NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
const NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
||||
const NGi: typeof import('naive-ui')['NGi']
|
||||
const NGrid: typeof import('naive-ui')['NGrid']
|
||||
const NIcon: typeof import('naive-ui')['NIcon']
|
||||
const NImage: typeof import('naive-ui')['NImage']
|
||||
const NInput: typeof import('naive-ui')['NInput']
|
||||
const NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||
const NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
@ -166,6 +233,10 @@ declare global {
|
||||
const NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
const NModal: typeof import('naive-ui')['NModal']
|
||||
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
const NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||
const NPopover: typeof import('naive-ui')['NPopover']
|
||||
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 NSpace: typeof import('naive-ui')['NSpace']
|
||||
@ -177,9 +248,11 @@ declare global {
|
||||
const NThing: typeof import('naive-ui')['NThing']
|
||||
const NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
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 RestUpload: typeof import('./../components/common/rest-upload/rest-upload.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']
|
||||
@ -188,5 +261,8 @@ declare global {
|
||||
const TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default']
|
||||
const TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
|
||||
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 WangEditor: typeof import('./../components/common/wang-editor.vue')['default']
|
||||
const WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
|
||||
}
|
||||
53
apps/admin/src/typings/elegant-router.d.ts
vendored
@ -20,13 +20,35 @@ declare module "@elegant-router/types" {
|
||||
"403": "/403";
|
||||
"404": "/404";
|
||||
"500": "/500";
|
||||
"admin-home": "/admin-home";
|
||||
"competition": "/competition";
|
||||
"competition_competition-add": "/competition/competition-add";
|
||||
"competition_competition-detail": "/competition/competition-detail";
|
||||
"competition_competition-list": "/competition/competition-list";
|
||||
"home": "/home";
|
||||
"dictionary": "/dictionary";
|
||||
"iframe-page": "/iframe-page/:url";
|
||||
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
|
||||
"question-store": "/question-store";
|
||||
"rank": "/rank";
|
||||
"rank_rank-detail": "/rank/rank-detail";
|
||||
"rank_rank-list": "/rank/rank-list";
|
||||
"results": "/results";
|
||||
"results_results-detail": "/results/results-detail";
|
||||
"results_results-list": "/results/results-list";
|
||||
"template": "/template";
|
||||
"template_template-detail": "/template/template-detail";
|
||||
"template_template-list": "/template/template-list";
|
||||
"test": "/test";
|
||||
"user": "/user";
|
||||
"user_analysis": "/user/analysis";
|
||||
"user_cover": "/user/cover";
|
||||
"user_draw": "/user/draw";
|
||||
"user_game": "/user/game";
|
||||
"user_groups": "/user/groups";
|
||||
"user_home": "/user/home";
|
||||
"user_rank-list": "/user/rank-list";
|
||||
"user_rules": "/user/rules";
|
||||
"user_teams": "/user/teams";
|
||||
};
|
||||
|
||||
/**
|
||||
@ -61,10 +83,17 @@ declare module "@elegant-router/types" {
|
||||
| "403"
|
||||
| "404"
|
||||
| "500"
|
||||
| "admin-home"
|
||||
| "competition"
|
||||
| "home"
|
||||
| "dictionary"
|
||||
| "iframe-page"
|
||||
| "login"
|
||||
| "question-store"
|
||||
| "rank"
|
||||
| "results"
|
||||
| "template"
|
||||
| "test"
|
||||
| "user"
|
||||
>;
|
||||
|
||||
/**
|
||||
@ -86,10 +115,28 @@ declare module "@elegant-router/types" {
|
||||
| "500"
|
||||
| "iframe-page"
|
||||
| "login"
|
||||
| "admin-home"
|
||||
| "competition_competition-add"
|
||||
| "competition_competition-detail"
|
||||
| "competition_competition-list"
|
||||
| "home"
|
||||
| "dictionary"
|
||||
| "question-store"
|
||||
| "rank_rank-detail"
|
||||
| "rank_rank-list"
|
||||
| "results_results-detail"
|
||||
| "results_results-list"
|
||||
| "template_template-detail"
|
||||
| "template_template-list"
|
||||
| "test"
|
||||
| "user_analysis"
|
||||
| "user_cover"
|
||||
| "user_draw"
|
||||
| "user_game"
|
||||
| "user_groups"
|
||||
| "user_home"
|
||||
| "user_rank-list"
|
||||
| "user_rules"
|
||||
| "user_teams"
|
||||
>;
|
||||
|
||||
/**
|
||||
|
||||