Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
+151
View File
@@ -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,
};
});
+144
View File
@@ -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,
};
});
+1
View File
@@ -0,0 +1 @@
export * from './auth';
+102
View File
@@ -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