From b5f77782a0ebc9b1e2d994394c3d573cab450f81 Mon Sep 17 00:00:00 2001 From: wecoding Date: Fri, 15 May 2026 23:51:19 +0800 Subject: [PATCH 1/5] feat(admin): add bulk management actions --- .../Content/Library/Questions/Detail.tsx | 60 +----- .../Questions/components/SectionTitle.tsx | 10 + .../components/SortableItemWrapper.tsx | 37 ++++ .../Questions/components/UnitInputNumber.tsx | 13 ++ .../pages/Content/Library/Questions/index.tsx | 201 +++++++++++++++++- .../Papers/Detail/components/SectionTitle.tsx | 11 + .../components/SortableQuestionWrapper.tsx | 36 ++++ .../src/pages/Content/Papers/Detail/index.tsx | 48 +---- apps/admin/src/pages/Content/Papers/index.tsx | 132 +++++++++++- .../src/pages/Examination/ExamList/index.tsx | 139 +++++++++++- apps/admin/src/utils/request.ts | 9 + internal/api/batch.go | 17 ++ internal/api/exam.go | 14 ++ internal/api/exam_test.go | 88 ++++++++ internal/api/library.go | 42 ++++ internal/api/library_test.go | 134 ++++++++++++ internal/api/types.go | 9 + internal/exam/batch.go | 55 +++++ internal/exam/snapshot_service_test.go | 28 +++ internal/library/batch.go | 83 ++++++++ internal/library/paper.go | 3 + 21 files changed, 1058 insertions(+), 111 deletions(-) create mode 100644 apps/admin/src/pages/Content/Library/Questions/components/SectionTitle.tsx create mode 100644 apps/admin/src/pages/Content/Library/Questions/components/SortableItemWrapper.tsx create mode 100644 apps/admin/src/pages/Content/Library/Questions/components/UnitInputNumber.tsx create mode 100644 apps/admin/src/pages/Content/Papers/Detail/components/SectionTitle.tsx create mode 100644 apps/admin/src/pages/Content/Papers/Detail/components/SortableQuestionWrapper.tsx create mode 100644 internal/api/batch.go create mode 100644 internal/api/exam_test.go create mode 100644 internal/exam/batch.go create mode 100644 internal/library/batch.go diff --git a/apps/admin/src/pages/Content/Library/Questions/Detail.tsx b/apps/admin/src/pages/Content/Library/Questions/Detail.tsx index 7d3d9db..ffd5efe 100644 --- a/apps/admin/src/pages/Content/Library/Questions/Detail.tsx +++ b/apps/admin/src/pages/Content/Library/Questions/Detail.tsx @@ -22,10 +22,8 @@ import { import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { SortableContext, - useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; import type { AdminQuestion, QuestionType } from '@examora/types'; import { DIFFICULTY_OPTIONS, @@ -42,7 +40,6 @@ import { Empty, Form, Input, - InputNumber, Popconfirm, Radio, Row, @@ -53,6 +50,9 @@ import { import dayjs from 'dayjs'; import React, { useEffect, useMemo, useState } from 'react'; import { requestErrorMessage } from '@/utils/request'; +import { SectionTitle } from './components/SectionTitle'; +import { SortableItemWrapper } from './components/SortableItemWrapper'; +import { UnitInputNumber } from './components/UnitInputNumber'; import './index.less'; interface QuestionOption { @@ -62,17 +62,6 @@ interface QuestionOption { const OPTION_KEYS = 'ABCDEFGH'.split(''); -const UnitInputNumber: React.FC< - React.ComponentProps & { unit: string } -> = ({ unit, style, ...props }) => ( - - - - -); - const normalizeOptions = (options: QuestionOption[] | undefined) => (options || []) .map((option, index) => ({ @@ -292,49 +281,6 @@ const DIFFICULTY_TAGS: Record = { Sub-components ============================================================ */ -const SectionTitle: React.FC<{ - children: React.ReactNode; - style?: React.CSSProperties; -}> = ({ children, style }) => ( -

- {children} -

-); - -interface SortableItemWrapperProps { - id: string; - children: (handlers: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - listeners: any; - setActivatorNodeRef: (node: HTMLElement | null) => void; - }) => React.ReactNode; -} - -const SortableItemWrapper: React.FC = ({ - id, - children, -}) => { - const { - attributes, - listeners, - setNodeRef, - setActivatorNodeRef, - transform, - transition, - } = useSortable({ id }); - - const style: React.CSSProperties = { - transform: CSS.Translate.toString(transform), - transition, - }; - - return ( -
- {children({ listeners, setActivatorNodeRef })} -
- ); -}; - const OptionAnswerControl: React.FC<{ type: 'SINGLE_CHOICE' | 'MULTIPLE_CHOICE'; optionIndex: number; diff --git a/apps/admin/src/pages/Content/Library/Questions/components/SectionTitle.tsx b/apps/admin/src/pages/Content/Library/Questions/components/SectionTitle.tsx new file mode 100644 index 0000000..21d213a --- /dev/null +++ b/apps/admin/src/pages/Content/Library/Questions/components/SectionTitle.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export const SectionTitle: React.FC<{ + children: React.ReactNode; + style?: React.CSSProperties; +}> = ({ children, style }) => ( +

+ {children} +

+); diff --git a/apps/admin/src/pages/Content/Library/Questions/components/SortableItemWrapper.tsx b/apps/admin/src/pages/Content/Library/Questions/components/SortableItemWrapper.tsx new file mode 100644 index 0000000..356051b --- /dev/null +++ b/apps/admin/src/pages/Content/Library/Questions/components/SortableItemWrapper.tsx @@ -0,0 +1,37 @@ +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import React from 'react'; + +interface SortableItemWrapperProps { + id: string; + children: (handlers: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listeners: any; + setActivatorNodeRef: (node: HTMLElement | null) => void; + }) => React.ReactNode; +} + +export const SortableItemWrapper: React.FC = ({ + id, + children, +}) => { + const { + attributes, + listeners, + setNodeRef, + setActivatorNodeRef, + transform, + transition, + } = useSortable({ id }); + + const style: React.CSSProperties = { + transform: CSS.Translate.toString(transform), + transition, + }; + + return ( +
+ {children({ listeners, setActivatorNodeRef })} +
+ ); +}; diff --git a/apps/admin/src/pages/Content/Library/Questions/components/UnitInputNumber.tsx b/apps/admin/src/pages/Content/Library/Questions/components/UnitInputNumber.tsx new file mode 100644 index 0000000..b86d724 --- /dev/null +++ b/apps/admin/src/pages/Content/Library/Questions/components/UnitInputNumber.tsx @@ -0,0 +1,13 @@ +import { Button, InputNumber, Space } from 'antd'; +import React from 'react'; + +export const UnitInputNumber: React.FC< + React.ComponentProps & { unit: string } +> = ({ unit, style, ...props }) => ( + + + + +); diff --git a/apps/admin/src/pages/Content/Library/Questions/index.tsx b/apps/admin/src/pages/Content/Library/Questions/index.tsx index d448462..6f8748b 100644 --- a/apps/admin/src/pages/Content/Library/Questions/index.tsx +++ b/apps/admin/src/pages/Content/Library/Questions/index.tsx @@ -9,6 +9,7 @@ import { } from '@ant-design/icons'; import { type ActionType, + FooterToolbar, PageContainer, type ProColumns, ProTable, @@ -31,8 +32,12 @@ import { Tooltip, } from 'antd'; import dayjs from 'dayjs'; -import React, { useMemo, useRef } from 'react'; -import { proTableSortParams, requestErrorMessage } from '@/utils/request'; +import React, { useMemo, useRef, useState } from 'react'; +import { + type BatchActionResult, + proTableSortParams, + requestErrorMessage, +} from '@/utils/request'; import './index.less'; const DIFFICULTY_CLASS: Record = { @@ -57,8 +62,9 @@ export const QuestionsPageContent: React.FC = ({ fixedType, }) => { const intl = useIntl(); - const { message } = AntdApp.useApp(); + const { message, modal } = AntdApp.useApp(); const actionRef = useRef(null); + const [selectedRows, setSelectedRows] = useState([]); const isProgrammingOnly = fixedType === 'PROGRAMMING'; // i18n label maps @@ -282,6 +288,157 @@ export const QuestionsPageContent: React.FC = ({ }); }; + const reloadAndClearSelection = () => { + setSelectedRows([]); + actionRef.current?.reload(); + }; + + const showBatchResult = (result?: BatchActionResult) => { + if (!result) { + return; + } + if (result.failed_count > 0) { + modal.warning({ + title: intl.formatMessage({ + id: 'pages.batch.partialFailure', + defaultMessage: '部分操作失败', + }), + content: ( +
+

+ {intl.formatMessage( + { + id: 'pages.batch.summary', + defaultMessage: '成功 {success} 项,失败 {failed} 项。', + }, + { + success: result.success_count, + failed: result.failed_count, + }, + )} +

+
    + {result.failures.slice(0, 5).map((failure) => ( +
  • + #{failure.id}: {failure.reason} +
  • + ))} +
+
+ ), + }); + return; + } + message.success( + intl.formatMessage( + { + id: 'pages.batch.success', + defaultMessage: '成功处理 {count} 项', + }, + { count: result.success_count }, + ), + ); + }; + + const runBatchQuestionStatus = (status: 'DRAFT' | 'PUBLISHED') => { + const isPublishing = status === 'PUBLISHED'; + modal.confirm({ + title: intl.formatMessage({ + id: isPublishing + ? 'pages.questions.batchPublishConfirmTitle' + : 'pages.questions.batchUnpublishConfirmTitle', + defaultMessage: isPublishing ? '确认批量发布' : '确认批量下架', + }), + content: intl.formatMessage( + { + id: 'pages.questions.batchStatusConfirmContent', + defaultMessage: '将处理已选择的 {count} 道题目。', + }, + { count: selectedRows.length }, + ), + okText: intl.formatMessage({ + id: isPublishing + ? 'pages.questions.publish' + : 'pages.questions.unpublish', + defaultMessage: isPublishing ? '发布' : '下架', + }), + cancelText: intl.formatMessage({ + id: 'pages.questions.cancel', + defaultMessage: '取消', + }), + onOk: async () => { + try { + const response = await request<{ + code: number; + data: BatchActionResult; + }>('/api/admin/questions/batch/status', { + method: 'PATCH', + data: { ids: selectedRows.map((item) => item.id), status }, + skipErrorHandler: true, + }); + showBatchResult(response.data); + reloadAndClearSelection(); + } catch (error) { + message.error( + requestErrorMessage(error) || + intl.formatMessage({ + id: 'pages.questions.statusUpdateError', + defaultMessage: '状态更新失败', + }), + ); + } + }, + }); + }; + + const runBatchDeleteQuestions = () => { + modal.confirm({ + title: intl.formatMessage({ + id: 'pages.questions.batchDeleteConfirmTitle', + defaultMessage: '确认批量删除', + }), + content: intl.formatMessage( + { + id: 'pages.questions.batchDeleteConfirmContent', + defaultMessage: + '确定要删除已选择的 {count} 道题目吗?此操作不可撤销。', + }, + { count: selectedRows.length }, + ), + okText: intl.formatMessage({ + id: 'pages.questions.delete', + defaultMessage: '删除', + }), + okType: 'danger', + cancelText: intl.formatMessage({ + id: 'pages.questions.cancel', + defaultMessage: '取消', + }), + onOk: async () => { + try { + const response = await request<{ + code: number; + data: BatchActionResult; + }>('/api/admin/questions/batch', { + method: 'DELETE', + data: { ids: selectedRows.map((item) => item.id) }, + skipErrorHandler: true, + }); + showBatchResult(response.data); + reloadAndClearSelection(); + } catch (error) { + message.error( + requestErrorMessage(error) || + intl.formatMessage({ + id: 'pages.questions.deleteError', + defaultMessage: '删除题目失败', + }), + ); + } + }, + }); + }; + const columns: ProColumns[] = [ { title: intl.formatMessage({ @@ -525,6 +682,10 @@ export const QuestionsPageContent: React.FC = ({ setting: true, }} rowKey="id" + rowSelection={{ + selectedRowKeys: selectedRows.map((item) => item.id), + onChange: (_, rows) => setSelectedRows(rows), + }} search={{ labelWidth: 'auto', span: { @@ -625,6 +786,40 @@ export const QuestionsPageContent: React.FC = ({ , ]} /> + {selectedRows.length > 0 && ( + + + + + + )} ); }; diff --git a/apps/admin/src/pages/Content/Papers/Detail/components/SectionTitle.tsx b/apps/admin/src/pages/Content/Papers/Detail/components/SectionTitle.tsx new file mode 100644 index 0000000..9da4ee5 --- /dev/null +++ b/apps/admin/src/pages/Content/Papers/Detail/components/SectionTitle.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export const SectionTitle: React.FC<{ + children: React.ReactNode; + extra?: React.ReactNode; +}> = ({ children, extra }) => ( +
+

{children}

+ {extra} +
+); diff --git a/apps/admin/src/pages/Content/Papers/Detail/components/SortableQuestionWrapper.tsx b/apps/admin/src/pages/Content/Papers/Detail/components/SortableQuestionWrapper.tsx new file mode 100644 index 0000000..5b94f9b --- /dev/null +++ b/apps/admin/src/pages/Content/Papers/Detail/components/SortableQuestionWrapper.tsx @@ -0,0 +1,36 @@ +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import React from 'react'; + +interface SortableQuestionWrapperProps { + id: string; + children: (handlers: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listeners: any; + setActivatorNodeRef: (node: HTMLElement | null) => void; + }) => React.ReactNode; +} + +export const SortableQuestionWrapper: React.FC< + SortableQuestionWrapperProps +> = ({ id, children }) => { + const { + attributes, + listeners, + setNodeRef, + setActivatorNodeRef, + transform, + transition, + } = useSortable({ id }); + + const style: React.CSSProperties = { + transform: CSS.Translate.toString(transform), + transition, + }; + + return ( +
+ {children({ listeners, setActivatorNodeRef })} +
+ ); +}; diff --git a/apps/admin/src/pages/Content/Papers/Detail/index.tsx b/apps/admin/src/pages/Content/Papers/Detail/index.tsx index 670ad44..d9ee00d 100644 --- a/apps/admin/src/pages/Content/Papers/Detail/index.tsx +++ b/apps/admin/src/pages/Content/Papers/Detail/index.tsx @@ -27,10 +27,8 @@ import { import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { SortableContext, - useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; import type { AdminPaperOutline, AdminQuestion } from '@examora/types'; import { DIFFICULTY_OPTIONS, QUESTION_TYPE_OPTIONS } from '@examora/types'; import { history, request, useIntl } from '@umijs/max'; @@ -50,6 +48,8 @@ import { import dayjs from 'dayjs'; import React, { useEffect, useMemo, useState } from 'react'; import { requestErrorMessage } from '@/utils/request'; +import { SectionTitle } from './components/SectionTitle'; +import { SortableQuestionWrapper } from './components/SortableQuestionWrapper'; import { moveQuestionWithinSection, normalizeQuestions, @@ -129,50 +129,6 @@ const parseQuestionDragId = (id: string) => { }; }; -const SectionTitle: React.FC<{ - children: React.ReactNode; - extra?: React.ReactNode; -}> = ({ children, extra }) => ( -
-

{children}

- {extra} -
-); - -interface SortableQuestionWrapperProps { - id: string; - children: (handlers: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - listeners: any; - setActivatorNodeRef: (node: HTMLElement | null) => void; - }) => React.ReactNode; -} - -const SortableQuestionWrapper: React.FC = ({ - id, - children, -}) => { - const { - attributes, - listeners, - setNodeRef, - setActivatorNodeRef, - transform, - transition, - } = useSortable({ id }); - - const style: React.CSSProperties = { - transform: CSS.Translate.toString(transform), - transition, - }; - - return ( -
- {children({ listeners, setActivatorNodeRef })} -
- ); -}; - const PaperDetailContent: React.FC = () => { const intl = useIntl(); const { message } = AntdApp.useApp(); diff --git a/apps/admin/src/pages/Content/Papers/index.tsx b/apps/admin/src/pages/Content/Papers/index.tsx index 3ff8561..0575ba6 100644 --- a/apps/admin/src/pages/Content/Papers/index.tsx +++ b/apps/admin/src/pages/Content/Papers/index.tsx @@ -6,6 +6,7 @@ import { } from '@ant-design/icons'; import { type ActionType, + FooterToolbar, PageContainer, type ProColumns, ProTable, @@ -13,8 +14,12 @@ import { import { history, request, useIntl } from '@umijs/max'; import { App as AntdApp, Button, Dropdown, Space, Tag, Tooltip } from 'antd'; import dayjs from 'dayjs'; -import React, { useMemo, useRef } from 'react'; -import { proTableSortParams, requestErrorMessage } from '@/utils/request'; +import React, { useMemo, useRef, useState } from 'react'; +import { + type BatchActionResult, + proTableSortParams, + requestErrorMessage, +} from '@/utils/request'; import './index.less'; interface Paper { @@ -35,6 +40,7 @@ const PapersPageContent: React.FC = () => { const intl = useIntl(); const { message, modal } = AntdApp.useApp(); const actionRef = useRef(null); + const [selectedRows, setSelectedRows] = useState([]); const statusLabelMap: Record = useMemo( () => ({ @@ -109,6 +115,102 @@ const PapersPageContent: React.FC = () => { }); }; + const showBatchResult = (result?: BatchActionResult) => { + if (!result) { + return; + } + if (result.failed_count > 0) { + modal.warning({ + title: intl.formatMessage({ + id: 'pages.batch.partialFailure', + defaultMessage: '部分操作失败', + }), + content: ( +
+

+ {intl.formatMessage( + { + id: 'pages.batch.summary', + defaultMessage: '成功 {success} 项,失败 {failed} 项。', + }, + { + success: result.success_count, + failed: result.failed_count, + }, + )} +

+
    + {result.failures.slice(0, 5).map((failure) => ( +
  • + #{failure.id}: {failure.reason} +
  • + ))} +
+
+ ), + }); + return; + } + message.success( + intl.formatMessage( + { + id: 'pages.batch.success', + defaultMessage: '成功处理 {count} 项', + }, + { count: result.success_count }, + ), + ); + }; + + const runBatchDeletePapers = () => { + modal.confirm({ + title: intl.formatMessage({ + id: 'pages.papers.batchDeleteConfirmTitle', + defaultMessage: '确认批量删除', + }), + content: intl.formatMessage( + { + id: 'pages.papers.batchDeleteConfirmContent', + defaultMessage: + '确定要删除已选择的 {count} 份试卷吗?此操作不可撤销。', + }, + { count: selectedRows.length }, + ), + okText: intl.formatMessage({ + id: 'pages.papers.delete', + defaultMessage: '删除', + }), + okType: 'danger', + cancelText: intl.formatMessage({ + id: 'pages.papers.cancel', + defaultMessage: '取消', + }), + onOk: async () => { + try { + const response = await request<{ + code: number; + data: BatchActionResult; + }>('/api/admin/papers/batch', { + method: 'DELETE', + data: { ids: selectedRows.map((item) => item.id) }, + skipErrorHandler: true, + }); + showBatchResult(response.data); + setSelectedRows([]); + actionRef.current?.reload(); + } catch (error) { + message.error( + requestErrorMessage(error) || + intl.formatMessage({ + id: 'pages.papers.deleteError', + defaultMessage: '删除试卷失败', + }), + ); + } + }, + }); + }; + const columns: ProColumns[] = [ { title: intl.formatMessage({ @@ -315,6 +417,10 @@ const PapersPageContent: React.FC = () => { setting: true, }} rowKey="id" + rowSelection={{ + selectedRowKeys: selectedRows.map((item) => item.id), + onChange: (_, rows) => setSelectedRows(rows), + }} search={{ labelWidth: 'auto', span: { @@ -403,6 +509,28 @@ const PapersPageContent: React.FC = () => { , ]} /> + {selectedRows.length > 0 && ( + + + + )} ); }; diff --git a/apps/admin/src/pages/Examination/ExamList/index.tsx b/apps/admin/src/pages/Examination/ExamList/index.tsx index d2103e4..9084b37 100644 --- a/apps/admin/src/pages/Examination/ExamList/index.tsx +++ b/apps/admin/src/pages/Examination/ExamList/index.tsx @@ -1,5 +1,7 @@ -import { PlusOutlined } from '@ant-design/icons'; +import { PlusOutlined, StopOutlined } from '@ant-design/icons'; import { + type ActionType, + FooterToolbar, PageContainer, type ProColumns, ProTable, @@ -7,7 +9,9 @@ import { import { history, request, useIntl } from '@umijs/max'; import { App as AntdApp, Button, Tag } from 'antd'; import dayjs from 'dayjs'; -import React, { useMemo } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; +import type { BatchActionResult } from '@/utils/request'; +import { requestErrorMessage } from '@/utils/request'; interface Exam { id: number; @@ -39,7 +43,9 @@ const statusColors: Record = { const ExamListContent: React.FC = () => { const intl = useIntl(); - const { message } = AntdApp.useApp(); + const { message, modal } = AntdApp.useApp(); + const actionRef = useRef(null); + const [selectedRows, setSelectedRows] = useState([]); const statusLabelMap: Record = useMemo( () => ({ @@ -78,6 +84,105 @@ const ExamListContent: React.FC = () => { [statusLabelMap], ); + const closableSelectedRows = selectedRows.filter((record) => + ['PUBLISHED', 'RUNNING'].includes(record.status), + ); + + const showBatchResult = (result?: BatchActionResult) => { + if (!result) { + return; + } + if (result.failed_count > 0) { + modal.warning({ + title: intl.formatMessage({ + id: 'pages.batch.partialFailure', + defaultMessage: '部分操作失败', + }), + content: ( +
+

+ {intl.formatMessage( + { + id: 'pages.batch.summary', + defaultMessage: '成功 {success} 项,失败 {failed} 项。', + }, + { + success: result.success_count, + failed: result.failed_count, + }, + )} +

+
    + {result.failures.slice(0, 5).map((failure) => ( +
  • + #{failure.id}: {failure.reason} +
  • + ))} +
+
+ ), + }); + return; + } + message.success( + intl.formatMessage( + { + id: 'pages.batch.success', + defaultMessage: '成功处理 {count} 项', + }, + { count: result.success_count }, + ), + ); + }; + + const runBatchCloseExams = () => { + modal.confirm({ + title: intl.formatMessage({ + id: 'pages.exams.batchCloseConfirmTitle', + defaultMessage: '确认批量关闭', + }), + content: intl.formatMessage( + { + id: 'pages.exams.batchCloseConfirmContent', + defaultMessage: '将关闭已选择的 {count} 场可关闭考试。', + }, + { count: closableSelectedRows.length }, + ), + okText: intl.formatMessage({ + id: 'pages.exams.close', + defaultMessage: '关闭', + }), + okType: 'danger', + cancelText: intl.formatMessage({ + id: 'pages.questions.cancel', + defaultMessage: '取消', + }), + onOk: async () => { + try { + const response = await request<{ + code: number; + data: BatchActionResult; + }>('/api/admin/exams/batch/close', { + method: 'POST', + data: { ids: closableSelectedRows.map((item) => item.id) }, + skipErrorHandler: true, + }); + showBatchResult(response.data); + setSelectedRows([]); + actionRef.current?.reload(); + } catch (error) { + message.error( + requestErrorMessage(error) || + intl.formatMessage({ + id: 'pages.exams.closeError', + defaultMessage: '关闭考试失败', + }), + ); + } + }, + }); + }; + const columns: ProColumns[] = [ { title: intl.formatMessage({ @@ -207,6 +312,7 @@ const ExamListContent: React.FC = () => { })} > + actionRef={actionRef} cardBordered={{ search: true, table: true, @@ -231,6 +337,10 @@ const ExamListContent: React.FC = () => { setting: true, }} rowKey="id" + rowSelection={{ + selectedRowKeys: selectedRows.map((item) => item.id), + onChange: (_, rows) => setSelectedRows(rows), + }} search={false} request={async (params) => { try { @@ -273,6 +383,29 @@ const ExamListContent: React.FC = () => { , ]} /> + {selectedRows.length > 0 && ( + + + + )} ); }; diff --git a/apps/admin/src/utils/request.ts b/apps/admin/src/utils/request.ts index 523705e..162929d 100644 --- a/apps/admin/src/utils/request.ts +++ b/apps/admin/src/utils/request.ts @@ -11,3 +11,12 @@ export const proTableSortParams = ( sort_order: sort[sortField] === 'ascend' ? 'asc' : 'desc', }; }; + +export type BatchActionResult = { + success_count: number; + failed_count: number; + failures: Array<{ + id: number; + reason: string; + }>; +}; diff --git a/internal/api/batch.go b/internal/api/batch.go new file mode 100644 index 0000000..2c094a2 --- /dev/null +++ b/internal/api/batch.go @@ -0,0 +1,17 @@ +package api + +func normalizeBatchIDs(ids []uint64) []uint64 { + seen := make(map[uint64]struct{}, len(ids)) + normalized := make([]uint64, 0, len(ids)) + for _, id := range ids { + if id == 0 { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + normalized = append(normalized, id) + } + return normalized +} diff --git a/internal/api/exam.go b/internal/api/exam.go index 4eda5d2..7b08553 100644 --- a/internal/api/exam.go +++ b/internal/api/exam.go @@ -10,6 +10,7 @@ import ( func (s *Server) registerExamAdminRoutes(admin *gin.RouterGroup) { admin.GET("/exams", s.listExams) admin.POST("/exams", s.createExam) + admin.POST("/exams/batch/close", s.batchCloseExams) admin.GET("/exams/:id", s.getExam) admin.PUT("/exams/:id", s.updateExam) admin.POST("/exams/:id/publish", s.publishExamWithSnapshot) @@ -101,6 +102,19 @@ func (s *Server) closeExam(c *gin.Context) { response.NoContent(c) } +func (s *Server) batchCloseExams(c *gin.Context) { + req, ok := bindJSON[batchIDsRequest](c) + if !ok { + return + } + if len(normalizeBatchIDs(req.IDs)) == 0 { + response.BadRequest(c, "ids are required") + return + } + result := s.exam.BatchCloseExams(c.Request.Context(), req.IDs) + response.Success(c, result) +} + // M1: Candidate exam flow handlers func (s *Server) publishExamWithSnapshot(c *gin.Context) { diff --git a/internal/api/exam_test.go b/internal/api/exam_test.go new file mode 100644 index 0000000..32b2305 --- /dev/null +++ b/internal/api/exam_test.go @@ -0,0 +1,88 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + + "github.com/coding-hui/examora/internal/exam" + examstore "github.com/coding-hui/examora/internal/exam/store" + "github.com/coding-hui/examora/internal/infra/database" + "github.com/coding-hui/examora/internal/infra/transaction" + "github.com/coding-hui/examora/internal/library" + librarystore "github.com/coding-hui/examora/internal/library/store" +) + +type apiRecordingJudgeDispatcher struct{} + +func (apiRecordingJudgeDispatcher) CreateAndEnqueue(context.Context, uint64, uint64, uint64, string) error { + return nil +} + +func newExamAPIRouter(t *testing.T) (*gin.Engine, *exam.Service) { + t.Helper() + + gin.SetMode(gin.TestMode) + + db, err := database.Open(filepath.Join(t.TempDir(), "examora-api-exam-test.db")) + require.NoError(t, err) + require.NoError(t, database.AutoMigrate(db)) + + libraryStore := librarystore.New(db) + _, err = library.ProvideService(libraryStore, transaction.NewManager(db)) + require.NoError(t, err) + + examStore := examstore.New(db) + service, err := exam.ProvideService(examStore, libraryStore, apiRecordingJudgeDispatcher{}, db) + require.NoError(t, err) + + server := &Server{exam: service} + router := gin.New() + admin := router.Group("/api/admin") + server.registerExamAdminRoutes(admin) + return router, service +} + +func TestBatchCloseExamsEndpointReturnsPartialFailures(t *testing.T) { + router, service := newExamAPIRouter(t) + ctx := t.Context() + + published, err := service.CreateExam(ctx, exam.SaveExamCommand{ + Title: "Published exam", + Status: exam.StatusPublished, + }) + require.NoError(t, err) + draft, err := service.CreateExam(ctx, exam.SaveExamCommand{ + Title: "Draft exam", + Status: exam.StatusDraft, + }) + require.NoError(t, err) + + bodyBytes, err := json.Marshal(map[string]any{"ids": []uint64{published.ID, draft.ID}}) + require.NoError(t, err) + req := httptest.NewRequest(http.MethodPost, "/api/admin/exams/batch/close", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + router.ServeHTTP(resp, req) + + require.Equal(t, http.StatusOK, resp.Code) + var body struct { + Code int `json:"code"` + Data exam.BatchResult `json:"data"` + } + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &body)) + require.Equal(t, 0, body.Code) + require.Equal(t, 1, body.Data.SuccessCount) + require.Equal(t, 1, body.Data.FailedCount) + + closed, err := service.GetExam(ctx, published.ID) + require.NoError(t, err) + require.Equal(t, exam.StatusClosed, closed.Status) +} diff --git a/internal/api/library.go b/internal/api/library.go index 64f53b6..6bf6110 100644 --- a/internal/api/library.go +++ b/internal/api/library.go @@ -10,6 +10,8 @@ import ( func (s *Server) registerLibraryAdminRoutes(admin *gin.RouterGroup) { admin.GET("/questions", s.listQuestions) admin.POST("/questions", s.createQuestion) + admin.PATCH("/questions/batch/status", s.batchPatchQuestionStatus) + admin.DELETE("/questions/batch", s.batchDeleteQuestions) admin.GET("/questions/:id", s.getQuestion) admin.PUT("/questions/:id", s.updateQuestion) admin.PATCH("/questions/:id", s.patchQuestion) @@ -19,6 +21,7 @@ func (s *Server) registerLibraryAdminRoutes(admin *gin.RouterGroup) { admin.GET("/papers", s.listPapers) admin.POST("/papers", s.createPaper) + admin.DELETE("/papers/batch", s.batchDeletePapers) admin.GET("/papers/:id", s.getPaper) admin.PUT("/papers/:id", s.updatePaper) admin.DELETE("/papers/:id", s.deletePaper) @@ -137,6 +140,32 @@ func (s *Server) deleteQuestion(c *gin.Context) { response.NoContent(c) } +func (s *Server) batchPatchQuestionStatus(c *gin.Context) { + req, ok := bindJSON[batchQuestionStatusRequest](c) + if !ok { + return + } + if len(normalizeBatchIDs(req.IDs)) == 0 { + response.BadRequest(c, "ids are required") + return + } + result := s.library.BatchPatchQuestionStatus(c.Request.Context(), req.IDs, req.Status) + response.Success(c, result) +} + +func (s *Server) batchDeleteQuestions(c *gin.Context) { + req, ok := bindJSON[batchIDsRequest](c) + if !ok { + return + } + if len(normalizeBatchIDs(req.IDs)) == 0 { + response.BadRequest(c, "ids are required") + return + } + result := s.library.BatchDeleteQuestions(c.Request.Context(), req.IDs) + response.Success(c, result) +} + func (s *Server) addTestCase(c *gin.Context) { id, ok := parseUintParam(c, "id") if !ok { @@ -241,6 +270,19 @@ func (s *Server) deletePaper(c *gin.Context) { response.NoContent(c) } +func (s *Server) batchDeletePapers(c *gin.Context) { + req, ok := bindJSON[batchIDsRequest](c) + if !ok { + return + } + if len(normalizeBatchIDs(req.IDs)) == 0 { + response.BadRequest(c, "ids are required") + return + } + result := s.library.BatchDeletePapers(c.Request.Context(), req.IDs) + response.Success(c, result) +} + func (s *Server) getPaperOutline(c *gin.Context) { id, ok := parseUintParam(c, "id") if !ok { diff --git a/internal/api/library_test.go b/internal/api/library_test.go index 0cb64a7..6eedd7f 100644 --- a/internal/api/library_test.go +++ b/internal/api/library_test.go @@ -194,6 +194,108 @@ func TestListQuestionsEndpointNormalizesUnsafeSortAndPaging(t *testing.T) { require.Equal(t, "Safe sort", body.Data.Items[0].Title) } +func TestBatchPatchQuestionStatusEndpointReturnsPartialFailures(t *testing.T) { + router, service := newLibraryAPIRouter(t) + ctx := t.Context() + + ready, err := service.CreateQuestion(ctx, library.SaveQuestionCommand{ + Type: library.QuestionTypeTrueFalse, + Title: "Ready", + Content: map[string]any{"text": "Go is compiled."}, + Answer: map[string]any{"correct": true}, + Status: library.QuestionStatusDraft, + }) + require.NoError(t, err) + bodyBytes, err := json.Marshal(map[string]any{ + "ids": []uint64{ready.ID, 99999}, + "status": library.QuestionStatusPublished, + }) + require.NoError(t, err) + req := httptest.NewRequest(http.MethodPatch, "/api/admin/questions/batch/status", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + router.ServeHTTP(resp, req) + + require.Equal(t, http.StatusOK, resp.Code) + var body struct { + Code int `json:"code"` + Data library.BatchResult `json:"data"` + } + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &body)) + require.Equal(t, 0, body.Code) + require.Equal(t, 1, body.Data.SuccessCount) + require.Equal(t, 1, body.Data.FailedCount) + require.Len(t, body.Data.Failures, 1) + + published, err := service.GetQuestion(ctx, ready.ID) + require.NoError(t, err) + require.Equal(t, library.QuestionStatusPublished, published.Status) +} + +func TestBatchDeleteQuestionsEndpointProtectsReferencedQuestions(t *testing.T) { + router, service := newLibraryAPIRouter(t) + ctx := t.Context() + + referenced, err := service.CreateQuestion(ctx, library.SaveQuestionCommand{ + Type: library.QuestionTypeTrueFalse, + Title: "Referenced", + Content: map[string]any{"text": "Go is compiled."}, + Answer: map[string]any{"correct": true}, + Status: library.QuestionStatusPublished, + }) + require.NoError(t, err) + loose, err := service.CreateQuestion(ctx, library.SaveQuestionCommand{ + Type: library.QuestionTypeTrueFalse, + Title: "Loose", + Content: map[string]any{"text": "Go has maps."}, + Answer: map[string]any{"correct": true}, + Status: library.QuestionStatusDraft, + }) + require.NoError(t, err) + paper, err := service.CreatePaper(ctx, library.SavePaperCommand{ + Title: "Backend paper", + Status: library.PaperStatusDraft, + }) + require.NoError(t, err) + _, err = service.AddPaperQuestion(ctx, paper.ID, library.AddPaperQuestionCommand{ + QuestionID: referenced.ID, + Score: 5, + SortOrder: 1, + }) + require.NoError(t, err) + + bodyBytes, err := json.Marshal(map[string]any{"ids": []uint64{referenced.ID, loose.ID}}) + require.NoError(t, err) + req := httptest.NewRequest(http.MethodDelete, "/api/admin/questions/batch", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + router.ServeHTTP(resp, req) + + require.Equal(t, http.StatusOK, resp.Code) + var body struct { + Data library.BatchResult `json:"data"` + } + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &body)) + require.Equal(t, 1, body.Data.SuccessCount) + require.Equal(t, 1, body.Data.FailedCount) + require.Contains(t, body.Data.Failures[0].Reason, library.ErrQuestionReferenced.Error()) + _, err = service.GetQuestion(ctx, loose.ID) + require.Error(t, err) + _, err = service.GetQuestion(ctx, referenced.ID) + require.NoError(t, err) +} + +func TestBatchDeleteQuestionsEndpointRejectsEmptyIDs(t *testing.T) { + router, _ := newLibraryAPIRouter(t) + + req := httptest.NewRequest(http.MethodDelete, "/api/admin/questions/batch", bytes.NewReader([]byte(`{"ids":[]}`))) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + router.ServeHTTP(resp, req) + + require.Equal(t, http.StatusBadRequest, resp.Code) +} + func TestListPapersEndpointSupportsFiltersAndSummary(t *testing.T) { router, service := newLibraryAPIRouter(t) ctx := t.Context() @@ -248,6 +350,38 @@ func TestListPapersEndpointSupportsFiltersAndSummary(t *testing.T) { require.Equal(t, 12.5, body.Data.Items[0].TotalScore) } +func TestBatchDeletePapersEndpointReturnsPartialFailures(t *testing.T) { + router, service := newLibraryAPIRouter(t) + ctx := t.Context() + + first, err := service.CreatePaper(ctx, library.SavePaperCommand{ + Title: "First", + Status: library.PaperStatusDraft, + }) + require.NoError(t, err) + second, err := service.CreatePaper(ctx, library.SavePaperCommand{ + Title: "Second", + Status: library.PaperStatusDraft, + }) + require.NoError(t, err) + + bodyBytes, err := json.Marshal(map[string]any{"ids": []uint64{first.ID, second.ID, 99999}}) + require.NoError(t, err) + req := httptest.NewRequest(http.MethodDelete, "/api/admin/papers/batch", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + router.ServeHTTP(resp, req) + + require.Equal(t, http.StatusOK, resp.Code) + var body struct { + Data library.BatchResult `json:"data"` + } + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &body)) + require.Equal(t, 2, body.Data.SuccessCount) + require.Equal(t, 1, body.Data.FailedCount) + require.Len(t, body.Data.Failures, 1) +} + func TestListPaperQuestionsEndpointReturnsQuestionSummary(t *testing.T) { router, service := newLibraryAPIRouter(t) diff --git a/internal/api/types.go b/internal/api/types.go index 14333e5..5ed1fad 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -31,6 +31,15 @@ type patchQuestionRequest struct { Status string `json:"status"` } +type batchIDsRequest struct { + IDs []uint64 `json:"ids"` +} + +type batchQuestionStatusRequest struct { + IDs []uint64 `json:"ids"` + Status string `json:"status"` +} + type saveTestCaseRequest struct { ID uint64 `json:"id"` Input string `json:"input"` diff --git a/internal/exam/batch.go b/internal/exam/batch.go new file mode 100644 index 0000000..9e9ad61 --- /dev/null +++ b/internal/exam/batch.go @@ -0,0 +1,55 @@ +package exam + +import "context" + +type BatchFailure struct { + ID uint64 `json:"id"` + Reason string `json:"reason"` +} + +type BatchResult struct { + SuccessCount int `json:"success_count"` + FailedCount int `json:"failed_count"` + Failures []BatchFailure `json:"failures"` +} + +func (r *BatchResult) recordSuccess() { + r.SuccessCount++ +} + +func (r *BatchResult) recordFailure(id uint64, err error) { + reason := "operation failed" + if err != nil { + reason = err.Error() + } + r.Failures = append(r.Failures, BatchFailure{ID: id, Reason: reason}) + r.FailedCount = len(r.Failures) +} + +func (s *Service) BatchCloseExams(ctx context.Context, ids []uint64) BatchResult { + result := BatchResult{Failures: []BatchFailure{}} + for _, id := range uniqueIDs(ids) { + if err := s.CloseExam(ctx, id); err != nil { + result.recordFailure(id, err) + continue + } + result.recordSuccess() + } + return result +} + +func uniqueIDs(ids []uint64) []uint64 { + seen := make(map[uint64]struct{}, len(ids)) + unique := make([]uint64, 0, len(ids)) + for _, id := range ids { + if id == 0 { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + unique = append(unique, id) + } + return unique +} diff --git a/internal/exam/snapshot_service_test.go b/internal/exam/snapshot_service_test.go index 8d69635..377568b 100644 --- a/internal/exam/snapshot_service_test.go +++ b/internal/exam/snapshot_service_test.go @@ -75,6 +75,34 @@ func strPtr(value string) *string { return &value } +func TestBatchCloseExamsReturnsPartialFailures(t *testing.T) { + fx := newExamFixture(t) + ctx := context.Background() + + published, err := fx.exams.CreateExam(ctx, exam.SaveExamCommand{ + Title: "Published exam", + Status: exam.StatusPublished, + }) + require.NoError(t, err) + draft, err := fx.exams.CreateExam(ctx, exam.SaveExamCommand{ + Title: "Draft exam", + Status: exam.StatusDraft, + }) + require.NoError(t, err) + + result := fx.exams.BatchCloseExams(ctx, []uint64{published.ID, draft.ID, 99999}) + + require.Equal(t, 1, result.SuccessCount) + require.Equal(t, 2, result.FailedCount) + require.Len(t, result.Failures, 2) + closed, err := fx.exams.GetExam(ctx, published.ID) + require.NoError(t, err) + require.Equal(t, exam.StatusClosed, closed.Status) + unchanged, err := fx.exams.GetExam(ctx, draft.ID) + require.NoError(t, err) + require.Equal(t, exam.StatusDraft, unchanged.Status) +} + func publishableExam(t *testing.T, fx *examFixture) *exam.Exam { t.Helper() ctx := context.Background() diff --git a/internal/library/batch.go b/internal/library/batch.go new file mode 100644 index 0000000..b72b915 --- /dev/null +++ b/internal/library/batch.go @@ -0,0 +1,83 @@ +package library + +import ( + "context" + "strings" +) + +type BatchFailure struct { + ID uint64 `json:"id"` + Reason string `json:"reason"` +} + +type BatchResult struct { + SuccessCount int `json:"success_count"` + FailedCount int `json:"failed_count"` + Failures []BatchFailure `json:"failures"` +} + +func (r *BatchResult) recordSuccess() { + r.SuccessCount++ +} + +func (r *BatchResult) recordFailure(id uint64, err error) { + reason := "operation failed" + if err != nil { + reason = err.Error() + } + r.Failures = append(r.Failures, BatchFailure{ID: id, Reason: reason}) + r.FailedCount = len(r.Failures) +} + +func (s *Service) BatchPatchQuestionStatus(ctx context.Context, ids []uint64, status string) BatchResult { + result := BatchResult{Failures: []BatchFailure{}} + status = strings.ToUpper(strings.TrimSpace(status)) + for _, id := range uniqueIDs(ids) { + if _, err := s.PatchQuestionStatus(ctx, id, status); err != nil { + result.recordFailure(id, err) + continue + } + result.recordSuccess() + } + return result +} + +func (s *Service) BatchDeleteQuestions(ctx context.Context, ids []uint64) BatchResult { + result := BatchResult{Failures: []BatchFailure{}} + for _, id := range uniqueIDs(ids) { + if err := s.DeleteQuestion(ctx, id); err != nil { + result.recordFailure(id, err) + continue + } + result.recordSuccess() + } + return result +} + +func (s *Service) BatchDeletePapers(ctx context.Context, ids []uint64) BatchResult { + result := BatchResult{Failures: []BatchFailure{}} + for _, id := range uniqueIDs(ids) { + if err := s.DeletePaper(ctx, id); err != nil { + result.recordFailure(id, err) + continue + } + result.recordSuccess() + } + return result +} + +func uniqueIDs(ids []uint64) []uint64 { + seen := make(map[uint64]struct{}, len(ids)) + unique := make([]uint64, 0, len(ids)) + for _, id := range ids { + if id == 0 { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + unique = append(unique, id) + } + return unique +} diff --git a/internal/library/paper.go b/internal/library/paper.go index 13fe383..055d44f 100644 --- a/internal/library/paper.go +++ b/internal/library/paper.go @@ -127,6 +127,9 @@ func (s *Service) PaperExists(ctx context.Context, id uint64) (bool, error) { } func (s *Service) DeletePaper(ctx context.Context, id uint64) error { + if _, err := s.store.GetPaper(ctx, id); err != nil { + return err + } return s.store.DeletePaper(ctx, id) } From c425d99d3f24e68ae04085f57d47702cba3c3fe4 Mon Sep 17 00:00:00 2001 From: wecoding Date: Sat, 16 May 2026 09:23:31 +0800 Subject: [PATCH 2/5] fix(admin): align list action menus --- apps/admin/src/locales/en-US/pages.ts | 1 + apps/admin/src/locales/zh-CN/pages.ts | 1 + .../Content/Library/Questions/index.less | 8 ++++ .../admin/src/pages/Content/Papers/index.less | 6 ++- .../src/pages/Examination/ExamList/index.less | 7 +++ .../src/pages/Examination/ExamList/index.tsx | 47 ++++++++++++++----- 6 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 apps/admin/src/pages/Examination/ExamList/index.less diff --git a/apps/admin/src/locales/en-US/pages.ts b/apps/admin/src/locales/en-US/pages.ts index 3eebd4d..a90fdee 100644 --- a/apps/admin/src/locales/en-US/pages.ts +++ b/apps/admin/src/locales/en-US/pages.ts @@ -372,6 +372,7 @@ export default { 'Create and manage exams, set time range and duration, and manage exam participants.', 'pages.exams.listTitle': 'Exam List', 'pages.exams.create': 'Create Exam', + 'pages.exams.more': 'More', 'pages.exams.publish': 'Publish', 'pages.exams.fetchError': 'Failed to fetch exam list', 'pages.exams.columns.id': 'ID', diff --git a/apps/admin/src/locales/zh-CN/pages.ts b/apps/admin/src/locales/zh-CN/pages.ts index ff704fb..f2c3d0b 100644 --- a/apps/admin/src/locales/zh-CN/pages.ts +++ b/apps/admin/src/locales/zh-CN/pages.ts @@ -349,6 +349,7 @@ export default { '创建和管理考试,设置考试时间、时长和参与考生,支持线上监考。', 'pages.exams.listTitle': '考试列表', 'pages.exams.create': '创建考试', + 'pages.exams.more': '更多', 'pages.exams.publish': '发布', 'pages.exams.fetchError': '获取考试列表失败', 'pages.exams.columns.id': 'ID', diff --git a/apps/admin/src/pages/Content/Library/Questions/index.less b/apps/admin/src/pages/Content/Library/Questions/index.less index 38d4bd1..bf1ff69 100644 --- a/apps/admin/src/pages/Content/Library/Questions/index.less +++ b/apps/admin/src/pages/Content/Library/Questions/index.less @@ -62,6 +62,14 @@ html.examora-dark .question-type-icon { outline-offset: 2px; } +.question-actions-cell { + display: inline-flex; + align-items: center; + justify-content: flex-start; + min-width: 64px; + text-align: left; +} + .question-actions-cell a { color: var(--examora-text); transition: color 150ms ease; diff --git a/apps/admin/src/pages/Content/Papers/index.less b/apps/admin/src/pages/Content/Papers/index.less index 7facf84..a429a92 100644 --- a/apps/admin/src/pages/Content/Papers/index.less +++ b/apps/admin/src/pages/Content/Papers/index.less @@ -59,5 +59,9 @@ } .paper-actions-cell { - text-align: right; + display: inline-flex; + align-items: center; + justify-content: flex-start; + min-width: 64px; + text-align: left; } diff --git a/apps/admin/src/pages/Examination/ExamList/index.less b/apps/admin/src/pages/Examination/ExamList/index.less new file mode 100644 index 0000000..1b8ca34 --- /dev/null +++ b/apps/admin/src/pages/Examination/ExamList/index.less @@ -0,0 +1,7 @@ +.exam-actions-cell { + display: inline-flex; + align-items: center; + justify-content: flex-start; + min-width: 64px; + text-align: left; +} diff --git a/apps/admin/src/pages/Examination/ExamList/index.tsx b/apps/admin/src/pages/Examination/ExamList/index.tsx index 9084b37..1a9af71 100644 --- a/apps/admin/src/pages/Examination/ExamList/index.tsx +++ b/apps/admin/src/pages/Examination/ExamList/index.tsx @@ -1,4 +1,4 @@ -import { PlusOutlined, StopOutlined } from '@ant-design/icons'; +import { DownOutlined, PlusOutlined, StopOutlined } from '@ant-design/icons'; import { type ActionType, FooterToolbar, @@ -7,11 +7,12 @@ import { ProTable, } from '@ant-design/pro-components'; import { history, request, useIntl } from '@umijs/max'; -import { App as AntdApp, Button, Tag } from 'antd'; +import { App as AntdApp, Button, Dropdown, Space, Tag } from 'antd'; import dayjs from 'dayjs'; import React, { useMemo, useRef, useState } from 'react'; import type { BatchActionResult } from '@/utils/request'; import { requestErrorMessage } from '@/utils/request'; +import './index.less'; interface Exam { id: number; @@ -282,18 +283,40 @@ const ExamListContent: React.FC = () => { render: (_: unknown, record) => record.status === 'DRAFT' ? [ - , + + history.push( + `/examination/exams/${record.id}/publish`, + ), + }, + ], + }} + trigger={['click']} + > + e.preventDefault()}> + + {intl.formatMessage({ + id: 'pages.exams.more', + defaultMessage: '更多', + })} + + + + + , ] : [], }, From 973f7d5e351184bbfd5226423c4cdf09b39f4a61 Mon Sep 17 00:00:00 2001 From: wecoding Date: Sat, 16 May 2026 09:27:02 +0800 Subject: [PATCH 3/5] chore(desktop): add formatting checks --- apps/desktop/biome.json | 51 ++ apps/desktop/package.json | 5 +- apps/desktop/pnpm-lock.yaml | 1151 +++++++++++++++++++++++++++++++++++ apps/desktop/src/App.vue | 303 ++++----- apps/desktop/src/main.ts | 6 +- apps/desktop/vite.config.ts | 4 +- tsconfig.base.json | 12 + 7 files changed, 1387 insertions(+), 145 deletions(-) create mode 100644 apps/desktop/biome.json create mode 100644 apps/desktop/pnpm-lock.yaml create mode 100644 tsconfig.base.json diff --git a/apps/desktop/biome.json b/apps/desktop/biome.json new file mode 100644 index 0000000..ba17def --- /dev/null +++ b/apps/desktop/biome.json @@ -0,0 +1,51 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "css": { + "parser": { + "tailwindDirectives": true + } + }, + "files": { + "ignoreUnknown": true, + "includes": [ + "**/*", + "!**/dist", + "!**/server", + "!**/public", + "!**/coverage", + "!**/node_modules", + "!**/src-tauri", + "!biome.json", + "!**/.worktrees" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "off" + }, + "correctness": { + "noUnusedVariables": "off", + "useUniqueElementIds": "off", + "useExhaustiveDependencies": "off" + }, + "a11y": { + "noStaticElementInteractions": "off", + "useValidAnchor": "off", + "useKeyWithClickEvents": "off" + } + } + }, + "javascript": { + "jsxRuntime": "reactClassic", + "formatter": { + "quoteStyle": "single" + } + } +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 56763b3..e061fd9 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -6,7 +6,9 @@ "scripts": { "dev": "vite", "build": "vite build", - "typecheck": "vue-tsc --noEmit" + "typecheck": "vue-tsc --noEmit", + "biome:lint": "biome lint", + "biome": "biome check --write" }, "dependencies": { "@examora/client": "workspace:*", @@ -15,6 +17,7 @@ "vue": "^3.5.13" }, "devDependencies": { + "@biomejs/biome": "^2.4.15", "@vitejs/plugin-vue": "^5.2.1", "typescript": "^5.7.3", "vite": "^6.0.5", diff --git a/apps/desktop/pnpm-lock.yaml b/apps/desktop/pnpm-lock.yaml new file mode 100644 index 0000000..267552f --- /dev/null +++ b/apps/desktop/pnpm-lock.yaml @@ -0,0 +1,1151 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@examora/client': + specifier: workspace:* + version: link:../../packages/client + '@examora/types': + specifier: workspace:* + version: link:../../packages/types + '@logto/vue': + specifier: ^3.0.13 + version: 3.0.13(vue@3.5.34(typescript@5.9.3)) + vue: + specifier: ^3.5.13 + version: 3.5.34(typescript@5.9.3) + devDependencies: + '@biomejs/biome': + specifier: ^2.4.15 + version: 2.4.15 + '@vitejs/plugin-vue': + specifier: ^5.2.1 + version: 5.2.4(vite@6.4.2)(vue@3.5.34(typescript@5.9.3)) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.0.5 + version: 6.4.2 + vue-tsc: + specifier: ^2.2.0 + version: 2.2.12(typescript@5.9.3) + +packages: + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.4.15': + resolution: {integrity: sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.15': + resolution: {integrity: sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.15': + resolution: {integrity: sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.15': + resolution: {integrity: sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.15': + resolution: {integrity: sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.15': + resolution: {integrity: sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.15': + resolution: {integrity: sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.15': + resolution: {integrity: sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.15': + resolution: {integrity: sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@logto/browser@3.0.13': + resolution: {integrity: sha512-SlZ76XiVh2es6eFB1M+ldV6b60eC3+eeKoRQ8/AvOlpwHhrY/v2FPw5LOd/vZ+WYjzDqsxNtOMdhTdliHZ7V1g==} + + '@logto/client@3.1.8': + resolution: {integrity: sha512-f6NcPOV/K1IpPm4ccARWeYpQVMeN4mfikGg+5Qw1rcIPYPUpD5BmDsQbVTAnDepCMbC7syzRerZmbwL8S3UL+A==} + + '@logto/js@6.1.2': + resolution: {integrity: sha512-YB/TfixPGI0Spbs8LXiKuASOKFUE9VmlTkXiPfgg3UXQsIPTU71KjKxEXZRePu3xdPNhsZ6WtnRfRvvcpP+KGQ==} + + '@logto/vue@3.0.13': + resolution: {integrity: sha512-cDq5rbhTyXCczFQLiv4+nlYZ6XNUIt3xw3tDQn5BY7GOqslge3Pdy1PlkjMDFgNQqHfoh9V5d1Qwg7Zi5IJBqg==} + peerDependencies: + vue: ^3.0.0 + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@silverhand/essentials@2.9.3': + resolution: {integrity: sha512-OM9pyGc/yYJMVQw+fFOZZaTHXDWc45sprj+ky+QjC9inhf5w51L1WBmzAwFuYkHAwO1M19fxVf2sTH9KKP48yg==} + engines: {node: '>=18.12.0', pnpm: ^10.0.0} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.15': + resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} + + '@volar/source-map@2.4.15': + resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} + + '@volar/typescript@2.4.15': + resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} + + '@vue/compiler-core@3.5.34': + resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==} + + '@vue/compiler-dom@3.5.34': + resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==} + + '@vue/compiler-sfc@3.5.34': + resolution: {integrity: sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==} + + '@vue/compiler-ssr@3.5.34': + resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/language-core@2.2.12': + resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.34': + resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==} + + '@vue/runtime-core@3.5.34': + resolution: {integrity: sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==} + + '@vue/runtime-dom@3.5.34': + resolution: {integrity: sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==} + + '@vue/server-renderer@3.5.34': + resolution: {integrity: sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==} + peerDependencies: + vue: 3.5.34 + + '@vue/shared@3.5.34': + resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} + + alien-signals@1.0.13: + resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + camelcase-keys@9.1.3: + resolution: {integrity: sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==} + engines: {node: '>=16'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + map-obj@5.0.0: + resolution: {integrity: sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + quick-lru@6.1.2: + resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} + engines: {node: '>=12'} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-tsc@2.2.12: + resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.34: + resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + +snapshots: + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@biomejs/biome@2.4.15': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.15 + '@biomejs/cli-darwin-x64': 2.4.15 + '@biomejs/cli-linux-arm64': 2.4.15 + '@biomejs/cli-linux-arm64-musl': 2.4.15 + '@biomejs/cli-linux-x64': 2.4.15 + '@biomejs/cli-linux-x64-musl': 2.4.15 + '@biomejs/cli-win32-arm64': 2.4.15 + '@biomejs/cli-win32-x64': 2.4.15 + + '@biomejs/cli-darwin-arm64@2.4.15': + optional: true + + '@biomejs/cli-darwin-x64@2.4.15': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.15': + optional: true + + '@biomejs/cli-linux-arm64@2.4.15': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.15': + optional: true + + '@biomejs/cli-linux-x64@2.4.15': + optional: true + + '@biomejs/cli-win32-arm64@2.4.15': + optional: true + + '@biomejs/cli-win32-x64@2.4.15': + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@logto/browser@3.0.13': + dependencies: + '@logto/client': 3.1.8 + '@silverhand/essentials': 2.9.3 + js-base64: 3.7.8 + + '@logto/client@3.1.8': + dependencies: + '@logto/js': 6.1.2 + '@silverhand/essentials': 2.9.3 + camelcase-keys: 9.1.3 + jose: 5.10.0 + + '@logto/js@6.1.2': + dependencies: + '@silverhand/essentials': 2.9.3 + camelcase-keys: 9.1.3 + + '@logto/vue@3.0.13(vue@3.5.34(typescript@5.9.3))': + dependencies: + '@logto/browser': 3.0.13 + '@silverhand/essentials': 2.9.3 + vue: 3.5.34(typescript@5.9.3) + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@silverhand/essentials@2.9.3': {} + + '@types/estree@1.0.8': {} + + '@vitejs/plugin-vue@5.2.4(vite@6.4.2)(vue@3.5.34(typescript@5.9.3))': + dependencies: + vite: 6.4.2 + vue: 3.5.34(typescript@5.9.3) + + '@volar/language-core@2.4.15': + dependencies: + '@volar/source-map': 2.4.15 + + '@volar/source-map@2.4.15': {} + + '@volar/typescript@2.4.15': + dependencies: + '@volar/language-core': 2.4.15 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.34': + dependencies: + '@babel/parser': 7.29.3 + '@vue/shared': 3.5.34 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.34': + dependencies: + '@vue/compiler-core': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/compiler-sfc@3.5.34': + dependencies: + '@babel/parser': 7.29.3 + '@vue/compiler-core': 3.5.34 + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.14 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.34': + dependencies: + '@vue/compiler-dom': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/language-core@2.2.12(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.15 + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.34 + alien-signals: 1.0.13 + minimatch: 9.0.9 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + + '@vue/reactivity@3.5.34': + dependencies: + '@vue/shared': 3.5.34 + + '@vue/runtime-core@3.5.34': + dependencies: + '@vue/reactivity': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/runtime-dom@3.5.34': + dependencies: + '@vue/reactivity': 3.5.34 + '@vue/runtime-core': 3.5.34 + '@vue/shared': 3.5.34 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.34(vue@3.5.34(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + vue: 3.5.34(typescript@5.9.3) + + '@vue/shared@3.5.34': {} + + alien-signals@1.0.13: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + camelcase-keys@9.1.3: + dependencies: + camelcase: 8.0.0 + map-obj: 5.0.0 + quick-lru: 6.1.2 + type-fest: 4.41.0 + + camelcase@8.0.0: {} + + csstype@3.2.3: {} + + de-indent@1.0.2: {} + + entities@7.0.1: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + estree-walker@2.0.2: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + + he@1.2.0: {} + + jose@5.10.0: {} + + js-base64@3.7.8: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + map-obj@5.0.0: {} + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.0 + + muggle-string@0.4.1: {} + + nanoid@3.3.12: {} + + path-browserify@1.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + quick-lru@6.1.2: {} + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + source-map-js@1.2.1: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + type-fest@4.41.0: {} + + typescript@5.9.3: {} + + vite@6.4.2: + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.14 + rollup: 4.60.4 + tinyglobby: 0.2.16 + optionalDependencies: + fsevents: 2.3.3 + + vscode-uri@3.1.0: {} + + vue-tsc@2.2.12(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.15 + '@vue/language-core': 2.2.12(typescript@5.9.3) + typescript: 5.9.3 + + vue@3.5.34(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-sfc': 3.5.34 + '@vue/runtime-dom': 3.5.34 + '@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@5.9.3)) + '@vue/shared': 3.5.34 + optionalDependencies: + typescript: 5.9.3 diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 319a1a3..2f79831 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1,109 +1,118 @@