diff --git a/apps/admin/.env b/apps/admin/.env index 1dc05b6..63afcaf 100644 --- a/apps/admin/.env +++ b/apps/admin/.env @@ -25,7 +25,7 @@ VITE_ICON_LOCAL_PREFIX=icon-local VITE_AUTH_ROUTE_MODE=static # 是否强制登录: Y (开启) | N (关闭) -VITE_AUTH_ROUTE_FORCE_LOGIN=N +VITE_AUTH_ROUTE_FORCE_LOGIN=Y # 静态认证路由的主页 VITE_ROUTE_HOME=home diff --git a/apps/admin/.env.dev b/apps/admin/.env.dev index 4c4d257..a26e12c 100644 --- a/apps/admin/.env.dev +++ b/apps/admin/.env.dev @@ -3,9 +3,9 @@ # VITE_SERVICE_BASE_URL=http://192.168.5.140:9999 -# VITE_SERVICE_BASE_URL=https://api.qyzhjy.com +VITE_SERVICE_BASE_URL=https://api.qyzhjy.com -VITE_SERVICE_BASE_URL=http://172.16.10.130:5000 +# VITE_SERVICE_BASE_URL=http://172.16.10.130:5000 VITE_OTHER_SERVICE_BASE_URL= `{ "demo": "http://localhost:9528" diff --git a/apps/admin/src/components/common/rest-draggable-resizable/rest-draggable-resizable.vue b/apps/admin/src/components/common/rest-draggable-resizable/rest-draggable-resizable.vue new file mode 100644 index 0000000..ec020ce --- /dev/null +++ b/apps/admin/src/components/common/rest-draggable-resizable/rest-draggable-resizable.vue @@ -0,0 +1,327 @@ + + + + + diff --git a/apps/admin/src/router/elegant/routes.ts b/apps/admin/src/router/elegant/routes.ts index 4795fc6..1034fc1 100644 --- a/apps/admin/src/router/elegant/routes.ts +++ b/apps/admin/src/router/elegant/routes.ts @@ -222,7 +222,9 @@ export const generatedRoutes: GeneratedRoute[] = [ title: 'user', i18nKey: 'route.user', icon: 'material-symbols:account-circle', - order: 6 + order: 6, + hideInMenu: true, + constant: true } } ]; diff --git a/apps/admin/src/service/api/book.ts b/apps/admin/src/service/api/book.ts new file mode 100644 index 0000000..f6378c3 --- /dev/null +++ b/apps/admin/src/service/api/book.ts @@ -0,0 +1,219 @@ + +import { request } from '../request'; + +export interface BookPageQuestionAddInput { + bookId: number; + bookPageId: number; + no: string; + type: number; + score: number; + subjectId?: number; + answerTime: number; +} + +export interface BookReviewInput { + bookId: number; + bookPageId: number; + id?: number; // questionId + [key: string]: any; +} + +export interface QuestionFromInfo { + [key: string]: any; +} + +export interface BookPageQuestion { + id: number; + type?: number; + [key: string]: any; +} + +export interface QuestionsImages { + [key: string]: any; +} + +export interface BookCatalog { + name: string; + type: number; + url?: string; +} + +export interface BookCatalogImportInput { + bookId: number; + bookCatalogs: BookCatalog[]; + index: number; + pdfUrl: string; +} + +export interface BookBaseData { + width?: number; + height?: number; +} + +export interface CatalogTree { + id: number; + name: string; + type: number; + verifyStatus?: number; + childs?: CatalogTree[]; + [key: string]: any; +} + +export function importBookPageAxios(data: BookCatalogImportInput) { + return request({ + url: '/admin/v1/book/page/import', + method: 'post', + data, + }); +} + +export function editBookCatalogAxios(data: any) { + return request({ + url: '/admin/v1/book/catalog/edit', + method: 'post', + data, + }); +} + +export function getBookCatalogTreeAxios(params: { bookId: number }) { + return request({ + url: '/admin/v1/book/catalog/tree', + method: 'get', + params, + }); +} + +export function getBookBaseInfoAxios(params: { id: number }) { + return request({ + url: '/admin/v1/book/base/info', + method: 'get', + params, + }); +} + +export function getBookPageDetailAxios(params: { pageId: number }) { + return request({ + url: '/admin/v1/book/page/detail', + method: 'get', + params, + }); +} + +export function setStartPageAxios(data: any) { + return request({ + url: '/admin/v1/book/page/start/set', + method: 'post', + data, + }); +} + +export function addBookQuestionAxios(data: BookPageQuestionAddInput) { + return request({ + url: '/admin/v1/book/question/add', + method: 'post', + data, + }); +} + +export function delBookQuestionAxios(id: number) { + return request({ + url: `/admin/v1/book/question/delete`, + method: 'post', + data: { id }, + }); +} + +export function updateBookPageLayoutAxios(data: { id: number; layout: string; questionsImages?: any[] }) { + return request({ + url: '/admin/v1/book/page/layout/update', + method: 'post', + data, + }); +} + +export function getQuestionInfoAxios(id: number) { + return request({ + url: `/admin/v1/book/question/info`, + method: 'get', + params: { id }, + }); +} + +export function editQuestionInfoAxios(data: any) { + return request({ + url: '/admin/v1/book/question/edit', + method: 'post', + data, + }); +} + +export function getQuestionBankNavAxios() { + return request({ + url: '/admin/v1/book/question/nav', + method: 'get', + }); +} + +export function getKnowledgePointDetailsAxios(params: any) { + return request({ + url: '/admin/v1/knowledge/point/details', + method: 'get', + params, + }); +} + +export function updateKeywordAnalysisAxios(data: any) { + return request({ + url: '/admin/v1/book/question/analysis/update', + method: 'post', + data, + }); +} + +export function releaseTaskAxios(data: any) { + return request({ + url: '/admin/v1/book/task/release', + method: 'post', + data, + }); +} + +export function fetchBookCategoryList(params: any) { + return request({ + url: '/admin/v1/book/category/list', + method: 'post', // Assuming post based on usage pattern, or get? Original didn't specify method in prop, usually list is get or post. Let's assume post for list with search params. + data: params, + }); +} + +export function addBookAreaAxios(data: any) { + return request({ + url: '/admin/v1/book/area/add', + method: 'post', + data, + }); +} + +export function updateBookAreaAxios(data: any) { + return request({ + url: '/admin/v1/book/area/update', + method: 'post', + data, + }); +} + +export function delBookAreaAxios(id: number) { + return request({ + url: '/admin/v1/book/area/delete', + method: 'post', + data: { id }, + }); +} + +export function fetchSubjectList(params: any) { + return request({ + url: '/admin/v1/subject/pagelist', + method: 'post', + data: params, + }); +} diff --git a/apps/admin/src/service/api/question.ts b/apps/admin/src/service/api/question.ts index cd6102b..e1736b8 100644 --- a/apps/admin/src/service/api/question.ts +++ b/apps/admin/src/service/api/question.ts @@ -1,5 +1,16 @@ import { request } from '../request' +/** 获取所有题目列表 */ +export function fetchGetQuestionListAll(params?: any) { + return request({ + url: '/Base/ActivityMain/GetQuestionListAll', + method: 'get', + params: params || {}, + }) +} + + + /** 新增题目 */ export function fetchAddQuestion(data?: Api.Question.AddParams) { return request({ diff --git a/apps/admin/src/typings/components.d.ts b/apps/admin/src/typings/components.d.ts index cd6c79c..05830f0 100644 --- a/apps/admin/src/typings/components.d.ts +++ b/apps/admin/src/typings/components.d.ts @@ -65,10 +65,19 @@ declare module 'vue' { IconLocalBanner: typeof import('~icons/local/banner')['default'] IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'] + IconMdiDelete: typeof import('~icons/mdi/delete')['default'] IconMdiDrag: typeof import('~icons/mdi/drag')['default'] IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default'] IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default'] + IconMdiPlus: typeof import('~icons/mdi/plus')['default'] IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] + IconSolarAddSquareLinear: typeof import('~icons/solar/add-square-linear')['default'] + IconSolarArchiveUpMinimlisticOutline: typeof import('~icons/solar/archive-up-minimlistic-outline')['default'] + IconSolarCloudUploadLinear: typeof import('~icons/solar/cloud-upload-linear')['default'] + IconSolarPen2Linear: typeof import('~icons/solar/pen2-linear')['default'] + IconSolarQuestionCircleLinear: typeof import('~icons/solar/question-circle-linear')['default'] + IconSolarQuestionCircleOutline: typeof import('~icons/solar/question-circle-outline')['default'] + IconSolarTrashBinMinimalisticOutline: typeof import('~icons/solar/trash-bin-minimalistic-outline')['default'] 'IconStreamlineSharp:typeAreaRemix': typeof import('~icons/streamline-sharp/type-area-remix')['default'] IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default'] IconUilSearch: typeof import('~icons/uil/search')['default'] @@ -103,6 +112,7 @@ declare module 'vue' { NImageGroup: typeof import('naive-ui')['NImageGroup'] NInput: typeof import('naive-ui')['NInput'] NInputGroup: typeof import('naive-ui')['NInputGroup'] + NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] NInputNumber: typeof import('naive-ui')['NInputNumber'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutContent: typeof import('naive-ui')['NLayoutContent'] @@ -122,6 +132,7 @@ declare module 'vue' { NScrollbar: typeof import('naive-ui')['NScrollbar'] NSelect: typeof import('naive-ui')['NSelect'] NSpace: typeof import('naive-ui')['NSpace'] + NSpin: typeof import('naive-ui')['NSpin'] NStatistic: typeof import('naive-ui')['NStatistic'] NSwitch: typeof import('naive-ui')['NSwitch'] NTab: typeof import('naive-ui')['NTab'] @@ -131,12 +142,15 @@ declare module 'vue' { NTextarea: typeof import('naive-ui')['NTextarea'] NThing: typeof import('naive-ui')['NThing'] NTooltip: typeof import('naive-ui')['NTooltip'] + NTree: typeof import('naive-ui')['NTree'] + NTreeSelect: typeof import('naive-ui')['NTreeSelect'] NUpload: typeof import('naive-ui')['NUpload'] NWatermark: typeof import('naive-ui')['NWatermark'] 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'] + RestDraggableResizable: typeof import('./../components/common/rest-draggable-resizable/rest-draggable-resizable.vue')['default'] RestHoverAciton: typeof import('./../components/common/rest-hover-aciton/rest-hover-aciton.vue')['default'] RestImgViewer: typeof import('./../components/common/rest-img-viewer/rest-img-viewer.vue')['default'] RestSelectFile: typeof import('./../components/common/rest-select-file/rest-select-file.vue')['default'] @@ -210,10 +224,19 @@ declare global { const IconLocalBanner: typeof import('~icons/local/banner')['default'] const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'] + const IconMdiDelete: typeof import('~icons/mdi/delete')['default'] const IconMdiDrag: typeof import('~icons/mdi/drag')['default'] const IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default'] const IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default'] + const IconMdiPlus: typeof import('~icons/mdi/plus')['default'] const IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] + const IconSolarAddSquareLinear: typeof import('~icons/solar/add-square-linear')['default'] + const IconSolarArchiveUpMinimlisticOutline: typeof import('~icons/solar/archive-up-minimlistic-outline')['default'] + const IconSolarCloudUploadLinear: typeof import('~icons/solar/cloud-upload-linear')['default'] + const IconSolarPen2Linear: typeof import('~icons/solar/pen2-linear')['default'] + const IconSolarQuestionCircleLinear: typeof import('~icons/solar/question-circle-linear')['default'] + const IconSolarQuestionCircleOutline: typeof import('~icons/solar/question-circle-outline')['default'] + const IconSolarTrashBinMinimalisticOutline: typeof import('~icons/solar/trash-bin-minimalistic-outline')['default'] const 'IconStreamlineSharp:typeAreaRemix': typeof import('~icons/streamline-sharp/type-area-remix')['default'] const IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default'] const IconUilSearch: typeof import('~icons/uil/search')['default'] @@ -248,6 +271,7 @@ declare global { const NImageGroup: typeof import('naive-ui')['NImageGroup'] const NInput: typeof import('naive-ui')['NInput'] const NInputGroup: typeof import('naive-ui')['NInputGroup'] + const NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] const NInputNumber: typeof import('naive-ui')['NInputNumber'] const NLayout: typeof import('naive-ui')['NLayout'] const NLayoutContent: typeof import('naive-ui')['NLayoutContent'] @@ -267,6 +291,7 @@ declare global { const NScrollbar: typeof import('naive-ui')['NScrollbar'] const NSelect: typeof import('naive-ui')['NSelect'] const NSpace: typeof import('naive-ui')['NSpace'] + const NSpin: typeof import('naive-ui')['NSpin'] const NStatistic: typeof import('naive-ui')['NStatistic'] const NSwitch: typeof import('naive-ui')['NSwitch'] const NTab: typeof import('naive-ui')['NTab'] @@ -276,12 +301,15 @@ declare global { const NTextarea: typeof import('naive-ui')['NTextarea'] const NThing: typeof import('naive-ui')['NThing'] const NTooltip: typeof import('naive-ui')['NTooltip'] + const NTree: typeof import('naive-ui')['NTree'] + const NTreeSelect: typeof import('naive-ui')['NTreeSelect'] const NUpload: typeof import('naive-ui')['NUpload'] const NWatermark: typeof import('naive-ui')['NWatermark'] const 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 RestDraggableResizable: typeof import('./../components/common/rest-draggable-resizable/rest-draggable-resizable.vue')['default'] const RestHoverAciton: typeof import('./../components/common/rest-hover-aciton/rest-hover-aciton.vue')['default'] const RestImgViewer: typeof import('./../components/common/rest-img-viewer/rest-img-viewer.vue')['default'] const RestSelectFile: typeof import('./../components/common/rest-select-file/rest-select-file.vue')['default'] diff --git a/apps/admin/src/utils/common.ts b/apps/admin/src/utils/common.ts index 6d0a033..95945c8 100644 --- a/apps/admin/src/utils/common.ts +++ b/apps/admin/src/utils/common.ts @@ -56,3 +56,32 @@ export function toggleHtmlClass(className: string) { remove, } } + +/** + * Join path segments + * @param paths + */ +export function browserPathJoin(...paths: string[]) { + return paths.join('/').replace(/\/+/g, '/') +} + +/** + * 版本比较 + * @param v1 + * @param v2 + * @param separator 分隔符 默认. + * @returns 1 v1>v2 | -1 v1 num2) return 1 + if (num1 < num2) return -1 + } + return 0 +} diff --git a/apps/admin/src/utils/event-bus.ts b/apps/admin/src/utils/event-bus.ts new file mode 100644 index 0000000..d279226 --- /dev/null +++ b/apps/admin/src/utils/event-bus.ts @@ -0,0 +1,23 @@ +export class EventBus { + private listeners: Record = {}; + + on(event: string, callback: Function) { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(callback); + } + + off(event: string, callback: Function) { + if (!this.listeners[event]) return; + this.listeners[event] = this.listeners[event].filter(cb => cb !== callback); + } + + emit(event: string, data?: any) { + if (!this.listeners[event]) return; + this.listeners[event].forEach(cb => cb(data)); + } +} + +export const $mitt = new EventBus(); +export const open_book_topic_edit = 'open_book_topic_edit'; diff --git a/apps/admin/src/views/template/template-detail/def-data.ts b/apps/admin/src/views/template/template-detail/def-data.ts new file mode 100644 index 0000000..f1f9b14 --- /dev/null +++ b/apps/admin/src/views/template/template-detail/def-data.ts @@ -0,0 +1,36 @@ + +import type { QuestionInfo } from './types'; + +export const topicTypeList = [ + { label: '单选题', value: 1 }, + { label: '多选题', value: 2 }, + { label: '判断题', value: 3 }, + { label: '填空题', value: 4 }, + { label: '简答题', value: 5 }, +]; + +export const topicType2List = [ + { label: '普通题', value: 100 }, + { label: '大题号', value: 101 }, + { label: '子题号', value: 102 }, +]; + +export function defAnswerInfo(x: number) { + return { + x: x, + y: 0, + w: 100, + h: 50, + }; +} + +export function defQuestionInfo(no: string, type: number, y: number, pageWidth: number): QuestionInfo { + return { + no, + type, + x: 0, + y: y, + w: pageWidth, + h: 100, // Default height + }; +} diff --git a/apps/admin/src/views/template/template-detail/index.vue b/apps/admin/src/views/template/template-detail/index.vue index 1c1f34f..4049b65 100644 --- a/apps/admin/src/views/template/template-detail/index.vue +++ b/apps/admin/src/views/template/template-detail/index.vue @@ -1,8 +1,74 @@ - - + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/left-tree/components/import-bubble.vue b/apps/admin/src/views/template/template-detail/modules/left-tree/components/import-bubble.vue new file mode 100644 index 0000000..2841ff3 --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/left-tree/components/import-bubble.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/left-tree/components/menu-bubble.vue b/apps/admin/src/views/template/template-detail/modules/left-tree/components/menu-bubble.vue new file mode 100644 index 0000000..77bf932 --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/left-tree/components/menu-bubble.vue @@ -0,0 +1,123 @@ + + + diff --git a/apps/admin/src/views/template/template-detail/modules/left-tree/left-tree.vue b/apps/admin/src/views/template/template-detail/modules/left-tree/left-tree.vue new file mode 100644 index 0000000..02c86db --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/left-tree/left-tree.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/middle-book/components/menu-bubble.vue b/apps/admin/src/views/template/template-detail/modules/middle-book/components/menu-bubble.vue new file mode 100644 index 0000000..f1f3d3c --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/middle-book/components/menu-bubble.vue @@ -0,0 +1,95 @@ + + + diff --git a/apps/admin/src/views/template/template-detail/modules/middle-book/components/topic-item-module.vue b/apps/admin/src/views/template/template-detail/modules/middle-book/components/topic-item-module.vue new file mode 100644 index 0000000..57439d7 --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/middle-book/components/topic-item-module.vue @@ -0,0 +1,312 @@ + + + + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/middle-book/middle-book.vue b/apps/admin/src/views/template/template-detail/modules/middle-book/middle-book.vue new file mode 100644 index 0000000..09399c0 --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/middle-book/middle-book.vue @@ -0,0 +1,118 @@ + + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/add-analysis-bubble.vue b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/add-analysis-bubble.vue new file mode 100644 index 0000000..8334399 --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/add-analysis-bubble.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/add-topic-form-bubble.vue b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/add-topic-form-bubble.vue new file mode 100644 index 0000000..7d5d20a --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/add-topic-form-bubble.vue @@ -0,0 +1,250 @@ + + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/edit-info-bubble.vue b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/edit-info-bubble.vue new file mode 100644 index 0000000..780bacf --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/edit-info-bubble.vue @@ -0,0 +1,458 @@ + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/release-task-bubble.vue b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/release-task-bubble.vue new file mode 100644 index 0000000..8bed023 --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/release-task-bubble.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/right-topic-item.vue b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/right-topic-item.vue new file mode 100644 index 0000000..7b96d1a --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/components/right-topic-item.vue @@ -0,0 +1,195 @@ + + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/right-topic-list-normal.vue b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/right-topic-list-normal.vue new file mode 100644 index 0000000..ab5b2b7 --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-normal/right-topic-list-normal.vue @@ -0,0 +1,139 @@ + + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-special/components/add-topic-form-bubble.vue b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-special/components/add-topic-form-bubble.vue new file mode 100644 index 0000000..41113e9 --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-special/components/add-topic-form-bubble.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-special/components/right-topic-item.vue b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-special/components/right-topic-item.vue new file mode 100644 index 0000000..3458fa5 --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-special/components/right-topic-item.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-special/right-topic-list-special.vue b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-special/right-topic-list-special.vue new file mode 100644 index 0000000..7acf226 --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/right-topic/components/right-topic-list-special/right-topic-list-special.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/apps/admin/src/views/template/template-detail/modules/right-topic/right-topic-list.vue b/apps/admin/src/views/template/template-detail/modules/right-topic/right-topic-list.vue new file mode 100644 index 0000000..dab107c --- /dev/null +++ b/apps/admin/src/views/template/template-detail/modules/right-topic/right-topic-list.vue @@ -0,0 +1,31 @@ + + + + + + diff --git a/apps/admin/src/views/template/template-detail/pdf-util.ts b/apps/admin/src/views/template/template-detail/pdf-util.ts new file mode 100644 index 0000000..f78137a --- /dev/null +++ b/apps/admin/src/views/template/template-detail/pdf-util.ts @@ -0,0 +1,11 @@ +import type { ImageInfo } from './types'; + +export async function convertPDFToImageFiles(file: File): Promise<{ list: ImageInfo[], pdf: File }> { + console.warn('PDF conversion requires pdfjs-dist, which is missing. Returning empty list.'); + return { list: [], pdf: file }; +} + +export async function getPDFPageWidth(file: File): Promise { + console.warn('PDF conversion requires pdfjs-dist, which is missing. Returning 0.'); + return 0; +} diff --git a/apps/admin/src/views/template/template-detail/types.ts b/apps/admin/src/views/template/template-detail/types.ts new file mode 100644 index 0000000..947a9bf --- /dev/null +++ b/apps/admin/src/views/template/template-detail/types.ts @@ -0,0 +1,100 @@ + +export interface BookBaseData { + width?: number; + height?: number; +} + +export interface QuestionInfo { + type?: number; + x: number; + y: number; + w: number; + h: number; + no: string; +} + +export interface LayoutItem { + questionId: number; + question: QuestionInfo; + answerList?: any[]; // Added based on topic-item-module usage +} + +export interface BookPageQuestion { + id: number; + type?: number; + [key: string]: any; +} + +export interface BookPageAreas { + id: number; + [key: string]: any; +} + +export interface CurrBookPageData { + url?: string; + bookPageId?: number; + layout: LayoutItem[]; + questions: BookPageQuestion[]; + areas: BookPageAreas[]; +} + +export interface CurrBookPageAllInfo { + bookBaseData: BookBaseData; + currBookPageData: CurrBookPageData; +} + +export type QuestionType = number; + +export interface QuestionsImages { + // Add properties as needed based on usage in util.ts + [key: string]: any; +} + +export interface CropImageOptions { + x: number; + y: number; + width: number; + height: number; +} + +export interface AddBookTopicForm { + type?: 100 | 101 | 102; + pageNumb?: number; + bigTpicNumb?: number; + subTopicNumb?: number; +} + +export interface BookPageOtherAddInput { + bookId: number; + bookPageId: number; + no: string; + type: number; +} + +export interface BookPageOtherUpdateInput { + bookId: number; + bookPageId: number; + no: string; + type: number; + id: number; +} + +export interface TopicInfo { + question: QuestionInfo; + answerList?: any[]; + questionId: number; +} + +export interface AnswerInfo { + x: number; + y: number; + w: number; + h: number; +} + +export interface ImageInfo { + file: File; + width: number; + height: number; + url?: string; +} diff --git a/apps/admin/src/views/template/template-detail/util.ts b/apps/admin/src/views/template/template-detail/util.ts new file mode 100644 index 0000000..75e8909 --- /dev/null +++ b/apps/admin/src/views/template/template-detail/util.ts @@ -0,0 +1,138 @@ + +import { ref } from 'vue'; +import pLimit from 'p-limit'; +import { type BookPageQuestion, type QuestionsImages, updateBookPageLayoutAxios } from '@/service/api/book'; +import { getAliOssTokenAxios } from '@/service/api/upload'; +import { initOSSClient } from '@/utils/oss'; +import type { CropImageOptions, CurrBookPageAllInfo, QuestionInfo } from './types'; +import { compareVersion } from '@/utils/common'; + +// Crop image function implementation +const cropImage = async (url: string, client: any, options: CropImageOptions, id: number): Promise => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.src = url; + img.onload = async () => { + try { + const canvas = document.createElement('canvas'); + canvas.width = options.width; + canvas.height = options.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Canvas context not available')); + return; + } + + // Draw cropped image + ctx.drawImage( + img, + options.x, + options.y, + options.width, + options.height, + 0, + 0, + options.width, + options.height + ); + + // Convert to Blob + canvas.toBlob(async (blob) => { + if (!blob) { + reject(new Error('Canvas to Blob failed')); + return; + } + + const file = new File([blob], `question_${id}_${Date.now()}.png`, { type: 'image/png' }); + const path = `book/questions/${id}/${file.name}`; + + try { + // Upload to OSS + // Assuming client.put or multipartUpload. + // Since initOSSClient returns an OSS client, we can use it. + // Using put for small files (cropped images are usually small) + const result = await client.put(path, file); + + // Return the result format expected by the API + // The API expects a map of ID to URL? Or array of objects? + // Based on usage: `tasks.push(...)` and `imageFiles.push(...list)` + // And `updateBookPageLayoutAxios` takes `questionsImages: imageFiles` + // Let's return an object with the ID as key and URL as value, or just the URL object if QuestionsImages is a type. + // Looking at previous usage `urlObj[item.id!] = item`, it seems `QuestionsImages` might be `{ [id]: url }`. + + resolve({ [id]: result.url }); + } catch (err) { + reject(err); + } + }, 'image/png'); + } catch (err) { + reject(err); + } + }; + img.onerror = (err) => { + reject(new Error('Image load failed')); + }; + }); +}; + +/** 提交 */ +export function usePageLayoutSubmit() { + const submmitLoading = ref(false); + + async function pageLayoutSubmit(currBookPage: CurrBookPageAllInfo) { + try { + submmitLoading.value = true; + const urlObj: Record = {}; + currBookPage!.currBookPageData.questions.forEach((item) => { + if (item.id) { + urlObj[item.id!] = item; + } + }); + + // 获取阿里云OSS STS + const aliOssSTS = await getAliOssTokenAxios(); + const client = initOSSClient(aliOssSTS); + + // 截取图片并上传 (5个并发) + const imageFiles: QuestionsImages[] = []; + const limit = pLimit(5); + const tasks: Promise[] = []; + currBookPage!.currBookPageData.layout.forEach((item) => { + const question: QuestionInfo = item.question; + if (currBookPage!.currBookPageData.url) { + const cropImageOptions: CropImageOptions = { x: question.x, y: question.y, width: question.w, height: question.h }; + const url = currBookPage!.currBookPageData.url; + //截取图片并添加到上传队列 + tasks.push(limit(() => cropImage(url, client, cropImageOptions, item.questionId!))); + } + }); + + const list = await Promise.all(tasks); + imageFiles.push(...list); + + currBookPage!.currBookPageData.layout = currBookPage!.currBookPageData.layout.sort((a, b) => { + return compareVersion(a.question.no, b.question.no, '-'); + }); + await updateBookPageLayoutAxios({ + id: currBookPage!.currBookPageData!.bookPageId!, + layout: JSON.stringify(currBookPage!.currBookPageData.layout), + questionsImages: imageFiles, + }); + submmitLoading.value = false; + return Promise.resolve(); + } catch (error) { + console.log('error====', error); + submmitLoading.value = false; + return Promise.reject(error); + } + } + return { + pageLayoutSubmit, + submmitLoading, + }; +} + +export function getPDFPageWidth(width: number) { + return width; +}