提交 45f5b58f authored 作者: 王鹏飞's avatar 王鹏飞

chore: 广西交付

上级 71d4446e
VITE_LOGIN_URL=http://172.16.3.203:1001/auth/login/index VITE_LOGIN_URL=http://172.16.30.141:1001/auth/login/index
VITE_LAB_URL=http://172.16.3.203:1012/bi/viewer?proc=0&action=index VITE_LAB_URL=http://172.16.30.141:1012/bi/viewer?proc=0&action=index
VITE_DML_URL=http://172.16.3.203:1006 VITE_DML_URL=http://172.16.30.141:1006
VITE_DML_PRO_URL=http://172.16.3.203:1007 VITE_DML_PRO_URL=http://172.16.30.141:1007
VITE_SAAS_LEARN_URL=http://172.16.3.203:1009 VITE_SAAS_LEARN_URL=http://172.16.30.141:1009
VITE_QA_CENTER_URL=http://172.16.3.203:1004 VITE_QA_CENTER_URL=http://172.16.30.141:1004
VITE_X_LEARNING_URL=https://x-learning.zijing.chat VITE_X_LEARNING_URL=https://x-learning.zijing.chat
VITE_EXAM_SHOW_URL=https://exam-show.zijing.chat VITE_EXAM_SHOW_URL=https://exam-show.zijing.chat
VITE_SWSJFXS_LOGIN_URL=http://172.16.3.203:1001/swsjfxs/login/index VITE_SWSJFXS_LOGIN_URL=http://172.16.30.141:1001/swsjfxs/login/index
VITE_SYS_FLAG=chat VITE_SYS_FLAG=chat
VITE_STATIC_URL=https://saas-lab-api VITE_STATIC_URL=https://saas-lab-api
VITE_SAAS_BI_URL=http://172.16.3.203:1014/data/dashboard VITE_SAAS_BI_URL=http://172.16.30.141:1014/data/dashboard
VITE_SAAS_DML_LIVE_URL=http://172.16.3.203:1015/live/test VITE_SAAS_DML_LIVE_URL=http://172.16.30.141:1015/live/test
\ No newline at end of file VITE_SAAS_AI_URL=http://172.16.30.141:1016
\ No newline at end of file
...@@ -3,13 +3,19 @@ ...@@ -3,13 +3,19 @@
"Component": true, "Component": true,
"ComponentPublicInstance": true, "ComponentPublicInstance": true,
"ComputedRef": true, "ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true, "EffectScope": true,
"ExtractDefaultPropTypes": true, "ExtractDefaultPropTypes": true,
"ExtractPropTypes": true, "ExtractPropTypes": true,
"ExtractPublicPropTypes": true, "ExtractPublicPropTypes": true,
"InjectionKey": true, "InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true, "PropType": true,
"Ref": true, "Ref": true,
"ShallowRef": true,
"Slot": true,
"Slots": true,
"VNode": true, "VNode": true,
"WritableComputedRef": true, "WritableComputedRef": true,
"asyncComputed": true, "asyncComputed": true,
...@@ -28,7 +34,10 @@ ...@@ -28,7 +34,10 @@
"createInjectionState": true, "createInjectionState": true,
"createProjection": true, "createProjection": true,
"createReactiveFn": true, "createReactiveFn": true,
"createRef": true,
"createReusableTemplate": true,
"createSharedComposable": true, "createSharedComposable": true,
"createTemplatePromise": true,
"createUnrefFn": true, "createUnrefFn": true,
"customRef": true, "customRef": true,
"debouncedRef": true, "debouncedRef": true,
...@@ -40,14 +49,17 @@ ...@@ -40,14 +49,17 @@
"extendRef": true, "extendRef": true,
"getCurrentInstance": true, "getCurrentInstance": true,
"getCurrentScope": true, "getCurrentScope": true,
"getCurrentWatcher": true,
"h": true, "h": true,
"ignorableWatch": true, "ignorableWatch": true,
"inject": true, "inject": true,
"injectLocal": true,
"isDefined": true, "isDefined": true,
"isProxy": true, "isProxy": true,
"isReactive": true, "isReactive": true,
"isReadonly": true, "isReadonly": true,
"isRef": true, "isRef": true,
"isShallow": true,
"logicAnd": true, "logicAnd": true,
"logicNot": true, "logicNot": true,
"logicOr": true, "logicOr": true,
...@@ -62,6 +74,7 @@ ...@@ -62,6 +74,7 @@
"onBeforeUpdate": true, "onBeforeUpdate": true,
"onClickOutside": true, "onClickOutside": true,
"onDeactivated": true, "onDeactivated": true,
"onElementRemoval": true,
"onErrorCaptured": true, "onErrorCaptured": true,
"onKeyStroke": true, "onKeyStroke": true,
"onLongPress": true, "onLongPress": true,
...@@ -73,8 +86,10 @@ ...@@ -73,8 +86,10 @@
"onStartTyping": true, "onStartTyping": true,
"onUnmounted": true, "onUnmounted": true,
"onUpdated": true, "onUpdated": true,
"onWatcherCleanup": true,
"pausableWatch": true, "pausableWatch": true,
"provide": true, "provide": true,
"provideLocal": true,
"reactify": true, "reactify": true,
"reactifyObject": true, "reactifyObject": true,
"reactive": true, "reactive": true,
...@@ -115,11 +130,14 @@ ...@@ -115,11 +130,14 @@
"until": true, "until": true,
"useAbs": true, "useAbs": true,
"useActiveElement": true, "useActiveElement": true,
"useAnimate": true,
"useArrayDifference": true,
"useArrayEvery": true, "useArrayEvery": true,
"useArrayFilter": true, "useArrayFilter": true,
"useArrayFind": true, "useArrayFind": true,
"useArrayFindIndex": true, "useArrayFindIndex": true,
"useArrayFindLast": true, "useArrayFindLast": true,
"useArrayIncludes": true,
"useArrayJoin": true, "useArrayJoin": true,
"useArrayMap": true, "useArrayMap": true,
"useArrayReduce": true, "useArrayReduce": true,
...@@ -139,9 +157,11 @@ ...@@ -139,9 +157,11 @@
"useCeil": true, "useCeil": true,
"useClamp": true, "useClamp": true,
"useClipboard": true, "useClipboard": true,
"useClipboardItems": true,
"useCloned": true, "useCloned": true,
"useColorMode": true, "useColorMode": true,
"useConfirmDialog": true, "useConfirmDialog": true,
"useCountdown": true,
"useCounter": true, "useCounter": true,
"useCssModule": true, "useCssModule": true,
"useCssVar": true, "useCssVar": true,
...@@ -181,6 +201,7 @@ ...@@ -181,6 +201,7 @@
"useFullscreen": true, "useFullscreen": true,
"useGamepad": true, "useGamepad": true,
"useGeolocation": true, "useGeolocation": true,
"useId": true,
"useIdle": true, "useIdle": true,
"useImage": true, "useImage": true,
"useInfiniteScroll": true, "useInfiniteScroll": true,
...@@ -200,6 +221,7 @@ ...@@ -200,6 +221,7 @@
"useMemoize": true, "useMemoize": true,
"useMemory": true, "useMemory": true,
"useMin": true, "useMin": true,
"useModel": true,
"useMounted": true, "useMounted": true,
"useMouse": true, "useMouse": true,
"useMouseInElement": true, "useMouseInElement": true,
...@@ -213,6 +235,8 @@ ...@@ -213,6 +235,8 @@
"useOnline": true, "useOnline": true,
"usePageLeave": true, "usePageLeave": true,
"useParallax": true, "useParallax": true,
"useParentElement": true,
"usePerformanceObserver": true,
"usePermission": true, "usePermission": true,
"usePointer": true, "usePointer": true,
"usePointerLock": true, "usePointerLock": true,
...@@ -223,6 +247,7 @@ ...@@ -223,6 +247,7 @@
"usePreferredDark": true, "usePreferredDark": true,
"usePreferredLanguages": true, "usePreferredLanguages": true,
"usePreferredReducedMotion": true, "usePreferredReducedMotion": true,
"usePreferredReducedTransparency": true,
"usePrevious": true, "usePrevious": true,
"useProjection": true, "useProjection": true,
"useRafFn": true, "useRafFn": true,
...@@ -231,6 +256,7 @@ ...@@ -231,6 +256,7 @@
"useRound": true, "useRound": true,
"useRoute": true, "useRoute": true,
"useRouter": true, "useRouter": true,
"useSSRWidth": true,
"useScreenOrientation": true, "useScreenOrientation": true,
"useScreenSafeArea": true, "useScreenSafeArea": true,
"useScriptTag": true, "useScriptTag": true,
...@@ -249,6 +275,7 @@ ...@@ -249,6 +275,7 @@
"useSum": true, "useSum": true,
"useSupported": true, "useSupported": true,
"useSwipe": true, "useSwipe": true,
"useTemplateRef": true,
"useTemplateRefsList": true, "useTemplateRefsList": true,
"useTextDirection": true, "useTextDirection": true,
"useTextSelection": true, "useTextSelection": true,
...@@ -262,7 +289,6 @@ ...@@ -262,7 +289,6 @@
"useTimeoutPoll": true, "useTimeoutPoll": true,
"useTimestamp": true, "useTimestamp": true,
"useTitle": true, "useTitle": true,
"useToFixed": true,
"useToNumber": true, "useToNumber": true,
"useToString": true, "useToString": true,
"useToggle": true, "useToggle": true,
...@@ -286,8 +312,10 @@ ...@@ -286,8 +312,10 @@
"watchArray": true, "watchArray": true,
"watchAtMost": true, "watchAtMost": true,
"watchDebounced": true, "watchDebounced": true,
"watchDeep": true,
"watchEffect": true, "watchEffect": true,
"watchIgnorable": true, "watchIgnorable": true,
"watchImmediate": true,
"watchOnce": true, "watchOnce": true,
"watchPausable": true, "watchPausable": true,
"watchPostEffect": true, "watchPostEffect": true,
...@@ -295,13 +323,6 @@ ...@@ -295,13 +323,6 @@
"watchThrottled": true, "watchThrottled": true,
"watchTriggerable": true, "watchTriggerable": true,
"watchWithFilter": true, "watchWithFilter": true,
"whenever": true, "whenever": true
"DirectiveBinding": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"onWatcherCleanup": true,
"useId": true,
"useModel": true,
"useTemplateRef": true
} }
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// @ts-nocheck // @ts-nocheck
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import // Generated by unplugin-auto-import
// biome-ignore lint: disable
export {} export {}
declare global { declare global {
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: typeof import('vue')['EffectScope']
...@@ -22,7 +23,10 @@ declare global { ...@@ -22,7 +23,10 @@ declare global {
const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createProjection: typeof import('@vueuse/math')['createProjection'] const createProjection: typeof import('@vueuse/math')['createProjection']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createRef: typeof import('@vueuse/core')['createRef']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef'] const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
...@@ -34,14 +38,17 @@ declare global { ...@@ -34,14 +38,17 @@ declare global {
const extendRef: typeof import('@vueuse/core')['extendRef'] const extendRef: typeof import('@vueuse/core')['extendRef']
const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope'] const getCurrentScope: typeof import('vue')['getCurrentScope']
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
const h: typeof import('vue')['h'] const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject'] const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined'] const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy'] const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive'] const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly'] const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef'] const isRef: typeof import('vue')['isRef']
const isShallow: typeof import('vue')['isShallow']
const logicAnd: typeof import('@vueuse/math')['logicAnd'] const logicAnd: typeof import('@vueuse/math')['logicAnd']
const logicNot: typeof import('@vueuse/math')['logicNot'] const logicNot: typeof import('@vueuse/math')['logicNot']
const logicOr: typeof import('@vueuse/math')['logicOr'] const logicOr: typeof import('@vueuse/math')['logicOr']
...@@ -56,6 +63,7 @@ declare global { ...@@ -56,6 +63,7 @@ declare global {
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated'] const onDeactivated: typeof import('vue')['onDeactivated']
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
const onErrorCaptured: typeof import('vue')['onErrorCaptured'] const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress'] const onLongPress: typeof import('@vueuse/core')['onLongPress']
...@@ -70,6 +78,7 @@ declare global { ...@@ -70,6 +78,7 @@ declare global {
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide'] const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify'] const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive'] const reactive: typeof import('vue')['reactive']
...@@ -110,11 +119,14 @@ declare global { ...@@ -110,11 +119,14 @@ declare global {
const until: typeof import('@vueuse/core')['until'] const until: typeof import('@vueuse/core')['until']
const useAbs: typeof import('@vueuse/math')['useAbs'] const useAbs: typeof import('@vueuse/math')['useAbs']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
...@@ -134,9 +146,11 @@ declare global { ...@@ -134,9 +146,11 @@ declare global {
const useCeil: typeof import('@vueuse/math')['useCeil'] const useCeil: typeof import('@vueuse/math')['useCeil']
const useClamp: typeof import('@vueuse/math')['useClamp'] const useClamp: typeof import('@vueuse/math')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard'] const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned'] const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode'] const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCounter: typeof import('@vueuse/core')['useCounter'] const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule'] const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar'] const useCssVar: typeof import('@vueuse/core')['useCssVar']
...@@ -210,6 +224,8 @@ declare global { ...@@ -210,6 +224,8 @@ declare global {
const useOnline: typeof import('@vueuse/core')['useOnline'] const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax'] const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission'] const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer'] const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
...@@ -220,6 +236,7 @@ declare global { ...@@ -220,6 +236,7 @@ declare global {
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePrevious: typeof import('@vueuse/core')['usePrevious'] const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useProjection: typeof import('@vueuse/math')['useProjection'] const useProjection: typeof import('@vueuse/math')['useProjection']
const useRafFn: typeof import('@vueuse/core')['useRafFn'] const useRafFn: typeof import('@vueuse/core')['useRafFn']
...@@ -228,6 +245,7 @@ declare global { ...@@ -228,6 +245,7 @@ declare global {
const useRound: typeof import('@vueuse/math')['useRound'] const useRound: typeof import('@vueuse/math')['useRound']
const useRoute: typeof import('vue-router')['useRoute'] const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter'] const useRouter: typeof import('vue-router')['useRouter']
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
...@@ -284,8 +302,10 @@ declare global { ...@@ -284,8 +302,10 @@ declare global {
const watchArray: typeof import('@vueuse/core')['watchArray'] const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect'] const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce'] const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable'] const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect'] const watchPostEffect: typeof import('vue')['watchPostEffect']
...@@ -298,6 +318,6 @@ declare global { ...@@ -298,6 +318,6 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue') import('vue')
} }
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -15,15 +15,15 @@ ...@@ -15,15 +15,15 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@microsoft/fetch-event-source": "^2.0.1", "@fortaine/fetch-event-source": "^3.0.6",
"@tinymce/tinymce-vue": "^5.0.0", "@tinymce/tinymce-vue": "^5.0.0",
"@unhead/vue": "^2.0.14",
"@vant/area-data": "^1.5.1", "@vant/area-data": "^1.5.1",
"@vueuse/core": "^9.13.0", "@vueuse/core": "^13.6.0",
"@vueuse/head": "^1.0.26", "@vueuse/integrations": "^13.6.0",
"@vueuse/integrations": "^9.13.0", "@vueuse/math": "^13.6.0",
"@vueuse/math": "^9.13.0",
"ali-oss": "^6.17.1", "ali-oss": "^6.17.1",
"axios": "^1.3.3", "axios": "^1.10.0",
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
"countup.js": "^2.6.2", "countup.js": "^2.6.2",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
...@@ -34,13 +34,13 @@ ...@@ -34,13 +34,13 @@
"html2pdf.js": "^0.10.1", "html2pdf.js": "^0.10.1",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"pinia": "^2.1.7", "pinia": "^3.0.3",
"print-js": "^1.6.0", "print-js": "^1.6.0",
"ua-parser-js": "^1.0.33", "ua-parser-js": "^1.0.33",
"universal-cookie": "^4.0.4", "universal-cookie": "^8.0.1",
"video.js": "^7.21.1", "video.js": "^7.21.1",
"vue": "^3.5.12", "vue": "^3.5.18",
"vue-router": "^4.4.5", "vue-router": "^4.5.1",
"vuedraggable": "^4.1.0" "vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
...@@ -52,8 +52,8 @@ ...@@ -52,8 +52,8 @@
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/ua-parser-js": "^0.7.36", "@types/ua-parser-js": "^0.7.36",
"@types/video.js": "^7.3.52", "@types/video.js": "^7.3.52",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^6.0.1",
"@vue-macros/reactivity-transform": "^1.1.3", "@vue-macros/reactivity-transform": "^1.1.6",
"@vue/eslint-config-typescript": "^14.1.3", "@vue/eslint-config-typescript": "^14.1.3",
"@vue/tsconfig": "^0.6.0", "@vue/tsconfig": "^0.6.0",
"chalk": "^5.2.0", "chalk": "^5.2.0",
...@@ -61,10 +61,10 @@ ...@@ -61,10 +61,10 @@
"eslint-plugin-vue": "^9.30.0", "eslint-plugin-vue": "^9.30.0",
"sass": "^1.58.3", "sass": "^1.58.3",
"typescript": "~5.6.3", "typescript": "~5.6.3",
"unplugin-auto-import": "^0.17.8", "unplugin-auto-import": "^20.0.0",
"vite": "^5.4.10", "vite": "^6.3.5",
"vite-plugin-checker": "^0.8.0", "vite-plugin-checker": "^0.10.2",
"vite-plugin-mkcert": "^1.17.6", "vite-plugin-mkcert": "^1.17.8",
"vue-tsc": "^2.1.10" "vue-tsc": "^2.1.10"
} }
} }
<script setup> <script setup>
import { useHead } from '@vueuse/head' import { useHead } from '@unhead/vue'
import { useAppConfig } from '@/composables/useAppConfig' import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig() const appConfig = useAppConfig()
......
...@@ -3,6 +3,7 @@ interface Props { ...@@ -3,6 +3,7 @@ interface Props {
title?: string title?: string
hasCardBackground?: boolean hasCardBackground?: boolean
hasBodyBackground?: boolean hasBodyBackground?: boolean
loading?: boolean
} }
withDefaults(defineProps<Props>(), { hasBodyBackground: true }) withDefaults(defineProps<Props>(), { hasBodyBackground: true })
</script> </script>
...@@ -17,7 +18,7 @@ withDefaults(defineProps<Props>(), { hasBodyBackground: true }) ...@@ -17,7 +18,7 @@ withDefaults(defineProps<Props>(), { hasBodyBackground: true })
</div> </div>
</slot> </slot>
</div> </div>
<div class="app-card-bd" :class="{ 'has-background': hasBodyBackground }"> <div class="app-card-bd" :class="{ 'has-background': hasBodyBackground }" v-loading="loading">
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
......
...@@ -80,7 +80,7 @@ function handleClick(path: string) { ...@@ -80,7 +80,7 @@ function handleClick(path: string) {
v-permission="item.tag" v-permission="item.tag"
v-if="item.children"> v-if="item.children">
<template #title> <template #title>
{{ item.name }} <router-link :to="item.path">{{ item.name }}</router-link>
</template> </template>
<el-menu-item <el-menu-item
:index="subitem.path" :index="subitem.path"
...@@ -88,11 +88,11 @@ function handleClick(path: string) { ...@@ -88,11 +88,11 @@ function handleClick(path: string) {
:key="subitem.path" :key="subitem.path"
v-permission="subitem.tag" v-permission="subitem.tag"
@click="handleClick(subitem.path)"> @click="handleClick(subitem.path)">
{{ subitem.name }} <router-link :to="subitem.path">{{ subitem.name }}</router-link>
</el-menu-item> </el-menu-item>
</el-sub-menu> </el-sub-menu>
<el-menu-item :index="item.path" v-permission="item.tag" @click="handleClick(item.path)" v-else> <el-menu-item :index="item.path" v-permission="item.tag" @click="handleClick(item.path)" v-else>
{{ item.name }} <router-link :to="item.path">{{ item.name }}</router-link>
</el-menu-item> </el-menu-item>
</template> </template>
</el-menu> </el-menu>
......
import { fetchEventSource } from '@fortaine/fetch-event-source'
interface AIMessage {
id: string
role: string
content: string
}
interface AIRequestOptions {
onUpdate?: (content: string, message?: AIMessage) => void
isReplace?: boolean
}
interface AIRequestData {
model?: string
prompt?: string
messages?: AIMessage[]
}
export function useAI() {
const messages = ref<AIMessage[]>([])
const isLoading = ref(false)
function post(data: AIRequestData, options: AIRequestOptions = {}) {
const { onUpdate, isReplace = true } = options
const { model = 'qwen-long', prompt, messages: messagesData, ...rest } = data
const params = { model, messages: messagesData || [{ role: 'user', content: prompt }], ...rest }
isLoading.value = true
return new Promise((resolve, reject) => {
let content = ''
fetchEventSource('/api/lab/v1/experiment/qwen/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
async onopen(response) {
if (response.ok) {
return
} else {
isLoading.value = false
reject(response)
}
},
onmessage(res) {
console.log(res.data)
if (res.data === '[DONE]') {
isLoading.value = false
resolve(content)
return
}
try {
const message = JSON.parse(res.data)
const id = message.id
const messageIndex = messages.value.findIndex((session) => session.id === id)
content += message?.choices[0]?.delta.content || ''
content = content.replace(/<think>[\s\S]*?<\/think>/g, '')
if (isReplace) {
content = content.replaceAll('\n', '<br/>')
}
if (messageIndex === -1) {
messages.value.push({ id, role: 'assistant', content })
} else {
messages.value[messageIndex].content = content
}
if (onUpdate) {
onUpdate(content, messages.value.at(-1))
}
} catch (error) {
console.log(error)
}
},
onerror(err) {
isLoading.value = false
reject(err)
},
})
})
}
return { messages, post, isLoading }
}
const appConfigList = [ const appConfigList = [
{
system: 'ai',
title: '人工智能应用实践教学平台',
logo: '/logo.svg',
hosts: ['saas-dml-web'],
dmlURL: import.meta.env.VITE_DML_PRO_URL,
},
{ {
system: 'dml', system: 'dml',
title: 'AI直播实践教学平台', title: '人工智能应用实践教学平台',
logo: '/logo.svg', logo: '/logo.svg',
hosts: ['saas-dml-web'], hosts: ['saas-dml-web'],
dmlURL: import.meta.env.VITE_SAAS_DML_LIVE_URL, dmlURL: import.meta.env.VITE_DML_PRO_URL,
}, },
{ {
system: 'default', system: 'default',
...@@ -45,6 +52,7 @@ const appConfigList = [ ...@@ -45,6 +52,7 @@ const appConfigList = [
], ],
}, },
], ],
// liveMonitor: true,
}, },
{ {
system: 'game', system: 'game',
......
import { useDevicesList, useUserMedia } from '@vueuse/core'
export const readBlobAsBase64 = (blob: Blob): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => {
try {
const base64Data = (reader.result as string).split(',')[2]
resolve(base64Data)
} catch (error) {
reject(error)
}
}
reader.onerror = () => {
reject(new Error('Failed to read file'))
}
reader.readAsDataURL(blob)
})
}
interface UseLiveProps {
enabledUserMedia?: boolean
onStart?: () => void
onRecord?: (data: Blob) => void
onStop?: (blob: Blob) => void
}
export function useLive({ enabledUserMedia = true, onStart, onRecord, onStop }: UseLiveProps) {
const startTime = ref(0)
const endTime = ref(0)
const duration = computed(() => (endTime.value - startTime.value) / 1000)
const currentTime = ref(0)
// 获取设备列表并设置默认设备
const { videoInputs: cameras, audioInputs: microphones } = useDevicesList({ requestPermissions: enabledUserMedia })
const currentCamera = computed(() => cameras.value[0]?.deviceId)
const currentMicrophone = computed(() => microphones.value[0]?.deviceId)
// 创建媒体流
const { stream, restart } = useUserMedia({
enabled: enabledUserMedia,
constraints: { video: { deviceId: currentCamera as any }, audio: { deviceId: currentMicrophone as any } },
})
// 录像设置
const recordedChunks = ref<Blob[]>([])
let mediaRecorder: MediaRecorder | null = null
let timeUpdateInterval: number | null = null
// 初始化MediaRecorder
const initializeMediaRecorder = () => {
if (!stream.value) return
mediaRecorder = new MediaRecorder(stream.value, { mimeType: 'video/webm' })
mediaRecorder.ondataavailable = handleDataAvailable
mediaRecorder.onstart = handleStart
mediaRecorder.onstop = handleStop
}
// 数据可用时处理
const handleDataAvailable = (event: BlobEvent) => {
if (event.data.size > 0) {
recordedChunks.value.push(event.data)
onRecord && onRecord(event.data)
}
}
// 录像开始时处理
const handleStart = () => {
startTime.value = Date.now()
onStart && onStart()
// Update currentTime every 100ms while recording
timeUpdateInterval = setInterval(() => {
currentTime.value = Math.floor((Date.now() - startTime.value) / 1000)
}, 1000 * 5)
}
// 录像停止时处理
const handleStop = () => {
endTime.value = Date.now()
const blob = new Blob(recordedChunks.value, { type: mediaRecorder?.mimeType })
onStop && onStop(blob)
recordedChunks.value = []
// Clear the interval when recording stops
if (timeUpdateInterval !== null) {
clearInterval(timeUpdateInterval)
timeUpdateInterval = null
}
}
// 开始录制
const start = () => {
if (!mediaRecorder) initializeMediaRecorder()
recordedChunks.value = []
mediaRecorder?.start(100) // 每100ms触发一次dataavailable事件
}
// 停止录制
const stop = () => {
if (mediaRecorder) mediaRecorder.stop()
mediaRecorder = null
}
// 重新开始
const handleRestart = async () => {
// 停止录制
stop()
// 重新获取流
await restart()
// 开始录制
start()
}
return { stream, start, stop, restart: handleRestart, startTime, endTime, duration, currentTime }
}
import { useUserStore } from '@/stores/user'
import { useLive, readBlobAsBase64 } from '@/composables/useLive'
import { useSocket } from '@/composables/useSocket'
import md5 from 'blueimp-md5'
import { ElMessageBox } from 'element-plus'
import { usePermission, useIntervalFn } from '@vueuse/core'
export function useLiveMonitor({ autoStart = false }: { autoStart?: boolean } = {}) {
const userStore = useUserStore()
const ssoId = userStore.user?.id
const fileUrl = ref('')
const fileName = computed(() => md5(`${ssoId}${startTime.value}`))
// WebSocket 设置
const { send } = useSocket({
onMessage: (ws, event) => {
try {
const data = JSON.parse(event.data)
if (data?.type === 'video_rtc') {
fileUrl.value = data.data.uri
}
} catch (error) {
console.error('Failed to parse message:', error)
}
},
})
const { startTime, start, stop, restart } = useLive({
enabledUserMedia: autoStart,
onRecord: async (blob) => {
const base64Data = await readBlobAsBase64(blob)
const jsonData = JSON.stringify({
type: 'send',
sso_id: ssoId,
data: { type: 'video_rtc', channel: 'rtc', data: { video: base64Data, file_name: fileName.value } },
})
send(jsonData)
},
})
const cameraPermission = usePermission('camera')
const hasMessageBox = ref(false)
const showMessageBox = () => {
if (hasMessageBox.value) return
hasMessageBox.value = true
ElMessageBox.alert('本次考试要求全程开启摄像头,请点击‘确定’允许摄像头访问,以便正常参加考试。', '温馨提示', {
confirmButtonText: '确定',
// beforeClose: (action, instance, done) => {
// console.log('stream', stream.value)
// console.log('cameraPermission', cameraPermission.value)
// if (stream.value && cameraPermission.value === 'granted') done()
// },
callback: () => {
hasMessageBox.value = false
restart()
},
})
}
useIntervalFn(() => {
if (cameraPermission.value === 'denied') showMessageBox()
}, 1000 * 10)
onMounted(() => {
if (autoStart) showMessageBox()
})
onUnmounted(() => {
stop()
})
return { fileUrl, fileName, start, stop }
}
import { useWebSocket, type UseWebSocketOptions } from '@vueuse/core'
import { useUserStore } from '@/stores/user'
export function useSocket(options: UseWebSocketOptions) {
const userStore = useUserStore()
const ssoId = userStore.user?.id
const defaultOptions = {
autoReconnect: true,
onConnected: () => {
send(JSON.stringify({ type: 'login', sso_id: ssoId }))
},
heartbeat: { message: JSON.stringify({ type: 'health', sso_id: ssoId }), interval: 1000 * 50, pongTimeout: 1000 },
}
const { status, data, send, open, close } = useWebSocket('wss://saas-lab-api.ezijing.com/wss', {
...defaultOptions,
...options,
})
return { status, data, send, open, close }
}
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import { createHead } from '@vueuse/head' import { createHead } from '@unhead/vue/client'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
......
import httpRequest from '@/utils/axios'
import type { CaseCreateItem } from './types'
// 获取实验列表
export function getExperimentList(params: { course_id?: string }) {
return httpRequest.get('/api/lab/v1/teacher/cases/experiments', { params })
}
// 获取案例列表
export function getCaseList(params?: {
name?: string
type?: string
experiment_name?: string
page?: number
page_size?: number
}) {
return httpRequest.get('/api/lab/v1/teacher/cases/list', { params })
}
// 获取案例详情
export function getCase(params: { id: string }) {
return httpRequest.get('/api/lab/v1/teacher/cases/view', { params })
}
// 创建案例
export function createCase(data: CaseCreateItem) {
return httpRequest.post('/api/lab/v1/teacher/cases/create', data)
}
// 更新案例
export function updateCase(data: CaseCreateItem) {
return httpRequest.post('/api/lab/v1/teacher/cases/update', data)
}
// 删除案例
export function deleteCase(params: { id: string }) {
return httpRequest.post('/api/lab/v1/teacher/cases/delete', params)
}
// 关联实验、删除实验
export function bindExperiment(params: { case_id: string; experiment_id: string; type: 'add' | 'delete' }) {
return httpRequest.post('/api/lab/v1/teacher/cases/experiments', params)
}
<script setup lang="ts">
import type { CaseStep } from '../types'
import { Edit, Delete, Plus } from '@element-plus/icons-vue'
import AppEditor from '@/components/base/AppEditor.vue'
import { dmlMenus } from '@/utils/dmlMenus'
// 步骤类型枚举
enum StepType {
ORIGIN = 1, // 案例原文
TASK = 2, // 任务
SUMMARY = 3, // 案例总结
}
interface Props {
modelValue: CaseStep[]
}
interface Emits {
(e: 'update:modelValue', value: CaseStep[]): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// 编辑相关状态
const editOriginDialogVisible = ref(false)
const editTaskDialogVisible = ref(false)
const editingStep = ref<CaseStep | null>(null)
// 操作步骤相关状态
const selectedStep = ref<number>(1)
const hasOperationSteps = ref<boolean>(false)
const operationSteps = ref<
Array<{ id: number; content: string; relatedFunction?: { id: number; name: string; path: string } }>
>([])
// 计算属性:确保始终有固定的首尾节点
const steps = computed({
get: () => {
const currentSteps = [...props.modelValue]
// 确保有案例原文节点
if (!currentSteps.find((s) => s.type === StepType.ORIGIN)) {
currentSteps.unshift({
id: 'origin',
type: StepType.ORIGIN,
name: '案例原文',
content: '',
})
}
// 确保有案例总结节点
if (!currentSteps.find((s) => s.type === StepType.SUMMARY)) {
currentSteps.push({
id: 'summary',
type: StepType.SUMMARY,
name: '案例总结',
})
}
return currentSteps
},
set: (value) => {
emit('update:modelValue', value)
},
})
// 计算属性:当前选中步骤的内容
const currentStepContent = computed({
get: () => {
const step = operationSteps.value.find((s) => s.id === selectedStep.value)
return step?.content || ''
},
set: (value: string) => {
const stepIndex = operationSteps.value.findIndex((s) => s.id === selectedStep.value)
if (stepIndex !== -1) {
operationSteps.value[stepIndex].content = value
}
},
})
// 计算属性:当前选中步骤的关联功能ID
const currentStepRelatedFunctionId = computed({
get: () => {
const step = operationSteps.value.find((s) => s.id === selectedStep.value)
return step?.relatedFunction?.id || null
},
set: (value: number | null) => {
const stepIndex = operationSteps.value.findIndex((s) => s.id === selectedStep.value)
if (stepIndex !== -1) {
if (value) {
// 根据ID查找对应的功能信息
const findFunction = (menus: any[]): any => {
for (const menu of menus) {
if (menu.id === value) {
return { id: menu.id, name: menu.name, path: menu.path }
}
if (menu.children) {
const found = findFunction(menu.children)
if (found) return found
}
}
return null
}
const functionInfo = findFunction(dmlMenus)
if (functionInfo) {
operationSteps.value[stepIndex].relatedFunction = functionInfo
}
} else {
delete operationSteps.value[stepIndex].relatedFunction
}
}
},
})
// 编辑步骤
function editStep(step: CaseStep) {
editingStep.value = { ...step }
if (step.type === StepType.ORIGIN) {
editOriginDialogVisible.value = true
} else if (step.type === StepType.TASK) {
// 恢复任务的操作步骤数据
if (step.config?.operationSteps) {
operationSteps.value = [...step.config.operationSteps]
hasOperationSteps.value = operationSteps.value.length > 0
selectedStep.value = operationSteps.value.length > 0 ? 1 : 1
} else {
// 重置操作步骤状态
operationSteps.value = []
hasOperationSteps.value = false
selectedStep.value = 1
}
editTaskDialogVisible.value = true
}
}
// 保存编辑
function saveEdit() {
if (editingStep.value) {
const index = steps.value.findIndex((s) => s.id === editingStep.value!.id)
if (index !== -1) {
const newSteps = [...steps.value]
const updatedStep = { ...editingStep.value }
// 如果是任务,保存操作步骤内容
if (editingStep.value.type === StepType.TASK && hasOperationSteps.value) {
updatedStep.config = {
operationSteps: operationSteps.value,
}
}
newSteps[index] = updatedStep
steps.value = newSteps
}
}
editOriginDialogVisible.value = false
editTaskDialogVisible.value = false
editingStep.value = null
}
// 添加任务
function addTask(afterIndex: number) {
const newTask: CaseStep = {
id: `task_${Date.now()}`,
type: StepType.TASK,
name: `任务${steps.value.filter((s) => s.type === StepType.TASK).length + 1}`,
}
const newSteps = [...steps.value]
newSteps.splice(afterIndex + 1, 0, newTask)
steps.value = newSteps
}
// 删除任务
function deleteTask(stepId: string) {
const index = steps.value.findIndex((s) => s.id === stepId)
if (index !== -1 && steps.value[index].type === StepType.TASK) {
const newSteps = [...steps.value]
newSteps.splice(index, 1)
steps.value = newSteps
}
}
// 添加操作步骤
function addOperationStep() {
if (!hasOperationSteps.value) {
hasOperationSteps.value = true
}
const newStepId = operationSteps.value.length + 1
operationSteps.value.push({
id: newStepId,
content: '',
relatedFunction: undefined,
})
selectedStep.value = newStepId
}
// 删除操作步骤
function deleteStep(stepId: number) {
// 删除指定步骤
const stepIndex = operationSteps.value.findIndex((s) => s.id === stepId)
if (stepIndex !== -1) {
operationSteps.value.splice(stepIndex, 1)
// 重新整理步骤编号
operationSteps.value.forEach((step, index) => {
step.id = index + 1
})
// 调整选中状态
if (operationSteps.value.length === 0) {
// 如果没有步骤了,重置状态
hasOperationSteps.value = false
selectedStep.value = 1
} else if (selectedStep.value >= stepId) {
selectedStep.value = Math.max(1, selectedStep.value - 1)
}
}
}
</script>
<template>
<div class="case-steps">
<div class="steps-container">
<template v-for="(step, index) in steps" :key="step.id">
<!-- 步骤节点 -->
<div
class="step-node"
:class="{
origin: step.type === StepType.ORIGIN,
task: step.type === StepType.TASK,
summary: step.type === StepType.SUMMARY,
}">
<span class="step-name">{{ step.name }}</span>
<!-- 遮罩层 -->
<div class="node-overlay">
<!-- 编辑按钮 -->
<el-button
v-if="step.type === StepType.ORIGIN || step.type === StepType.TASK"
size="small"
class="action-btn edit-btn"
type="primary"
circle
:icon="Edit"
@click="editStep(step)">
</el-button>
<!-- 删除按钮(仅任务节点) -->
<el-button
v-if="step.type === StepType.TASK"
size="small"
class="action-btn delete-btn"
@click="deleteTask(step.id)"
type="danger"
:icon="Delete"
circle>
</el-button>
</div>
</div>
<!-- 连接线和添加按钮 -->
<div v-if="index < steps.length - 1" class="step-line-container">
<div class="step-line"></div>
<el-button
v-if="step.type === StepType.TASK || step.type === StepType.ORIGIN"
class="add-task-btn"
size="small"
:icon="Plus"
@click="addTask(index)"></el-button>
</div>
</template>
</div>
</div>
<!-- 编辑案例原文弹窗 -->
<el-dialog v-model="editOriginDialogVisible" title="编辑案例原文" width="800px">
<el-form v-if="editingStep && editingStep.type === StepType.ORIGIN" :model="editingStep" label-position="top">
<el-form-item label="名称">
<el-input v-model="editingStep.name" />
</el-form-item>
<el-form-item label="内容">
<AppEditor v-model="editingStep.content" placeholder="请输入案例原文内容" hasAI />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editOriginDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveEdit">保存</el-button>
</template>
</el-dialog>
<!-- 编辑任务弹窗 -->
<el-dialog v-model="editTaskDialogVisible" title="编辑任务" width="800px">
<el-form v-if="editingStep && editingStep.type === StepType.TASK" :model="editingStep" label-position="top">
<el-form-item label="任务名称">
<el-input v-model="editingStep.name" placeholder="请输入任务名称" />
</el-form-item>
<el-form-item label="任务描述">
<AppEditor v-model="editingStep.content" placeholder="请输入任务描述" hasAI />
</el-form-item>
<!-- 操作步骤 -->
<el-form-item label="操作步骤">
<div class="steps-selector">
<div class="steps-row">
<div
v-for="step in operationSteps"
:key="step.id"
class="step-circle"
:class="{ active: selectedStep === step.id }"
@click="selectedStep = step.id">
{{ step.id }}
<el-button size="small" type="danger" class="delete-step-btn" circle @click.stop="deleteStep(step.id)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
<el-button type="primary" @click="addOperationStep" class="add-step-btn"> 添加操作步骤 </el-button>
</div>
</div>
</el-form-item>
<!-- 步骤内容编辑器 -->
<el-form-item v-if="hasOperationSteps && selectedStep" :label="`步骤${selectedStep}:`">
<el-tree-select
v-model="currentStepRelatedFunctionId"
placeholder="请选择关联功能"
:render-after-expand="false"
:data="dmlMenus"
node-key="id"
:props="{ label: 'name' }"
clearable
style="width: 100%; margin-bottom: 10px" />
<AppEditor v-model="currentStepContent" placeholder="在这里开始编辑您的富媒体内容..." hasAI />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editTaskDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveEdit">保存</el-button>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.case-steps {
margin: 20px 0;
width: 100%;
min-width: 0; /* 确保flex子元素可以收缩 */
.steps-container {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 10px;
overflow-x: auto;
overflow-y: hidden;
padding: 20px 0;
/* 自定义滚动条样式 */
&::-webkit-scrollbar {
height: 8px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
&:hover {
background: #a8a8a8;
}
}
}
.step-node {
position: relative;
width: 120px;
height: 120px;
border: 2px solid #dcdfe6;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
cursor: pointer;
transition: all 0.3s;
flex-shrink: 0; /* 防止圆形被压缩 */
&.origin {
border-color: #409eff;
background: #f0f6ff;
&:hover {
box-shadow: 0 0 10px rgba(64, 158, 255, 0.3);
.node-overlay {
opacity: 1;
visibility: visible;
}
}
}
&.task {
border-color: #e6a23c;
background: #fdf6ec;
&:hover {
box-shadow: 0 0 10px rgba(230, 162, 60, 0.3);
.node-overlay {
opacity: 1;
visibility: visible;
}
}
}
&.summary {
border-color: #67c23a;
background: #f6ffed;
&:hover {
box-shadow: 0 0 10px rgba(103, 194, 58, 0.3);
.node-overlay {
opacity: 1;
visibility: visible;
}
}
}
.step-name {
font-size: 14px;
font-weight: 500;
text-align: center;
line-height: 1.2;
z-index: 2;
position: relative;
}
.node-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 3;
}
.action-btn {
width: 32px;
height: 32px;
padding: 0;
font-size: 14px;
border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
&:hover {
transform: scale(1.1);
}
}
}
.step-line-container {
position: relative;
display: flex;
align-items: center;
cursor: pointer;
padding: 10px 0;
flex-shrink: 0; /* 防止连接线被压缩 */
.step-line {
width: 80px;
height: 2px;
background: var(--main-color);
position: relative;
transition: all 0.3s;
&::after {
content: '';
position: absolute;
right: -5px;
top: -3px;
width: 0;
height: 0;
border-left: 8px solid var(--main-color);
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
}
}
&:hover .step-line {
background: #ff7875;
box-shadow: 0 0 8px rgba(245, 108, 108, 0.4);
}
.add-task-btn {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 10;
opacity: 0;
visibility: hidden;
}
&:hover .add-task-btn {
opacity: 1;
visibility: visible;
}
}
}
/* 操作步骤样式 */
.no-steps {
display: flex;
justify-content: center;
padding: 20px 0;
}
.steps-selector {
.steps-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.step-circle {
position: relative;
width: 32px;
height: 32px;
border: 2px solid var(--main-color);
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #fef0f0;
.delete-step-btn {
opacity: 1;
visibility: visible;
}
}
&.active {
background: var(--main-color);
color: #fff;
border-color: var(--main-color);
box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.2);
}
.delete-step-btn {
position: absolute;
top: -8px;
right: -8px;
width: 16px;
height: 16px;
padding: 0;
font-size: 8px;
opacity: 0;
visibility: hidden;
transition: all 0.2s;
z-index: 10;
}
}
}
/* 步骤编辑器样式 */
.step-editor-header {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
</style>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { CaseItem, CaseFormData, CaseStep } from '../types'
import { useMapStore } from '@/stores/map'
import CaseSteps from './CaseSteps.vue'
import { pick } from 'lodash-es'
interface Props {
data?: CaseItem
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'submit', data: any): void
}>()
const router = useRouter()
// 实验类型
const types = useMapStore().getMapValuesByKey('experiment_type')
const formRef = $ref<FormInstance>()
const form = reactive<CaseFormData>({
name: '',
times: '',
type: '',
content: '',
})
// 步骤数据
const steps = ref<CaseStep[]>([{ id: 'task1', type: 2, name: '任务1' }])
watch(
() => props.data,
(value) => {
if (value) {
Object.assign(form, value)
try {
steps.value = JSON.parse(value.content)
} catch (error) {
steps.value = []
console.error(error)
}
}
}
)
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入案例名称', trigger: 'blur' }],
times: [{ required: true, message: '请输入案例课时数', trigger: 'blur' }],
type: [{ required: true, message: '请选择案例所属实验类型', trigger: 'change' }],
})
// 提交
function handleSubmit() {
formRef?.validate().then(() => {
const submitData = { ...pick(form, ['name', 'times', 'type']), content: JSON.stringify(steps.value) }
emit('submit', submitData)
})
}
</script>
<template>
<h4>基础信息</h4>
<el-form ref="formRef" :model="form" :rules="rules" inline>
<el-form-item label="案例名称" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="案例课时数" prop="times">
<el-input v-model="form.times"></el-input>
</el-form-item>
<el-form-item label="案例所属实验类型" prop="type">
<el-select v-model="form.type">
<el-option v-for="item in types" :key="item.id" v-bind="item"></el-option>
</el-select>
</el-form-item>
</el-form>
<el-divider />
<h4>案例步骤</h4>
<!-- 步骤流程图 -->
<CaseSteps v-model="steps" />
<el-row justify="center">
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存</el-button>
<el-button round auto-insert-space @click="router.back()">取消</el-button>
</el-row>
</template>
<script setup lang="ts">
import { getExperimentList } from '../api'
defineEmits<{
(e: 'bind', id: string): void
}>()
// 列表配置
const listOptions = computed(() => {
return {
remote: {
httpRequest: getExperimentList,
callback(res: any) {
return { list: res }
},
},
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '实验名称', prop: 'name' },
{ label: '操作', slots: 'table-x', width: 180 },
],
}
})
</script>
<template>
<el-dialog title="选择实验" width="600px">
<AppList v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button text type="primary"
><router-link :to="`/admin/lab/experiment/${row.id}`" target="_blank">查看</router-link></el-button
>
<el-button text type="primary" @click="$emit('bind', row.id)">关联</el-button>
</template>
</AppList>
</el-dialog>
</template>
<script setup lang="ts">
import type { ExperimentItem } from '../types'
import { CirclePlus } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue'
import { bindExperiment } from '../api'
import { ElMessage, ElMessageBox } from 'element-plus'
const SelectExperiment = defineAsyncComponent(() => import('./SelectExperiment.vue'))
const props = defineProps<{
id: string
experiments?: ExperimentItem[]
}>()
const emit = defineEmits<{
(e: 'update'): void
}>()
const appList = ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = computed(() => {
return {
data: props.experiments,
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '实验名称', prop: 'name' },
{ label: '实验类型', prop: 'type_name' },
{ label: '更新人', prop: 'updated_operator_name' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 180 },
],
}
})
const dialogVisible = ref(false)
const handleAdd = () => {
dialogVisible.value = true
}
const handleRemove = (row: ExperimentItem) => {
ElMessageBox.confirm('确定要删除吗?', '提示').then(() => {
bindExperiment({ case_id: props.id, experiment_id: row.id, type: 'delete' }).then(() => {
ElMessage({ message: '删除成功', type: 'success' })
emit('update')
})
})
}
const handleBind = (id: string) => {
bindExperiment({ case_id: props.id, experiment_id: id, type: 'add' }).then(() => {
ElMessage({ message: '关联成功', type: 'success' })
emit('update')
dialogVisible.value = false
})
}
</script>
<template>
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-button type="primary" :icon="CirclePlus" @click="handleAdd">关联实验</el-button>
</template>
<template #table-x="{ row }">
<el-button text type="primary"
><router-link :to="`/admin/lab/experiment/${row.id}`" target="_blank">查看实验</router-link></el-button
>
<el-button text type="danger" @click="handleRemove(row)" v-permission="'v1-backend-experiment-class-add'"
>删除</el-button
>
</template>
</AppList>
<SelectExperiment v-model="dialogVisible" @bind="handleBind" v-if="dialogVisible" />
</template>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/admin/lab',
redirect: '/admin/lab/example',
},
{
path: '/admin/lab/example',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/Index.vue') },
{ path: 'create', component: () => import('./views/Create.vue') },
{ path: ':id/edit', component: () => import('./views/Update.vue'), props: true },
{ path: ':id', component: () => import('./views/View.vue'), props: true },
],
},
]
export interface CaseItem {
created_operator_name: string
created_time: string
delete_time: string
experiment_id: string
id: string
name: string
content: string
status: string
status_name: string
type_name: string
type: string
times: string
updated_operator_name: string
updated_time: string
experiments?: ExperimentItem[]
}
export interface CaseFormData {
id?: string
name: string
type: string
times: string
content: string
steps?: CaseStep[]
}
export type CaseCreateItem = CaseFormData
export type CaseUpdateItem = CaseCreateItem & { id: string }
export interface ExperimentItem {
id: string
name: string
type: string
type_name: string
created_time: string
status: string
created_operator_name: string
updated_time: string
updated_operator: string
updated_operator_name: string
}
export interface CaseStep {
id: string
type: 1 | 2 | 3 // 1: 案例原文, 2: 任务, 3: 案例总结
name: string
content?: string
config?: any
}
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { createCase } from '../api'
import Form from '../components/Form.vue'
import type { CaseFormData } from '../types'
const router = useRouter()
const handleSubmit = async (data: CaseFormData) => {
await createCase(data)
ElMessage.success('创建成功')
router.back()
}
</script>
<template>
<AppCard title="新增案例">
<Form @submit="handleSubmit" />
</AppCard>
</template>
<script setup lang="ts">
import type { CaseItem } from '../types'
import { CirclePlus } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue'
import { useMapStore } from '@/stores/map'
import { getCaseList, deleteCase } from '../api'
import { ElMessage, ElMessageBox } from 'element-plus'
// 实验类型
const types = useMapStore().getMapValuesByKey('experiment_type')
const route = useRoute()
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getCaseList,
params: { name: '', experiment_id: route.query.experiment_id || '' },
},
filters: [
{ type: 'select', prop: 'type', label: '实验类型', options: types },
{ type: 'input', prop: 'name', label: '案例名称', placeholder: '请输入案例名称' },
{ type: 'input', prop: 'experiment_name', label: '实验名称', placeholder: '请输入实验名称' },
],
columns: [
{ label: '案例名称', prop: 'name' },
{ label: '实验类型', prop: 'type_name' },
{ label: '课时数', prop: 'times' },
{ label: '生效状态', prop: 'status_name' },
{ label: '更新人', prop: 'updated_operator_name' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 250, fixed: 'right' },
],
}
})
// 删除
function handleDelete(row: CaseItem) {
ElMessageBox.confirm('确定要删除吗?', '提示').then(() => {
deleteCase({ id: row.id }).then(() => {
ElMessage({ message: '删除成功', type: 'success' })
appList?.refetch()
})
})
}
</script>
<template>
<AppCard title="案例管理">
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<router-link to="/admin/lab/example/create"
><el-button type="primary" :icon="CirclePlus">新增案例</el-button></router-link
>
</template>
<template #table-x="{ row }">
<el-button type="primary" round><router-link :to="`/admin/lab/example/${row.id}`">查看</router-link></el-button>
<el-button type="primary" round
><router-link :to="`/admin/lab/example/${row.id}/edit`">编辑</router-link></el-button
>
<el-button type="danger" round @click="handleDelete(row)">删除</el-button>
</template>
</AppList>
</AppCard>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { updateCase, getCase } from '../api'
import Form from '../components/Form.vue'
import type { CaseItem, CaseFormData } from '../types'
const props = defineProps<{ id: string }>()
const router = useRouter()
const handleSubmit = async (data: CaseFormData) => {
await updateCase({ ...data, id: props.id })
ElMessage.success('创建成功')
router.back()
}
const detail = ref<CaseItem>()
const loading = ref(false)
function fetchInfo() {
if (!props.id) return
loading.value = true
getCase({ id: props.id })
.then((res) => {
detail.value = res.data
})
.finally(() => {
loading.value = false
})
}
onMounted(() => {
fetchInfo()
})
</script>
<template>
<AppCard title="编辑案例" :loading="loading">
<Form @submit="handleSubmit" :data="detail" />
</AppCard>
</template>
<script setup lang="ts">
import type { CaseItem } from '../types'
import ViewExperiment from '../components/ViewExperiment.vue'
import { getCase } from '../api'
interface Props {
id: string
}
const props = defineProps<Props>()
const detail = ref<CaseItem | null>(null)
const loading = ref(false)
function fetchInfo() {
if (!props.id) return
loading.value = true
getCase({ id: props.id })
.then((res) => {
detail.value = res.data
})
.finally(() => {
loading.value = false
})
}
onMounted(() => {
fetchInfo()
})
</script>
<template>
<AppCard title="查看案例" :loading="loading">
<template v-if="detail">
<el-descriptions>
<el-descriptions-item label="案例名称:">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="实验类型:">{{ detail.type_name }}</el-descriptions-item>
<el-descriptions-item label="课时数:">{{ detail.times }}</el-descriptions-item>
<el-descriptions-item label="有效状态:">{{ detail.status_name }}</el-descriptions-item>
<el-descriptions-item label="创建人:">{{ detail.created_operator_name }}</el-descriptions-item>
<el-descriptions-item label="创建时间:">{{ detail.created_time }}</el-descriptions-item>
</el-descriptions>
</template>
<el-divider />
<ViewExperiment :id="id" :experiments="detail?.experiments" @update="fetchInfo" />
</AppCard>
</template>
...@@ -5,6 +5,7 @@ import { ElMessage } from 'element-plus' ...@@ -5,6 +5,7 @@ import { ElMessage } from 'element-plus'
import { getTripConfig, updateTripConfig, getLiveCommodity } from '../api' import { getTripConfig, updateTripConfig, getLiveCommodity } from '../api'
import { useConnection, useUserAttr, useMetaEvent, useTag, useGroup, useMaterial } from '../composables/useAllData' import { useConnection, useUserAttr, useMetaEvent, useTag, useGroup, useMaterial } from '../composables/useAllData'
import { useDocumentVisibility } from '@vueuse/core' import { useDocumentVisibility } from '@vueuse/core'
import { dmlMenus } from '@/utils/dmlMenus'
import { useAppConfig } from '@/composables/useAppConfig' import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig() const appConfig = useAppConfig()
...@@ -29,94 +30,6 @@ const dmlURL = computed(() => { ...@@ -29,94 +30,6 @@ const dmlURL = computed(() => {
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/trip/template?experiment_id=${props.data.id}` return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/trip/template?experiment_id=${props.data.id}`
}) })
const experimentConfig: any = [
{
id: 1,
name: '基础配置',
is_checked: false,
pid: 0,
children: [
{ id: 2, name: '连接管理', is_checked: false, pid: 1, children: [] },
{ id: 3, name: '用户属性管理', is_checked: false, pid: 1, children: [] },
{ id: 4, name: '事件属性管理', is_checked: false, pid: 1, children: [] },
],
},
{
id: 5,
name: '营销策划',
is_checked: false,
pid: 0,
children: [],
},
{
id: 6,
name: '用户画像',
is_checked: false,
pid: 0,
children: [],
},
{
id: 7,
name: '用户识别',
is_checked: false,
pid: 0,
children: [
{ id: 8, name: '标签管理', is_checked: false, pid: 7, children: [] },
{ id: 9, name: '群组管理', is_checked: false, pid: 7, children: [] },
{ id: 71, name: '运营策略管理', is_checked: false, pid: 7, children: [] },
],
},
{
id: 10,
name: '营销内容设计',
is_checked: false,
pid: 0,
children: [
{ id: 11, name: '文本资料管理', is_checked: false, pid: 10, children: [] },
{ id: 12, name: '图片资料管理', is_checked: false, pid: 10, children: [] },
{ id: 13, name: '卡券资料管理', is_checked: false, pid: 10, children: [] },
{ id: 14, name: '视频资料管理', is_checked: false, pid: 10, children: [] },
{ id: 15, name: 'H5资料管理', is_checked: false, pid: 10, children: [] },
{ id: 16, name: '二维码资料管理', is_checked: false, pid: 10, children: [] },
{ id: 17, name: '语言资料管理', is_checked: false, pid: 10, children: [] },
{ id: 18, name: '小程序资料管理', is_checked: false, pid: 10, children: [] },
],
},
{
id: 19,
name: '自动化营销',
is_checked: false,
pid: 0,
children: [],
},
{
id: 20,
name: '直播带货',
is_checked: false,
pid: 0,
children: [
{ id: 21, name: '商品品类管理', is_checked: false, pid: 20, children: [] },
{ id: 22, name: '商品属性管理', is_checked: false, pid: 20, children: [] },
{ id: 23, name: '商品管理', is_checked: false, pid: 20, children: [] },
{ id: 24, name: '直播练习', is_checked: false, pid: 20, children: [] },
{ id: 25, name: '直播话术管理', is_checked: false, pid: 20, children: [] },
{ id: 201, name: '订单管理', is_checked: false, pid: 20, children: [] },
],
},
{
id: 26,
name: '数据分析',
is_checked: false,
pid: 0,
children: [
{ id: 27, name: '用户分析', is_checked: false, pid: 26, children: [] },
{ id: 28, name: '标签群组分析', is_checked: false, pid: 26, children: [] },
{ id: 29, name: '事件分析', is_checked: false, pid: 26, children: [] },
{ id: 30, name: '营销分析', is_checked: false, pid: 26, children: [] },
],
},
]
const formRef = $ref<FormInstance>() const formRef = $ref<FormInstance>()
const form = reactive({ const form = reactive({
experiment_id: props.data.id, experiment_id: props.data.id,
...@@ -132,7 +45,7 @@ const form = reactive({ ...@@ -132,7 +45,7 @@ const form = reactive({
tag_ids: [], tag_ids: [],
group_ids: [], group_ids: [],
material_ids: [], material_ids: [],
auth_config: experimentConfig, auth_config: dmlMenus,
is_use_common_live_commodities: 0, is_use_common_live_commodities: 0,
live_commodity_ids: [], live_commodity_ids: [],
}) })
...@@ -190,9 +103,9 @@ function fetchInfo() { ...@@ -190,9 +103,9 @@ function fetchInfo() {
} }
// Ensure auth_config structure is maintained // Ensure auth_config structure is maintained
let authConfig = experimentConfig let authConfig = dmlMenus
if (data.auth_config && data.auth_config.length > 0) { if (data.auth_config && data.auth_config.length > 0) {
authConfig = mergeConfig(experimentConfig, data.auth_config) authConfig = mergeConfig(dmlMenus, data.auth_config)
} }
Object.assign(form, { Object.assign(form, {
......
...@@ -93,6 +93,13 @@ const dmlURL = computed(() => { ...@@ -93,6 +93,13 @@ const dmlURL = computed(() => {
const liveURL = computed(() => { const liveURL = computed(() => {
return `${import.meta.env.VITE_SAAS_DML_LIVE_URL}?experiment_id=${props.id}` return `${import.meta.env.VITE_SAAS_DML_LIVE_URL}?experiment_id=${props.id}`
}) })
const biURL = computed(() => {
return `${import.meta.env.VITE_SAAS_BI_URL}?experiment_id=${props.id}`
})
const aiURL = computed(() => {
return `${import.meta.env.VITE_SAAS_AI_URL}?experiment_id=${props.id}`
})
// 复制 // 复制
let copyDialogVisible = $ref(false) let copyDialogVisible = $ref(false)
async function handleCopy() { async function handleCopy() {
...@@ -123,6 +130,12 @@ function handleUpdateGradeRules() { ...@@ -123,6 +130,12 @@ function handleUpdateGradeRules() {
<el-button type="primary" v-if="false"> <el-button type="primary" v-if="false">
<a :href="liveURL" target="_blank">进入直播平台</a> <a :href="liveURL" target="_blank">进入直播平台</a>
</el-button> </el-button>
<el-button type="primary" v-if="detail?.type === '5'">
<a :href="biURL" target="_blank">进入BI实验平台</a>
</el-button>
<el-button type="primary" v-if="detail?.type === '6'">
<a :href="aiURL" target="_blank">进入AI实验平台</a>
</el-button>
<el-button type="primary" @click="gradeRulesVisible = true">查看成绩规则</el-button> <el-button type="primary" @click="gradeRulesVisible = true">查看成绩规则</el-button>
<el-button type="primary" @click="reportRulesVisible = true">查看报告规则</el-button> <el-button type="primary" @click="reportRulesVisible = true">查看报告规则</el-button>
<el-button type="primary" @click="handleCopy()">复制实验</el-button> <el-button type="primary" @click="handleCopy()">复制实验</el-button>
......
...@@ -61,6 +61,12 @@ const learnURL = import.meta.env.VITE_SAAS_LEARN_URL ...@@ -61,6 +61,12 @@ const learnURL = import.meta.env.VITE_SAAS_LEARN_URL
background-size: contain; background-size: contain;
} }
} }
.system-ai {
.bg {
background: url(@/assets/images/ai_home_student_bg.png) no-repeat center center;
background-size: contain;
}
}
.system-swsjfxs { .system-swsjfxs {
.bg { .bg {
background: url(@/assets/images/swsjfxs_home_student_bg.png) no-repeat center center; background: url(@/assets/images/swsjfxs_home_student_bg.png) no-repeat center center;
......
...@@ -5,7 +5,7 @@ import { getExperimentList } from '../api' ...@@ -5,7 +5,7 @@ import { getExperimentList } from '../api'
const router = useRouter() const router = useRouter()
let list = $ref<ExperimentItem[]>([]) let list = $ref<ExperimentItem[]>([])
function fetchList() { function fetchList() {
getExperimentList().then(res => { getExperimentList().then((res) => {
list = res.data.list list = res.data.list
}) })
} }
...@@ -49,6 +49,12 @@ function handleChange(id: string, type: number) { ...@@ -49,6 +49,12 @@ function handleChange(id: string, type: number) {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.system-ai {
.bg {
background: url(@/assets/images/ai_home_teacher_bg.png) no-repeat center center;
background-size: contain;
}
}
.system-dml { .system-dml {
.bg { .bg {
background: url(@/assets/images/dml_home_teacher_bg.png) no-repeat center center; background: url(@/assets/images/dml_home_teacher_bg.png) no-repeat center center;
......
...@@ -140,3 +140,13 @@ export function getExperimentExamList(params: { experiment_id: string }) { ...@@ -140,3 +140,13 @@ export function getExperimentExamList(params: { experiment_id: string }) {
export function getExperimentScoreDetail(params: { experiment_id: string; type: string }) { export function getExperimentScoreDetail(params: { experiment_id: string; type: string }) {
return httpRequest.get('/api/lab/v1/student/experiment-question/score-detail', { params }) return httpRequest.get('/api/lab/v1/student/experiment-question/score-detail', { params })
} }
// 获取实验案例详情(包含步骤数据)
export function getExperimentCaseDetail(params: { experiment_id: string }) {
return httpRequest.get('/api/lab/v1/student/experiment-cases2/detail', { params })
}
// 获取实验案例示例数据
export function getExperimentExample(params: { experiment_id: string }) {
return httpRequest.get('/api/lab/v1/student/experiment-cases2/detail', { params })
}
<script setup lang="ts">
import { useExample } from '../composables/useExample'
interface Props {
experiment_id: string
}
const props = defineProps<Props>()
const emits = defineEmits(['empty', 'goToFunction'])
const experimentId = computed(() => props.experiment_id)
const { example, tasks, currentTask, operationSteps, selectedTask, selectedStep, currentStep, switchTask, switchStep } =
useExample(experimentId, emits)
const goToFunction = (path: string) => {
emits('goToFunction', path)
}
</script>
<template>
<div class="example-container">
<template v-if="example">
<!-- 头部信息 -->
<div class="header">
<div class="title">{{ example.name }}</div>
<div class="times">{{ example.times }}学时</div>
</div>
<!-- 案例原文 -->
<div class="section">
<div class="section-title">案例原文</div>
<div class="content-area">
<div class="content-text" v-html="example.content.find((item) => item.type === 1)?.content || ''"></div>
</div>
</div>
<!-- 任务选择 -->
<div class="section" v-if="tasks.length > 0">
<div class="task-buttons">
<button
v-for="task in tasks"
:key="task.id"
class="task-button"
:class="{ active: selectedTask === task.id }"
@click="switchTask(task.id)">
{{ task.name }}
</button>
</div>
</div>
<!-- 任务描述 -->
<div class="section" v-if="currentTask">
<div class="section-title">任务描述</div>
<div class="content-area">
<div class="content-text" v-html="currentTask?.content || ''"></div>
</div>
</div>
<!-- 操作步骤 -->
<div class="section" v-if="operationSteps.length > 0">
<div class="section-title">操作步骤:</div>
<div class="steps-container">
<button
v-for="(step, index) in operationSteps"
:key="step.id"
class="step-button"
:class="{ active: selectedStep === step.id }"
@click="switchStep(step.id)">
{{ index + 1 }}
</button>
</div>
<div class="content-area">
<div class="content-text" v-html="currentStep?.content"></div>
</div>
<!-- 快速进入按钮 -->
<div class="quick-access" v-if="currentStep?.relatedFunction">
<el-button plain type="primary" @click="goToFunction(currentStep.relatedFunction.path)">快速进入</el-button>
</div>
</div>
</template>
<el-empty description="暂无数据" v-else />
</div>
</template>
<style lang="scss" scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 1px solid #e8e8e8;
.title {
font-size: 20px;
font-weight: 600;
color: #333;
}
.times {
font-size: 16px;
color: #666;
background: #f5f5f5;
padding: 4px 12px;
border-radius: 4px;
}
}
.section {
margin-bottom: 30px;
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
}
}
.content-area {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 6px;
min-height: 100px;
max-height: 300px;
overflow-y: auto;
padding: 15px;
.content-text {
line-height: 1.6;
color: #333;
:deep(p) {
margin-bottom: 10px;
}
}
/* 自定义滚动条 */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
}
.task-buttons {
display: flex;
gap: 15px;
margin-bottom: 20px;
.task-button {
padding: 10px 20px;
border: 2px solid var(--main-color);
border-radius: 20px;
background: #fff;
color: var(--main-color);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #f8f8f8;
}
&.active {
background: var(--main-color);
color: #fff;
}
}
}
.steps-container {
display: flex;
gap: 10px;
margin-bottom: 20px;
.step-button {
width: 40px;
height: 40px;
border: 2px solid var(--main-color);
border-radius: 50%;
background: #fff;
color: var(--main-color);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background: #f8f8f8;
}
&.active {
background: var(--main-color);
color: #fff;
}
}
}
.quick-access {
margin-top: 20px;
text-align: center;
}
</style>
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { useLive, readBlobAsBase64 } from '@/composables/useLive'
import { useSocket } from '@/composables/useSocket'
import md5 from 'blueimp-md5'
const userStore = useUserStore()
const ssoId = userStore.user?.id
const fileUrl = ref('')
const fileName = computed(() => md5(`${ssoId}${startTime.value}`))
// WebSocket 设置
const { send } = useSocket({
onMessage: (ws, event) => {
try {
const data = JSON.parse(event.data)
if (data?.type === 'video_rtc') {
fileUrl.value = data.data.uri
}
} catch (error) {
console.error('Failed to parse message:', error)
}
},
})
const { startTime, start, stop } = useLive({
onRecord: async (blob) => {
console.log('onRecord', blob)
const base64Data = await readBlobAsBase64(blob)
const jsonData = JSON.stringify({
type: 'send',
sso_id: ssoId,
data: { type: 'video_rtc', channel: 'rtc', data: { video: base64Data, file_name: fileName.value } },
})
send(jsonData)
},
})
onMounted(() => {
start()
})
onUnmounted(() => {
stop()
})
</script>
<template>
<div class="live"></div>
</template>
import type { Ref } from 'vue'
import { getExperimentExample } from '../api'
// 定义案例步骤类型
interface CaseStep {
id: string
type: number
name: string
content?: string
config?: {
operationSteps?: Array<{
id: number
content: string
relatedFunction?: {
id: number
name: string
path: string
}
}>
}
}
// 定义案例详情类型
interface CaseDetail {
id: string
name: string
times: string
type: string
status: string
created_operator: string
created_time: string
updated_operator: string
updated_time: string
content: CaseStep[]
delete_time: string
status_name: string
created_operator_name: string
updated_operator_name: string
type_name: string
}
export function useExample(experimentId: Ref<string>, emits: any) {
const example = ref<CaseDetail>()
const selectedTask = ref('task1')
const selectedStep = ref(1)
const tasks = computed(() => example.value?.content.filter((item) => item.type === 2) || [])
const currentTask = computed(() => {
return example.value?.content.find((item) => item.id === selectedTask.value)
})
const operationSteps = computed(() => {
return currentTask.value?.config?.operationSteps || []
})
const currentStep = computed(() => {
return operationSteps.value.find((item) => item.id === selectedStep.value)
})
const fetchExample = async () => {
const res = await getExperimentExample({ experiment_id: experimentId.value })
if (res.data.detail) {
// 解析案例内容
try {
const content = JSON.parse(res.data.detail.content || '[]')
example.value = { ...res.data.detail, content: content }
selectedTask.value = tasks.value[0].id
selectedStep.value = operationSteps.value[0].id
} catch (error) {
console.error('获取案例数据失败:', error)
}
} else {
example.value = undefined
emits('empty')
}
}
watchEffect(() => {
if (experimentId.value) {
fetchExample()
}
})
// 切换任务
const switchTask = (taskId: string) => {
selectedTask.value = taskId
selectedStep.value = 1
}
// 切换操作步骤
const switchStep = (stepId: number) => {
selectedStep.value = stepId
}
return {
example,
tasks,
currentTask,
operationSteps,
selectedTask,
selectedStep,
currentStep,
switchTask,
switchStep,
}
}
...@@ -11,7 +11,10 @@ import { saveAs } from 'file-saver' ...@@ -11,7 +11,10 @@ import { saveAs } from 'file-saver'
import html2pdf from 'html2pdf.js' import html2pdf from 'html2pdf.js'
import { useCookies } from '@vueuse/integrations/useCookies' import { useCookies } from '@vueuse/integrations/useCookies'
import { useAppConfig } from '@/composables/useAppConfig' import { useAppConfig } from '@/composables/useAppConfig'
import { useLiveMonitor } from '@/composables/useLiveMonitor'
const appConfig = useAppConfig() const appConfig = useAppConfig()
useLiveMonitor({ autoStart: !!appConfig.liveMonitor })
const Question = defineAsyncComponent(() => import('../components/Question.vue')) const Question = defineAsyncComponent(() => import('../components/Question.vue'))
const Info = defineAsyncComponent(() => import('../components/Info.vue')) const Info = defineAsyncComponent(() => import('../components/Info.vue'))
...@@ -26,6 +29,7 @@ const PrepareDialog = defineAsyncComponent(() => import('../components/PrepareDi ...@@ -26,6 +29,7 @@ const PrepareDialog = defineAsyncComponent(() => import('../components/PrepareDi
const ResultDialog = defineAsyncComponent(() => import('../components/ResultDialog.vue')) const ResultDialog = defineAsyncComponent(() => import('../components/ResultDialog.vue'))
const ReportPreview = defineAsyncComponent(() => import('../components/ReportPreview.vue')) const ReportPreview = defineAsyncComponent(() => import('../components/ReportPreview.vue'))
const Exam = defineAsyncComponent(() => import('../components/Exam.vue')) const Exam = defineAsyncComponent(() => import('../components/Exam.vue'))
const Example = defineAsyncComponent(() => import('../components/Example.vue'))
const route = useRoute() const route = useRoute()
...@@ -103,13 +107,15 @@ const examURL = ref('') ...@@ -103,13 +107,15 @@ const examURL = ref('')
// 右侧 // 右侧
const cookies = useCookies(['TGC']) const cookies = useCookies(['TGC'])
const LAB_URL: any = computed(() => { const LAB_URL = computed<string>(() => {
if (tabActive.value === 'exam' && examURL.value && experimentInfo?.exam_status === 1) return examURL.value if (tabActive.value === 'exam' && examURL.value && experimentInfo?.exam_status === 1) return examURL.value
if (experimentInfo?.type === 4) if (experimentInfo?.type === 4)
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}?experiment_id=${form.experiment_id}` return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}${functionPath.value}?experiment_id=${
if (experimentInfo?.type === 5) form.experiment_id
return `${appConfig.dmlURL || import.meta.env.VITE_SAAS_BI_URL}?experiment_id=${form.experiment_id}` }`
return `${appConfig.dmlURL || import.meta.env.VITE_LAB_URL}&token=${cookies.get('TGC')}` if (experimentInfo?.type === 5) return `${import.meta.env.VITE_SAAS_BI_URL}?experiment_id=${form.experiment_id}`
if (experimentInfo?.type === 6) return `${import.meta.env.VITE_SAAS_AI_URL}?experiment_id=${form.experiment_id}`
return `${import.meta.env.VITE_LAB_URL}&token=${cookies.get('TGC')}`
}) })
let iframeKey = $ref(Date.now()) let iframeKey = $ref(Date.now())
...@@ -246,6 +252,10 @@ const empty = ref<string[]>([]) ...@@ -246,6 +252,10 @@ const empty = ref<string[]>([])
function handleEmpty(name: string) { function handleEmpty(name: string) {
empty.value.push(name) empty.value.push(name)
} }
const functionPath = ref('')
function handleGoToFunction(path: string) {
functionPath.value = path
}
</script> </script>
<template> <template>
...@@ -268,6 +278,12 @@ function handleEmpty(name: string) { ...@@ -268,6 +278,12 @@ function handleEmpty(name: string) {
<el-tab-pane label="实验信息" name="info"> <el-tab-pane label="实验信息" name="info">
<Info :data="experimentInfo"></Info> <Info :data="experimentInfo"></Info>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="实验案例" name="example" v-if="!empty.includes('example')">
<Example
:experiment_id="form.experiment_id"
@empty="handleEmpty('example')"
@goToFunction="handleGoToFunction"></Example>
</el-tab-pane>
<el-tab-pane label="案例原文" name="case" v-if="!empty.includes('case')"> <el-tab-pane label="案例原文" name="case" v-if="!empty.includes('case')">
<Case <Case
:course_id="form.course_id" :course_id="form.course_id"
...@@ -311,8 +327,8 @@ function handleEmpty(name: string) { ...@@ -311,8 +327,8 @@ function handleEmpty(name: string) {
<template #right> <template #right>
<AppCard> <AppCard>
<div class="exam-status" v-if="appConfig.system === 'x' && experimentInfo?.exam_status === 1"> <div class="exam-status" v-if="appConfig.system === 'x' && experimentInfo?.exam_status === 1">
<el-button type="primary" @click="tabActive = 'qa'">实操系统</el-button>
<el-button type="primary" @click="tabActive = 'exam'">理论试题</el-button> <el-button type="primary" @click="tabActive = 'exam'">理论试题</el-button>
<el-button type="primary" @click="tabActive = 'qa'">实操环境</el-button>
</div> </div>
<el-row justify="space-between" v-else> <el-row justify="space-between" v-else>
<div> <div>
......
...@@ -3,7 +3,7 @@ import { useUserStore } from '@/stores/user' ...@@ -3,7 +3,7 @@ import { useUserStore } from '@/stores/user'
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: [{ path: '/:pathMatch(.*)*', redirect: '/' }] routes: [{ path: '/:pathMatch(.*)*', redirect: '/' }],
}) })
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
...@@ -18,7 +18,13 @@ router.beforeEach(async (to, from, next) => { ...@@ -18,7 +18,13 @@ router.beforeEach(async (to, from, next) => {
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
user.isLogin ? next() : next('/401') if (!user.isLogin) {
next('/401')
return
}
}
if (to.path === '/' && user.role?.id === 1) {
next('/student/lab')
return return
} }
next() next()
......
...@@ -21,22 +21,21 @@ interface IValuesList { ...@@ -21,22 +21,21 @@ interface IValuesList {
value: string value: string
} }
export const useMapStore = defineStore({ export const useMapStore = defineStore('map', {
id: 'map',
state: (): State => { state: (): State => {
return { return {
mapList: [] mapList: [],
} }
}, },
getters: { getters: {
getMapValuesByKey: state => { getMapValuesByKey: (state) => {
return (key: string) => state.mapList.find(map => map.key === key)?.values || [] return (key: string) => state.mapList.find((map) => map.key === key)?.values || []
} },
}, },
actions: { actions: {
async getMapList() { async getMapList() {
const res = await getMapList() const res = await getMapList()
this.mapList = res.data || [] this.mapList = res.data || []
} },
} },
}) })
...@@ -14,7 +14,7 @@ const studentMenus: IMenuItem[] = [ ...@@ -14,7 +14,7 @@ const studentMenus: IMenuItem[] = [
{ name: '我的实验', path: '/student/lab' }, { name: '我的实验', path: '/student/lab' },
{ name: '理论学习', path: import.meta.env.VITE_SAAS_LEARN_URL }, { name: '理论学习', path: import.meta.env.VITE_SAAS_LEARN_URL },
// { name: '我的大赛', path: '/student/contest' }, // { name: '我的大赛', path: '/student/contest' },
// { name: '大赛成绩查询', path: '/student/contest/score' } // { name: '大赛成绩查询', path: '/student/contest/score' },
] ]
// 管理员菜单 // 管理员菜单
const adminMenus: IMenuItem[] = [ const adminMenus: IMenuItem[] = [
...@@ -30,36 +30,36 @@ const adminMenus: IMenuItem[] = [ ...@@ -30,36 +30,36 @@ const adminMenus: IMenuItem[] = [
{ name: '实验操作视频管理', path: '/admin/lab/video', tag: 'v1-teacher-video' }, { name: '实验操作视频管理', path: '/admin/lab/video', tag: 'v1-teacher-video' },
{ name: '实验讨论交流', path: '/admin/lab/discuss', tag: 'v1-teacher-discussion' }, { name: '实验讨论交流', path: '/admin/lab/discuss', tag: 'v1-teacher-discussion' },
{ name: '实验成绩管理', path: '/admin/lab/score', tag: 'v1-teacher-record' }, { name: '实验成绩管理', path: '/admin/lab/score', tag: 'v1-teacher-record' },
{ name: '实验监控', path: '/admin/lab/dashboard' } { name: '实验监控', path: '/admin/lab/dashboard' },
] { name: '案例管理', path: '/admin/lab/example' },
],
}, },
{ // {
name: '技能大赛', // name: '技能大赛',
path: '/admin/contest', // path: '/admin/contest',
children: [ // children: [
{ name: '赛项管理', path: '/admin/contest/items', tag: 'competition' }, // { name: '赛项管理', path: '/admin/contest/items', tag: 'competition' },
{ name: '参赛选手管理', path: '/admin/contest/contestants', tag: 'competition-competitor' }, // { name: '参赛选手管理', path: '/admin/contest/contestants', tag: 'competition-competitor' },
{ name: '评分专家管理', path: '/admin/contest/experts', tag: 'expert' }, // { name: '评分专家管理', path: '/admin/contest/experts', tag: 'expert' },
{ name: '大赛训练答疑', path: '/admin/contest/discuss', tag: 'v1-teacher-train-discussion' }, // { name: '大赛训练答疑', path: '/admin/contest/discuss', tag: 'v1-teacher-train-discussion' },
{ name: '大赛监控', path: '/admin/contest/dashboard', tag: 'v1-expert-statistic' }, // { name: '大赛监控', path: '/admin/contest/dashboard', tag: 'v1-expert-statistic' },
{ name: '大赛评分', path: '/admin/contest/check', tag: 'v1-expert-check' }, // { name: '大赛评分', path: '/admin/contest/check', tag: 'v1-expert-check' },
{ name: '大赛发布成绩', path: '/admin/contest/score', tag: 'v1-expert-score' }, // { name: '大赛发布成绩', path: '/admin/contest/score', tag: 'v1-expert-score' },
{ name: '客户端日志', path: '/admin/contest/log', tag: '' } // { name: '客户端日志', path: '/admin/contest/log', tag: '' },
] // ],
}, // },
{ // {
name: '成绩分析', // name: '成绩分析',
path: '/admin/contest/analyze', // path: '/admin/contest/analyze',
children: [ // children: [
{ name: '赛项成绩画像', path: '/admin/contest/analyze/score' }, // { name: '赛项成绩画像', path: '/admin/contest/analyze/score' },
{ name: '学生个人成绩画像', path: '/admin/contest/analyze/student' } // { name: '学生个人成绩画像', path: '/admin/contest/analyze/student' },
] // ],
} // },
] ]
const appConfig = useAppConfig() const appConfig = useAppConfig()
export const useMenuStore = defineStore({ export const useMenuStore = defineStore('menu', {
id: 'menu',
state: (): State => ({ studentMenus, adminMenus }), state: (): State => ({ studentMenus, adminMenus }),
getters: { getters: {
menus: (state): IMenuItem[] => { menus: (state): IMenuItem[] => {
...@@ -69,6 +69,6 @@ export const useMenuStore = defineStore({ ...@@ -69,6 +69,6 @@ export const useMenuStore = defineStore({
} else { } else {
return appConfig.adminMenus || state.adminMenus return appConfig.adminMenus || state.adminMenus
} }
} },
} },
}) })
...@@ -17,18 +17,17 @@ interface State { ...@@ -17,18 +17,17 @@ interface State {
permissions: PermissionType[] permissions: PermissionType[]
} }
export const useUserStore = defineStore({ export const useUserStore = defineStore('user', {
id: 'user',
state: (): State => ({ state: (): State => ({
user: null, user: null,
role: null, role: null,
organization: null, organization: null,
project: null, project: null,
roles: [], roles: [],
permissions: [] permissions: [],
}), }),
getters: { getters: {
isLogin: state => !!state.user isLogin: (state) => !!state.user,
}, },
actions: { actions: {
async getUser() { async getUser() {
...@@ -46,6 +45,6 @@ export const useUserStore = defineStore({ ...@@ -46,6 +45,6 @@ export const useUserStore = defineStore({
async logout() { async logout() {
await logout() await logout()
this.user = null this.user = null
} },
} },
}) })
export interface DmlMenu {
id: number
name: string
is_checked: boolean
pid: number
children?: DmlMenu[]
path?: string
}
export const dmlMenus: DmlMenu[] = [
{
id: 1,
name: '基础配置',
is_checked: false,
pid: 0,
path: '/connect',
children: [
{ id: 2, name: '连接管理', is_checked: false, pid: 1, path: '/connect', children: [] },
{ id: 3, name: '用户属性管理', is_checked: false, pid: 1, path: '/metadata/user', children: [] },
{ id: 4, name: '事件属性管理', is_checked: false, pid: 1, path: '/metadata/event', children: [] },
],
},
{
id: 5,
name: '营销策划',
is_checked: false,
pid: 0,
path: '/market/my',
children: [],
},
{
id: 6,
name: '用户画像',
is_checked: false,
pid: 0,
path: '/user',
children: [],
},
{
id: 7,
name: '用户识别',
is_checked: false,
pid: 0,
path: '/label',
children: [
{ id: 8, name: '标签管理', is_checked: false, pid: 7, path: '/label', children: [] },
{ id: 9, name: '群组管理', is_checked: false, pid: 7, path: '/group', children: [] },
{ id: 71, name: '运营策略管理', is_checked: false, pid: 7, path: '/strategy', children: [] },
],
},
{
id: 10,
name: '营销内容设计',
is_checked: false,
pid: 0,
path: '/material',
children: [
{ id: 11, name: '文本资料管理', is_checked: false, pid: 10, path: '/material?type=1', children: [] },
{ id: 12, name: '图片资料管理', is_checked: false, pid: 10, path: '/material?type=2', children: [] },
{ id: 13, name: '卡券资料管理', is_checked: false, pid: 10, path: '/material?type=8', children: [] },
{ id: 14, name: '视频资料管理', is_checked: false, pid: 10, path: '/material?type=4', children: [] },
{ id: 15, name: 'H5资料管理', is_checked: false, pid: 10, path: '/material?type=5', children: [] },
{ id: 16, name: '二维码资料管理', is_checked: false, pid: 10, path: '/material?type=6', children: [] },
{ id: 17, name: '语音资料管理', is_checked: false, pid: 10, path: '/material?type=3', children: [] },
{ id: 18, name: '小程序资料管理', is_checked: false, pid: 10, path: '/material?type=7', children: [] },
],
},
{
id: 19,
name: '自动化营销',
is_checked: false,
pid: 0,
path: '/trip/my',
children: [],
},
{
id: 20,
name: '直播带货',
is_checked: false,
pid: 0,
path: '/live',
children: [
{ id: 21, name: '商品品类管理', is_checked: false, pid: 20, path: '/live/product/category', children: [] },
{ id: 22, name: '商品属性管理', is_checked: false, pid: 20, path: '/live/product/attr', children: [] },
{ id: 23, name: '商品管理', is_checked: false, pid: 20, path: '/live/product/management', children: [] },
{ id: 24, name: '直播练习', is_checked: false, pid: 20, path: '/live/test', children: [] },
{ id: 25, name: '直播话术管理', is_checked: false, pid: 20, path: '/live/talk', children: [] },
{ id: 201, name: '订单管理', is_checked: false, pid: 20, path: '/live/order', children: [] },
],
},
{
id: 26,
name: '数据分析',
is_checked: false,
pid: 0,
path: '/analyze',
children: [
{ id: 27, name: '用户分析', is_checked: false, pid: 26, path: '/analyze/user', children: [] },
{ id: 28, name: '标签群组分析', is_checked: false, pid: 26, path: '/analyze/label', children: [] },
{ id: 29, name: '事件分析', is_checked: false, pid: 26, path: '/analyze/event', children: [] },
{ id: 30, name: '营销分析', is_checked: false, pid: 26, path: '/analyze/marketing', children: [] },
],
},
]
...@@ -30,17 +30,17 @@ export default defineConfig(() => ({ ...@@ -30,17 +30,17 @@ export default defineConfig(() => ({
// cert: fs.readFileSync(path.join(__dirname, './https/ezijing.com.pem')) // cert: fs.readFileSync(path.join(__dirname, './https/ezijing.com.pem'))
// }, // },
proxy: { proxy: {
'/api': 'https://saas-lab.ezijing.com', // '/api/lab': {
// target: 'http://local-com-resource-api.frontend.ezijing.com',
// changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api\/lab/, ''),
// },
// '/api/resource': { // '/api/resource': {
// target: 'http://com-resource-admin-test.ezijing.com', // target: 'http://com-resource-admin-test.ezijing.com',
// changeOrigin: true, // changeOrigin: true,
// rewrite: path => path.replace(/^\/api\/resource/, '') // rewrite: path => path.replace(/^\/api\/resource/, '')
// }, // },
// '/api/lab': { '/api': 'https://saas-lab.ezijing.com',
// target: 'http://com-resource-api-test.ezijing.com',
// changeOrigin: true,
// rewrite: path => path.replace(/^\/api\/lab/, '')
// }
}, },
}, },
resolve: { resolve: {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论