Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import type { Application } from '#/api/core/application';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { getApplicationByCodeApi } from '#/api/core/application';
|
||||
|
||||
/**
|
||||
* 应用上下文 Store
|
||||
* 用于管理当前应用的状态,支持多应用架构
|
||||
*/
|
||||
export const useAppContextStore = defineStore('app-context', () => {
|
||||
// 当前应用编码(从 URL 参数中获取)
|
||||
const appCode = ref<null | string>(null);
|
||||
|
||||
// 当前应用详情
|
||||
const currentApp = ref<Application | null>(null);
|
||||
|
||||
// 是否为开发模式(/app-dev/{code}/)
|
||||
const isDevMode = ref<boolean>(false);
|
||||
|
||||
// 是否为子应用模式
|
||||
const isSubApp = computed(() => !!appCode.value);
|
||||
|
||||
// 是否为主应用模式
|
||||
const isMainApp = computed(() => !appCode.value);
|
||||
|
||||
/**
|
||||
* 从 URL 路径中初始化应用上下文
|
||||
* URL 格式:
|
||||
* - /app/:appCode/... (正常模式)
|
||||
* - /app-dev/:appCode/... (开发模式)
|
||||
*/
|
||||
async function initFromUrl() {
|
||||
const path = window.location.pathname;
|
||||
|
||||
// 先尝试匹配开发模式 /app-dev/{code}/
|
||||
const devMatch = path.match(/^\/app-dev\/([^/]+)/);
|
||||
if (devMatch && devMatch[1]) {
|
||||
appCode.value = devMatch[1];
|
||||
isDevMode.value = true;
|
||||
await loadCurrentApp();
|
||||
return;
|
||||
}
|
||||
|
||||
// 再尝试匹配正常模式 /app/{code}/
|
||||
const appMatch = path.match(/^\/app\/([^/]+)/);
|
||||
if (appMatch && appMatch[1]) {
|
||||
appCode.value = appMatch[1];
|
||||
isDevMode.value = false;
|
||||
await loadCurrentApp();
|
||||
return;
|
||||
}
|
||||
|
||||
// 主应用模式
|
||||
appCode.value = null;
|
||||
isDevMode.value = false;
|
||||
currentApp.value = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载当前应用详情
|
||||
*/
|
||||
async function loadCurrentApp() {
|
||||
if (!appCode.value) {
|
||||
currentApp.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const app = await getApplicationByCodeApi(appCode.value);
|
||||
currentApp.value = app;
|
||||
} catch (error) {
|
||||
console.error('Failed to load application:', error);
|
||||
currentApp.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前应用
|
||||
* @param code 应用编码
|
||||
* @param devMode 是否为开发模式
|
||||
*/
|
||||
async function setAppCode(code: null | string, devMode: boolean = false) {
|
||||
appCode.value = code;
|
||||
isDevMode.value = devMode;
|
||||
if (code) {
|
||||
await loadCurrentApp();
|
||||
} else {
|
||||
currentApp.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除应用上下文
|
||||
*/
|
||||
function clear() {
|
||||
appCode.value = null;
|
||||
isDevMode.value = false;
|
||||
currentApp.value = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子应用 URL
|
||||
* @param code 应用编码
|
||||
* @param devMode 是否为开发模式
|
||||
*/
|
||||
function getSubAppUrl(code: string, devMode: boolean = false): string {
|
||||
const baseUrl = window.location.origin;
|
||||
const prefix = devMode ? 'app-dev' : 'app';
|
||||
return `${baseUrl}/${prefix}/${code}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前上下文的路由路径
|
||||
* 在子应用模式下会自动添加 /app/{code} 或 /app-dev/{code} 前缀
|
||||
* @param path 原始路由路径(如 /ai-platform/workflow/editor/123)
|
||||
*/
|
||||
function getContextPath(path: string): string {
|
||||
if (!appCode.value) {
|
||||
return path;
|
||||
}
|
||||
const prefix = isDevMode.value ? 'app-dev' : 'app';
|
||||
return `/${prefix}/${appCode.value}${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置 store 状态
|
||||
*/
|
||||
function $reset() {
|
||||
appCode.value = null;
|
||||
isDevMode.value = false;
|
||||
currentApp.value = null;
|
||||
}
|
||||
|
||||
return {
|
||||
appCode,
|
||||
currentApp,
|
||||
isDevMode,
|
||||
isSubApp,
|
||||
isMainApp,
|
||||
initFromUrl,
|
||||
loadCurrentApp,
|
||||
setAppCode,
|
||||
clear,
|
||||
getSubAppUrl,
|
||||
getContextPath,
|
||||
$reset,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { Recordable, UserInfo } from '@vben/types';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||
|
||||
import { ElNotification } from 'element-plus';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const accessStore = useAccessStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
const loginLoading = ref(false);
|
||||
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param params 登录表单数据
|
||||
* @param onSuccess
|
||||
*/
|
||||
async function authLogin(
|
||||
params: Recordable<any>,
|
||||
onSuccess?: () => Promise<void> | void,
|
||||
) {
|
||||
// 异步处理用户登录操作并获取 accessToken
|
||||
let userInfo: null | UserInfo = null;
|
||||
try {
|
||||
loginLoading.value = true;
|
||||
const response = await loginApi(params);
|
||||
|
||||
// 提取两个 token(支持不同的响应格式)
|
||||
const accessToken = response.accessToken;
|
||||
const refreshToken = response.refreshToken;
|
||||
|
||||
// 如果成功获取到 accessToken
|
||||
if (accessToken) {
|
||||
// 将 accessToken 存储到 accessStore 中
|
||||
accessStore.setAccessToken(accessToken);
|
||||
if (refreshToken) {
|
||||
accessStore.setRefreshToken(refreshToken);
|
||||
}
|
||||
|
||||
// 获取用户信息并存储到 accessStore 中
|
||||
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||
fetchUserInfo(),
|
||||
getAccessCodesApi(),
|
||||
]);
|
||||
|
||||
userInfo = fetchUserInfoResult;
|
||||
|
||||
userStore.setUserInfo(userInfo);
|
||||
accessStore.setAccessCodes(accessCodes);
|
||||
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
} else {
|
||||
onSuccess
|
||||
? await onSuccess?.()
|
||||
: await router.push(
|
||||
userInfo.homePath || preferences.app.defaultHomePath,
|
||||
);
|
||||
}
|
||||
|
||||
if (userInfo?.realName) {
|
||||
ElNotification({
|
||||
message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
||||
title: $t('authentication.loginSuccess'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
};
|
||||
}
|
||||
|
||||
async function logout(redirect: boolean = true, callApi: boolean = true) {
|
||||
// 只有在 callApi 为 true 时才调用后端接口
|
||||
// 当被强制登出(401)时,不需要再调用后端接口
|
||||
if (callApi) {
|
||||
try {
|
||||
await logoutApi();
|
||||
} catch {
|
||||
// 不做任何处理
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
resetAllStores();
|
||||
} catch (error) {
|
||||
console.error('Reset stores failed:', error);
|
||||
}
|
||||
|
||||
accessStore.setLoginExpired(false);
|
||||
|
||||
// 回登录页带上当前路由地址
|
||||
await router.replace({
|
||||
path: LOGIN_PATH,
|
||||
query: redirect
|
||||
? {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
}
|
||||
: {},
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchUserInfo() {
|
||||
let userInfo: null | UserInfo = null;
|
||||
const backendUser = await getUserInfoApi();
|
||||
userInfo = {
|
||||
...backendUser,
|
||||
userId: (backendUser as any).id || backendUser.userId,
|
||||
realName: (backendUser as any).name || backendUser.realName || '',
|
||||
avatar: backendUser.avatar || '',
|
||||
roles: (backendUser as any).is_superuser ? ['super'] : ['user'],
|
||||
} as UserInfo;
|
||||
userStore.setUserInfo(userInfo);
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
$reset,
|
||||
authLogin,
|
||||
fetchUserInfo,
|
||||
loginLoading,
|
||||
logout,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './auth';
|
||||
@@ -0,0 +1,102 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { WikiSpace } from '#/types/zq-smart-table/table'
|
||||
import {
|
||||
getWikiSpacesApi,
|
||||
createWikiSpaceApi,
|
||||
updateWikiSpaceApi,
|
||||
deleteWikiSpaceApi,
|
||||
type WikiSpaceItem,
|
||||
} from '#/api/smart-table'
|
||||
|
||||
function wikiSpaceFromApi(s: WikiSpaceItem): WikiSpace {
|
||||
return {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
icon: s.icon || 'BookOpen',
|
||||
avatar: s.avatar || null,
|
||||
description: s.description,
|
||||
cover: s.cover || null,
|
||||
category: s.category || 'default',
|
||||
visibility: s.visibility || 'private',
|
||||
documentCount: s.document_count ?? 0,
|
||||
createdAt: s.sys_create_datetime,
|
||||
updatedAt: s.sys_update_datetime,
|
||||
creatorId: s.sys_creator_id,
|
||||
creatorName: s.creator_name,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWikiStore = defineStore('wiki', () => {
|
||||
const spaces = ref<WikiSpace[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function loadSpaces() {
|
||||
loading.value = true
|
||||
try {
|
||||
const raw = await getWikiSpacesApi()
|
||||
const list = Array.isArray(raw) ? raw : (raw as any)?.data ?? []
|
||||
spaces.value = list.map(wikiSpaceFromApi)
|
||||
} catch (e) {
|
||||
console.error('[Wiki] Failed to load spaces', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createSpace(data: {
|
||||
name: string
|
||||
icon?: string
|
||||
avatar?: string
|
||||
description?: string
|
||||
category?: string
|
||||
visibility?: string
|
||||
}) {
|
||||
try {
|
||||
const raw = await createWikiSpaceApi(data)
|
||||
const item = (raw as any)?.data ?? raw
|
||||
const space = wikiSpaceFromApi(item)
|
||||
spaces.value.push(space)
|
||||
return space.id
|
||||
} catch (e) {
|
||||
console.error('[Wiki] Failed to create space', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSpace(spaceId: string, data: Record<string, any>) {
|
||||
try {
|
||||
const raw = await updateWikiSpaceApi(spaceId, data)
|
||||
const item = (raw as any)?.data ?? raw
|
||||
const idx = spaces.value.findIndex((s) => s.id === spaceId)
|
||||
if (idx !== -1) {
|
||||
spaces.value[idx] = wikiSpaceFromApi(item)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Wiki] Failed to update space', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSpace(spaceId: string) {
|
||||
try {
|
||||
await deleteWikiSpaceApi(spaceId)
|
||||
spaces.value = spaces.value.filter((s) => s.id !== spaceId)
|
||||
} catch (e) {
|
||||
console.error('[Wiki] Failed to delete space', e)
|
||||
}
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
spaces.value = []
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
spaces,
|
||||
loading,
|
||||
loadSpaces,
|
||||
createSpace,
|
||||
updateSpace,
|
||||
deleteSpace,
|
||||
$reset,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,199 @@
|
||||
import type { Edge, Node } from '@vue-flow/core';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export interface WorkflowState {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
}
|
||||
|
||||
export const useWorkflowEditorStore = defineStore('workflow-editor', () => {
|
||||
// 历史记录
|
||||
const history = ref<WorkflowState[]>([]);
|
||||
const historyIndex = ref(-1);
|
||||
const maxHistory = 50;
|
||||
|
||||
// 是否正在执行撤销/重做操作
|
||||
const isUndoRedo = ref(false);
|
||||
|
||||
// 当前工作流 ID
|
||||
const workflowId = ref<string>('');
|
||||
|
||||
// 是否有未保存的更改
|
||||
const hasUnsavedChanges = ref(false);
|
||||
|
||||
// 自动保存定时器
|
||||
let autoSaveTimer: null | ReturnType<typeof setTimeout> = null;
|
||||
|
||||
// 计算属性
|
||||
const canUndo = computed(() => historyIndex.value > 0);
|
||||
const canRedo = computed(() => historyIndex.value < history.value.length - 1);
|
||||
const currentState = computed(
|
||||
() => history.value[historyIndex.value] || null,
|
||||
);
|
||||
|
||||
// 初始化工作流
|
||||
function initWorkflow(id: string, initialState?: WorkflowState) {
|
||||
workflowId.value = id;
|
||||
history.value = [];
|
||||
historyIndex.value = -1;
|
||||
hasUnsavedChanges.value = false;
|
||||
|
||||
if (initialState) {
|
||||
saveHistory(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存历史状态(带防抖)
|
||||
let saveDebounceTimer: null | ReturnType<typeof setTimeout> = null;
|
||||
|
||||
function saveHistory(state: WorkflowState, immediate = false) {
|
||||
if (isUndoRedo.value) return;
|
||||
|
||||
const doSave = () => {
|
||||
// 深拷贝状态
|
||||
const clonedState: WorkflowState = {
|
||||
nodes: JSON.parse(JSON.stringify(state.nodes)),
|
||||
edges: JSON.parse(JSON.stringify(state.edges)),
|
||||
};
|
||||
|
||||
// 如果当前不在最新位置,删除后面的历史
|
||||
if (historyIndex.value < history.value.length - 1) {
|
||||
history.value = history.value.slice(0, historyIndex.value + 1);
|
||||
}
|
||||
|
||||
// 检查是否与上一个状态相同(避免重复保存)
|
||||
const lastState = history.value[history.value.length - 1];
|
||||
if (lastState) {
|
||||
const isSame =
|
||||
JSON.stringify(lastState.nodes) ===
|
||||
JSON.stringify(clonedState.nodes) &&
|
||||
JSON.stringify(lastState.edges) === JSON.stringify(clonedState.edges);
|
||||
if (isSame) return;
|
||||
}
|
||||
|
||||
history.value.push(clonedState);
|
||||
|
||||
// 限制历史记录数量
|
||||
if (history.value.length > maxHistory) {
|
||||
history.value.shift();
|
||||
}
|
||||
|
||||
historyIndex.value = history.value.length - 1;
|
||||
hasUnsavedChanges.value = true;
|
||||
};
|
||||
|
||||
if (immediate) {
|
||||
doSave();
|
||||
} else {
|
||||
// 防抖:300ms 内的多次调用只执行最后一次
|
||||
if (saveDebounceTimer) {
|
||||
clearTimeout(saveDebounceTimer);
|
||||
}
|
||||
saveDebounceTimer = setTimeout(doSave, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// 撤销
|
||||
function undo(): null | WorkflowState {
|
||||
if (!canUndo.value) return null;
|
||||
|
||||
isUndoRedo.value = true;
|
||||
historyIndex.value--;
|
||||
const state = history.value[historyIndex.value];
|
||||
|
||||
// 延迟重置标志,避免恢复状态时触发 saveHistory
|
||||
setTimeout(() => {
|
||||
isUndoRedo.value = false;
|
||||
}, 100);
|
||||
|
||||
return state ? JSON.parse(JSON.stringify(state)) : null;
|
||||
}
|
||||
|
||||
// 重做
|
||||
function redo(): null | WorkflowState {
|
||||
if (!canRedo.value) return null;
|
||||
|
||||
isUndoRedo.value = true;
|
||||
historyIndex.value++;
|
||||
const state = history.value[historyIndex.value];
|
||||
|
||||
setTimeout(() => {
|
||||
isUndoRedo.value = false;
|
||||
}, 100);
|
||||
|
||||
return state ? JSON.parse(JSON.stringify(state)) : null;
|
||||
}
|
||||
|
||||
// 标记为已保存
|
||||
function markAsSaved() {
|
||||
hasUnsavedChanges.value = false;
|
||||
}
|
||||
|
||||
// 清除历史
|
||||
function clearHistory() {
|
||||
history.value = [];
|
||||
historyIndex.value = -1;
|
||||
}
|
||||
|
||||
// 设置自动保存回调
|
||||
function triggerAutoSave(callback: () => void, delay = 2000) {
|
||||
if (autoSaveTimer) {
|
||||
clearTimeout(autoSaveTimer);
|
||||
}
|
||||
autoSaveTimer = setTimeout(() => {
|
||||
if (hasUnsavedChanges.value) {
|
||||
callback();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// 清理
|
||||
function cleanup() {
|
||||
if (autoSaveTimer) {
|
||||
clearTimeout(autoSaveTimer);
|
||||
autoSaveTimer = null;
|
||||
}
|
||||
if (saveDebounceTimer) {
|
||||
clearTimeout(saveDebounceTimer);
|
||||
saveDebounceTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置 store 状态
|
||||
function $reset() {
|
||||
history.value = [];
|
||||
historyIndex.value = -1;
|
||||
isUndoRedo.value = false;
|
||||
workflowId.value = '';
|
||||
hasUnsavedChanges.value = false;
|
||||
cleanup();
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
history,
|
||||
historyIndex,
|
||||
isUndoRedo,
|
||||
workflowId,
|
||||
hasUnsavedChanges,
|
||||
|
||||
// 计算属性
|
||||
canUndo,
|
||||
canRedo,
|
||||
currentState,
|
||||
|
||||
// 方法
|
||||
initWorkflow,
|
||||
saveHistory,
|
||||
undo,
|
||||
redo,
|
||||
markAsSaved,
|
||||
clearHistory,
|
||||
triggerAutoSave,
|
||||
cleanup,
|
||||
$reset,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user