diff --git a/apps/admin/.env b/apps/admin/.env index 81a565b..4790452 100644 --- a/apps/admin/.env +++ b/apps/admin/.env @@ -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="阅读比赛配置" # 图标名称的前缀 @@ -23,16 +31,16 @@ VITE_ROUTE_HOME=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 diff --git a/apps/admin/.env.test b/apps/admin/.env.dev similarity index 50% rename from apps/admin/.env.test rename to apps/admin/.env.dev index dd18d7b..e546636 100644 --- a/apps/admin/.env.test +++ b/apps/admin/.env.dev @@ -1,5 +1,10 @@ # backend service base url, test environment -VITE_SERVICE_BASE_URL=https://mock.apifox.cn/m1/3109515-0-default +# 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 + # other backend service base url, test environment VITE_OTHER_SERVICE_BASE_URL= `{ diff --git a/apps/admin/build/config/proxy.ts b/apps/admin/build/config/proxy.ts index 6867d07..5bd3a8c 100644 --- a/apps/admin/build/config/proxy.ts +++ b/apps/admin/build/config/proxy.ts @@ -18,7 +18,6 @@ export function createViteProxy(env: Env.ImportMeta, enable: boolean) { const isEnableProxyLog = env.VITE_PROXY_LOG === 'Y' const { baseURL, proxyPattern, other } = createServiceConfig(env) - console.log('baseURL:', baseURL, 'proxyPattern:', proxyPattern, 'other:', other) const proxy: Record = createProxyItem({ baseURL, proxyPattern }, isEnableProxyLog) diff --git a/apps/admin/package.json b/apps/admin/package.json index ab69e3b..4e80b7e 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -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,6 +25,7 @@ }, "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:*", @@ -32,7 +33,10 @@ "@sa/hooks": "workspace:*", "@sa/materials": "workspace:*", "@sa/utils": "workspace:*", + "@sapphire/snowflake": "^3.5.5", "@vueuse/core": "14.1.0", + "@zumer/snapdom": "^2.0.1", + "ali-oss": "^6.23.0", "clipboard": "2.0.11", "dayjs": "1.11.19", "defu": "6.1.4", @@ -42,6 +46,7 @@ "mathlive": "^0.108.2", "naive-ui": "2.43.2", "nprogress": "0.2.0", + "path-browserify": "^1.0.1", "pinia": "3.0.4", "quill-toolbar-tip": "^0.1.0", "tailwind-merge": "3.4.0", @@ -56,8 +61,10 @@ "@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", diff --git a/apps/admin/src/assets/imgs/副本第九届“阅读之星”题目示例 (1).xlsx b/apps/admin/src/assets/imgs/副本第九届“阅读之星”题目示例 (1).xlsx new file mode 100644 index 0000000..cd81eab Binary files /dev/null and b/apps/admin/src/assets/imgs/副本第九届“阅读之星”题目示例 (1).xlsx differ diff --git a/apps/admin/src/components/common/rest-basic-editor/editor-util.ts b/apps/admin/src/components/common/rest-basic-editor/editor-util.ts index f2f3f67..7b81ef8 100644 --- a/apps/admin/src/components/common/rest-basic-editor/editor-util.ts +++ b/apps/admin/src/components/common/rest-basic-editor/editor-util.ts @@ -1,7 +1,7 @@ -import type { IEditorConfig, ToolbarOptions } from '@opentiny/fluent-editor/types/config/types' +import type { IEditorConfig, MathliveModule, Range, ToolbarOptions } from '@opentiny/fluent-editor' import type OSS from 'ali-oss' import type { GetEditorConfigParams, MyToolbarOption, UpFile } from './types' -import { default as FluentEditor, generateToolbarTip, type MathliveModule, type Range } from '@opentiny/fluent-editor' +import FluentEditor, { generateToolbarTip } from '@opentiny/fluent-editor' import katex from 'katex' import QuillToolbarTip, { type QuillToolbarTipOptions } from 'quill-toolbar-tip' import { ref } from 'vue' @@ -20,7 +20,7 @@ export const upFileList = ref([]) */ export function initEditor(container: HTMLElement, params: IEditorConfig): FluentEditor { // 需要依赖 katex - window.katex = katex + window.katex = katex as any // 处理图片给每个图片添加类名和最大宽度 const ImageBlot: any = FluentEditor.import('formats/image') @@ -51,8 +51,7 @@ export function initEditor(container: HTMLElement, params: IEditorConfig): Fluen // 居中问题 const config = { scope: Parchment.Scope.BLOCK, whitelist: ['right', 'center', 'justify'] } - new Parchment.StyleAttributor('align', 'text-align', config) - const Align = FluentEditor.import('attributors/style/align') + const Align = new Parchment.StyleAttributor('align', 'text-align', config) FluentEditor.register(Align, true) // 工具栏配置 @@ -95,7 +94,9 @@ export async function getEditorConfig(params: GetEditorConfigParams): Promise upFileAxios(file, `${Date.now()}_${index}`))) return paths } - catch (error) { + catch (error: any) { + window.$message?.error(`上传失败: ${error.message}`) + // eslint-disable-next-line no-console console.log('error====', error) return files.map(() => false) } @@ -190,9 +191,12 @@ async function upFileAxios(file: File, id: string): Promise { const item: UpFile = { id, name: file.name, progress: 0 } try { upFileList.value.push(item) - const tokenRes = await getAliOssTokenAxios() + const { data: tokenData } = await getAliOssTokenAxios() + if (!tokenData) { + return false + } // 初始化OSS客户端 - const client = initOSSClient(tokenRes) + const client = initOSSClient(tokenData) const path = browserPathJoin(`temp/${Date.now()}`, file!.name) // 上传文件 const res: OSS.MultipartUploadResult = await uploadFileToOSS(client, file, path, (progress) => { @@ -203,9 +207,9 @@ async function upFileAxios(file: File, id: string): Promise { return Promise.resolve(url) // oxlint-disable-next-line no-unused-vars } - catch (error) { + catch (error: any) { upFileList.value.splice(upFileList.value.indexOf(item), 1) - window.$message?.error('上传失败') + window.$message?.error(`上传失败: ${error.message}`) return Promise.resolve(false) } } diff --git a/apps/admin/src/components/common/rest-upload/rest-upload.vue b/apps/admin/src/components/common/rest-upload/rest-upload.vue new file mode 100644 index 0000000..e929699 --- /dev/null +++ b/apps/admin/src/components/common/rest-upload/rest-upload.vue @@ -0,0 +1,489 @@ + + + + + + diff --git a/apps/admin/src/constants/business.ts b/apps/admin/src/constants/business.ts index 893b5d4..427e9bb 100644 --- a/apps/admin/src/constants/business.ts +++ b/apps/admin/src/constants/business.ts @@ -1,3 +1,4 @@ +import type { SelectOption } from 'naive-ui' import { transformRecordToOption } from '@/utils/common' export const enableStatusRecord: Record = { @@ -5,21 +6,21 @@ export const enableStatusRecord: Record = { 2: '禁用', } -export const enableStatusOptions = transformRecordToOption(enableStatusRecord) +export const enableStatusOptions: SelectOption[] = transformRecordToOption(enableStatusRecord) as unknown as SelectOption[] export const userGenderRecord: Record = { 1: '男', 2: '女', } -export const userGenderOptions = transformRecordToOption(userGenderRecord) +export const userGenderOptions: SelectOption[] = transformRecordToOption(userGenderRecord) as unknown as SelectOption[] export const menuTypeRecord: Record = { 1: '目录', 2: '菜单', } -export const menuTypeOptions = transformRecordToOption(menuTypeRecord) +export const menuTypeOptions: SelectOption[] = transformRecordToOption(menuTypeRecord) as unknown as SelectOption[] export const menuIconTypeRecord: Record = { 1: 'iconify', @@ -42,8 +43,32 @@ export const roundTypeRecord: Record = { + TEMPLATE_DICTATION_HINT: '汉字听写-提示', + TEMPLATE_DICTATION_HOMOPHONE: '汉字听写-同音', + TEMPLATE_COMPONENT_ADD: '汉字加一加', + TEMPLATE_WORD_DICTATION: '词语听写', + TEMPLATE_IDIOM_TEXT_REQ: '成语-文字要求', + TEMPLATE_IDIOM_IMAGE: '成语-看图', +} -export const scoreTypeOptions = transformRecordToOption(scoreTypeRecord) +export const timeRecord: Record = { + 5: '5s', + 10: '10s', + 15: '15s', + 20: '20s', + 30: '30s', + 45: '45s', + 60: '60s', + 90: '90s', +} -export const menuIconTypeOptions = transformRecordToOption(menuIconTypeRecord) +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[] diff --git a/apps/admin/src/hooks/common/table.ts b/apps/admin/src/hooks/common/table.ts index 7983373..52a81e1 100644 --- a/apps/admin/src/hooks/common/table.ts +++ b/apps/admin/src/hooks/common/table.ts @@ -176,7 +176,18 @@ export function useTableOperate( data: Ref, idKey: keyof TableData, getData: () => Promise, -) { +): { + drawerVisible: Ref + openDrawer: () => void + closeDrawer: () => void + operateType: Ref + handleAdd: () => void + editingData: Ref + handleEdit: (id: TableData[keyof TableData]) => void + checkedRowKeys: Ref + onBatchDeleted: () => Promise + onDeleted: () => Promise + } { const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean() const operateType = shallowRef('add') @@ -230,17 +241,23 @@ export function useTableOperate( } } +/** + * 默认的表格数据转换函数 + * + * @param response 原始的 API 响应数据 + * @returns 转换后的分页数据 + */ export function defaultTransform( response: FlatResponseData>, ): PaginationData { const { data, error } = response if (!error) { - const { records, current, size, total } = data + const { list, currentPage: pageNum, pageSize: size, total } = data return { - data: records, - pageNum: current, + data: list, + pageNum, pageSize: size, total, } diff --git a/apps/admin/src/locales/langs/en-us.ts b/apps/admin/src/locales/langs/en-us.ts index 3cc0590..536d1f9 100644 --- a/apps/admin/src/locales/langs/en-us.ts +++ b/apps/admin/src/locales/langs/en-us.ts @@ -235,6 +235,9 @@ const local: App.I18n.Schema = { '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', diff --git a/apps/admin/src/locales/langs/zh-cn.ts b/apps/admin/src/locales/langs/zh-cn.ts index adccda2..615af0f 100644 --- a/apps/admin/src/locales/langs/zh-cn.ts +++ b/apps/admin/src/locales/langs/zh-cn.ts @@ -231,6 +231,9 @@ const local: App.I18n.Schema = { 'rank': '名次排行', 'rank_rank-detail': '名次详情', 'rank_rank-list': '名次列表', + 'template_template-detail': '模板详情', + 'template_template-list': '模板列表', + 'user': '用户管理', 'results': '实时结果', 'competition_competition-add': '比赛添加', 'competition_competition-detail': '比赛详情', diff --git a/apps/admin/src/plugins/app.ts b/apps/admin/src/plugins/app.ts index c1aba01..99956fa 100644 --- a/apps/admin/src/plugins/app.ts +++ b/apps/admin/src/plugins/app.ts @@ -89,6 +89,10 @@ export function setupAppVersionNotification() { } } +/** + * 获取HTML构建时间 + * @returns 构建时间字符串或null + */ async function getHtmlBuildTime(): Promise { const baseUrl = import.meta.env.VITE_BASE_URL || '/' diff --git a/apps/admin/src/router/elegant/imports.ts b/apps/admin/src/router/elegant/imports.ts index 6421027..b5f51fa 100644 --- a/apps/admin/src/router/elegant/imports.ts +++ b/apps/admin/src/router/elegant/imports.ts @@ -28,6 +28,7 @@ export const views: Record Promise import("@/views/rank/rank-detail/index.vue"), "rank_rank-list": () => import("@/views/rank/rank-list/index.vue"), results: () => import("@/views/results/index.vue"), - template: () => import("@/views/template/index.vue"), + "template_template-detail": () => import("@/views/template/template-detail/index.vue"), + "template_template-list": () => import("@/views/template/template-list/index.vue"), user: () => import("@/views/user/index.vue"), }; diff --git a/apps/admin/src/router/elegant/routes.ts b/apps/admin/src/router/elegant/routes.ts index 6f7bb9b..4795fc6 100644 --- a/apps/admin/src/router/elegant/routes.ts +++ b/apps/admin/src/router/elegant/routes.ts @@ -181,13 +181,38 @@ export const generatedRoutes: GeneratedRoute[] = [ { name: 'template', path: '/template', - component: 'layout.base$view.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: 'user', diff --git a/apps/admin/src/router/elegant/transform.ts b/apps/admin/src/router/elegant/transform.ts index 68c225f..32a6320 100644 --- a/apps/admin/src/router/elegant/transform.ts +++ b/apps/admin/src/router/elegant/transform.ts @@ -179,6 +179,8 @@ const routeMap: RouteMap = { "rank_rank-list": "/rank/rank-list", "results": "/results", "template": "/template", + "template_template-detail": "/template/template-detail", + "template_template-list": "/template/template-list", "user": "/user" }; diff --git a/apps/admin/src/router/index.ts b/apps/admin/src/router/index.ts index d02ab7e..28b527a 100644 --- a/apps/admin/src/router/index.ts +++ b/apps/admin/src/router/index.ts @@ -18,7 +18,7 @@ const historyCreatorMap: Record 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(), }) diff --git a/apps/admin/src/service/api/auth.ts b/apps/admin/src/service/api/auth.ts index 4a262c7..40bd0cd 100644 --- a/apps/admin/src/service/api/auth.ts +++ b/apps/admin/src/service/api/auth.ts @@ -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({ - url: '/auth/login', + url: '/admin/v1/user/login', method: 'post', data: { - userName, - password, + account, + pwd, }, }) } /** Get user info */ -export function fetchGetUserInfo() { - return request({ url: '/auth/getUserInfo' }) +export function fetchGetUserInfo(userId: number) { + return request({ url: `admin/v1/manager/detail/${userId}` }) } /** diff --git a/apps/admin/src/service/api/index.ts b/apps/admin/src/service/api/index.ts index 82bba03..36766d7 100644 --- a/apps/admin/src/service/api/index.ts +++ b/apps/admin/src/service/api/index.ts @@ -1,3 +1,4 @@ export * from './auth' +export * from './rank' export * from './route' -export * from './system-manage' +export * from './template' diff --git a/apps/admin/src/service/api/question.ts b/apps/admin/src/service/api/question.ts new file mode 100644 index 0000000..cd6102b --- /dev/null +++ b/apps/admin/src/service/api/question.ts @@ -0,0 +1,10 @@ +import { request } from '../request' + +/** 新增题目 */ +export function fetchAddQuestion(data?: Api.Question.AddParams) { + return request({ + url: '/Base/ActivityMain/AddBase_QuestionList', + method: 'post', + data: data || {}, + }) +} diff --git a/apps/admin/src/service/api/rank.ts b/apps/admin/src/service/api/rank.ts new file mode 100644 index 0000000..6307511 --- /dev/null +++ b/apps/admin/src/service/api/rank.ts @@ -0,0 +1,10 @@ +import { request } from '../request' + +/** get user list */ +export function fetchRankList(data?: Api.Rank.UserSearchParams) { + return request({ + url: '/admin/v1/class/pagelist', + method: 'post', + data: data || {}, + }) +} diff --git a/apps/admin/src/service/api/system-manage.ts b/apps/admin/src/service/api/system-manage.ts index 82b9cee..a650bbf 100644 --- a/apps/admin/src/service/api/system-manage.ts +++ b/apps/admin/src/service/api/system-manage.ts @@ -24,8 +24,8 @@ export function fetchGetAllRoles() { /** get user list */ export function fetchGetUserList(params?: Api.SystemManage.UserSearchParams) { return request({ - url: '/systemManage/getUserList', - method: 'get', + url: '/admin/v1/class/pagelist', + method: 'post', params, }) } diff --git a/apps/admin/src/service/api/template.ts b/apps/admin/src/service/api/template.ts new file mode 100644 index 0000000..4107432 --- /dev/null +++ b/apps/admin/src/service/api/template.ts @@ -0,0 +1,10 @@ +import { request } from '../request' + +/** get template list */ +export function fetchTemplateList(data?: Api.Template.TemplateSearchParams) { + return request>({ + url: '/admin/v1/book/list', + method: 'post', + data: data || {}, + }) +} diff --git a/apps/admin/src/service/request/index.ts b/apps/admin/src/service/request/index.ts index 6534d4c..6facc60 100644 --- a/apps/admin/src/service/request/index.ts +++ b/apps/admin/src/service/request/index.ts @@ -10,36 +10,45 @@ 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>) { return response.data.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 +60,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 +92,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 +108,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 +136,48 @@ export const request = createFlatRequest( }, ) +/** + * 演示请求 + */ export const demoRequest = createRequest( { baseURL: otherBaseURL.demo, }, + /** + * 演示请求响应数据转换 + */ { + // 响应数据转换 transform(response: AxiosResponse) { 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 } diff --git a/apps/admin/src/service/request/shared.ts b/apps/admin/src/service/request/shared.ts index 1612afb..9533280 100644 --- a/apps/admin/src/service/request/shared.ts +++ b/apps/admin/src/service/request/shared.ts @@ -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 = [] diff --git a/apps/admin/src/service/request/type.ts b/apps/admin/src/service/request/type.ts index 6b62722..91b5d35 100644 --- a/apps/admin/src/service/request/type.ts +++ b/apps/admin/src/service/request/type.ts @@ -1,7 +1,7 @@ export interface RequestInstanceState { - /** the promise of refreshing token */ + /** 刷新token Promise */ refreshTokenPromise: Promise | null - /** the request error message stack */ + /** 请求错误信息栈 */ errMsgStack: string[] [key: string]: unknown } diff --git a/apps/admin/src/store/modules/auth/index.ts b/apps/admin/src/store/modules/auth/index.ts index a07d78e..d9238dd 100644 --- a/apps/admin/src/store/modules/auth/index.ts +++ b/apps/admin/src/store/modules/auth/index.ts @@ -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() diff --git a/apps/admin/src/store/modules/auth/shared.ts b/apps/admin/src/store/modules/auth/shared.ts index 6842a81..c7e813a 100644 --- a/apps/admin/src/store/modules/auth/shared.ts +++ b/apps/admin/src/store/modules/auth/shared.ts @@ -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') } diff --git a/apps/admin/src/typings/api/Competition.d.ts b/apps/admin/src/typings/api/Competition.d.ts index 3dc34a1..680facd 100644 --- a/apps/admin/src/typings/api/Competition.d.ts +++ b/apps/admin/src/typings/api/Competition.d.ts @@ -31,6 +31,21 @@ declare namespace Api { */ 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 QuestionTemplateId = + | 'TEMPLATE_DICTATION_HINT' + | 'TEMPLATE_DICTATION_HOMOPHONE' + | 'TEMPLATE_COMPONENT_ADD' + | 'TEMPLATE_WORD_DICTATION' + | 'TEMPLATE_IDIOM_TEXT_REQ' + | 'TEMPLATE_IDIOM_IMAGE' /** 汉字听写-提示 | 汉字听写-同音 | 汉字加一加 | 词语听写 | 成语-文字要求 | 成语-看图 */ + /** question score type */ type QuestionScoreType = '1' | '2' /** 固定分数 | 答题个数 */ diff --git a/apps/admin/src/typings/api/auth.d.ts b/apps/admin/src/typings/api/auth.d.ts index f07fe1d..0f3dcc1 100644 --- a/apps/admin/src/typings/api/auth.d.ts +++ b/apps/admin/src/typings/api/auth.d.ts @@ -8,6 +8,7 @@ declare namespace Api { interface LoginToken { token: string refreshToken: string + id: number } interface UserInfo { diff --git a/apps/admin/src/typings/api/common.d.ts b/apps/admin/src/typings/api/common.d.ts index c558b78..3fa31dd 100644 --- a/apps/admin/src/typings/api/common.d.ts +++ b/apps/admin/src/typings/api/common.d.ts @@ -7,21 +7,21 @@ 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 } /** common params of paginating query list data */ interface PaginatingQueryRecord extends PaginatingCommonParams { - records: T[] + list: T[] } /** common search params of table */ - type CommonSearchParams = Pick + type CommonSearchParams = Pick /** * enable status diff --git a/apps/admin/src/typings/api/question.d.ts b/apps/admin/src/typings/api/question.d.ts new file mode 100644 index 0000000..7f30fc0 --- /dev/null +++ b/apps/admin/src/typings/api/question.d.ts @@ -0,0 +1,66 @@ +/** + * Namespace Api + * + * All backend api type + */ +declare namespace Api { + namespace Question { + /** 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 extends PaginatingCommonParams { + records: T[] + } + + /** add params */ + interface AddParams { + /** question id */ + id: string + /** question name */ + name: string + /** question content */ + questionContent: string + } + + /** common search params of table */ + type CommonSearchParams = Pick + + /** + * 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 = { + /** 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 + } +} diff --git a/apps/admin/src/typings/api/rank.d.ts b/apps/admin/src/typings/api/rank.d.ts new file mode 100644 index 0000000..ed2b8e9 --- /dev/null +++ b/apps/admin/src/typings/api/rank.d.ts @@ -0,0 +1,64 @@ +/** + * 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 extends PaginatingCommonParams { + records: T[] + } + + /** common search params of table */ + type CommonSearchParams = Pick + + /** + * 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 = { + /** 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 + } +} diff --git a/apps/admin/src/typings/api/template.d.ts b/apps/admin/src/typings/api/template.d.ts new file mode 100644 index 0000000..d0c823d --- /dev/null +++ b/apps/admin/src/typings/api/template.d.ts @@ -0,0 +1,72 @@ +/** + * 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 + } + + /** 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 extends PaginatingCommonParams { + records: T[] + } + + /** common search params of table */ + type CommonSearchParams = Pick + + /** + * 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 = { + /** 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 + } +} diff --git a/apps/admin/src/typings/components.d.ts b/apps/admin/src/typings/components.d.ts index 4a89675..cd6c79c 100644 --- a/apps/admin/src/typings/components.d.ts +++ b/apps/admin/src/typings/components.d.ts @@ -99,6 +99,8 @@ declare module 'vue' { NFormItemGi: typeof import('naive-ui')['NFormItemGi'] NGi: typeof import('naive-ui')['NGi'] NGrid: typeof import('naive-ui')['NGrid'] + NImage: typeof import('naive-ui')['NImage'] + NImageGroup: typeof import('naive-ui')['NImageGroup'] NInput: typeof import('naive-ui')['NInput'] NInputGroup: typeof import('naive-ui')['NInputGroup'] NInputNumber: typeof import('naive-ui')['NInputNumber'] @@ -114,6 +116,7 @@ declare module 'vue' { NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] NPopconfirm: typeof import('naive-ui')['NPopconfirm'] NPopover: typeof import('naive-ui')['NPopover'] + NProgress: typeof import('naive-ui')['NProgress'] NRadio: typeof import('naive-ui')['NRadio'] NRadioGroup: typeof import('naive-ui')['NRadioGroup'] NScrollbar: typeof import('naive-ui')['NScrollbar'] @@ -128,11 +131,17 @@ declare module 'vue' { NTextarea: typeof import('naive-ui')['NTextarea'] NThing: typeof import('naive-ui')['NThing'] NTooltip: typeof import('naive-ui')['NTooltip'] + NUpload: typeof import('naive-ui')['NUpload'] NWatermark: typeof import('naive-ui')['NWatermark'] PageHeader: typeof import('./../components/common/page-header.vue')['default'] PinToggler: typeof import('./../components/common/pin-toggler.vue')['default'] ReloadButton: typeof import('./../components/common/reload-button.vue')['default'] RestBasicEditor: typeof import('./../components/common/rest-basic-editor/rest-basic-editor.vue')['default'] + RestHoverAciton: typeof import('./../components/common/rest-hover-aciton/rest-hover-aciton.vue')['default'] + RestImgViewer: typeof import('./../components/common/rest-img-viewer/rest-img-viewer.vue')['default'] + RestSelectFile: typeof import('./../components/common/rest-select-file/rest-select-file.vue')['default'] + RestUpload: typeof import('./../components/common/rest-upload/rest-upload.vue')['default'] + RestVideoViewer: typeof import('./../components/common/rest-video-viewer/rest-video-viewer.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] SoybeanAvatar: typeof import('./../components/custom/soybean-avatar.vue')['default'] @@ -235,6 +244,8 @@ declare global { const NFormItemGi: typeof import('naive-ui')['NFormItemGi'] const NGi: typeof import('naive-ui')['NGi'] const NGrid: typeof import('naive-ui')['NGrid'] + const NImage: typeof import('naive-ui')['NImage'] + const NImageGroup: typeof import('naive-ui')['NImageGroup'] const NInput: typeof import('naive-ui')['NInput'] const NInputGroup: typeof import('naive-ui')['NInputGroup'] const NInputNumber: typeof import('naive-ui')['NInputNumber'] @@ -250,6 +261,7 @@ declare global { const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] const NPopconfirm: typeof import('naive-ui')['NPopconfirm'] const NPopover: typeof import('naive-ui')['NPopover'] + const NProgress: typeof import('naive-ui')['NProgress'] const NRadio: typeof import('naive-ui')['NRadio'] const NRadioGroup: typeof import('naive-ui')['NRadioGroup'] const NScrollbar: typeof import('naive-ui')['NScrollbar'] @@ -264,11 +276,17 @@ declare global { const NTextarea: typeof import('naive-ui')['NTextarea'] const NThing: typeof import('naive-ui')['NThing'] const NTooltip: typeof import('naive-ui')['NTooltip'] + const NUpload: typeof import('naive-ui')['NUpload'] const NWatermark: typeof import('naive-ui')['NWatermark'] const PageHeader: typeof import('./../components/common/page-header.vue')['default'] const PinToggler: typeof import('./../components/common/pin-toggler.vue')['default'] const ReloadButton: typeof import('./../components/common/reload-button.vue')['default'] const RestBasicEditor: typeof import('./../components/common/rest-basic-editor/rest-basic-editor.vue')['default'] + const RestHoverAciton: typeof import('./../components/common/rest-hover-aciton/rest-hover-aciton.vue')['default'] + const RestImgViewer: typeof import('./../components/common/rest-img-viewer/rest-img-viewer.vue')['default'] + const RestSelectFile: typeof import('./../components/common/rest-select-file/rest-select-file.vue')['default'] + const RestUpload: typeof import('./../components/common/rest-upload/rest-upload.vue')['default'] + const RestVideoViewer: typeof import('./../components/common/rest-video-viewer/rest-video-viewer.vue')['default'] const RouterLink: typeof import('vue-router')['RouterLink'] const RouterView: typeof import('vue-router')['RouterView'] const SoybeanAvatar: typeof import('./../components/custom/soybean-avatar.vue')['default'] diff --git a/apps/admin/src/typings/elegant-router.d.ts b/apps/admin/src/typings/elegant-router.d.ts index 6cb632b..b3da2c6 100644 --- a/apps/admin/src/typings/elegant-router.d.ts +++ b/apps/admin/src/typings/elegant-router.d.ts @@ -33,6 +33,8 @@ declare module "@elegant-router/types" { "rank_rank-list": "/rank/rank-list"; "results": "/results"; "template": "/template"; + "template_template-detail": "/template/template-detail"; + "template_template-list": "/template/template-list"; "user": "/user"; }; @@ -106,7 +108,8 @@ declare module "@elegant-router/types" { | "rank_rank-detail" | "rank_rank-list" | "results" - | "template" + | "template_template-detail" + | "template_template-list" | "user" >; diff --git a/apps/admin/src/typings/storage.d.ts b/apps/admin/src/typings/storage.d.ts index fa747f0..6d0acd4 100644 --- a/apps/admin/src/typings/storage.d.ts +++ b/apps/admin/src/typings/storage.d.ts @@ -39,5 +39,8 @@ declare namespace StorageType { } /** The last login user id */ lastLoginUserId: string + /** The user id */ + userId: number + } } diff --git a/apps/admin/src/utils/data.ts b/apps/admin/src/utils/data.ts new file mode 100644 index 0000000..34217c4 --- /dev/null +++ b/apps/admin/src/utils/data.ts @@ -0,0 +1,335 @@ +import { isObject } from './verify' + +export interface GroupKey> { + /** 子节点的key */ + children: string + /** 根据那个字段分组 */ + groupKey: keyof T + /** 组的id,默认于groupKey相同 */ + value?: keyof T + /** 组名的key,默认于groupKey相同 */ + label: keyof T +} +export interface GroupProps { valueKey?: string, labelKey?: string, childrenKey?: string } + +/** + * 数组对象去重 (根据某个字段去重) + * @param arr - 需要去重的数组 + * @param key - 需要根据那个字段去重 + */ +export function filterRepeat, K extends keyof T>(arr: T[], key: K): T[] { + const res = new Map() + return arr.filter(item => !res.has(item[key]) && res.set(item[key], 1)) +} + +/** + * 根据key合并数组去掉重复 + * 有重复的就保留arr2的 + */ +export function mergeArrFun, K extends keyof T>(arr1: T[], arr2: T[], key: K): T[] { + const res = new Map() + arr1.forEach((item) => { + res.set(item[key], item) + }) + arr2.forEach((item) => { + res.set(item[key], item) + }) + const arr: T[] = [] + + res.forEach((val) => { + arr.push(val) + }) + return arr +} + +/** + * 根据路径获取对象的值 + * @param obj - 对象 + * @param path - 如 'a.c'只支持点路径,不支持 'a[0]' + */ +export function getValueByPath(obj: Record, path: string): unknown { + const paths = path.split('.') + let res: Record = obj + let result: unknown = '' + paths.forEach((item: string) => { + if (item in res && typeof res[item] === 'object') { + res = res[item] as Record + } + else { + result = res[item] + } + }) + return result +} + +/** + * 根据路径 或一个路径数组得到一个由该路径值组成的一个字符串 (多由于唯一字段) + * @param obj - 对象 + * @param paths - 如 ['a.c','a' 'a.c.d']只支持点路径,不支持 ['a[0]'] + */ +export function getOnlyValue(obj: Record, paths: string[] | string): string { + if (Array.isArray(paths)) { + let onlyValue = '' + paths.forEach((key) => { + onlyValue += `_${String(getValueByPath(obj, key)) || ''}` + }) + return onlyValue + } + else { + return obj[paths] as string + } +} + +/** + * 删除指定的元素并返回删除后的数组 [用于需要返回新数组而不是直接修改原数组的情况] + */ +export function mySplice(array: T[], index: number): T[] { + return array.slice(0, index).concat(array.slice(index + 1)) +} + +/** + * 在数组后面添加一个元素并返回添加后的数组 [用于需要返回新数组而不是直接修改原数组的情况] + */ +export function myPush(array: T[], item: T): T[] { + return array.concat([item]) +} + +/** + * 特定的值删除数组中的元素并返回删除后的数组 [用于需要返回新数组而不是直接修改原数组的情况] + */ +export function myDelItem(array: T[], item: T): T[] { + const index = array.indexOf(item) + return index >= 0 ? mySplice(array, index) : array +} + +/** + * 得到两个数组的交集 + */ +export function myGetIntersection(arr1: T[], arr2: T[]): T[] { + if (Array.isArray(arr1) && Array.isArray(arr2)) { + return arr1.filter(item => arr2.includes(item)) + } + else { + return [] + } +} + +/** + * 判断两个数组是否有交集 + */ +export function myHaveIntersection(arr1: T[], arr2: T[]): boolean { + if (Array.isArray(arr1) && Array.isArray(arr2)) { + const index = arr1.findIndex(item => arr2.includes(item)) + return index >= 0 + } + else { + return false + } +} + +/** + * 判断两个对象内容是否相等 + * @param obj1 - 需要判断的对象1 + * @param obj2 - 需要判断的对象2 + * @param notComparisonParam - notComparisonParam 不判断对象中的那些字段 [字符串数组] + */ +export function myEqual(obj1: unknown, obj2: unknown, notComparisonParam: string[] = []): boolean { + // 判断两个对象是否指向同一内存或者全等,指向同一内存或全等返回true + if (obj1 === obj2) { + return true + } + // 类型相同才比较,不同则直接返回false + const obj1Type = typeof obj1 + const obj2Type = typeof obj2 + // 注意 null 的typeof 也为 'object' + if (obj1Type === obj2Type && obj1 !== null && obj2 !== null) { + if (isObject(obj1) && isObject(obj2)) { + const a = JSON.parse(JSON.stringify(obj1)) + const b = JSON.parse(JSON.stringify(obj2)) + notComparisonParam.forEach((item) => { + delete a[item] + delete b[item] + }) + // 获取两个对象键值数组 + const aProps = Object.getOwnPropertyNames(a) + const bProps = Object.getOwnPropertyNames(b) + // 判断两个对象键值数组长度是否一致,不一致返回false + if (aProps.length !== bProps.length) { + return false + } + // 遍历对象的键值 + for (const prop in a) { + // 判断a的键值,在b中是否存在,不存在,返回false + if (Object.hasOwn(b, prop)) { + // 判断a的键值是否为对象,是则递归,不是对象直接判断键值是否相等,不相等返回false + if (isObject(a[prop])) { + if (!myEqual(a[prop], b[prop])) { + return false + } + } + else if (a[prop] !== b[prop]) { + return false + } + } + else { + return false + } + } + return true + } + else { + // 不是对象且上面已经判断了不全等则返回false + return false + } + } + else { + return false + } +} + +/** + * 把字符串转为Json对象 + */ +export function myStrToJson>(obj: T | string): T | undefined { + if (typeof obj === 'string') { + try { + return JSON.parse(obj) as T + // oxlint-disable-next-line no-unused-vars + } + // eslint-disable-next-line unused-imports/no-unused-vars + catch (err) { + return undefined + } + } + else { + return undefined + } +} + +/** + * 反转对象的键值对(不会改变原对象) + */ +export function invertObjKeyValues>(obj: T) { + const newObj: Record = {} + for (const key in obj) { + const val = obj[key] + if (val) { + newObj[val] = key + } + } + return newObj +} + +/** + * 判断两个对象内容是否相等 + * @param obj1 - 需要判断的对象1 + * @param obj2 - 需要判断的对象2 + * @param notComparisonParam - notComparisonParam 不判断对象中的那些字段 [字符串数组] + */ +export function isEqual(obj1: unknown, obj2: unknown, notComparisonParam: string[] = []): boolean { + // 判断两个对象是否指向同一内存或者全等,指向同一内存或全等返回true + if (obj1 === obj2) { + return true + } + // 类型相同才比较,不同则直接返回false + const obj1Type = typeof obj1 + const obj2Type = typeof obj2 + // 注意 null 的typeof 也为 'object' + if (obj1Type === obj2Type && obj1 !== null && obj2 !== null) { + if (isObject(obj1) && isObject(obj2)) { + const a = JSON.parse(JSON.stringify(obj1)) + const b = JSON.parse(JSON.stringify(obj2)) + notComparisonParam.forEach((item) => { + delete a[item] + delete b[item] + }) + // 获取两个对象键值数组 + const aProps = Object.getOwnPropertyNames(a) + const bProps = Object.getOwnPropertyNames(b) + // 判断两个对象键值数组长度是否一致,不一致返回false + if (aProps.length !== bProps.length) { + return false + } + // 遍历对象的键值 + for (const prop in a) { + // 判断a的键值,在b中是否存在,不存在,返回false + if (Object.hasOwn(b, prop)) { + // 判断a的键值是否为对象,是则递归,不是对象直接判断键值是否相等,不相等返回false + if (isObject(a[prop])) { + if (!isEqual(a[prop], b[prop])) { + return false + } + } + else if (a[prop] !== b[prop]) { + return false + } + } + else { + return false + } + } + return true + } + else { + // 不是对象且上面已经判断了不全等则返回false + return false + } + } + else { + return false + } +} +let my__OnlyId = 10000 + +/** 得到唯一id */ +export function getOnlyId() { + my__OnlyId++ + return my__OnlyId +} + +/** + * 递归分组函数 + * @param data 原始数据数组 + * @param keys 分组键数组 + * @returns 分组后的树形结构 + */ +export function groupByKeys>(data: T[], groupKeys: GroupKey[]): any[] { + // 递归分组函数 + const groupRecursively = (items: T[], keys: GroupKey[]): any[] => { + if (keys.length === 0) { + return [] + } + + // 获取当前分组键和剩余键 + const [currentKey, ...remainingKeys] = keys + const { groupKey, value, label, children = 'children' } = currentKey! + const valueKey = value || groupKey + const labelKey = label || groupKey + + // 使用 reduce 按当前键分组 + const groupedMap = items.reduce((acc: Map, item) => { + const keyValue = item[valueKey] + if (!acc.has(keyValue)) { + acc.set(keyValue, []) + } + acc.get(keyValue)!.push(item) + return acc + }, new Map()) + // 构建分组节点 + return Array.from(groupedMap.entries()).map(([keyValue, groupItems]) => { + // 获取当前分组的标签值(取第一条数据的对应字段) + const labelValue = groupItems[0]![labelKey] + + // 递归处理下一级分组 + const list: T[] = groupRecursively(groupItems, remainingKeys) + // 构建当前节点 + return { + [valueKey]: keyValue, + [labelKey]: labelValue, + [children]: list, + } + }) + } + + return groupRecursively(data, groupKeys) +} diff --git a/apps/admin/src/utils/date.ts b/apps/admin/src/utils/date.ts index 6168937..76ddf54 100644 --- a/apps/admin/src/utils/date.ts +++ b/apps/admin/src/utils/date.ts @@ -1,4 +1,5 @@ import path from 'path-browserify' +import { nextTick } from 'vue' export function getCurrentYear(): Date { return new Date() diff --git a/apps/admin/src/utils/file.ts b/apps/admin/src/utils/file.ts index 3d8bbaa..6384280 100644 --- a/apps/admin/src/utils/file.ts +++ b/apps/admin/src/utils/file.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { snapdom } from '@zumer/snapdom' import { getTrueRandomInt } from './date' import { getSnowflake } from './rest' @@ -101,7 +102,7 @@ export function fileToBase64(file: File): Promise<{ id: string, img: string }> { resolve({ id: `id_${date}_${num}`, img: Base64Url }) } img.onerror = () => { - reject('加载图片失败,002') + reject(new Error('加载图片失败,002')) } } else { @@ -109,7 +110,7 @@ export function fileToBase64(file: File): Promise<{ id: string, img: string }> { } } reader.onerror = () => { - reject('加载图片失败,001') + reject(new Error('加载图片失败,001')) } }) } @@ -305,12 +306,11 @@ export async function htmlToJpgImgFile(html: string, op: { width: number }): Pro width: op.width, scale: 1, type, - format: type, - filename: name, backgroundColor: '#ffffff', }) const blob = await res.toBlob({ type, backgroundColor: '#ffffff' }) + console.log('截图完成,blob大小:', (blob.size / 1024).toFixed(2), 'KB') // 将blob转换为base64以便在浏览器查看 @@ -321,6 +321,7 @@ export async function htmlToJpgImgFile(html: string, op: { width: number }): Pro } reader.readAsDataURL(blob) }) + // eslint-disable-next-line unused-imports/no-unused-vars const base64 = await base64Promise // console.log("blob=====", blob); // console.log("图片base64=====", base64); diff --git a/apps/admin/src/utils/service.ts b/apps/admin/src/utils/service.ts index 499f6c4..864090a 100644 --- a/apps/admin/src/utils/service.ts +++ b/apps/admin/src/utils/service.ts @@ -17,7 +17,7 @@ export function createServiceConfig(env: Env.ImportMeta) { } const httpConfig: App.Service.SimpleServiceConfig = { - baseURL: VITE_SERVICE_BASE_URL, + baseURL: VITE_SERVICE_BASE_URL, // 服务基础URL other, } @@ -27,16 +27,15 @@ export function createServiceConfig(env: Env.ImportMeta) { return { key, baseURL: httpConfig.other[key], - proxyPattern: createProxyPattern(key), + proxyPattern: createProxyPattern(key), // 其他服务基础URL的代理模式 } }) const config: App.Service.ServiceConfig = { - baseURL: httpConfig.baseURL, - proxyPattern: createProxyPattern(), + baseURL: httpConfig.baseURL, // 服务基础URL + proxyPattern: createProxyPattern(), // 服务基础URL的代理模式 other: otherConfig, } - return config } diff --git a/apps/admin/src/utils/upload-util.ts b/apps/admin/src/utils/upload-util.ts new file mode 100644 index 0000000..cd39db3 --- /dev/null +++ b/apps/admin/src/utils/upload-util.ts @@ -0,0 +1,105 @@ +// 定义文件上传任务类型 +interface UploadTask { + myFlie: R + resolve: (res: K) => void + reject: (error: Error) => void +} +type UploadFile = (myFlie: R) => Promise + +/** + * 文件上传服务类(单例模式), + */ +export class FileUploadService { + /** 当前上传的数量 */ + private currentUploads = 0 + /** 一次最多上传多少个,剩下的加入队列 */ + private concurrency = 5 + /** 上传队列 */ + private uploadTaskList: UploadTask[] = [] + /** 上传的方法 */ + private uploadFile: UploadFile + + /** + * 私有构造函数 + */ + public constructor(_uploadFile: UploadFile, _concurrency?: number) { + this.currentUploads = 0 + this.concurrency = _concurrency || 5 // 默认并发数为2,可以根据需要调整 + this.uploadTaskList = [] + this.uploadFile = _uploadFile + } + + /** + * 执行文件上传 + * @param filesToUpload - 要上传的文件对象数组 + * @param concurrency - 并发上传数,默认为构造函数中设置的并发数 + */ + public upload(filesToUpload: R[], concurrency: number = this.concurrency): Promise>[]> { + return new Promise>[]>((resolve, reject) => { + const uploadPromises = filesToUpload.map(file => this.addUploadTask(file)) + this.concurrency = concurrency // 更新并发数 + + // 返回所有上传任务的 Promise + Promise.allSettled(uploadPromises) + .then((res) => { + resolve(res) // 所有任务完成后调用总体解决函数 + }) + .catch((err) => { + console.error('文件上传过程中出现错误:') + reject(err) // 如果有任务失败,直接调用总体拒绝函数 + }) + }) + } + + /** + * 添加上传任务到队列 + * @param file - 要上传的文件对象 + */ + private addUploadTask(myFlie: R): Promise { + return new Promise((resolve, reject) => { + if (this.currentUploads < this.concurrency) { + // 如果当前上传数小于并发数,则直接上传 + this.currentUploads++ + this.uploadFile(myFlie) + .then((res: K) => { + this.currentUploads-- + resolve(res) + this.processQueue() + }) + .catch((error) => { + this.currentUploads-- + console.error(`上传失败,文件:`, myFlie, '错误====', error) + reject(error) + this.processQueue() + }) + } + else { + // 如果当前上传数达到并发数,则加入队列等待 + this.uploadTaskList.push({ myFlie, resolve, reject }) + } + }) + } + + /** + * 处理上传队列,依次执行上传任务 + */ + private processQueue(): void { + if (this.uploadTaskList.length > 0) { + const { myFlie, resolve, reject } = this.uploadTaskList.shift()! + this.addUploadTask(myFlie) + .then((res: K) => { + resolve(res) // 在完成上传后,调用任务的 resolve + }) + .catch((err) => { + reject(err) + }) + } + } + + /** + * 停止上传 + */ + public abort(): void { + this.uploadTaskList = [] + } +} diff --git a/apps/admin/src/utils/verify-number.ts b/apps/admin/src/utils/verify-number.ts new file mode 100644 index 0000000..3ab114e --- /dev/null +++ b/apps/admin/src/utils/verify-number.ts @@ -0,0 +1,340 @@ +export type $InputVerify = + | 'add_float' + | 'add_int_or_zero' + | 'add_int' + | 'all_float' + | 'all_int_or_zero' + | 'all_int' + | 'minus_float' + | 'minus_int_or_zero' + | 'minus_int' +/** ********************整数**********************整数 */ +/** ********************整数**********************整数 */ +/** ********************整数**********************整数 */ +// 正整数(不含0) +export const add_int_numb_RegEx = /^[1-9]\d*$/ + +// 负整数(不含0) +export const minus_int_numb_RegEx = /^-[1-9]\d*$/ + +// 整数 ( 不含0(正负都可以) +export const all_int_numb_RegEx = /^-?[1-9]\d*$/ + +// 正整数(含0) +export const add_int_or_zero_numb_RegEx = /^[1-9]\d*$|^0$/ + +// 负整数 (含0) +export const minus_int_or_zero_numb_RegEx = /^-[1-9]\d*$|^0$/ + +// 正负整数 (含0) +export const all_int_or_zero_numb_RegEx = /^-?[1-9]\d*$|^0$/ + +/** ********************浮点数**********************浮点数 */ +/** ********************浮点数**********************浮点数 */ +/** ********************浮点数**********************浮点数 */ + +// 正浮点数 (不含0) +export const add_float_numb_RegEx = /(^[1-9]\d*|^0)\.\d+$/ + +// 负浮点数(不含0) +export const minus_float_numb_RegEx = /(^-[1-9]\d*|^-0)\.\d+$/ + +// 正浮点数 (含0) +export const add_float_ro_zero_numb_RegEx = /(^[1-9]\d*|^0)\.\d+$|^0$/ + +// 负浮点数(含0) +export const minus_float_ro_zero_numb_RegEx = /(^-[1-9]\d*|^-0)\.\d+$|^0$/ + +// 浮点数(包含正负浮点数不含0) +export const all_float_numb_RegEx = /(^-?[1-9]\d*|^-?0)\.\d+$/ + +// 浮点数(包含正负浮点数和0) +export const all_float_or_zero_numb_RegEx = /(^-?[1-9]\d*|^-?0)\.\d+$|^0$/ + +// 所有合法数字 (整数,小数,正数,负数,0) 【不推荐用正则验证直接用isNaN(numb)并判断大于小于就可以了】 +export const all_numb = /(^-?[1-9]\d*|^-?0)(\.\d+)?$/ + +/** ********************数字输入时正则**********************数字输入时正则 */ +/** ********************数字输入时正则**********************数字输入时正则 */ +/** ********************数字输入时正则**********************数字输入时正则 */ + +// 正整数 (不含0) 主要用于输入时验证 +export const add_int_numb_input_RegEx = /^[1-9]\d*/ + +// 负整数(不含0) 主要用于输入时验证 +export const minus_int_numb_input_RegEx = /^-?[1-9]\d*/ + +// 正负整数(不含0) 主要用于输入时验证 +export const all_int_numb_input_RegEx = /^-?[1-9]\d*/ + +// 正整数 (含0) 主要用于输入时验证 +export const add_int_or_zero_numb_input_RegEx = /^[1-9]\d*$|^0$/ + +// 负整数 (含0) 主要用于输入时验证 +export const minus_int_or_zero_numb_input_RegEx = /^-?[1-9]\d*$|^0/ + +// 正负整数(含0) 主要用于输入时验证 +export const all_int_or_zero_numb_input_RegEx = /^-?\d*/ + +// 正浮点数 主要用于输入时验证 +export const add_float_numb_input_RegEx = /^\d+\.?\d*/ + +// 负浮点数 主要用于输入时验证 +export const minus_float_numb_input_RegEx = /^-?\d*\.?\d*/ + +// 正负浮点数 主要用于输入时验证 【这里就可以验证合法的数字输入了包含 0. - -0. 】 +export const all_float_numb_input_RegEx = /^-?\d*\.?\d*/ + +/** *******************数字验证****************数字验证 */ +/** *******************数字验证****************数字验证 */ +/** *******************数字验证****************数字验证 */ +/** + * 正整数 (不含0) + */ +export function verifyAddIntNumb(str: string): boolean { + return Boolean(add_int_numb_RegEx.test(str)) +} + +/** + * 负整数(不含0) + */ +export function verifyMinusIntNumb(str: string): boolean { + return Boolean(minus_int_numb_RegEx.test(str)) +} + +/** + * 正负整数(不含0) + */ +export function verifyAllIntNumb(str: string): boolean { + return Boolean(all_int_numb_RegEx.test(str)) +} + +/** + * 正整数 (含0) + */ +export function verifyAddIntRoZeroNumb(str: string): boolean { + return Boolean(add_int_or_zero_numb_RegEx.test(str)) +} + +/** + * 负整数 (含0) + */ +export function verifyMinusIntRoZeroNumb(str: string): boolean { + return Boolean(minus_int_or_zero_numb_RegEx.test(str)) +} + +/** + * 正 负 整数(含0) + */ +export function verifyAllIntRoZeroNumb(str: string): boolean { + return Boolean(all_int_or_zero_numb_RegEx.test(str)) +} + +/** + * 正浮点数 + */ +export function verifyAddFloatNumb(str: string): boolean { + return Boolean(add_float_numb_RegEx.test(str)) +} + +/** + * 负浮点数 + */ +export function verifyMinusFloatNumb(str: string): boolean { + return Boolean(minus_float_numb_RegEx.test(str)) +} + +/** + * 正负浮点数 + */ +export function verifyAllsFloatNumb(str: string): boolean { + return Boolean(all_float_numb_RegEx.test(str)) +} + +/** *******************输入时验证****************输入时验证 */ +/** *******************输入时验证****************输入时验证 */ +/** *******************输入时验证****************输入时验证 */ + +/** + * 正整数 (不含0) 主要用于输入时验证 + */ +export function verifyAddIntNumbInput(str: number | string): string { + let numb = String(str) + numb = String(Number.parseInt(numb)) + const numbArr = numb.match(add_int_numb_input_RegEx) + numb = Array.isArray(numbArr) ? numbArr[0] : '' + numb = numb.replace(/^0+/g, '0') + return numb +} + +/** + * 负整数(不含0) 主要用于输入时验证 + */ +export function verifyMinusIntNumbInput(str: number | string): string { + let numb = String(str) + numb = String(Number.parseInt(numb)) + const numbArr = numb.match(minus_int_numb_input_RegEx) + numb = Array.isArray(numbArr) ? numbArr[0] : '' + numb = numb.replace(/^0+/g, '0') + numb = numb !== '' && numb !== '0' && numb.startsWith('-') ? numb : `-${numb}` + return numb +} + +/** + * 正负整数(不含0) 主要用于输入时验证 + */ +export function verifyAllIntNumbInput(str: number | string): string { + let numb = String(str) + numb = String(Number.parseInt(numb)) + const numbArr = numb.match(all_int_numb_input_RegEx) + numb = Array.isArray(numbArr) ? numbArr[0] : '' + numb = numb.replace(/^0+/g, '0') + return numb +} + +/** + * 正整数 (含0) 主要用于输入时验证 + */ +export function verifyAddIntRoZeroNumbInput(str: number | string): string { + let numb = String(str) + numb = String(Number.parseInt(numb)) + const numbArr = numb.match(minus_int_or_zero_numb_input_RegEx) + numb = Array.isArray(numbArr) ? numbArr[0] : '' + numb = numb.replace(/^0+/g, '0') + return numb +} + +/** + * 负整数 (含0) 主要用于输入时验证 + */ +export function verifyMinusIntRoZeroNumbInput(str: number | string): string { + const numbArr = String(str).match(minus_int_or_zero_numb_input_RegEx) + let numb = Array.isArray(numbArr) ? numbArr[0] : '' + numb = numb.replace(/^0+/g, '0') + numb = numb.replace(/^-0+/g, '0') + numb = numb !== '' && numb !== '0' && numb.startsWith('-') ? numb : `-${numb}` + return numb +} + +/** + * 正 负 整数(含0) 主要用于输入时验证 + */ +export function verifyAllIntRoZeroNumbInput(str: number | string): string { + const numbArr = String(str).match(all_int_or_zero_numb_input_RegEx) + let numb = Array.isArray(numbArr) ? numbArr[0] : '' + numb = numb.replace(/^0+/g, '0') + numb = numb.replace(/^-0+/g, '-') + numb = !Number.isNaN(Number(numb)) ? String(Number(numb)) : numb + return numb +} + +/** + * 正浮点数 主要用于输入时验证 + */ +export function verifyAddFloatNumbInput(str: number | string): string { + let numb = str + if (str === '.') { + numb = '0.' + } + else { + const numbArr = String(numb).match(add_float_numb_input_RegEx) + numb = Array.isArray(numbArr) ? numbArr[0] : '' + numb = numb.replace(/^0+/g, '0') + numb = numb.replace(/^0+\./g, '0.') + numb = numb.match(/^0+[1-9]+/) ? numb.replace(/^0+/g, '') : numb + } + return numb +} + +/** + * 负浮点数 主要用于输入时验证 + */ +export function verifyMinusFloatNumbInput(str: number | string): string { + let numb = str + if (str === '.') { + numb = '-0.' + } + else { + const numbArr = String(str).match(minus_float_numb_input_RegEx) + numb = Array.isArray(numbArr) ? numbArr[0] : '' + numb = numb.replace(/^0+/g, '0') + numb = numb.replace(/^-0+/g, '-0') + numb = numb.replace(/^0+\./g, '0.') + numb = numb.replace(/^-\./g, '-0.') + numb = numb.match(/^0+[1-9]+/) ? numb.replace(/^0+/g, '') : numb + numb = numb !== '' && numb !== '0' && numb.startsWith('-') ? numb : `-${numb}` + } + return numb +} + +/** + * 正负浮点数 主要用于输入时验证 + */ +export function verifyAllFloatNumbInput(str: number | string): string { + let numb = str + if (str === '.') { + numb = '0.' + } + else { + const numbArr = String(str).match(all_float_numb_input_RegEx) + numb = Array.isArray(numbArr) ? numbArr[0] : '' + numb = numb.replace(/^0+/g, '0') + numb = numb.replace(/^-0+/g, '-0') + numb = numb.replace(/^0+\./g, '0.') + numb = numb.replace(/^-\./g, '-0.') + numb = numb.match(/^0+[1-9]+/) ? numb.replace(/^0+/g, '') : numb + } + return numb +} + +/** + * 验证数字 + * @param verify - 验证类型 + * @param numb - 需要验证的数字 + * @param oldNumb - 修改之前的 + */ +export function verifyNumbInput(verify: $InputVerify | undefined, numb: number | string, oldNumb: number | string) { + let newNumb = Number.isNaN(Number(numb)) && numb !== '.' && numb !== '-' ? oldNumb : numb + if (verify === 'add_int') { + // 正整数 (不含0) 主要用于输入时验证 + newNumb = verifyAddIntNumbInput(newNumb) + } + else if (verify === 'minus_int') { + // 负整数(不含0) 主要用于输入时验证 + newNumb = verifyMinusIntNumbInput(newNumb) + } + else if (verify === 'all_int') { + // 正负整数(不含0) 主要用于输入时验证 + newNumb = verifyAllIntNumbInput(newNumb) + } + else if (verify === 'add_int_or_zero') { + // 正整数 (含0) 主要用于输入时验证 + newNumb = verifyAddIntRoZeroNumbInput(newNumb) + } + else if (verify === 'minus_int_or_zero') { + // 负整数 (含0) 主要用于输入时验证 + newNumb = verifyMinusIntRoZeroNumbInput(newNumb) + } + else if (verify === 'all_int_or_zero') { + // 正负整数(含0) 主要用于输入时验证 + newNumb = verifyAllIntRoZeroNumbInput(newNumb) + } + else if (verify === 'add_float') { + // 正浮点数 主要用于输入时验证 + newNumb = verifyAddFloatNumbInput(newNumb) + } + else if (verify === 'minus_float') { + // 负浮点数 主要用于输入时验证 + newNumb = verifyMinusFloatNumbInput(newNumb) + } + else if (verify === 'all_float') { + // 正负浮点数 主要用于输入时验证 + newNumb = verifyAllFloatNumbInput(newNumb) + } + if (newNumb !== '') { + return newNumb + } + else { + return !Number.isNaN(Number(oldNumb)) ? oldNumb : '' + } +} diff --git a/apps/admin/src/utils/verify.ts b/apps/admin/src/utils/verify.ts new file mode 100644 index 0000000..33aeddb --- /dev/null +++ b/apps/admin/src/utils/verify.ts @@ -0,0 +1,162 @@ +/** 双字节字符(包括汉字在内,即占两个英文字符位置的字符) */ +export const double_byte_RegEx = /[^\x00-\xFF]/ // eslint-disable-line no-control-regex + +/** 中文字符 */ +export const chinese_RegEx = /[\u4E00-\u9FA5]/ + +/** 密码至少包含字母、数字、特殊字符中的两种,且不能出现4个大小连续或相同的数字 */ +export const password_RegEx + // eslint-disable-next-line regexp/optimal-quantifier-concatenation + = /^(?!.*(.)\1{3})(?!.*(0123|1234|2345|3456|4567|5678|6789|9876|8765|7654|6543|5432|4321))(?=.*[a-z])(?=.*\d|.*[!@#¥%&*.,])[a-z\d!@#¥%&*.,]+$/i + +/** 密码至少包含字母、数字、特殊字符中的两种,且不能出现4个大小连续或相同的数字,且为 8到 24 位 */ +export const password_RegEx2 + // eslint-disable-next-line regexp/optimal-quantifier-concatenation + = /^(?!.*(.)\1{3})(?!.*(0123|1234|2345|3456|4567|5678|6789|9876|8765|7654|6543|5432|4321))(?=.*[a-z])(?=.*\d|.*[!@#¥%&*.,])[a-z\d!@#¥%&*.,]{8,24}$/i +/** 验证电话号 */ +export const tel_RegEx = /^1\d{10}$/ + +/** 邮箱 */ +export const email_RegEx = /^([\w-]+)@([\w-]+\.?)*\w\.[a-z]{2,10}$/i + +/** 网址(不带参数) */ +export const url_RegEx = /^(https?:\/\/)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/\S*)?$/ +/** 网址(带参数) */ +export const url_RegEx2 = /^(https?:\/\/)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/[^\s?#]*)?(\?[^?\s#]*)?(#\S*)?$/ + +/** 身份证 */ +export const IDcard_RegEx = /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9X])$/i + +/** 视频 */ +// eslint-disable-next-line regexp/no-dupe-disjunctions +export const video_RegEx = /\.(mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv|m3u8)/i + +/** + * 验证密码合法性 + */ +export function verifyPassword(str: string): boolean { + return Boolean(password_RegEx.test(str)) +} + +/** + * 验证电话号 + */ +export function verifyTel(str: string): boolean { + return Boolean(tel_RegEx.test(str)) +} + +/** + * 验证邮箱 + */ +export function verifyEmail(str: string): boolean { + return Boolean(email_RegEx.test(str)) +} + +/** + * 验证身份证 + */ +export function verifyIDcard(str: string): boolean { + if (!IDcard_RegEx.test(str)) { + return false + } + try { + // 出生年月日校验 + const year = str.slice(6, 10) // 身份证年 + const month = str.slice(10, 12) // 身份证月 + const date = str.slice(12, 14) // 身份证日 + const d = new Date(`${year}/${month}/${date}`) + const dY = Number(d.getFullYear()) + const dM = Number(d.getMonth()) + 1 + const dD = Number(d.getDate()) + if (Number(year) === dY && Number(month) === dM && Number(date) === dD) { + // 限制起始年份为1850且不能超过今天 + if (d.getTime() <= new Date().getTime() && dY >= 1850) { + return true + } + else { + return false + } + } + else { + return false + } + // oxlint-disable-next-line no-unused-vars + } + // eslint-disable-next-line unused-imports/no-unused-vars + catch (_) { + return false + } +} + +/** + * 验证网址 + * @param str - 网址 + * @param isParam - 可以携带参数 默认为 true + */ +export function verifyUrl(str: string, isParam = true): boolean { + return Boolean(isParam ? url_RegEx2.test(str) : url_RegEx.test(str)) +} + +/** + * 是否为纯对象 不包含 Array,undefined function 等等 + */ +export function isObject(obj: unknown): obj is Record { + return Object.prototype.toString.call(obj) === '[object Object]' +} + +/** + * 是否为dom + */ +export function isDOM(obj: unknown): obj is HTMLElement { + if (typeof HTMLElement === 'object') { + return obj instanceof HTMLElement + } + else if (obj && typeof obj === 'object') { + return (obj as Record).nodeType === 1 && typeof (obj as Record).nodeName === 'string' + } + else { + return false + } +} + +/** + * 是否为日期对象 + */ +export function isDate(obj: unknown): obj is Date { + return Object.prototype.toString.call(obj) === '[object Date]' +} + +/** + * 是否为Math对象 + */ +export function iIsMath(obj: unknown): obj is Math { + return Object.prototype.toString.call(obj) === '[object Math]' +} + +/** + * 是否为RegExp对象 + */ +export function isRegExp(obj: unknown): obj is RegExp { + return Object.prototype.toString.call(obj) === '[object RegExp]' +} + +/** + * 是否为json对象 + */ +export function isJson(obj: unknown): obj is Record | unknown[] { + return Boolean(isObject(obj) || Array.isArray(obj)) +} + +/** + * 是否为空对象(传入对象为假也代表是空对象,如false,null,0等) + */ +export function isNullObj(obj: unknown): boolean { + return Boolean(!obj || JSON.stringify(obj) === '{}') +} + +/** + * 判断是否为小屏幕 + */ +export function isSmallScreen(): boolean { + return document.documentElement.clientWidth < 900 +} diff --git a/apps/admin/src/views/_builtin/login/modules/pwd-login.vue b/apps/admin/src/views/_builtin/login/modules/pwd-login.vue index 1c5fbfa..d4d6ce6 100644 --- a/apps/admin/src/views/_builtin/login/modules/pwd-login.vue +++ b/apps/admin/src/views/_builtin/login/modules/pwd-login.vue @@ -20,7 +20,7 @@ interface FormModel { } const model: FormModel = reactive({ - userName: 'Admin', + userName: '111111', password: '123456', }) diff --git a/apps/admin/src/views/competition/competition-add/index.vue b/apps/admin/src/views/competition/competition-add/index.vue index 7175836..8a52796 100644 --- a/apps/admin/src/views/competition/competition-add/index.vue +++ b/apps/admin/src/views/competition/competition-add/index.vue @@ -35,7 +35,7 @@ const competitionData = ref({ rounds: [ // 赛题包配置 { id: 'round_default', - name: '第一轮:主赛环节', + name: '星·辞海遨游', roundType: roundTypeOptions[0].value, questions: Array.from({ length: 10 }).map(() => ({ value: null, @@ -203,7 +203,7 @@ function handleReset() { type="primary" size="large" class="px-10 text-lg font-bold shadow-blue-500/30 shadow-lg" @click="nextStep" > - {{ currentStep === 4 ? '确认发布比赛' : '下一步' }} + {{ currentStep === 3 ? '确认发布比赛' : '下一步' }}